From 0e4a111f81cceed275d9bec2695f6e401fb654d8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 12 Nov 2021 02:02:25 +0900 Subject: refactoring Resolve #7779 --- 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 - 1326 files changed, 129675 deletions(-) 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 (limited to 'src') 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; -} 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; - - /** - * Deprecated: The function might be deleted in future release. - */ - type Deprecated = 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>); - - public setByASN1ObjectArray(asn1ObjectArray: ASN1Object[]): void; - - public appendASN1Object(asn1Object: ASN1Object): void; - } - - class DERSequence extends DERAbstractStructured { - constructor(params?: Partial>); - - public getFreshValueHex(): ASN1V; - } - - //// ASN1HEX TYPES - - /** - * ASN.1 DER encoded data (hexadecimal string) - */ - type ASN1S = HexString; - - /** - * index of something - */ - type Idx = ASN1S extends { [idx: string]: unknown } ? string : ASN1S extends { [idx: number]: unknown } ? number : never; - - /** - * byte length of something - */ - type ByteLength = 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): ByteLength; - - public static getL(s: ASN1S, idx: Idx): ASN1L; - - public static getVblen(s: ASN1S, idx: Idx): ByteLength; - - public static getVidx(s: ASN1S, idx: Idx): Idx; - - public static getV(s: ASN1S, idx: Idx): ASN1V; - - public static getTLV(s: ASN1S, idx: Idx): ASN1TLV; - - public static getNextSiblingIdx(s: ASN1S, idx: Idx): Idx; - - public static getChildIdx(h: ASN1S, pos: Idx): Idx[]; - - public static getNthChildIdx(h: ASN1S, idx: Idx, nth: Nth): Idx; - - public static getIdxbyList(h: ASN1S, currentIndex: Idx, nthList: Mutable, checkingTag?: string): Idx>; - - public static getTLVbyList(h: ASN1S, currentIndex: Idx, nthList: Mutable, checkingTag?: string): ASN1TLV; - - // tslint:disable-next-line:bool-param-default - public static getVbyList(h: ASN1S, currentIndex: Idx, nthList: Mutable, checkingTag?: string, removeUnusedbits?: boolean): ASN1V; - - public static hextooidstr(hex: ASN1OIDV): OID; - - public static dump(hexOrObj: ASN1S | ASN1Object, flags?: Record, idx?: Idx, 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): 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): void; - - protected drShiftTo(n: number, r: Mutable): void; - - protected lShiftTo(n: number, r: Mutable): void; - - protected rShiftTo(n: number, r: Mutable): void; - - protected subTo(a: BigInteger, r: Mutable): void; - - protected multiplyTo(a: BigInteger, r: Mutable): void; - - protected squareTo(r: Mutable): void; - - protected divRemTo(m: BigInteger, q: Mutable, r: Mutable): 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): void; - - public mulTo(x: BigInteger, r: Mutable): void; - - public sqrTo(x: BigInteger, y: BigInteger, r: Mutable): void; - } - - class Montgomery { - constructor(m: BigInteger); - - public convert(x: BigInteger): BigInteger; - - public revert(x: BigInteger): BigInteger; - - public reduce(x: Mutable): void; - - public mulTo(x: BigInteger, r: Mutable): void; - - public sqrTo(x: BigInteger, y: BigInteger, r: Mutable): 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<'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, passcode: string): HexString; - - private _getPlainPKCS8HexFromEncryptedPKCS8PEM(pkcs8PEM: PEM, passcode: string): HexString; - - public getKeyFromEncryptedPKCS8PEM(prvKeyHex: HexString): ReturnType; - - public parsePlainPrivatePKCS8Hex(pkcs8PrvHex: HexString): { - algparam: ASN1V | null; - algoid: ASN1V; - keyidx: Idx; - }; - - public getKeyFromPlainPrivatePKCS8PEM(prvKeyHex: HexString): ReturnType; - - 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[]; - - public getHexValueArrayOfChildrenFromHex(hPrivateKey: PEM): Idx[]; - - 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): void; - } - - //// X509 TYPES - - type ExtInfo = { - critical: boolean; - oid: OID; - vidx: Idx; - }; - - 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>; - - public getPublicKeyContentIdx(): Idx>; - - 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; - - 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): string; - - public static hex2rdn(hex: HexString, idx?: Idx): string; - - public static hex2attrTypeValue(hex: HexString, idx?: Idx): 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; - 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 { - 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 { - const isWellKnownPort = (port: number) => port < 1024; - - async function isPortAvailable(port: number): Promise { - 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 { - 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 { - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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; - -type ArrayElement = A extends readonly (infer T)[] ? T : never; - -export class Storage { - 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 }; - - 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(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(key: K, value: ArrayElement): 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(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(); - -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; - y: Ref; - q: Ref; - 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, me: Record | null | undefined, mutedWords: string[][]): Promise { - // 自分自身 - 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; 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 { - 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; - -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; - public aiscript?: AiScript; - public pageVarUpdatedCallback?: values.VFn; - public canvases: Record = {}; - public vars: Ref> = ref({}); - public page: Record; - - 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 { - const values: Record = {}; - - 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) => { - 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; - 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) => ReturnType; -}; - -export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null; - -export const literalDefs: Record = { - 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 = { - 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[]; - public name: string; - - constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) { - this.layerdStates = layerdStates; - this.name = name || 'anonymous'; - } - - @autobind - public createChildScope(states: Record, 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 = { - 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 = { - 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 = 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; -}; 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: { -// : -// } -// -// proptype一覧: -// * image ... 画像選択ダイアログを出し、その画像のURLが格納されます -// * color ... 色選択コントロールを出し、選択された色が格納されます - -// 家具にカスタムテクスチャを適用できるようにするには、textureプロパティに以下の追加の情報を含めます: -// 便宜上そのUVのどの部分にカスタムテクスチャを貼り合わせるかのエリアをテクスチャエリアと呼びます。 -// UVは1024*1024だと仮定します。 -// -// : { -// prop: <プロパティ名>, -// uv: { -// x: <テクスチャエリアX座標>, -// y: <テクスチャエリアY座標>, -// width: <テクスチャエリアの幅>, -// height: <テクスチャエリアの高さ>, -// }, -// } -// -// には、カスタムテクスチャを適用したいメッシュ名を指定します -// <プロパティ名>には、カスタムテクスチャとして使用する画像を格納するプロパティ(前述)名を指定します - -// 家具にカスタムカラーを適用できるようにするには、colorプロパティに以下の追加の情報を含めます: -// -// : <プロパティ名> -// -// には、カスタムカラーを適用したいマテリアル名を指定します -// <プロパティ名>には、カスタムカラーとして使用する色を格納するプロパティ(前述)名を指定します - -[ - { - 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((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(); - -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; -}; - -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 { - 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): 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('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; - }[] - }, - 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; - 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(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(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(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(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; -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 { - 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 { - 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 { - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 (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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - - - 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 @@ - - - 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 @@ - - - - - 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 ", - "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 += ''; - appendChildren(node.childNodes); - text += ''; - break; - } - - case 's': - case 'del': - { - text += '~~'; - appendChildren(node.childNodes); - text += '~~'; - break; - } - - case 'i': - case 'em': - { - text += ''; - appendChildren(node.childNodes); - text += ''; - break; - } - - // block 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 ()
-			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) => 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('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 `

${doc.body.innerHTML}

`; -} 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 { - private cache: Map; - private lifetime: number; - - constructor(lifetime: Cache['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): Promise { - 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 extends Context { - 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 { - return new ID(true, false); - } - - public makeNullable(): ID { - return new ID(false, true); - } - - public makeOptionalNullable(): ID { - 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 { - 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 { - 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 { - // 自分自身 - 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 { - // 指定したユーザーの指定したノートのリノートがいくつあるか数える - 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 { - // 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 { - 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 { - 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) { - 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) { - 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, 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 { - 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 { - 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 { - const getStat = util.promisify(fs.stat); - return (await getStat(path)).size; -} - -/** - * Calculate MD5 hash - */ -async function calcHash(path: string): Promise { - 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 { - 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> { - 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 { - 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): 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): 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(Infinity); - -export async function getUserKeypair(userId: User['id']): Promise { - 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, '

$2

'); - -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(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 { - 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 { - 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 { - 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 = { - 'like': '👍', - 'love': '❤', // ここに記述する場合は異体字セレクタを入れない - 'laugh': '😆', - 'hmm': '🤔', - 'surprise': '😮', - 'congrats': '🎉', - 'angry': '💢', - 'confused': '😥', - 'rip': '😇', - 'pudding': '🍮', - 'star': '⭐', -}; - -export async function getFallbackReaction(): Promise { - const meta = await fetchMeta(); - return meta.useStarForReactionFallback ? '⭐' : '👍'; -} - -export function convertLegacyReactions(reactions: Record) { - const _reactions = {} as Record; - - 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; - - 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 { - 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 = ObjType<(typeof refs[x])['properties']>; - -export interface Schema extends SimpleSchema { - items?: Schema; - properties?: Obj; - ref?: keyof typeof refs; -} - -type NonUndefinedPropertyNames = { - [K in keyof T]: T[K]['optional'] extends true ? never : K -}[keyof T]; - -type UndefinedPropertyNames = { - [K in keyof T]: T[K]['optional'] extends true ? K : never -}[keyof T]; - -type OnlyRequired = Pick>; -type OnlyOptional = Pick>; - -export interface Obj extends SimpleObj { [key: string]: Schema; } - -export type ObjType = - { [P in keyof OnlyOptional]?: SchemaType } & - { [P in keyof OnlyRequired]: SchemaType }; - -// https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2 -type MyType = { - 0: any; - 1: SchemaType; -}[T extends Schema ? 1 : 0]; - -type NullOrUndefined

= - p['nullable'] extends true - ? p['optional'] extends true - ? (T | null | undefined) - : (T | null) - : p['optional'] extends true - ? (T | undefined) - : T; - -export type SchemaType

= - p['type'] extends 'number' ? NullOrUndefined : - p['type'] extends 'string' ? NullOrUndefined : - p['type'] extends 'boolean' ? NullOrUndefined : - p['type'] extends 'array' ? NullOrUndefined>[]> : - p['type'] extends 'object' ? ( - p['ref'] extends keyof typeof refs - ? NullOrUndefined> - : NullOrUndefined>> - ) : - p['type'] extends 'any' ? NullOrUndefined : - 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) { - 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) { - 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) { - 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) { - 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; -} 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; - - /** - * 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) { - 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[]; - - @Column('jsonb', { - default: [] - }) - public variables: Record[]; - - @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) { - 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) { - 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; - - @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) { - 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) { - 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) { - 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; - - @Column('jsonb', { - default: {}, - comment: 'The room data of the User.' - }) - public room: Record; - - @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; - - @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) { - 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) { - 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) { - 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) { - 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 { - 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 { - public async pack( - src: Antenna['id'] | Antenna, - ): Promise> { - 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 { - public async pack( - src: App['id'] | App, - me?: { id: User['id'] } | null | undefined, - options?: { - detail?: boolean, - includeSecret?: boolean, - includeProfileImageIds?: boolean - } - ): Promise> { - 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 { - 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 { - public async pack( - src: Blocking['id'] | Blocking, - me?: { id: User['id'] } | null | undefined - ): Promise> { - 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 { - public async pack( - src: Channel['id'] | Channel, - me?: { id: User['id'] } | null | undefined, - ): Promise> { - 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 { - public async pack( - src: Clip['id'] | Clip, - ): Promise> { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 | null>; - public async pack(src: DriveFile, options?: PackOptions): Promise>; - public async pack( - src: DriveFile['id'] | DriveFile, - options?: PackOptions - ): Promise | 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 { - public validateFolderName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) - ); - } - - public async pack( - src: DriveFolder['id'] | DriveFolder, - options?: { - detail: boolean - } - ): Promise> { - 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 { - public async pack( - src: Emoji['id'] | Emoji, - ): Promise> { - 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 { - 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 { - 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> { - 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 { - 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 { - public async pack( - src: GalleryPost['id'] | GalleryPost, - me?: { id: User['id'] } | null | undefined, - ): Promise> { - 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 { - public async pack( - src: ReversiGame['id'] | ReversiGame, - me?: { id: User['id'] } | null | undefined, - options?: { - detail?: boolean - } - ): Promise> { - 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 { - public async pack( - src: ReversiMatching['id'] | ReversiMatching, - me: { id: User['id'] } - ): Promise> { - 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 { - public async pack( - src: Hashtag, - ): Promise> { - 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 { - 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> { - 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 { - 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 { - public async pack( - src: Muting['id'] | Muting, - me?: { id: User['id'] } | null | undefined - ): Promise> { - 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 { - 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 { - public async pack( - src: NoteReaction['id'] | NoteReaction, - me?: { id: User['id'] } | null | undefined, - options?: { - withNote: boolean; - }, - ): Promise> { - 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 { - public validateCw(x: string) { - return x.trim().length <= 100; - } - - public async isVisibleForMe(note: Note, meId: User['id'] | null): Promise { - // 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; - }; - } - ): Promise> { - 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(); - 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 { - public async pack( - src: Notification['id'] | Notification, - options: { - _hintForEachNotes_?: { - myReactions: Map; - }; - } - ): Promise> { - 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(); - 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 { - 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 { - public async pack( - src: Page['id'] | Page, - me?: { id: User['id'] } | null | undefined, - ): Promise> { - const meId = me ? me.id : null; - const page = typeof src === 'object' ? src : await this.findOneOrFail(src); - - const attachedFiles: Promise[] = []; - 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 { -} 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 { - 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 { - 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 { - public async pack( - src: UserGroup['id'] | UserGroup, - ): Promise> { - 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 { - public async pack( - src: UserList['id'] | UserList, - ): Promise> { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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> { - 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(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(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(f: Predicate, xs: T[]): number { - return xs.filter(f).length; -} - -/** - * Count the number of elements that is equal to the element - */ -export function count(a: T, xs: T[]): number { - return countIf(x => x === a, xs); -} - -/** - * Concatenate an array of arrays - */ -export function concat(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(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(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(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(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(f: EndoRelation, 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(f: (x: T) => S, xs: T[]): T[][] { - return groupBy((a, b) => f(a) === f(b), xs); -} - -export function groupByX(collections: T[], keySelector: (x: T) => string) { - return collections.reduce((obj: Record, 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(f: Predicate, 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(x: T | T[] | undefined): T[] { - return Array.isArray(x) ? x : x != null ? [x] : []; -} - -export function toSingle(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 extends Promise ? U : T; - -type AwaitAll = { - [P in keyof T]: Await; -}; - -export async function awaitAll(obj: T): Promise> { - 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 { - isJust(): this is IJust; -} - -export interface IJust extends IMaybe { - get(): T; -} - -export function just(value: T): IJust { - return { - isJust: () => true, - get: () => value - }; -} - -export function nothing(): IMaybe { - 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 = (a: T) => boolean; - -export type Relation = (a: T, b: U) => boolean; - -export type EndoRelation = Relation; 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); - - 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 = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''' -}; - -const beginingOfCDATA = ''; - -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(name: string, limitPerSec = -1) { - return new Bull(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): Promise { - 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, done: any): Promise { - 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, done: any): Promise { - 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((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, done: any): Promise { - 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((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, done: any): Promise { - 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((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, done: any): Promise { - 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((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((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((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, done: any): Promise { - 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((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, done: any): Promise { - 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, done: any): Promise { - 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, done: any): Promise { - 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, done: any): Promise { - 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 | 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/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(1000 * 60 * 60); - -export default async (job: Bull.Job) => { - 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): Promise => { - 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 { - 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) => { - 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 | Bull.ProcessPromiseFunction>; - -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 | 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 { - 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('deliver', config.deliverJobPerSec || 128); -export const inboxQueue = initializeQueue('inbox', config.inboxJobPerSec || 16); -export const dbQueue = initializeQueue('db'); -export const objectStorageQueue = initializeQueue('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; -}; - -type PrivateKey = { - privateKeyPem: string; - keyId: string; -}; - -export function createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record }) { - 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 }) { - 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) { - const dst: Record = {}; - 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, b: Record) { - 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 { - const toGroups = groupingAudience(getApIds(to), actor); - const ccGroups = groupingAudience(getApIds(cc), actor); - - const others = unique(concat([toGroups.other, ccGroups.other])); - - const limit = promiseLimit(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 { - 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 { - 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 { - 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 { - 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 { - 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(); - - // 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 => { - // ※ 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 => { - 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 => { - 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 => { - 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 { - 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 => { - // ※ 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 => { - 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 { - 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 { - 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 => { - 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 { - 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 => { - // 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 => { - 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 { - 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 => { - 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 => { - // ※ 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 = { - "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 = '

.

'; - - 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 { - 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 { - 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 => { - 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 { - // 投稿者が凍結されていたらスキップ - 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 { - // 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(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 { - const dbResolver = new DbResolver(); - return await dbResolver.getNoteFromApId(object); -} - -/** - * Noteを作成します。 - */ -export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { - 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))) - .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 => { - 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 { - 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 { - 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); - })); -} 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 { - 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 { - 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 { - 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; - - 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 { - 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(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 { - 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 => { - 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 { - 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 => { - 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 { - 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 { - 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?:/)) - ? `
${new URL(field.value).href}` - : 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 { - 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; - private user?: ILocalUser; - - constructor() { - this.history = new Set(); - } - - public getHistory(): string[] { - return Array.from(this.history); - } - - public async resolveCollection(value: string | IObject): Promise { - 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 { - 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 { - 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 { - 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; - - // カーソルが指定されている場合 - 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 or Announce Activity - * @param note Note - */ -export async function packActivity(note: Note): Promise { - 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; - 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}) { - 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; - 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; - 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; - 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, - 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, 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, 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, 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, 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, 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, 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, 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, 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, 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(q: SelectQueryBuilder, 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 -) { - // 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((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 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 = { - [P in keyof T['params']]: NonNullable[P]['transform'] extends Function - ? ReturnType[P]['transform']> - : NonNullable[P]['default'] extends null | number | string - ? NonOptional[P]['validator']['get']>[0]> - : ReturnType[P]['validator']['get']>[0]; -}; - -export type Response = Record | void; - -type executor = - (params: Params, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any, cleanup?: Function) => - Promise>>; - -export default function (meta: T, cb: executor) - : (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => Promise { - 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(defs: T, params: any): [Params, 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; - 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; - 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: '' - } - } - } -}; - -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 ? '' : 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; - - 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 { - // 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); - - 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[] = []; - - 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 = 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; - - 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; - - 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:
${link}`, - `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; - const profileUpdates = {} as Partial; - - 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; - - 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:
${link}`, - `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((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:
${link}`, - `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((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((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((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((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((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((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((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((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((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((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 = {}; - private emitTypersIntervalId: ReturnType; - - @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 = {}; - private emitWatchersIntervalId: ReturnType; - - @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 = {}; - private emitTypersIntervalId: ReturnType; - - @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 = new Set(); - public muting: Set = new Set(); - public blocking: Set = new Set(); // "被"blocking - public followingChannels: Set = 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; - - 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(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(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(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(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 = { [K in keyof T]: { type: K; body: T[K]; } }; -type EventUnionFromDictionary< - T extends object, - U = Events -> = U[keyof U]; - -// name/messages(spec) pairs dictionary -export type StreamMessages = { - internal: { - name: 'internal'; - payload: EventUnionFromDictionary; - }; - broadcast: { - name: 'broadcast'; - payload: EventUnionFromDictionary; - }; - user: { - name: `user:${User['id']}`; - payload: EventUnionFromDictionary; - }; - main: { - name: `mainStream:${User['id']}`; - payload: EventUnionFromDictionary; - }; - drive: { - name: `driveStream:${User['id']}`; - payload: EventUnionFromDictionary; - }; - note: { - name: `noteStream:${Note['id']}`; - payload: EventUnionFromDictionary; - }; - channel: { - name: `channelStream:${Channel['id']}`; - payload: EventUnionFromDictionary; - }; - userList: { - name: `userListStream:${UserList['id']}`; - payload: EventUnionFromDictionary; - }; - antenna: { - name: `antennaStream:${Antenna['id']}`; - payload: EventUnionFromDictionary; - }; - messaging: { - name: `messagingStream:${User['id']}-${User['id']}`; - payload: EventUnionFromDictionary; - }; - groupMessaging: { - name: `messagingStream:${UserGroup['id']}`; - payload: EventUnionFromDictionary; - }; - messagingIndex: { - name: `messagingIndexStream:${User['id']}`; - payload: EventUnionFromDictionary; - }; - reversi: { - name: `reversiStream:${User['id']}`; - payload: EventUnionFromDictionary; - }; - reversiGame: { - name: `reversiGameStream:${ReversiGame['id']}`; - payload: EventUnionFromDictionary; - }; - admin: { - name: `adminStream:${User['id']}`; - payload: EventUnionFromDictionary; - }; - notes: { - name: 'notesStream'; - payload: Packed<'Note'>; - }; -}; - -// API event definitions -// ストリームごとのEmitterの辞書を用意 -type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter void }> }; -// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする -export type StreamEventEmitter = UnionToIntersection; -// { [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 = ` -

⚠エラーが発生しました

-

問題が解決しない場合は管理者までお問い合わせください。以下のオプションを試すこともできます:

- -
- ERROR CODE: ${code} -
- ${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' - -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! Back to Misskey\n成功しました。Misskeyを開き直してください。`; - - 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. Please retry.\n失敗しました。もう一度試してみてください。`); - 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', `

[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}

`) - } 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 }[]) => - `${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)}`).reduce((a, c) => a + c, '')}`; - -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 => ({ - 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 | 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(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; - -export default class ActiveUsersChart extends Chart { - constructor() { - super(name, schema); - } - - @autobind - protected genNewLog(latest: ActiveUsersLog): DeepPartial { - 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> { - 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; - -export default class DriveChart extends Chart { - constructor() { - super(name, schema); - } - - @autobind - protected genNewLog(latest: DriveLog): DeepPartial { - 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> { - 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; - -export default class FederationChart extends Chart { - constructor() { - super(name, schema); - } - - @autobind - protected genNewLog(latest: FederationLog): DeepPartial { - 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> { - 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; - -export default class HashtagChart extends Chart { - constructor() { - super(name, schema, true); - } - - @autobind - protected genNewLog(latest: HashtagLog): DeepPartial { - 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> { - 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; - -export default class InstanceChart extends Chart { - constructor() { - super(name, schema); - } - - @autobind - protected genNewLog(latest: InstanceLog): DeepPartial { - 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> { - 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; - -export default class NetworkChart extends Chart { - constructor() { - super(name, schema); - } - - @autobind - protected genNewLog(latest: NetworkLog): DeepPartial { - 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> { - return {}; - } - - @autobind - public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) { - const inc: DeepPartial = { - 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; - -export default class NotesChart extends Chart { - constructor() { - super(name, schema); - } - - @autobind - protected genNewLog(latest: NotesLog): DeepPartial { - 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> { - 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; - -export default class PerUserDriveChart extends Chart { - constructor() { - super(name, schema, true); - } - - @autobind - protected genNewLog(latest: PerUserDriveLog): DeepPartial { - 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> { - 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; - -export default class PerUserFollowingChart extends Chart { - constructor() { - super(name, schema, true); - } - - @autobind - protected genNewLog(latest: PerUserFollowingLog): DeepPartial { - 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> { - 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; - -export default class PerUserNotesChart extends Chart { - constructor() { - super(name, schema, true); - } - - @autobind - protected genNewLog(latest: PerUserNotesLog): DeepPartial { - 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> { - 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; - -export default class PerUserReactionsChart extends Chart { - constructor() { - super(name, schema, true); - } - - @autobind - protected genNewLog(latest: PerUserReactionsLog): DeepPartial { - 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> { - 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; - -export default class TestGroupedChart extends Chart { - private total = {} as Record; - - constructor() { - super(name, schema, true); - } - - @autobind - protected genNewLog(latest: TestGroupedLog): DeepPartial { - 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> { - 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; - -export default class TestUniqueChart extends Chart { - constructor() { - super(name, schema); - } - - @autobind - protected genNewLog(latest: TestUniqueLog): DeepPartial { - 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> { - 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; - -export default class TestChart extends Chart { - public total = 0; // publicにするのはテストのため - - constructor() { - super(name, schema); - } - - @autobind - protected genNewLog(latest: TestLog): DeepPartial { - 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> { - 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; - -export default class UsersChart extends Chart { - constructor() { - super(name, schema); - } - - @autobind - protected genNewLog(latest: UsersLog): DeepPartial { - 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> { - 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 = { - [P in keyof T]?: DeepPartial; -}; - -type ArrayValue = { - [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue; -}; - -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> { - private static readonly columnPrefix = '___'; - private static readonly columnDot = '_'; - - private name: string; - private buffer: { - diff: DeepPartial; - group: string | null; - }[] = []; - public schema: SimpleSchema; - protected repository: Repository; - - protected abstract genNewLog(latest: T): DeepPartial; - - /** - * @param logs 日時が新しい方が先頭 - */ - protected abstract aggregate(logs: T[]): T; - - protected abstract fetchActual(group: string | null): Promise>; - - @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): Record { - 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) { - const columns = {} as Record; - 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) { - const exec = (x: Obj) => { - const res = {} as Record; - 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) { - const query: Record = {}; - - 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(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 { - return this.repository.findOne({ - group: group, - }, { - order: { - date: -1 - } - }).then(x => x || null); - } - - @autobind - private async getCurrentLog(group: string | null = null): Promise { - 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, 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; - - 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 { - 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, group: string | null = null): Promise { - await this.commit(inc, group); - } - - @autobind - public async getChart(span: 'hour' | 'day', amount: number, cursor: Date | null, group: string | null = null): Promise> { - 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 = {} 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 -) { - 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); - - 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 { - // 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 { - 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 { - 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 { - return convertSharpToJpeg(await sharp(path), width, height); -} - -export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise { - 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 { - return convertSharpToWebp(await sharp(path), width, height); -} - -export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number): Promise { - 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 { - return convertSharpToPng(await sharp(path), width, height); -} - -export async function convertSharpToPng(sharp: sharp.Sharp, width: number, height: number): Promise { - 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 { - return convertSharpToPngOrJpeg(await sharp(path), width, height); -} - -export async function convertSharpToPngOrJpeg(sharp: sharp.Sharp, width: number, height: number): Promise { - 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 => { - 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 { - 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; - - 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 { - 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 { - 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 | 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 { - 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 | null): Promise { - 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 | null): Promise { - 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 | null): Promise { - 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 | null): Promise { - 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) { - 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(Infinity); - -export async function getInstanceActor(): Promise { - 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 | 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 | 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 | null, important = false): void { // 実行を継続できるが改善すべき状況で使う - this.log('warning', message, data, important); - } - - public succ(message: string, data?: Record | null, important = false): void { // 何かに成功した状況で使う - this.log('success', message, data, important); - } - - public debug(message: string, data?: Record | null, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報) - if (process.env.NODE_ENV != 'production' || envOption.verbose) { - this.log('debug', message, data, important); - } - } - - public info(message: string, data?: Record | 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(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 { - 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; - followingChannels: Set; - } -) { - const following = info?.following ? info.following : new Set((await Followings.find({ - where: { - followerId: userId - }, - select: ['followeeId'] - })).map(x => x.followeeId)); - const followingChannels = info?.followingChannels ? info.followingChannels : new Set((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(1000 * 60 * 60); - -export async function registerOrFetchInstanceDoc(host: string): Promise { - 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 { - 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: ` - - - - ${ subject } - - - -
-
- -
-
-

${ subject }

-
${ 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 = (type: K, value?: InternalStreamTypes[K]): void => { - this.publish('internal', type, typeof value === 'undefined' ? null : value); - } - - public publishUserEvent = (userId: User['id'], type: K, value?: UserStreamTypes[K]): void => { - this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishBroadcastStream = (type: K, value?: BroadcastTypes[K]): void => { - this.publish('broadcast', type, typeof value === 'undefined' ? null : value); - } - - public publishMainStream = (userId: User['id'], type: K, value?: MainStreamTypes[K]): void => { - this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishDriveStream = (userId: User['id'], type: K, value?: DriveStreamTypes[K]): void => { - this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishNoteStream = (noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void => { - this.publish(`noteStream:${noteId}`, type, { - id: noteId, - body: value - }); - } - - public publishChannelStream = (channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void => { - this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishUserListStream = (listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void => { - this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishAntennaStream = (antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void => { - this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishMessagingStream = (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 = (groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void => { - this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishMessagingIndexStream = (userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void => { - this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishReversiStream = (userId: User['id'], type: K, value?: ReversiStreamTypes[K]): void => { - this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); - } - - public publishReversiGameStream = (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 = (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 { - 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 { - 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 { - 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][]; -- cgit v1.2.3-freya