From e3ab697e6686712abb0102f76d4a2b68881f6926 Mon Sep 17 00:00:00 2001 From: s1idewhist1e Date: Sat, 29 Apr 2023 23:10:20 -0700 Subject: [PATCH] Remove ts from backend and prepare rust environment --- packages/backend/.gitignore | 7 + packages/backend/.mocharc.json | 10 - packages/backend/.swcrc | 25 - packages/backend/.vim/coc-settings.json | 5 + packages/backend/Cargo.lock | 14 + packages/backend/Cargo.toml | 16 + packages/backend/crates/server/Cargo.toml | 8 + packages/backend/crates/server/src/lib.rs | 15 + packages/backend/jsconfig.json | 13 - .../backend/native-utils/.cargo/config.toml | 3 - packages/backend/native-utils/.gitignore | 200 ---- packages/backend/native-utils/.npmignore | 13 - packages/backend/native-utils/Cargo.toml | 18 - .../native-utils/__test__/index.spec.mjs | 7 - packages/backend/native-utils/build.rs | 5 - .../npm/android-arm-eabi/README.md | 3 - .../npm/android-arm-eabi/package.json | 18 - .../native-utils/npm/android-arm64/README.md | 3 - .../npm/android-arm64/package.json | 18 - .../native-utils/npm/darwin-arm64/README.md | 3 - .../npm/darwin-arm64/package.json | 18 - .../npm/darwin-universal/README.md | 3 - .../npm/darwin-universal/package.json | 15 - .../native-utils/npm/darwin-x64/README.md | 3 - .../native-utils/npm/darwin-x64/package.json | 18 - .../native-utils/npm/freebsd-x64/README.md | 3 - .../native-utils/npm/freebsd-x64/package.json | 18 - .../npm/linux-arm-gnueabihf/README.md | 3 - .../npm/linux-arm-gnueabihf/package.json | 18 - .../npm/linux-arm64-gnu/README.md | 3 - .../npm/linux-arm64-gnu/package.json | 21 - .../npm/linux-arm64-musl/README.md | 3 - .../npm/linux-arm64-musl/package.json | 21 - .../native-utils/npm/linux-x64-gnu/README.md | 3 - .../npm/linux-x64-gnu/package.json | 21 - .../native-utils/npm/linux-x64-musl/README.md | 3 - .../npm/linux-x64-musl/package.json | 21 - .../npm/win32-arm64-msvc/README.md | 3 - .../npm/win32-arm64-msvc/package.json | 18 - .../npm/win32-ia32-msvc/README.md | 3 - .../npm/win32-ia32-msvc/package.json | 18 - .../native-utils/npm/win32-x64-msvc/README.md | 3 - .../npm/win32-x64-msvc/package.json | 18 - packages/backend/native-utils/package.json | 44 - packages/backend/native-utils/rustfmt.toml | 2 - packages/backend/native-utils/src/lib.rs | 2 - .../backend/native-utils/src/mastodon_api.rs | 70 -- packages/backend/ormconfig.js | 15 - packages/backend/package.json | 194 +--- packages/backend/rust-toolchain | 1 + packages/backend/rustfmt.toml | 1 + packages/backend/src/@types/hcaptcha.d.ts | 14 - .../backend/src/@types/http-signature.d.ts | 98 -- .../backend/src/@types/koa-json-body.d.ts | 15 - .../koa-remove-trailing-slashes/index.d.ts | 1 - packages/backend/src/@types/koa-slow.d.ts | 14 - packages/backend/src/@types/os-utils.d.ts | 33 - packages/backend/src/@types/package.json.d.ts | 10 - .../backend/src/@types/probe-image-size.d.ts | 37 - packages/backend/src/bin/migrate.rs | 4 + packages/backend/src/boot/index.ts | 89 -- packages/backend/src/boot/master.ts | 189 ---- packages/backend/src/boot/worker.ts | 20 - packages/backend/src/config/index.ts | 3 - packages/backend/src/config/load.ts | 68 -- packages/backend/src/config/types.ts | 133 --- packages/backend/src/const.ts | 71 -- packages/backend/src/daemons/janitor.ts | 20 - packages/backend/src/daemons/queue-stats.ts | 60 -- packages/backend/src/daemons/server-stats.ts | 79 -- packages/backend/src/db/elasticsearch.ts | 65 -- packages/backend/src/db/logger.ts | 3 - packages/backend/src/db/postgre.ts | 262 ----- packages/backend/src/db/redis.ts | 18 - packages/backend/src/db/sonic.ts | 49 - packages/backend/src/env.ts | 25 - packages/backend/src/global.d.ts | 2 - packages/backend/src/index.ts | 13 - packages/backend/src/main.rs | 5 + packages/backend/src/mfm/from-html.ts | 213 ---- packages/backend/src/mfm/to-html.ts | 174 ---- packages/backend/src/misc/acct.ts | 14 - packages/backend/src/misc/antenna-cache.ts | 36 - packages/backend/src/misc/api-permissions.ts | 35 - packages/backend/src/misc/app-lock.ts | 33 - packages/backend/src/misc/before-shutdown.ts | 103 -- packages/backend/src/misc/cache.ts | 88 -- packages/backend/src/misc/captcha.ts | 73 -- .../backend/src/misc/check-hit-antenna.ts | 137 --- packages/backend/src/misc/check-word-mute.ts | 78 -- packages/backend/src/misc/clone.ts | 24 - .../backend/src/misc/content-disposition.ts | 9 - packages/backend/src/misc/convert-host.ts | 28 - .../backend/src/misc/count-same-renotes.ts | 19 - packages/backend/src/misc/create-temp.ts | 24 - packages/backend/src/misc/detect-url-mime.ts | 15 - .../backend/src/misc/download-text-file.ts | 25 - packages/backend/src/misc/download-url.ts | 103 -- packages/backend/src/misc/emoji-regex.ts | 5 - .../misc/extract-custom-emojis-from-mfm.ts | 10 - packages/backend/src/misc/extract-hashtags.ts | 9 - packages/backend/src/misc/extract-mentions.ts | 13 - packages/backend/src/misc/fetch-meta.ts | 46 - .../backend/src/misc/fetch-proxy-account.ts | 11 - packages/backend/src/misc/fetch.ts | 171 ---- packages/backend/src/misc/gen-id.ts | 27 - packages/backend/src/misc/gen-identicon.ts | 114 --- packages/backend/src/misc/gen-key-pair.ts | 42 - packages/backend/src/misc/get-file-info.ts | 443 --------- packages/backend/src/misc/get-ip-hash.ts | 14 - packages/backend/src/misc/get-note-summary.ts | 52 - .../backend/src/misc/get-reaction-emoji.ts | 28 - packages/backend/src/misc/hard-limits.ts | 13 - packages/backend/src/misc/i18n.ts | 29 - packages/backend/src/misc/id/aid.ts | 25 - packages/backend/src/misc/id/meid.ts | 26 - packages/backend/src/misc/id/meidg.ts | 28 - packages/backend/src/misc/id/object-id.ts | 26 - .../backend/src/misc/identifiable-error.ts | 13 - .../src/misc/is-duplicate-key-value-error.ts | 3 - .../backend/src/misc/is-instance-muted.ts | 21 - packages/backend/src/misc/is-mime-image.ts | 20 - packages/backend/src/misc/is-quote.ts | 10 - packages/backend/src/misc/is-user-related.ts | 7 - packages/backend/src/misc/keypair-store.ts | 12 - packages/backend/src/misc/langmap.ts | 666 ------------- .../backend/src/misc/normalize-for-search.ts | 6 - packages/backend/src/misc/nyaize.ts | 21 - packages/backend/src/misc/password.ts | 20 - packages/backend/src/misc/populate-emojis.ts | 165 ---- packages/backend/src/misc/post.ts | 19 - packages/backend/src/misc/reaction-lib.ts | 136 --- packages/backend/src/misc/safe-for-sql.ts | 3 - packages/backend/src/misc/schema.ts | 222 ----- packages/backend/src/misc/secure-rndstr.ts | 24 - .../backend/src/misc/should-block-instance.ts | 20 - .../backend/src/misc/show-machine-info.ts | 17 - .../backend/src/misc/skipped-instances.ts | 63 -- packages/backend/src/misc/truncate.ts | 17 - packages/backend/src/misc/webhook-cache.ts | 49 - .../src/models/entities/abuse-user-report.ts | 86 -- .../src/models/entities/access-token.ts | 97 -- packages/backend/src/models/entities/ad.ts | 59 -- .../src/models/entities/announcement-read.ts | 43 - .../src/models/entities/announcement.ts | 43 - .../src/models/entities/antenna-note.ts | 50 - .../backend/src/models/entities/antenna.ts | 111 --- packages/backend/src/models/entities/app.ts | 60 -- .../models/entities/attestation-challenge.ts | 53 - .../src/models/entities/auth-session.ts | 50 - .../backend/src/models/entities/blocking.ts | 49 - .../src/models/entities/channel-following.ts | 50 - .../models/entities/channel-note-pining.ts | 42 - .../backend/src/models/entities/channel.ts | 82 -- .../backend/src/models/entities/clip-note.ts | 44 - packages/backend/src/models/entities/clip.ts | 51 - .../backend/src/models/entities/drive-file.ts | 206 ---- .../src/models/entities/drive-folder.ts | 56 -- packages/backend/src/models/entities/emoji.ts | 63 -- .../src/models/entities/follow-request.ts | 92 -- .../backend/src/models/entities/following.ts | 89 -- .../src/models/entities/gallery-like.ts | 40 - .../src/models/entities/gallery-post.ts | 86 -- .../backend/src/models/entities/hashtag.ts | 87 -- .../backend/src/models/entities/instance.ts | 164 ---- .../src/models/entities/messaging-message.ts | 96 -- packages/backend/src/models/entities/meta.ts | 502 ---------- .../src/models/entities/moderation-log.ts | 39 - .../backend/src/models/entities/muted-note.ts | 55 -- .../backend/src/models/entities/muting.ts | 55 -- .../src/models/entities/note-favorite.ts | 42 - .../src/models/entities/note-reaction.ts | 51 - .../src/models/entities/note-thread-muting.ts | 40 - .../src/models/entities/note-unread.ts | 70 -- .../src/models/entities/note-watching.ts | 59 -- packages/backend/src/models/entities/note.ts | 249 ----- .../src/models/entities/notification.ts | 180 ---- .../backend/src/models/entities/page-like.ts | 40 - packages/backend/src/models/entities/page.ts | 131 --- .../models/entities/password-reset-request.ts | 37 - .../backend/src/models/entities/poll-vote.ts | 47 - packages/backend/src/models/entities/poll.ts | 79 -- .../backend/src/models/entities/promo-note.ts | 35 - .../backend/src/models/entities/promo-read.ts | 42 - .../models/entities/registration-tickets.ts | 17 - .../src/models/entities/registry-item.ts | 65 -- packages/backend/src/models/entities/relay.ts | 19 - .../src/models/entities/renote-muting.ts | 49 - .../backend/src/models/entities/signin.ts | 42 - .../src/models/entities/sw-subscription.ts | 49 - .../src/models/entities/used-username.ts | 20 - .../models/entities/user-group-invitation.ts | 49 - .../src/models/entities/user-group-joining.ts | 49 - .../backend/src/models/entities/user-group.ts | 53 - .../backend/src/models/entities/user-ip.ts | 32 - .../src/models/entities/user-keypair.ts | 33 - .../src/models/entities/user-list-joining.ts | 49 - .../backend/src/models/entities/user-list.ts | 40 - .../src/models/entities/user-note-pining.ts | 42 - .../src/models/entities/user-pending.ts | 32 - .../src/models/entities/user-profile.ts | 239 ----- .../src/models/entities/user-publickey.ts | 41 - .../src/models/entities/user-security-key.ts | 55 -- packages/backend/src/models/entities/user.ts | 274 ------ .../backend/src/models/entities/webhook.ts | 89 -- packages/backend/src/models/id.ts | 4 - packages/backend/src/models/index.ts | 135 --- .../models/repositories/abuse-user-report.ts | 39 - .../src/models/repositories/antenna.ts | 36 - .../backend/src/models/repositories/app.ts | 45 - .../src/models/repositories/auth-session.ts | 21 - .../src/models/repositories/blocking.ts | 29 - .../src/models/repositories/channel.ts | 55 -- .../backend/src/models/repositories/clip.ts | 26 - .../src/models/repositories/drive-file.ts | 224 ----- .../src/models/repositories/drive-folder.ts | 50 - .../backend/src/models/repositories/emoji.ts | 25 - .../src/models/repositories/follow-request.ts | 20 - .../src/models/repositories/following.ts | 90 -- .../src/models/repositories/gallery-like.ts | 19 - .../src/models/repositories/gallery-post.ts | 41 - .../src/models/repositories/hashtag.ts | 21 - .../src/models/repositories/instance.ts | 44 - .../models/repositories/messaging-message.ts | 48 - .../models/repositories/moderation-logs.ts | 26 - .../backend/src/models/repositories/muting.ts | 30 - .../src/models/repositories/note-favorite.ts | 31 - .../src/models/repositories/note-reaction.ts | 56 -- .../backend/src/models/repositories/note.ts | 334 ------- .../src/models/repositories/notification.ts | 185 ---- .../src/models/repositories/page-like.ts | 23 - .../backend/src/models/repositories/page.ts | 99 -- .../backend/src/models/repositories/relay.ts | 4 - .../src/models/repositories/renote-muting.ts | 29 - .../backend/src/models/repositories/signin.ts | 8 - .../repositories/user-group-invitation.ts | 23 - .../src/models/repositories/user-group.ts | 23 - .../src/models/repositories/user-list.ts | 22 - .../backend/src/models/repositories/user.ts | 619 ------------ packages/backend/src/models/schema/antenna.ts | 118 --- packages/backend/src/models/schema/app.ts | 40 - .../backend/src/models/schema/blocking.ts | 30 - packages/backend/src/models/schema/channel.ts | 61 -- packages/backend/src/models/schema/clip.ts | 45 - .../backend/src/models/schema/drive-file.ts | 127 --- .../backend/src/models/schema/drive-folder.ts | 46 - packages/backend/src/models/schema/emoji.ts | 49 - .../src/models/schema/federation-instance.ts | 133 --- .../backend/src/models/schema/following.ts | 42 - .../backend/src/models/schema/gallery-post.ts | 83 -- packages/backend/src/models/schema/hashtag.ts | 41 - .../src/models/schema/messaging-message.ts | 87 -- packages/backend/src/models/schema/muting.ts | 36 - .../src/models/schema/note-favorite.ts | 30 - .../src/models/schema/note-reaction.ts | 29 - packages/backend/src/models/schema/note.ts | 200 ---- .../backend/src/models/schema/notification.ts | 79 -- packages/backend/src/models/schema/page.ts | 66 -- packages/backend/src/models/schema/queue.ts | 30 - .../src/models/schema/renote-muting.ts | 30 - .../backend/src/models/schema/user-group.ts | 40 - .../backend/src/models/schema/user-list.ts | 34 - packages/backend/src/models/schema/user.ts | 583 ----------- packages/backend/src/prelude/README.md | 3 - packages/backend/src/prelude/array.ts | 138 --- packages/backend/src/prelude/await-all.ts | 23 - packages/backend/src/prelude/math.ts | 3 - packages/backend/src/prelude/maybe.ts | 20 - packages/backend/src/prelude/relation.ts | 5 - packages/backend/src/prelude/string.ts | 15 - packages/backend/src/prelude/symbol.ts | 1 - packages/backend/src/prelude/time.ts | 54 - packages/backend/src/prelude/url.ts | 15 - packages/backend/src/prelude/xml.ts | 38 - packages/backend/src/queue/get-job-info.ts | 18 - packages/backend/src/queue/index.ts | 545 ----------- packages/backend/src/queue/initialize.ts | 37 - packages/backend/src/queue/logger.ts | 3 - .../processors/background/index-all-notes.ts | 79 -- .../src/queue/processors/background/index.ts | 12 - .../src/queue/processors/db/delete-account.ts | 102 -- .../queue/processors/db/delete-drive-files.ts | 61 -- .../queue/processors/db/export-blocking.ts | 105 -- .../processors/db/export-custom-emojis.ts | 130 --- .../queue/processors/db/export-following.ts | 113 --- .../src/queue/processors/db/export-mute.ts | 106 -- .../src/queue/processors/db/export-notes.ts | 132 --- .../queue/processors/db/export-user-lists.ts | 81 -- .../queue/processors/db/import-blocking.ts | 79 -- .../processors/db/import-custom-emojis.ts | 91 -- .../queue/processors/db/import-following.ts | 116 --- .../src/queue/processors/db/import-muting.ts | 89 -- .../src/queue/processors/db/import-posts.ts | 131 --- .../queue/processors/db/import-user-lists.ts | 96 -- .../backend/src/queue/processors/db/index.ts | 43 - .../backend/src/queue/processors/deliver.ts | 81 -- .../processors/ended-poll-notification.ts | 36 - .../backend/src/queue/processors/inbox.ts | 175 ---- .../object-storage/clean-remote-files.ts | 53 - .../processors/object-storage/delete-file.ts | 11 - .../queue/processors/object-storage/index.ts | 19 - .../system/check-expired-mutings.ts | 33 - .../queue/processors/system/clean-charts.ts | 44 - .../src/queue/processors/system/clean.ts | 21 - .../src/queue/processors/system/index.ts | 24 - .../queue/processors/system/resync-charts.ts | 24 - .../queue/processors/system/tick-charts.ts | 44 - .../src/queue/processors/webhook-deliver.ts | 66 -- packages/backend/src/queue/queues.ts | 41 - packages/backend/src/queue/types.ts | 75 -- .../src/remote/activitypub/ap-request.ts | 152 --- .../src/remote/activitypub/audience.ts | 104 -- .../src/remote/activitypub/check-fetch.ts | 97 -- .../src/remote/activitypub/db-resolver.ts | 189 ---- .../src/remote/activitypub/deliver-manager.ts | 171 ---- .../activitypub/kernel/accept/follow.ts | 32 - .../remote/activitypub/kernel/accept/index.ts | 28 - .../remote/activitypub/kernel/add/index.ts | 26 - .../activitypub/kernel/announce/index.ts | 23 - .../activitypub/kernel/announce/note.ts | 84 -- .../remote/activitypub/kernel/block/index.ts | 29 - .../remote/activitypub/kernel/create/index.ts | 51 - .../remote/activitypub/kernel/create/note.ts | 51 - .../remote/activitypub/kernel/delete/actor.ts | 30 - .../remote/activitypub/kernel/delete/index.ts | 55 -- .../remote/activitypub/kernel/delete/note.ts | 44 - .../remote/activitypub/kernel/flag/index.ts | 37 - .../src/remote/activitypub/kernel/follow.ts | 23 - .../src/remote/activitypub/kernel/index.ts | 111 --- .../src/remote/activitypub/kernel/like.ts | 28 - .../remote/activitypub/kernel/move/index.ts | 69 -- .../src/remote/activitypub/kernel/read.ts | 33 - .../activitypub/kernel/reject/follow.ts | 33 - .../remote/activitypub/kernel/reject/index.ts | 28 - .../remote/activitypub/kernel/remove/index.ts | 26 - .../remote/activitypub/kernel/undo/accept.ts | 30 - .../activitypub/kernel/undo/announce.ts | 22 - .../remote/activitypub/kernel/undo/block.ts | 24 - .../remote/activitypub/kernel/undo/follow.ts | 44 - .../remote/activitypub/kernel/undo/index.ts | 47 - .../remote/activitypub/kernel/undo/like.ts | 22 - .../remote/activitypub/kernel/update/index.ts | 38 - .../backend/src/remote/activitypub/logger.ts | 3 - .../src/remote/activitypub/misc/contexts.ts | 525 ---------- .../remote/activitypub/misc/get-note-html.ts | 8 - .../remote/activitypub/misc/html-to-mfm.ts | 11 - .../remote/activitypub/misc/ld-signature.ts | 143 --- .../src/remote/activitypub/models/icon.ts | 5 - .../remote/activitypub/models/identifier.ts | 5 - .../src/remote/activitypub/models/image.ts | 82 -- .../src/remote/activitypub/models/mention.ts | 36 - .../src/remote/activitypub/models/note.ts | 499 ---------- .../src/remote/activitypub/models/person.ts | 698 ------------- .../src/remote/activitypub/models/question.ts | 97 -- .../src/remote/activitypub/models/tag.ts | 25 - .../backend/src/remote/activitypub/perform.ts | 23 - .../src/remote/activitypub/renderer/accept.ts | 8 - .../src/remote/activitypub/renderer/add.ts | 9 - .../remote/activitypub/renderer/announce.ts | 37 - .../src/remote/activitypub/renderer/block.ts | 20 - .../src/remote/activitypub/renderer/create.ts | 17 - .../src/remote/activitypub/renderer/delete.ts | 9 - .../remote/activitypub/renderer/document.ts | 9 - .../src/remote/activitypub/renderer/emoji.ts | 17 - .../src/remote/activitypub/renderer/flag.ts | 20 - .../activitypub/renderer/follow-relay.ts | 14 - .../activitypub/renderer/follow-user.ts | 12 - .../src/remote/activitypub/renderer/follow.ts | 22 - .../remote/activitypub/renderer/hashtag.ts | 7 - .../src/remote/activitypub/renderer/image.ts | 9 - .../src/remote/activitypub/renderer/index.ts | 73 -- .../src/remote/activitypub/renderer/key.ts | 14 - .../src/remote/activitypub/renderer/like.ts | 37 - .../remote/activitypub/renderer/mention.ts | 13 - .../src/remote/activitypub/renderer/note.ts | 188 ---- .../renderer/ordered-collection-page.ts | 30 - .../renderer/ordered-collection.ts | 34 - .../src/remote/activitypub/renderer/person.ts | 104 -- .../remote/activitypub/renderer/question.ts | 27 - .../src/remote/activitypub/renderer/read.ts | 12 - .../src/remote/activitypub/renderer/reject.ts | 8 - .../src/remote/activitypub/renderer/remove.ts | 9 - .../remote/activitypub/renderer/tombstone.ts | 4 - .../src/remote/activitypub/renderer/undo.ts | 19 - .../src/remote/activitypub/renderer/update.ts | 15 - .../src/remote/activitypub/renderer/vote.ts | 29 - .../backend/src/remote/activitypub/request.ts | 58 -- .../src/remote/activitypub/resolver.ts | 169 ---- .../backend/src/remote/activitypub/type.ts | 354 ------- packages/backend/src/remote/logger.ts | 3 - packages/backend/src/remote/resolve-user.ts | 139 --- packages/backend/src/remote/webfinger.ts | 41 - packages/backend/src/server/activitypub.ts | 368 ------- .../src/server/activitypub/featured.ts | 61 -- .../src/server/activitypub/followers.ts | 119 --- .../src/server/activitypub/following.ts | 119 --- .../backend/src/server/activitypub/outbox.ts | 147 --- packages/backend/src/server/api/2fa.ts | 417 -------- .../backend/src/server/api/api-handler.ts | 123 --- .../backend/src/server/api/authenticate.ts | 103 -- packages/backend/src/server/api/call.ts | 192 ---- .../server/api/common/generate-block-query.ts | 54 - .../api/common/generate-channel-query.ts | 35 - .../api/common/generate-muted-note-query.ts | 16 - .../generate-muted-note-thread-query.ts | 24 - .../api/common/generate-muted-user-query.ts | 81 -- .../api/common/generate-native-user-token.ts | 3 - .../api/common/generate-replies-query.ts | 47 - .../api/common/generate-visibility-query.ts | 61 -- .../common/generated-muted-renote-query.ts | 28 - .../backend/src/server/api/common/getters.ts | 72 -- .../src/server/api/common/inject-featured.ts | 53 - .../src/server/api/common/inject-promo.ts | 36 - .../src/server/api/common/is-native-token.ts | 1 - .../api/common/make-pagination-query.ts | 42 - .../api/common/read-messaging-message.ts | 179 ---- .../server/api/common/read-notification.ts | 59 -- .../backend/src/server/api/common/signin.ts | 44 - .../backend/src/server/api/common/signup.ts | 141 --- .../backend/src/server/api/compatibility.ts | 20 - packages/backend/src/server/api/define.ts | 105 -- packages/backend/src/server/api/endpoints.ts | 805 --------------- .../api/endpoints/admin/abuse-user-reports.ts | 144 --- .../api/endpoints/admin/accounts/create.ts | 55 -- .../api/endpoints/admin/accounts/delete.ts | 58 -- .../api/endpoints/admin/accounts/hosted.ts | 116 --- .../server/api/endpoints/admin/ad/create.ts | 46 - .../server/api/endpoints/admin/ad/delete.ts | 34 - .../src/server/api/endpoints/admin/ad/list.ts | 32 - .../server/api/endpoints/admin/ad/update.ts | 58 -- .../endpoints/admin/announcements/create.ts | 78 -- .../endpoints/admin/announcements/delete.ts | 34 - .../api/endpoints/admin/announcements/list.ts | 104 -- .../endpoints/admin/announcements/update.ts | 42 - .../api/endpoints/admin/delete-account.ts | 29 - .../admin/delete-all-files-of-a-user.ts | 28 - .../admin/drive-capacity-override.ts | 43 - .../admin/drive/clean-remote-files.ts | 19 - .../api/endpoints/admin/drive/cleanup.ts | 27 - .../server/api/endpoints/admin/drive/files.ts | 89 -- .../api/endpoints/admin/drive/show-file.ts | 225 ----- .../endpoints/admin/emoji/add-aliases-bulk.ts | 47 - .../server/api/endpoints/admin/emoji/add.ts | 68 -- .../server/api/endpoints/admin/emoji/copy.ts | 88 -- .../api/endpoints/admin/emoji/delete-bulk.ts | 43 - .../api/endpoints/admin/emoji/delete.ts | 42 - .../api/endpoints/admin/emoji/import-zip.ts | 21 - .../api/endpoints/admin/emoji/list-remote.ts | 105 -- .../server/api/endpoints/admin/emoji/list.ts | 107 -- .../admin/emoji/remove-aliases-bulk.ts | 47 - .../endpoints/admin/emoji/set-aliases-bulk.ts | 46 - .../admin/emoji/set-category-bulk.ts | 45 - .../endpoints/admin/emoji/set-license-bulk.ts | 45 - .../api/endpoints/admin/emoji/update.ts | 59 -- .../admin/federation/delete-all-files.ts | 28 - .../refresh-remote-instance-metadata.ts | 29 - .../admin/federation/remove-all-following.ts | 37 - .../admin/federation/update-instance.ts | 34 - .../api/endpoints/admin/get-index-stats.ts | 27 - .../api/endpoints/admin/get-table-stats.ts | 49 - .../api/endpoints/admin/get-user-ips.ts | 30 - .../src/server/api/endpoints/admin/invite.ts | 50 - .../src/server/api/endpoints/admin/meta.ts | 570 ----------- .../api/endpoints/admin/moderators/add.ts | 39 - .../api/endpoints/admin/moderators/remove.ts | 35 - .../api/endpoints/admin/promo/create.ts | 54 - .../server/api/endpoints/admin/queue/clear.ts | 22 - .../endpoints/admin/queue/deliver-delayed.ts | 57 -- .../endpoints/admin/queue/inbox-delayed.ts | 57 -- .../server/api/endpoints/admin/queue/stats.ts | 70 -- .../server/api/endpoints/admin/relays/add.ts | 65 -- .../server/api/endpoints/admin/relays/list.ts | 51 - .../api/endpoints/admin/relays/remove.ts | 21 - .../api/endpoints/admin/reset-password.ts | 66 -- .../admin/resolve-abuse-user-report.ts | 47 - .../api/endpoints/admin/search/index-all.ts | 28 - .../server/api/endpoints/admin/send-email.ts | 23 - .../server/api/endpoints/admin/server-info.ts | 143 --- .../endpoints/admin/show-moderation-logs.ts | 79 -- .../server/api/endpoints/admin/show-user.ts | 78 -- .../server/api/endpoints/admin/show-users.ts | 149 --- .../api/endpoints/admin/silence-user.ts | 44 - .../api/endpoints/admin/suspend-user.ts | 87 -- .../api/endpoints/admin/unsilence-user.ts | 40 - .../api/endpoints/admin/unsuspend-user.ts | 37 - .../server/api/endpoints/admin/update-meta.ts | 543 ----------- .../api/endpoints/admin/update-user-note.ts | 33 - .../src/server/api/endpoints/admin/vacuum.ts | 35 - .../src/server/api/endpoints/announcements.ts | 103 -- .../server/api/endpoints/antennas/create.ts | 141 --- .../server/api/endpoints/antennas/delete.ts | 43 - .../src/server/api/endpoints/antennas/list.ts | 36 - .../server/api/endpoints/antennas/markread.ts | 43 - .../server/api/endpoints/antennas/notes.ts | 97 -- .../src/server/api/endpoints/antennas/show.ts | 48 - .../server/api/endpoints/antennas/update.ts | 157 --- .../src/server/api/endpoints/ap/get.ts | 36 - .../src/server/api/endpoints/ap/show.ts | 164 ---- .../src/server/api/endpoints/app/create.ts | 67 -- .../src/server/api/endpoints/app/show.ts | 46 - .../src/server/api/endpoints/auth/accept.ts | 76 -- .../api/endpoints/auth/session/generate.ts | 74 -- .../server/api/endpoints/auth/session/show.ts | 63 -- .../api/endpoints/auth/session/userkey.ts | 99 -- .../server/api/endpoints/blocking/create.ts | 91 -- .../server/api/endpoints/blocking/delete.ts | 87 -- .../src/server/api/endpoints/blocking/list.ts | 45 - .../server/api/endpoints/channels/create.ts | 68 -- .../server/api/endpoints/channels/featured.ts | 37 - .../server/api/endpoints/channels/follow.ts | 48 - .../server/api/endpoints/channels/followed.ts | 59 -- .../server/api/endpoints/channels/owned.ts | 45 - .../src/server/api/endpoints/channels/show.ts | 45 - .../server/api/endpoints/channels/timeline.ts | 84 -- .../server/api/endpoints/channels/unfollow.ts | 45 - .../server/api/endpoints/channels/update.ts | 90 -- .../api/endpoints/charts/active-users.ts | 31 - .../server/api/endpoints/charts/ap-request.ts | 31 - .../src/server/api/endpoints/charts/drive.ts | 31 - .../server/api/endpoints/charts/federation.ts | 31 - .../server/api/endpoints/charts/hashtag.ts | 33 - .../server/api/endpoints/charts/instance.ts | 33 - .../src/server/api/endpoints/charts/notes.ts | 31 - .../server/api/endpoints/charts/user/drive.ts | 33 - .../api/endpoints/charts/user/following.ts | 33 - .../server/api/endpoints/charts/user/notes.ts | 33 - .../api/endpoints/charts/user/reactions.ts | 33 - .../src/server/api/endpoints/charts/users.ts | 31 - .../server/api/endpoints/clips/add-note.ts | 74 -- .../src/server/api/endpoints/clips/create.ts | 46 - .../src/server/api/endpoints/clips/delete.ts | 40 - .../src/server/api/endpoints/clips/list.ts | 36 - .../src/server/api/endpoints/clips/notes.ts | 94 -- .../server/api/endpoints/clips/remove-note.ts | 57 -- .../src/server/api/endpoints/clips/show.ts | 52 - .../src/server/api/endpoints/clips/update.ts | 62 -- .../endpoints/compatibility/custom-emojis.ts | 37 - .../endpoints/compatibility/instance-info.ts | 232 ----- .../src/server/api/endpoints/custom-motd.ts | 33 - .../api/endpoints/custom-splash-icons.ts | 33 - .../backend/src/server/api/endpoints/drive.ts | 50 - .../src/server/api/endpoints/drive/files.ts | 72 -- .../endpoints/drive/files/attached-notes.ts | 61 -- .../endpoints/drive/files/caption-image.ts | 42 - .../endpoints/drive/files/check-existence.ts | 35 - .../api/endpoints/drive/files/create.ts | 129 --- .../api/endpoints/drive/files/delete.ts | 55 -- .../api/endpoints/drive/files/find-by-hash.ts | 41 - .../server/api/endpoints/drive/files/find.ts | 51 - .../server/api/endpoints/drive/files/show.ts | 89 -- .../api/endpoints/drive/files/update.ts | 116 --- .../endpoints/drive/files/upload-from-url.ts | 57 -- .../src/server/api/endpoints/drive/folders.ts | 57 -- .../api/endpoints/drive/folders/create.ts | 69 -- .../api/endpoints/drive/folders/delete.ts | 60 -- .../api/endpoints/drive/folders/find.ts | 47 - .../api/endpoints/drive/folders/show.ts | 50 - .../api/endpoints/drive/folders/update.ts | 118 --- .../src/server/api/endpoints/drive/stream.ts | 59 -- .../api/endpoints/email-address/available.ts | 38 - .../backend/src/server/api/endpoints/emoji.ts | 39 - .../src/server/api/endpoints/endpoint.ts | 27 - .../src/server/api/endpoints/endpoints.ts | 35 - .../api/endpoints/export-custom-emojis.ts | 22 - .../api/endpoints/federation/followers.ts | 46 - .../api/endpoints/federation/following.ts | 46 - .../api/endpoints/federation/instances.ts | 171 ---- .../api/endpoints/federation/show-instance.ts | 36 - .../server/api/endpoints/federation/stats.ts | 69 -- .../federation/update-remote-user.ts | 22 - .../server/api/endpoints/federation/users.ts | 45 - .../src/server/api/endpoints/fetch-rss.ts | 38 - .../server/api/endpoints/following/create.ts | 107 -- .../server/api/endpoints/following/delete.ts | 84 -- .../api/endpoints/following/invalidate.ts | 84 -- .../endpoints/following/requests/accept.ts | 50 - .../endpoints/following/requests/cancel.ts | 64 -- .../api/endpoints/following/requests/list.ts | 55 -- .../endpoints/following/requests/reject.ts | 41 - .../server/api/endpoints/gallery/featured.ts | 40 - .../server/api/endpoints/gallery/popular.ts | 37 - .../src/server/api/endpoints/gallery/posts.ts | 42 - .../api/endpoints/gallery/posts/create.ts | 81 -- .../api/endpoints/gallery/posts/delete.ts | 40 - .../api/endpoints/gallery/posts/like.ts | 61 -- .../api/endpoints/gallery/posts/show.ts | 45 - .../api/endpoints/gallery/posts/unlike.ts | 54 - .../api/endpoints/gallery/posts/update.ts | 84 -- .../api/endpoints/get-online-users-count.ts | 27 - .../src/server/api/endpoints/get-sounds.ts | 30 - .../src/server/api/endpoints/hashtags/list.ts | 112 --- .../server/api/endpoints/hashtags/search.ts | 42 - .../src/server/api/endpoints/hashtags/show.ts | 45 - .../server/api/endpoints/hashtags/trend.ts | 178 ---- .../server/api/endpoints/hashtags/users.ts | 92 -- .../backend/src/server/api/endpoints/i.ts | 31 - .../src/server/api/endpoints/i/2fa/done.ts | 42 - .../server/api/endpoints/i/2fa/key-done.ts | 151 --- .../api/endpoints/i/2fa/password-less.ts | 22 - .../api/endpoints/i/2fa/register-key.ts | 61 -- .../server/api/endpoints/i/2fa/register.ts | 57 -- .../server/api/endpoints/i/2fa/remove-key.ts | 48 - .../server/api/endpoints/i/2fa/unregister.ts | 33 - .../src/server/api/endpoints/i/apps.ts | 56 -- .../server/api/endpoints/i/authorized-apps.ts | 40 - .../server/api/endpoints/i/change-password.ts | 36 - .../server/api/endpoints/i/delete-account.ts | 35 - .../server/api/endpoints/i/export-blocking.ts | 22 - .../api/endpoints/i/export-following.ts | 25 - .../src/server/api/endpoints/i/export-mute.ts | 22 - .../server/api/endpoints/i/export-notes.ts | 22 - .../api/endpoints/i/export-user-lists.ts | 22 - .../src/server/api/endpoints/i/favorites.ts | 47 - .../server/api/endpoints/i/gallery/likes.ts | 60 -- .../server/api/endpoints/i/gallery/posts.ts | 45 - .../endpoints/i/get-word-muted-notes-count.ts | 38 - .../server/api/endpoints/i/import-blocking.ts | 60 -- .../api/endpoints/i/import-following.ts | 59 -- .../server/api/endpoints/i/import-muting.ts | 60 -- .../server/api/endpoints/i/import-posts.ts | 44 - .../api/endpoints/i/import-user-lists.ts | 59 -- .../src/server/api/endpoints/i/known-as.ts | 101 -- .../src/server/api/endpoints/i/move.ts | 174 ---- .../server/api/endpoints/i/notifications.ts | 186 ---- .../src/server/api/endpoints/i/page-likes.ts | 58 -- .../src/server/api/endpoints/i/pages.ts | 45 - .../backend/src/server/api/endpoints/i/pin.ts | 63 -- .../i/read-all-messaging-messages.ts | 48 - .../api/endpoints/i/read-all-unread-notes.ts | 28 - .../api/endpoints/i/read-announcement.ts | 60 -- .../api/endpoints/i/regenerate-token.ts | 56 -- .../api/endpoints/i/registry/get-all.ts | 40 - .../api/endpoints/i/registry/get-detail.ts | 52 - .../api/endpoints/i/registry/get-unsecure.ts | 50 - .../server/api/endpoints/i/registry/get.ts | 49 - .../endpoints/i/registry/keys-with-type.ts | 54 - .../server/api/endpoints/i/registry/keys.ts | 35 - .../server/api/endpoints/i/registry/remove.ts | 49 - .../server/api/endpoints/i/registry/scopes.ts | 32 - .../server/api/endpoints/i/registry/set.ts | 62 -- .../server/api/endpoints/i/revoke-token.ts | 31 - .../server/api/endpoints/i/signin-history.ts | 31 - .../src/server/api/endpoints/i/unpin.ts | 47 - .../server/api/endpoints/i/update-email.ts | 95 -- .../src/server/api/endpoints/i/update.ts | 307 ------ .../api/endpoints/i/user-group-invites.ts | 60 -- .../server/api/endpoints/i/webhooks/create.ts | 46 - .../server/api/endpoints/i/webhooks/delete.ts | 43 - .../server/api/endpoints/i/webhooks/list.ts | 24 - .../server/api/endpoints/i/webhooks/show.ts | 40 - .../server/api/endpoints/i/webhooks/update.ts | 61 -- .../server/api/endpoints/latest-version.ts | 29 - .../server/api/endpoints/messaging/history.ts | 112 --- .../api/endpoints/messaging/messages.ts | 182 ---- .../endpoints/messaging/messages/create.ts | 165 ---- .../endpoints/messaging/messages/delete.ts | 48 - .../api/endpoints/messaging/messages/read.ts | 57 -- .../backend/src/server/api/endpoints/meta.ts | 530 ---------- .../server/api/endpoints/miauth/gen-token.ts | 69 -- .../src/server/api/endpoints/mute/create.ts | 95 -- .../src/server/api/endpoints/mute/delete.ts | 74 -- .../src/server/api/endpoints/mute/list.ts | 45 - .../src/server/api/endpoints/my/apps.ts | 49 - .../backend/src/server/api/endpoints/notes.ts | 91 -- .../server/api/endpoints/notes/children.ts | 62 -- .../src/server/api/endpoints/notes/clips.ts | 59 -- .../api/endpoints/notes/conversation.ts | 81 -- .../src/server/api/endpoints/notes/create.ts | 308 ------ .../src/server/api/endpoints/notes/delete.ts | 57 -- .../api/endpoints/notes/favorites/create.ts | 62 -- .../api/endpoints/notes/favorites/delete.ts | 56 -- .../server/api/endpoints/notes/featured.ts | 82 -- .../api/endpoints/notes/global-timeline.ts | 121 --- .../api/endpoints/notes/hybrid-timeline.ts | 177 ---- .../api/endpoints/notes/local-timeline.ts | 151 --- .../server/api/endpoints/notes/mentions.ts | 108 -- .../endpoints/notes/polls/recommendation.ts | 85 -- .../server/api/endpoints/notes/polls/vote.ts | 184 ---- .../server/api/endpoints/notes/reactions.ts | 85 -- .../api/endpoints/notes/reactions/create.ts | 64 -- .../api/endpoints/notes/reactions/delete.ts | 54 - .../endpoints/notes/recommended-timeline.ts | 154 --- .../src/server/api/endpoints/notes/renotes.ts | 94 -- .../src/server/api/endpoints/notes/replies.ts | 78 -- .../api/endpoints/notes/search-by-tag.ts | 165 ---- .../src/server/api/endpoints/notes/search.ts | 254 ----- .../src/server/api/endpoints/notes/show.ts | 51 - .../src/server/api/endpoints/notes/state.ts | 79 -- .../endpoints/notes/thread-muting/create.ts | 58 -- .../endpoints/notes/thread-muting/delete.ts | 41 - .../server/api/endpoints/notes/timeline.ts | 169 ---- .../server/api/endpoints/notes/translate.ts | 94 -- .../server/api/endpoints/notes/unrenote.ts | 53 - .../api/endpoints/notes/user-list-timeline.ts | 164 ---- .../api/endpoints/notes/watching/create.ts | 38 - .../api/endpoints/notes/watching/delete.ts | 38 - .../api/endpoints/notifications/create.ts | 31 - .../notifications/mark-all-as-read.ts | 35 - .../api/endpoints/notifications/read.ts | 49 - .../src/server/api/endpoints/page-push.ts | 48 - .../src/server/api/endpoints/pages/create.ts | 123 --- .../src/server/api/endpoints/pages/delete.ts | 45 - .../server/api/endpoints/pages/featured.ts | 38 - .../src/server/api/endpoints/pages/like.ts | 61 -- .../src/server/api/endpoints/pages/show.ts | 75 -- .../src/server/api/endpoints/pages/unlike.ts | 54 - .../src/server/api/endpoints/pages/update.ts | 134 --- .../src/server/api/endpoints/patrons.ts | 28 - .../backend/src/server/api/endpoints/ping.ts | 32 - .../src/server/api/endpoints/pinned-users.ts | 52 - .../src/server/api/endpoints/promo/read.ts | 51 - .../api/endpoints/recommended-instances.ts | 33 - .../src/server/api/endpoints/release.ts | 28 - .../api/endpoints/renote-mute/create.ts | 68 -- .../api/endpoints/renote-mute/delete.ts | 63 -- .../server/api/endpoints/renote-mute/list.ts | 46 - .../api/endpoints/request-reset-password.ts | 76 -- .../src/server/api/endpoints/reset-db.ts | 29 - .../server/api/endpoints/reset-password.ts | 44 - .../src/server/api/endpoints/server-info.ts | 36 - .../backend/src/server/api/endpoints/stats.ts | 92 -- .../src/server/api/endpoints/sw/register.ts | 97 -- .../api/endpoints/sw/show-registration.ts | 59 -- .../src/server/api/endpoints/sw/unregister.ts | 25 - .../api/endpoints/sw/update-registration.ts | 44 - .../backend/src/server/api/endpoints/test.ts | 25 - .../api/endpoints/username/available.ts | 46 - .../backend/src/server/api/endpoints/users.ts | 134 --- .../src/server/api/endpoints/users/clips.ts | 47 - .../server/api/endpoints/users/followers.ts | 116 --- .../server/api/endpoints/users/following.ts | 116 --- .../api/endpoints/users/gallery/posts.ts | 45 - .../users/get-frequently-replied-users.ts | 124 --- .../api/endpoints/users/groups/create.ts | 49 - .../api/endpoints/users/groups/delete.ts | 42 - .../users/groups/invitations/accept.ts | 56 -- .../users/groups/invitations/reject.ts | 47 - .../api/endpoints/users/groups/invite.ts | 108 -- .../api/endpoints/users/groups/joined.ts | 48 - .../api/endpoints/users/groups/leave.ts | 53 - .../api/endpoints/users/groups/owned.ts | 38 - .../server/api/endpoints/users/groups/pull.ts | 73 -- .../server/api/endpoints/users/groups/show.ts | 58 -- .../api/endpoints/users/groups/transfer.ts | 85 -- .../api/endpoints/users/groups/update.ts | 55 -- .../api/endpoints/users/lists/create.ts | 40 - .../api/endpoints/users/lists/delete-all.ts | 35 - .../api/endpoints/users/lists/delete.ts | 42 - .../server/api/endpoints/users/lists/list.ts | 38 - .../server/api/endpoints/users/lists/pull.ts | 62 -- .../server/api/endpoints/users/lists/push.ts | 93 -- .../server/api/endpoints/users/lists/show.ts | 50 - .../api/endpoints/users/lists/update.ts | 55 -- .../src/server/api/endpoints/users/notes.ts | 144 --- .../src/server/api/endpoints/users/pages.ts | 48 - .../server/api/endpoints/users/reactions.ts | 71 -- .../api/endpoints/users/recommendation.ts | 68 -- .../server/api/endpoints/users/relation.ts | 151 --- .../api/endpoints/users/report-abuse.ts | 106 -- .../users/search-by-username-and-host.ts | 127 --- .../src/server/api/endpoints/users/search.ts | 149 --- .../src/server/api/endpoints/users/show.ts | 146 --- .../src/server/api/endpoints/users/stats.ts | 225 ----- packages/backend/src/server/api/error.ts | 36 - packages/backend/src/server/api/index.ts | 230 ----- packages/backend/src/server/api/limiter.ts | 85 -- packages/backend/src/server/api/logger.ts | 3 - .../mastodon/ApiMastodonCompatibleService.ts | 140 --- .../server/api/mastodon/endpoints/account.ts | 545 ----------- .../src/server/api/mastodon/endpoints/auth.ts | 81 -- .../server/api/mastodon/endpoints/filter.ts | 84 -- .../src/server/api/mastodon/endpoints/meta.ts | 111 --- .../api/mastodon/endpoints/notifications.ts | 90 -- .../server/api/mastodon/endpoints/search.ts | 135 --- .../server/api/mastodon/endpoints/status.ts | 445 --------- .../server/api/mastodon/endpoints/timeline.ts | 336 ------- .../backend/src/server/api/openapi/errors.ts | 71 -- .../src/server/api/openapi/gen-spec.ts | 226 ----- .../backend/src/server/api/openapi/schemas.ts | 66 -- .../backend/src/server/api/private/signin.ts | 270 ----- .../src/server/api/private/signup-pending.ts | 38 - .../backend/src/server/api/private/signup.ts | 123 --- .../backend/src/server/api/service/discord.ts | 333 ------- .../backend/src/server/api/service/github.ts | 296 ------ .../backend/src/server/api/service/twitter.ts | 226 ----- .../backend/src/server/api/stream/channel.ts | 99 -- .../src/server/api/stream/channels/admin.ts | 14 - .../src/server/api/stream/channels/antenna.ts | 63 -- .../src/server/api/stream/channels/channel.ts | 84 -- .../src/server/api/stream/channels/drive.ts | 14 - .../api/stream/channels/global-timeline.ts | 82 -- .../src/server/api/stream/channels/hashtag.ts | 52 - .../api/stream/channels/home-timeline.ts | 80 -- .../api/stream/channels/hybrid-timeline.ts | 97 -- .../src/server/api/stream/channels/index.ts | 35 - .../api/stream/channels/local-timeline.ts | 74 -- .../src/server/api/stream/channels/main.ts | 46 - .../api/stream/channels/messaging-index.ts | 14 - .../server/api/stream/channels/messaging.ts | 130 --- .../server/api/stream/channels/queue-stats.ts | 42 - .../stream/channels/recommended-timeline.ts | 95 -- .../api/stream/channels/server-stats.ts | 42 - .../server/api/stream/channels/user-list.ts | 72 -- .../backend/src/server/api/stream/index.ts | 621 ------------ .../backend/src/server/api/stream/types.ts | 292 ------ packages/backend/src/server/api/streaming.ts | 89 -- .../src/server/file/assets/bad-egg.png | Bin 1676 -> 0 bytes .../src/server/file/assets/cache-expired.png | Bin 6048 -> 0 bytes .../backend/src/server/file/assets/dummy.png | Bin 6285 -> 0 bytes .../src/server/file/assets/not-an-image.png | Bin 2780 -> 0 bytes .../file/assets/thumbnail-not-available.png | Bin 5705 -> 0 bytes .../src/server/file/assets/tombstone.png | Bin 5028 -> 0 bytes packages/backend/src/server/file/index.ts | 43 - .../src/server/file/send-drive-file.ts | 152 --- packages/backend/src/server/index.ts | 286 ------ packages/backend/src/server/nodeinfo.ts | 119 --- packages/backend/src/server/proxy/index.ts | 29 - .../backend/src/server/proxy/proxy-media.ts | 105 -- packages/backend/src/server/web/bios.css | 147 --- packages/backend/src/server/web/bios.js | 89 -- packages/backend/src/server/web/boot.js | 319 ------ packages/backend/src/server/web/cli.css | 92 -- packages/backend/src/server/web/cli.js | 72 -- packages/backend/src/server/web/feed.ts | 65 -- packages/backend/src/server/web/index.ts | 677 ------------- packages/backend/src/server/web/manifest.json | 74 -- packages/backend/src/server/web/manifest.ts | 18 - packages/backend/src/server/web/style.css | 127 --- .../backend/src/server/web/url-preview.ts | 88 -- .../backend/src/server/web/views/base.pug | 96 -- .../backend/src/server/web/views/bios.pug | 21 - .../backend/src/server/web/views/channel.pug | 20 - packages/backend/src/server/web/views/cli.pug | 23 - .../backend/src/server/web/views/clip.pug | 34 - .../backend/src/server/web/views/flush.pug | 71 -- .../src/server/web/views/gallery-post.pug | 36 - .../src/server/web/views/info-card.pug | 50 - .../backend/src/server/web/views/note.pug | 56 -- .../backend/src/server/web/views/page.pug | 34 - .../backend/src/server/web/views/user.pug | 42 - packages/backend/src/server/well-known.ts | 191 ---- .../src/services/add-note-to-antenna.ts | 61 -- .../backend/src/services/blocking/create.ts | 165 ---- .../backend/src/services/blocking/delete.ts | 37 - .../src/services/chart/charts/active-users.ts | 59 -- .../src/services/chart/charts/ap-request.ts | 39 - .../src/services/chart/charts/drive.ts | 43 - .../chart/charts/entities/active-users.ts | 17 - .../chart/charts/entities/ap-request.ts | 11 - .../services/chart/charts/entities/drive.ts | 16 - .../chart/charts/entities/federation.ts | 16 - .../services/chart/charts/entities/hashtag.ts | 10 - .../chart/charts/entities/instance.ts | 32 - .../services/chart/charts/entities/notes.ts | 22 - .../chart/charts/entities/per-user-drive.ts | 14 - .../charts/entities/per-user-following.ts | 20 - .../chart/charts/entities/per-user-notes.ts | 15 - .../charts/entities/per-user-reactions.ts | 10 - .../chart/charts/entities/test-grouped.ts | 11 - .../charts/entities/test-intersection.ts | 11 - .../chart/charts/entities/test-unique.ts | 9 - .../services/chart/charts/entities/test.ts | 11 - .../services/chart/charts/entities/users.ts | 14 - .../src/services/chart/charts/federation.ts | 142 --- .../src/services/chart/charts/hashtag.ts | 36 - .../src/services/chart/charts/instance.ts | 142 --- .../src/services/chart/charts/notes.ts | 54 - .../services/chart/charts/per-user-drive.ts | 48 - .../chart/charts/per-user-following.ts | 69 -- .../services/chart/charts/per-user-notes.ts | 54 - .../chart/charts/per-user-reactions.ts | 39 - .../src/services/chart/charts/test-grouped.ts | 41 - .../chart/charts/test-intersection.ts | 33 - .../src/services/chart/charts/test-unique.ts | 27 - .../backend/src/services/chart/charts/test.ts | 43 - .../src/services/chart/charts/users.ts | 45 - packages/backend/src/services/chart/core.ts | 922 ------------------ .../backend/src/services/chart/entities.ts | 57 -- packages/backend/src/services/chart/index.ts | 51 - .../src/services/create-notification.ts | 91 -- .../src/services/create-system-user.ts | 71 -- .../backend/src/services/delete-account.ts | 23 - .../backend/src/services/detect-sensitive.ts | 55 -- .../backend/src/services/drive/add-file.ts | 671 ------------- .../backend/src/services/drive/delete-file.ts | 107 -- .../drive/generate-video-thumbnail.ts | 29 - .../src/services/drive/image-processor.ts | 44 - .../src/services/drive/internal-storage.ts | 35 - packages/backend/src/services/drive/logger.ts | 3 - packages/backend/src/services/drive/s3.ts | 27 - .../src/services/drive/upload-from-url.ts | 81 -- .../src/services/fetch-instance-metadata.ts | 321 ------ .../backend/src/services/following/create.ts | 276 ------ .../backend/src/services/following/delete.ts | 113 --- .../backend/src/services/following/reject.ts | 134 --- .../services/following/requests/accept-all.ts | 24 - .../src/services/following/requests/accept.ts | 49 - .../src/services/following/requests/cancel.ts | 51 - .../src/services/following/requests/create.ts | 85 -- packages/backend/src/services/i/pin.ts | 118 --- packages/backend/src/services/i/update.ts | 21 - .../src/services/insert-moderation-log.ts | 17 - .../backend/src/services/instance-actor.ts | 28 - packages/backend/src/services/logger.ts | 199 ---- .../backend/src/services/messages/create.ts | 148 --- .../backend/src/services/messages/delete.ts | 50 - packages/backend/src/services/note/create.ts | 871 ----------------- packages/backend/src/services/note/delete.ts | 185 ---- .../backend/src/services/note/polls/update.ts | 23 - .../backend/src/services/note/polls/vote.ts | 87 -- .../src/services/note/reaction/create.ts | 169 ---- .../src/services/note/reaction/delete.ts | 70 -- packages/backend/src/services/note/read.ts | 173 ---- packages/backend/src/services/note/unread.ts | 59 -- packages/backend/src/services/note/unwatch.ts | 10 - packages/backend/src/services/note/watch.ts | 20 - .../backend/src/services/push-notification.ts | 116 --- .../register-or-fetch-instance-doc.ts | 33 - packages/backend/src/services/relay.ts | 109 --- .../src/services/send-email-notification.ts | 36 - packages/backend/src/services/send-email.ts | 132 --- packages/backend/src/services/stream.ts | 226 ----- packages/backend/src/services/suspend-user.ts | 47 - .../backend/src/services/unsuspend-user.ts | 45 - .../backend/src/services/update-hashtag.ts | 158 --- packages/backend/src/services/user-cache.ts | 63 -- .../backend/src/services/user-list/push.ts | 27 - .../services/validate-email-for-account.ts | 45 - packages/backend/src/types.ts | 25 - packages/backend/test/activitypub.ts | 102 -- packages/backend/test/ap-request.ts | 75 -- packages/backend/test/api-visibility.ts | 535 ---------- packages/backend/test/api.ts | 92 -- packages/backend/test/block.ts | 129 --- packages/backend/test/chart.ts | 575 ----------- packages/backend/test/docker-compose.yml | 15 - packages/backend/test/endpoints.ts | 865 ---------------- packages/backend/test/extract-mentions.ts | 50 - packages/backend/test/fetch-resource.ts | 213 ---- packages/backend/test/ff-visibility.ts | 283 ------ packages/backend/test/get-file-info.ts | 209 ---- packages/backend/test/loader.js | 37 - packages/backend/test/mfm.ts | 127 --- packages/backend/test/misc/mock-resolver.ts | 39 - packages/backend/test/mute.ts | 176 ---- packages/backend/test/note.ts | 517 ---------- packages/backend/test/prelude/maybe.ts | 18 - packages/backend/test/prelude/url.ts | 13 - packages/backend/test/reaction-lib.ts | 83 -- .../backend/test/resources/25000x25000.png | Bin 75933 -> 0 bytes packages/backend/test/resources/Lenna.jpg | Bin 25360 -> 0 bytes packages/backend/test/resources/Lenna.png | Bin 473831 -> 0 bytes packages/backend/test/resources/anime.gif | Bin 2248 -> 0 bytes packages/backend/test/resources/anime.png | Bin 1868 -> 0 bytes packages/backend/test/resources/emptyfile | 0 packages/backend/test/resources/image.svg | 1 - packages/backend/test/resources/rotate.jpg | Bin 12624 -> 0 bytes .../backend/test/resources/with-alpha.png | Bin 3772 -> 0 bytes .../backend/test/resources/with-xml-def.svg | 2 - packages/backend/test/streaming.ts | 766 --------------- packages/backend/test/thread-mute.ts | 161 --- packages/backend/test/tsconfig.json | 41 - packages/backend/test/user-notes.ts | 98 -- packages/backend/test/utils.ts | 403 -------- packages/backend/tsconfig.json | 48 - 965 files changed, 82 insertions(+), 75200 deletions(-) create mode 100644 packages/backend/.gitignore delete mode 100644 packages/backend/.mocharc.json delete mode 100644 packages/backend/.swcrc create mode 100644 packages/backend/.vim/coc-settings.json create mode 100644 packages/backend/Cargo.lock create mode 100644 packages/backend/Cargo.toml create mode 100644 packages/backend/crates/server/Cargo.toml create mode 100644 packages/backend/crates/server/src/lib.rs delete mode 100644 packages/backend/jsconfig.json delete mode 100644 packages/backend/native-utils/.cargo/config.toml delete mode 100644 packages/backend/native-utils/.gitignore delete mode 100644 packages/backend/native-utils/.npmignore delete mode 100644 packages/backend/native-utils/Cargo.toml delete mode 100644 packages/backend/native-utils/__test__/index.spec.mjs delete mode 100644 packages/backend/native-utils/build.rs delete mode 100644 packages/backend/native-utils/npm/android-arm-eabi/README.md delete mode 100644 packages/backend/native-utils/npm/android-arm-eabi/package.json delete mode 100644 packages/backend/native-utils/npm/android-arm64/README.md delete mode 100644 packages/backend/native-utils/npm/android-arm64/package.json delete mode 100644 packages/backend/native-utils/npm/darwin-arm64/README.md delete mode 100644 packages/backend/native-utils/npm/darwin-arm64/package.json delete mode 100644 packages/backend/native-utils/npm/darwin-universal/README.md delete mode 100644 packages/backend/native-utils/npm/darwin-universal/package.json delete mode 100644 packages/backend/native-utils/npm/darwin-x64/README.md delete mode 100644 packages/backend/native-utils/npm/darwin-x64/package.json delete mode 100644 packages/backend/native-utils/npm/freebsd-x64/README.md delete mode 100644 packages/backend/native-utils/npm/freebsd-x64/package.json delete mode 100644 packages/backend/native-utils/npm/linux-arm-gnueabihf/README.md delete mode 100644 packages/backend/native-utils/npm/linux-arm-gnueabihf/package.json delete mode 100644 packages/backend/native-utils/npm/linux-arm64-gnu/README.md delete mode 100644 packages/backend/native-utils/npm/linux-arm64-gnu/package.json delete mode 100644 packages/backend/native-utils/npm/linux-arm64-musl/README.md delete mode 100644 packages/backend/native-utils/npm/linux-arm64-musl/package.json delete mode 100644 packages/backend/native-utils/npm/linux-x64-gnu/README.md delete mode 100644 packages/backend/native-utils/npm/linux-x64-gnu/package.json delete mode 100644 packages/backend/native-utils/npm/linux-x64-musl/README.md delete mode 100644 packages/backend/native-utils/npm/linux-x64-musl/package.json delete mode 100644 packages/backend/native-utils/npm/win32-arm64-msvc/README.md delete mode 100644 packages/backend/native-utils/npm/win32-arm64-msvc/package.json delete mode 100644 packages/backend/native-utils/npm/win32-ia32-msvc/README.md delete mode 100644 packages/backend/native-utils/npm/win32-ia32-msvc/package.json delete mode 100644 packages/backend/native-utils/npm/win32-x64-msvc/README.md delete mode 100644 packages/backend/native-utils/npm/win32-x64-msvc/package.json delete mode 100644 packages/backend/native-utils/package.json delete mode 100644 packages/backend/native-utils/rustfmt.toml delete mode 100644 packages/backend/native-utils/src/lib.rs delete mode 100644 packages/backend/native-utils/src/mastodon_api.rs delete mode 100644 packages/backend/ormconfig.js create mode 100644 packages/backend/rust-toolchain create mode 100644 packages/backend/rustfmt.toml delete mode 100644 packages/backend/src/@types/hcaptcha.d.ts delete mode 100644 packages/backend/src/@types/http-signature.d.ts delete mode 100644 packages/backend/src/@types/koa-json-body.d.ts delete mode 100644 packages/backend/src/@types/koa-remove-trailing-slashes/index.d.ts delete mode 100644 packages/backend/src/@types/koa-slow.d.ts delete mode 100644 packages/backend/src/@types/os-utils.d.ts delete mode 100644 packages/backend/src/@types/package.json.d.ts delete mode 100644 packages/backend/src/@types/probe-image-size.d.ts create mode 100644 packages/backend/src/bin/migrate.rs delete mode 100644 packages/backend/src/boot/index.ts delete mode 100644 packages/backend/src/boot/master.ts delete mode 100644 packages/backend/src/boot/worker.ts delete mode 100644 packages/backend/src/config/index.ts delete mode 100644 packages/backend/src/config/load.ts delete mode 100644 packages/backend/src/config/types.ts delete mode 100644 packages/backend/src/const.ts delete mode 100644 packages/backend/src/daemons/janitor.ts delete mode 100644 packages/backend/src/daemons/queue-stats.ts delete mode 100644 packages/backend/src/daemons/server-stats.ts delete mode 100644 packages/backend/src/db/elasticsearch.ts delete mode 100644 packages/backend/src/db/logger.ts delete mode 100644 packages/backend/src/db/postgre.ts delete mode 100644 packages/backend/src/db/redis.ts delete mode 100644 packages/backend/src/db/sonic.ts delete mode 100644 packages/backend/src/env.ts delete mode 100644 packages/backend/src/global.d.ts delete mode 100644 packages/backend/src/index.ts create mode 100644 packages/backend/src/main.rs delete mode 100644 packages/backend/src/mfm/from-html.ts delete mode 100644 packages/backend/src/mfm/to-html.ts delete mode 100644 packages/backend/src/misc/acct.ts delete mode 100644 packages/backend/src/misc/antenna-cache.ts delete mode 100644 packages/backend/src/misc/api-permissions.ts delete mode 100644 packages/backend/src/misc/app-lock.ts delete mode 100644 packages/backend/src/misc/before-shutdown.ts delete mode 100644 packages/backend/src/misc/cache.ts delete mode 100644 packages/backend/src/misc/captcha.ts delete mode 100644 packages/backend/src/misc/check-hit-antenna.ts delete mode 100644 packages/backend/src/misc/check-word-mute.ts delete mode 100644 packages/backend/src/misc/clone.ts delete mode 100644 packages/backend/src/misc/content-disposition.ts delete mode 100644 packages/backend/src/misc/convert-host.ts delete mode 100644 packages/backend/src/misc/count-same-renotes.ts delete mode 100644 packages/backend/src/misc/create-temp.ts delete mode 100644 packages/backend/src/misc/detect-url-mime.ts delete mode 100644 packages/backend/src/misc/download-text-file.ts delete mode 100644 packages/backend/src/misc/download-url.ts delete mode 100644 packages/backend/src/misc/emoji-regex.ts delete mode 100644 packages/backend/src/misc/extract-custom-emojis-from-mfm.ts delete mode 100644 packages/backend/src/misc/extract-hashtags.ts delete mode 100644 packages/backend/src/misc/extract-mentions.ts delete mode 100644 packages/backend/src/misc/fetch-meta.ts delete mode 100644 packages/backend/src/misc/fetch-proxy-account.ts delete mode 100644 packages/backend/src/misc/fetch.ts delete mode 100644 packages/backend/src/misc/gen-id.ts delete mode 100644 packages/backend/src/misc/gen-identicon.ts delete mode 100644 packages/backend/src/misc/gen-key-pair.ts delete mode 100644 packages/backend/src/misc/get-file-info.ts delete mode 100644 packages/backend/src/misc/get-ip-hash.ts delete mode 100644 packages/backend/src/misc/get-note-summary.ts delete mode 100644 packages/backend/src/misc/get-reaction-emoji.ts delete mode 100644 packages/backend/src/misc/hard-limits.ts delete mode 100644 packages/backend/src/misc/i18n.ts delete mode 100644 packages/backend/src/misc/id/aid.ts delete mode 100644 packages/backend/src/misc/id/meid.ts delete mode 100644 packages/backend/src/misc/id/meidg.ts delete mode 100644 packages/backend/src/misc/id/object-id.ts delete mode 100644 packages/backend/src/misc/identifiable-error.ts delete mode 100644 packages/backend/src/misc/is-duplicate-key-value-error.ts delete mode 100644 packages/backend/src/misc/is-instance-muted.ts delete mode 100644 packages/backend/src/misc/is-mime-image.ts delete mode 100644 packages/backend/src/misc/is-quote.ts delete mode 100644 packages/backend/src/misc/is-user-related.ts delete mode 100644 packages/backend/src/misc/keypair-store.ts delete mode 100644 packages/backend/src/misc/langmap.ts delete mode 100644 packages/backend/src/misc/normalize-for-search.ts delete mode 100644 packages/backend/src/misc/nyaize.ts delete mode 100644 packages/backend/src/misc/password.ts delete mode 100644 packages/backend/src/misc/populate-emojis.ts delete mode 100644 packages/backend/src/misc/post.ts delete mode 100644 packages/backend/src/misc/reaction-lib.ts delete mode 100644 packages/backend/src/misc/safe-for-sql.ts delete mode 100644 packages/backend/src/misc/schema.ts delete mode 100644 packages/backend/src/misc/secure-rndstr.ts delete mode 100644 packages/backend/src/misc/should-block-instance.ts delete mode 100644 packages/backend/src/misc/show-machine-info.ts delete mode 100644 packages/backend/src/misc/skipped-instances.ts delete mode 100644 packages/backend/src/misc/truncate.ts delete mode 100644 packages/backend/src/misc/webhook-cache.ts delete mode 100644 packages/backend/src/models/entities/abuse-user-report.ts delete mode 100644 packages/backend/src/models/entities/access-token.ts delete mode 100644 packages/backend/src/models/entities/ad.ts delete mode 100644 packages/backend/src/models/entities/announcement-read.ts delete mode 100644 packages/backend/src/models/entities/announcement.ts delete mode 100644 packages/backend/src/models/entities/antenna-note.ts delete mode 100644 packages/backend/src/models/entities/antenna.ts delete mode 100644 packages/backend/src/models/entities/app.ts delete mode 100644 packages/backend/src/models/entities/attestation-challenge.ts delete mode 100644 packages/backend/src/models/entities/auth-session.ts delete mode 100644 packages/backend/src/models/entities/blocking.ts delete mode 100644 packages/backend/src/models/entities/channel-following.ts delete mode 100644 packages/backend/src/models/entities/channel-note-pining.ts delete mode 100644 packages/backend/src/models/entities/channel.ts delete mode 100644 packages/backend/src/models/entities/clip-note.ts delete mode 100644 packages/backend/src/models/entities/clip.ts delete mode 100644 packages/backend/src/models/entities/drive-file.ts delete mode 100644 packages/backend/src/models/entities/drive-folder.ts delete mode 100644 packages/backend/src/models/entities/emoji.ts delete mode 100644 packages/backend/src/models/entities/follow-request.ts delete mode 100644 packages/backend/src/models/entities/following.ts delete mode 100644 packages/backend/src/models/entities/gallery-like.ts delete mode 100644 packages/backend/src/models/entities/gallery-post.ts delete mode 100644 packages/backend/src/models/entities/hashtag.ts delete mode 100644 packages/backend/src/models/entities/instance.ts delete mode 100644 packages/backend/src/models/entities/messaging-message.ts delete mode 100644 packages/backend/src/models/entities/meta.ts delete mode 100644 packages/backend/src/models/entities/moderation-log.ts delete mode 100644 packages/backend/src/models/entities/muted-note.ts delete mode 100644 packages/backend/src/models/entities/muting.ts delete mode 100644 packages/backend/src/models/entities/note-favorite.ts delete mode 100644 packages/backend/src/models/entities/note-reaction.ts delete mode 100644 packages/backend/src/models/entities/note-thread-muting.ts delete mode 100644 packages/backend/src/models/entities/note-unread.ts delete mode 100644 packages/backend/src/models/entities/note-watching.ts delete mode 100644 packages/backend/src/models/entities/note.ts delete mode 100644 packages/backend/src/models/entities/notification.ts delete mode 100644 packages/backend/src/models/entities/page-like.ts delete mode 100644 packages/backend/src/models/entities/page.ts delete mode 100644 packages/backend/src/models/entities/password-reset-request.ts delete mode 100644 packages/backend/src/models/entities/poll-vote.ts delete mode 100644 packages/backend/src/models/entities/poll.ts delete mode 100644 packages/backend/src/models/entities/promo-note.ts delete mode 100644 packages/backend/src/models/entities/promo-read.ts delete mode 100644 packages/backend/src/models/entities/registration-tickets.ts delete mode 100644 packages/backend/src/models/entities/registry-item.ts delete mode 100644 packages/backend/src/models/entities/relay.ts delete mode 100644 packages/backend/src/models/entities/renote-muting.ts delete mode 100644 packages/backend/src/models/entities/signin.ts delete mode 100644 packages/backend/src/models/entities/sw-subscription.ts delete mode 100644 packages/backend/src/models/entities/used-username.ts delete mode 100644 packages/backend/src/models/entities/user-group-invitation.ts delete mode 100644 packages/backend/src/models/entities/user-group-joining.ts delete mode 100644 packages/backend/src/models/entities/user-group.ts delete mode 100644 packages/backend/src/models/entities/user-ip.ts delete mode 100644 packages/backend/src/models/entities/user-keypair.ts delete mode 100644 packages/backend/src/models/entities/user-list-joining.ts delete mode 100644 packages/backend/src/models/entities/user-list.ts delete mode 100644 packages/backend/src/models/entities/user-note-pining.ts delete mode 100644 packages/backend/src/models/entities/user-pending.ts delete mode 100644 packages/backend/src/models/entities/user-profile.ts delete mode 100644 packages/backend/src/models/entities/user-publickey.ts delete mode 100644 packages/backend/src/models/entities/user-security-key.ts delete mode 100644 packages/backend/src/models/entities/user.ts delete mode 100644 packages/backend/src/models/entities/webhook.ts delete mode 100644 packages/backend/src/models/id.ts delete mode 100644 packages/backend/src/models/index.ts delete mode 100644 packages/backend/src/models/repositories/abuse-user-report.ts delete mode 100644 packages/backend/src/models/repositories/antenna.ts delete mode 100644 packages/backend/src/models/repositories/app.ts delete mode 100644 packages/backend/src/models/repositories/auth-session.ts delete mode 100644 packages/backend/src/models/repositories/blocking.ts delete mode 100644 packages/backend/src/models/repositories/channel.ts delete mode 100644 packages/backend/src/models/repositories/clip.ts delete mode 100644 packages/backend/src/models/repositories/drive-file.ts delete mode 100644 packages/backend/src/models/repositories/drive-folder.ts delete mode 100644 packages/backend/src/models/repositories/emoji.ts delete mode 100644 packages/backend/src/models/repositories/follow-request.ts delete mode 100644 packages/backend/src/models/repositories/following.ts delete mode 100644 packages/backend/src/models/repositories/gallery-like.ts delete mode 100644 packages/backend/src/models/repositories/gallery-post.ts delete mode 100644 packages/backend/src/models/repositories/hashtag.ts delete mode 100644 packages/backend/src/models/repositories/instance.ts delete mode 100644 packages/backend/src/models/repositories/messaging-message.ts delete mode 100644 packages/backend/src/models/repositories/moderation-logs.ts delete mode 100644 packages/backend/src/models/repositories/muting.ts delete mode 100644 packages/backend/src/models/repositories/note-favorite.ts delete mode 100644 packages/backend/src/models/repositories/note-reaction.ts delete mode 100644 packages/backend/src/models/repositories/note.ts delete mode 100644 packages/backend/src/models/repositories/notification.ts delete mode 100644 packages/backend/src/models/repositories/page-like.ts delete mode 100644 packages/backend/src/models/repositories/page.ts delete mode 100644 packages/backend/src/models/repositories/relay.ts delete mode 100644 packages/backend/src/models/repositories/renote-muting.ts delete mode 100644 packages/backend/src/models/repositories/signin.ts delete mode 100644 packages/backend/src/models/repositories/user-group-invitation.ts delete mode 100644 packages/backend/src/models/repositories/user-group.ts delete mode 100644 packages/backend/src/models/repositories/user-list.ts delete mode 100644 packages/backend/src/models/repositories/user.ts delete mode 100644 packages/backend/src/models/schema/antenna.ts delete mode 100644 packages/backend/src/models/schema/app.ts delete mode 100644 packages/backend/src/models/schema/blocking.ts delete mode 100644 packages/backend/src/models/schema/channel.ts delete mode 100644 packages/backend/src/models/schema/clip.ts delete mode 100644 packages/backend/src/models/schema/drive-file.ts delete mode 100644 packages/backend/src/models/schema/drive-folder.ts delete mode 100644 packages/backend/src/models/schema/emoji.ts delete mode 100644 packages/backend/src/models/schema/federation-instance.ts delete mode 100644 packages/backend/src/models/schema/following.ts delete mode 100644 packages/backend/src/models/schema/gallery-post.ts delete mode 100644 packages/backend/src/models/schema/hashtag.ts delete mode 100644 packages/backend/src/models/schema/messaging-message.ts delete mode 100644 packages/backend/src/models/schema/muting.ts delete mode 100644 packages/backend/src/models/schema/note-favorite.ts delete mode 100644 packages/backend/src/models/schema/note-reaction.ts delete mode 100644 packages/backend/src/models/schema/note.ts delete mode 100644 packages/backend/src/models/schema/notification.ts delete mode 100644 packages/backend/src/models/schema/page.ts delete mode 100644 packages/backend/src/models/schema/queue.ts delete mode 100644 packages/backend/src/models/schema/renote-muting.ts delete mode 100644 packages/backend/src/models/schema/user-group.ts delete mode 100644 packages/backend/src/models/schema/user-list.ts delete mode 100644 packages/backend/src/models/schema/user.ts delete mode 100644 packages/backend/src/prelude/README.md delete mode 100644 packages/backend/src/prelude/array.ts delete mode 100644 packages/backend/src/prelude/await-all.ts delete mode 100644 packages/backend/src/prelude/math.ts delete mode 100644 packages/backend/src/prelude/maybe.ts delete mode 100644 packages/backend/src/prelude/relation.ts delete mode 100644 packages/backend/src/prelude/string.ts delete mode 100644 packages/backend/src/prelude/symbol.ts delete mode 100644 packages/backend/src/prelude/time.ts delete mode 100644 packages/backend/src/prelude/url.ts delete mode 100644 packages/backend/src/prelude/xml.ts delete mode 100644 packages/backend/src/queue/get-job-info.ts delete mode 100644 packages/backend/src/queue/index.ts delete mode 100644 packages/backend/src/queue/initialize.ts delete mode 100644 packages/backend/src/queue/logger.ts delete mode 100644 packages/backend/src/queue/processors/background/index-all-notes.ts delete mode 100644 packages/backend/src/queue/processors/background/index.ts delete mode 100644 packages/backend/src/queue/processors/db/delete-account.ts delete mode 100644 packages/backend/src/queue/processors/db/delete-drive-files.ts delete mode 100644 packages/backend/src/queue/processors/db/export-blocking.ts delete mode 100644 packages/backend/src/queue/processors/db/export-custom-emojis.ts delete mode 100644 packages/backend/src/queue/processors/db/export-following.ts delete mode 100644 packages/backend/src/queue/processors/db/export-mute.ts delete mode 100644 packages/backend/src/queue/processors/db/export-notes.ts delete mode 100644 packages/backend/src/queue/processors/db/export-user-lists.ts delete mode 100644 packages/backend/src/queue/processors/db/import-blocking.ts delete mode 100644 packages/backend/src/queue/processors/db/import-custom-emojis.ts delete mode 100644 packages/backend/src/queue/processors/db/import-following.ts delete mode 100644 packages/backend/src/queue/processors/db/import-muting.ts delete mode 100644 packages/backend/src/queue/processors/db/import-posts.ts delete mode 100644 packages/backend/src/queue/processors/db/import-user-lists.ts delete mode 100644 packages/backend/src/queue/processors/db/index.ts delete mode 100644 packages/backend/src/queue/processors/deliver.ts delete mode 100644 packages/backend/src/queue/processors/ended-poll-notification.ts delete mode 100644 packages/backend/src/queue/processors/inbox.ts delete mode 100644 packages/backend/src/queue/processors/object-storage/clean-remote-files.ts delete mode 100644 packages/backend/src/queue/processors/object-storage/delete-file.ts delete mode 100644 packages/backend/src/queue/processors/object-storage/index.ts delete mode 100644 packages/backend/src/queue/processors/system/check-expired-mutings.ts delete mode 100644 packages/backend/src/queue/processors/system/clean-charts.ts delete mode 100644 packages/backend/src/queue/processors/system/clean.ts delete mode 100644 packages/backend/src/queue/processors/system/index.ts delete mode 100644 packages/backend/src/queue/processors/system/resync-charts.ts delete mode 100644 packages/backend/src/queue/processors/system/tick-charts.ts delete mode 100644 packages/backend/src/queue/processors/webhook-deliver.ts delete mode 100644 packages/backend/src/queue/queues.ts delete mode 100644 packages/backend/src/queue/types.ts delete mode 100644 packages/backend/src/remote/activitypub/ap-request.ts delete mode 100644 packages/backend/src/remote/activitypub/audience.ts delete mode 100644 packages/backend/src/remote/activitypub/check-fetch.ts delete mode 100644 packages/backend/src/remote/activitypub/db-resolver.ts delete mode 100644 packages/backend/src/remote/activitypub/deliver-manager.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/accept/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/accept/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/add/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/announce/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/announce/note.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/block/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/create/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/create/note.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/delete/actor.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/delete/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/delete/note.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/flag/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/like.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/move/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/read.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/reject/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/reject/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/remove/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/accept.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/announce.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/block.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/index.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/undo/like.ts delete mode 100644 packages/backend/src/remote/activitypub/kernel/update/index.ts delete mode 100644 packages/backend/src/remote/activitypub/logger.ts delete mode 100644 packages/backend/src/remote/activitypub/misc/contexts.ts delete mode 100644 packages/backend/src/remote/activitypub/misc/get-note-html.ts delete mode 100644 packages/backend/src/remote/activitypub/misc/html-to-mfm.ts delete mode 100644 packages/backend/src/remote/activitypub/misc/ld-signature.ts delete mode 100644 packages/backend/src/remote/activitypub/models/icon.ts delete mode 100644 packages/backend/src/remote/activitypub/models/identifier.ts delete mode 100644 packages/backend/src/remote/activitypub/models/image.ts delete mode 100644 packages/backend/src/remote/activitypub/models/mention.ts delete mode 100644 packages/backend/src/remote/activitypub/models/note.ts delete mode 100644 packages/backend/src/remote/activitypub/models/person.ts delete mode 100644 packages/backend/src/remote/activitypub/models/question.ts delete mode 100644 packages/backend/src/remote/activitypub/models/tag.ts delete mode 100644 packages/backend/src/remote/activitypub/perform.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/accept.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/add.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/announce.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/block.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/create.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/delete.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/document.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/emoji.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/flag.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/follow-relay.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/follow-user.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/follow.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/hashtag.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/image.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/index.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/key.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/like.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/mention.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/note.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/ordered-collection.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/person.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/question.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/read.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/reject.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/remove.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/tombstone.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/undo.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/update.ts delete mode 100644 packages/backend/src/remote/activitypub/renderer/vote.ts delete mode 100644 packages/backend/src/remote/activitypub/request.ts delete mode 100644 packages/backend/src/remote/activitypub/resolver.ts delete mode 100644 packages/backend/src/remote/activitypub/type.ts delete mode 100644 packages/backend/src/remote/logger.ts delete mode 100644 packages/backend/src/remote/resolve-user.ts delete mode 100644 packages/backend/src/remote/webfinger.ts delete mode 100644 packages/backend/src/server/activitypub.ts delete mode 100644 packages/backend/src/server/activitypub/featured.ts delete mode 100644 packages/backend/src/server/activitypub/followers.ts delete mode 100644 packages/backend/src/server/activitypub/following.ts delete mode 100644 packages/backend/src/server/activitypub/outbox.ts delete mode 100644 packages/backend/src/server/api/2fa.ts delete mode 100644 packages/backend/src/server/api/api-handler.ts delete mode 100644 packages/backend/src/server/api/authenticate.ts delete mode 100644 packages/backend/src/server/api/call.ts delete mode 100644 packages/backend/src/server/api/common/generate-block-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-channel-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-muted-note-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-muted-note-thread-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-muted-user-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-native-user-token.ts delete mode 100644 packages/backend/src/server/api/common/generate-replies-query.ts delete mode 100644 packages/backend/src/server/api/common/generate-visibility-query.ts delete mode 100644 packages/backend/src/server/api/common/generated-muted-renote-query.ts delete mode 100644 packages/backend/src/server/api/common/getters.ts delete mode 100644 packages/backend/src/server/api/common/inject-featured.ts delete mode 100644 packages/backend/src/server/api/common/inject-promo.ts delete mode 100644 packages/backend/src/server/api/common/is-native-token.ts delete mode 100644 packages/backend/src/server/api/common/make-pagination-query.ts delete mode 100644 packages/backend/src/server/api/common/read-messaging-message.ts delete mode 100644 packages/backend/src/server/api/common/read-notification.ts delete mode 100644 packages/backend/src/server/api/common/signin.ts delete mode 100644 packages/backend/src/server/api/common/signup.ts delete mode 100644 packages/backend/src/server/api/compatibility.ts delete mode 100644 packages/backend/src/server/api/define.ts delete mode 100644 packages/backend/src/server/api/endpoints.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/accounts/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/accounts/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/ad/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/ad/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/ad/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/ad/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/announcements/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/announcements/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/announcements/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/announcements/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/delete-account.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/drive/files.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/drive/show-file.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/add.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/copy.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/emoji/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/get-index-stats.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/get-table-stats.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/get-user-ips.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/invite.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/meta.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/moderators/add.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/moderators/remove.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/promo/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/queue/clear.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/queue/stats.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/relays/add.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/relays/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/relays/remove.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/reset-password.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/search/index-all.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/send-email.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/server-info.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/show-user.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/show-users.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/silence-user.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/suspend-user.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/unsilence-user.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/update-meta.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/update-user-note.ts delete mode 100644 packages/backend/src/server/api/endpoints/admin/vacuum.ts delete mode 100644 packages/backend/src/server/api/endpoints/announcements.ts delete mode 100644 packages/backend/src/server/api/endpoints/antennas/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/antennas/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/antennas/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/antennas/markread.ts delete mode 100644 packages/backend/src/server/api/endpoints/antennas/notes.ts delete mode 100644 packages/backend/src/server/api/endpoints/antennas/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/antennas/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/ap/get.ts delete mode 100644 packages/backend/src/server/api/endpoints/ap/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/app/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/app/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/auth/accept.ts delete mode 100644 packages/backend/src/server/api/endpoints/auth/session/generate.ts delete mode 100644 packages/backend/src/server/api/endpoints/auth/session/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/auth/session/userkey.ts delete mode 100644 packages/backend/src/server/api/endpoints/blocking/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/blocking/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/blocking/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/channels/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/channels/featured.ts delete mode 100644 packages/backend/src/server/api/endpoints/channels/follow.ts delete mode 100644 packages/backend/src/server/api/endpoints/channels/followed.ts delete mode 100644 packages/backend/src/server/api/endpoints/channels/owned.ts delete mode 100644 packages/backend/src/server/api/endpoints/channels/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/channels/timeline.ts delete mode 100644 packages/backend/src/server/api/endpoints/channels/unfollow.ts delete mode 100644 packages/backend/src/server/api/endpoints/channels/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/active-users.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/ap-request.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/drive.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/federation.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/hashtag.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/instance.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/notes.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/user/drive.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/user/following.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/user/notes.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/user/reactions.ts delete mode 100644 packages/backend/src/server/api/endpoints/charts/users.ts delete mode 100644 packages/backend/src/server/api/endpoints/clips/add-note.ts delete mode 100644 packages/backend/src/server/api/endpoints/clips/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/clips/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/clips/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/clips/notes.ts delete mode 100644 packages/backend/src/server/api/endpoints/clips/remove-note.ts delete mode 100644 packages/backend/src/server/api/endpoints/clips/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/clips/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/compatibility/custom-emojis.ts delete mode 100644 packages/backend/src/server/api/endpoints/compatibility/instance-info.ts delete mode 100644 packages/backend/src/server/api/endpoints/custom-motd.ts delete mode 100644 packages/backend/src/server/api/endpoints/custom-splash-icons.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/caption-image.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/check-existence.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/find.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/folders.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/folders/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/folders/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/folders/find.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/folders/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/folders/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/drive/stream.ts delete mode 100644 packages/backend/src/server/api/endpoints/email-address/available.ts delete mode 100644 packages/backend/src/server/api/endpoints/emoji.ts delete mode 100644 packages/backend/src/server/api/endpoints/endpoint.ts delete mode 100644 packages/backend/src/server/api/endpoints/endpoints.ts delete mode 100644 packages/backend/src/server/api/endpoints/export-custom-emojis.ts delete mode 100644 packages/backend/src/server/api/endpoints/federation/followers.ts delete mode 100644 packages/backend/src/server/api/endpoints/federation/following.ts delete mode 100644 packages/backend/src/server/api/endpoints/federation/instances.ts delete mode 100644 packages/backend/src/server/api/endpoints/federation/show-instance.ts delete mode 100644 packages/backend/src/server/api/endpoints/federation/stats.ts delete mode 100644 packages/backend/src/server/api/endpoints/federation/update-remote-user.ts delete mode 100644 packages/backend/src/server/api/endpoints/federation/users.ts delete mode 100644 packages/backend/src/server/api/endpoints/fetch-rss.ts delete mode 100644 packages/backend/src/server/api/endpoints/following/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/following/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/following/invalidate.ts delete mode 100644 packages/backend/src/server/api/endpoints/following/requests/accept.ts delete mode 100644 packages/backend/src/server/api/endpoints/following/requests/cancel.ts delete mode 100644 packages/backend/src/server/api/endpoints/following/requests/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/following/requests/reject.ts delete mode 100644 packages/backend/src/server/api/endpoints/gallery/featured.ts delete mode 100644 packages/backend/src/server/api/endpoints/gallery/popular.ts delete mode 100644 packages/backend/src/server/api/endpoints/gallery/posts.ts delete mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/like.ts delete mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts delete mode 100644 packages/backend/src/server/api/endpoints/gallery/posts/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/get-online-users-count.ts delete mode 100644 packages/backend/src/server/api/endpoints/get-sounds.ts delete mode 100644 packages/backend/src/server/api/endpoints/hashtags/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/hashtags/search.ts delete mode 100644 packages/backend/src/server/api/endpoints/hashtags/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/hashtags/trend.ts delete mode 100644 packages/backend/src/server/api/endpoints/hashtags/users.ts delete mode 100644 packages/backend/src/server/api/endpoints/i.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/2fa/done.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/2fa/key-done.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/2fa/password-less.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/2fa/register-key.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/2fa/register.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/2fa/unregister.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/apps.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/authorized-apps.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/change-password.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/delete-account.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/export-blocking.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/export-following.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/export-mute.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/export-notes.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/export-user-lists.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/favorites.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/gallery/likes.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/gallery/posts.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/import-blocking.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/import-following.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/import-muting.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/import-posts.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/import-user-lists.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/known-as.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/move.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/notifications.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/page-likes.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/pages.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/pin.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/read-announcement.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/regenerate-token.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/get-all.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/get-detail.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/get.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/keys.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/remove.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/scopes.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/registry/set.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/revoke-token.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/signin-history.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/unpin.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/update-email.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/user-group-invites.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/webhooks/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/webhooks/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/webhooks/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/webhooks/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/webhooks/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/latest-version.ts delete mode 100644 packages/backend/src/server/api/endpoints/messaging/history.ts delete mode 100644 packages/backend/src/server/api/endpoints/messaging/messages.ts delete mode 100644 packages/backend/src/server/api/endpoints/messaging/messages/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/messaging/messages/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/messaging/messages/read.ts delete mode 100644 packages/backend/src/server/api/endpoints/meta.ts delete mode 100644 packages/backend/src/server/api/endpoints/miauth/gen-token.ts delete mode 100644 packages/backend/src/server/api/endpoints/mute/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/mute/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/mute/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/my/apps.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/children.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/clips.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/conversation.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/favorites/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/favorites/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/featured.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/global-timeline.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/local-timeline.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/mentions.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/polls/vote.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/reactions.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/reactions/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/reactions/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/renotes.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/replies.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/search-by-tag.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/search.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/state.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/timeline.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/translate.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/unrenote.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/watching/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/watching/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/notifications/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts delete mode 100644 packages/backend/src/server/api/endpoints/notifications/read.ts delete mode 100644 packages/backend/src/server/api/endpoints/page-push.ts delete mode 100644 packages/backend/src/server/api/endpoints/pages/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/pages/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/pages/featured.ts delete mode 100644 packages/backend/src/server/api/endpoints/pages/like.ts delete mode 100644 packages/backend/src/server/api/endpoints/pages/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/pages/unlike.ts delete mode 100644 packages/backend/src/server/api/endpoints/pages/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/patrons.ts delete mode 100644 packages/backend/src/server/api/endpoints/ping.ts delete mode 100644 packages/backend/src/server/api/endpoints/pinned-users.ts delete mode 100644 packages/backend/src/server/api/endpoints/promo/read.ts delete mode 100644 packages/backend/src/server/api/endpoints/recommended-instances.ts delete mode 100644 packages/backend/src/server/api/endpoints/release.ts delete mode 100644 packages/backend/src/server/api/endpoints/renote-mute/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/renote-mute/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/renote-mute/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/request-reset-password.ts delete mode 100644 packages/backend/src/server/api/endpoints/reset-db.ts delete mode 100644 packages/backend/src/server/api/endpoints/reset-password.ts delete mode 100644 packages/backend/src/server/api/endpoints/server-info.ts delete mode 100644 packages/backend/src/server/api/endpoints/stats.ts delete mode 100644 packages/backend/src/server/api/endpoints/sw/register.ts delete mode 100644 packages/backend/src/server/api/endpoints/sw/show-registration.ts delete mode 100644 packages/backend/src/server/api/endpoints/sw/unregister.ts delete mode 100644 packages/backend/src/server/api/endpoints/sw/update-registration.ts delete mode 100644 packages/backend/src/server/api/endpoints/test.ts delete mode 100644 packages/backend/src/server/api/endpoints/username/available.ts delete mode 100644 packages/backend/src/server/api/endpoints/users.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/clips.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/followers.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/following.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/gallery/posts.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/invite.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/joined.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/leave.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/owned.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/pull.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/transfer.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/groups/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/lists/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/lists/delete-all.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/lists/delete.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/lists/list.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/lists/pull.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/lists/push.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/lists/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/lists/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/notes.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/pages.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/reactions.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/recommendation.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/relation.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/report-abuse.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/search.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/show.ts delete mode 100644 packages/backend/src/server/api/endpoints/users/stats.ts delete mode 100644 packages/backend/src/server/api/error.ts delete mode 100644 packages/backend/src/server/api/index.ts delete mode 100644 packages/backend/src/server/api/limiter.ts delete mode 100644 packages/backend/src/server/api/logger.ts delete mode 100644 packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts delete mode 100644 packages/backend/src/server/api/mastodon/endpoints/account.ts delete mode 100644 packages/backend/src/server/api/mastodon/endpoints/auth.ts delete mode 100644 packages/backend/src/server/api/mastodon/endpoints/filter.ts delete mode 100644 packages/backend/src/server/api/mastodon/endpoints/meta.ts delete mode 100644 packages/backend/src/server/api/mastodon/endpoints/notifications.ts delete mode 100644 packages/backend/src/server/api/mastodon/endpoints/search.ts delete mode 100644 packages/backend/src/server/api/mastodon/endpoints/status.ts delete mode 100644 packages/backend/src/server/api/mastodon/endpoints/timeline.ts delete mode 100644 packages/backend/src/server/api/openapi/errors.ts delete mode 100644 packages/backend/src/server/api/openapi/gen-spec.ts delete mode 100644 packages/backend/src/server/api/openapi/schemas.ts delete mode 100644 packages/backend/src/server/api/private/signin.ts delete mode 100644 packages/backend/src/server/api/private/signup-pending.ts delete mode 100644 packages/backend/src/server/api/private/signup.ts delete mode 100644 packages/backend/src/server/api/service/discord.ts delete mode 100644 packages/backend/src/server/api/service/github.ts delete mode 100644 packages/backend/src/server/api/service/twitter.ts delete mode 100644 packages/backend/src/server/api/stream/channel.ts delete mode 100644 packages/backend/src/server/api/stream/channels/admin.ts delete mode 100644 packages/backend/src/server/api/stream/channels/antenna.ts delete mode 100644 packages/backend/src/server/api/stream/channels/channel.ts delete mode 100644 packages/backend/src/server/api/stream/channels/drive.ts delete mode 100644 packages/backend/src/server/api/stream/channels/global-timeline.ts delete mode 100644 packages/backend/src/server/api/stream/channels/hashtag.ts delete mode 100644 packages/backend/src/server/api/stream/channels/home-timeline.ts delete mode 100644 packages/backend/src/server/api/stream/channels/hybrid-timeline.ts delete mode 100644 packages/backend/src/server/api/stream/channels/index.ts delete mode 100644 packages/backend/src/server/api/stream/channels/local-timeline.ts delete mode 100644 packages/backend/src/server/api/stream/channels/main.ts delete mode 100644 packages/backend/src/server/api/stream/channels/messaging-index.ts delete mode 100644 packages/backend/src/server/api/stream/channels/messaging.ts delete mode 100644 packages/backend/src/server/api/stream/channels/queue-stats.ts delete mode 100644 packages/backend/src/server/api/stream/channels/recommended-timeline.ts delete mode 100644 packages/backend/src/server/api/stream/channels/server-stats.ts delete mode 100644 packages/backend/src/server/api/stream/channels/user-list.ts delete mode 100644 packages/backend/src/server/api/stream/index.ts delete mode 100644 packages/backend/src/server/api/stream/types.ts delete mode 100644 packages/backend/src/server/api/streaming.ts delete mode 100644 packages/backend/src/server/file/assets/bad-egg.png delete mode 100644 packages/backend/src/server/file/assets/cache-expired.png delete mode 100644 packages/backend/src/server/file/assets/dummy.png delete mode 100644 packages/backend/src/server/file/assets/not-an-image.png delete mode 100644 packages/backend/src/server/file/assets/thumbnail-not-available.png delete mode 100644 packages/backend/src/server/file/assets/tombstone.png delete mode 100644 packages/backend/src/server/file/index.ts delete mode 100644 packages/backend/src/server/file/send-drive-file.ts delete mode 100644 packages/backend/src/server/index.ts delete mode 100644 packages/backend/src/server/nodeinfo.ts delete mode 100644 packages/backend/src/server/proxy/index.ts delete mode 100644 packages/backend/src/server/proxy/proxy-media.ts delete mode 100644 packages/backend/src/server/web/bios.css delete mode 100644 packages/backend/src/server/web/bios.js delete mode 100644 packages/backend/src/server/web/boot.js delete mode 100644 packages/backend/src/server/web/cli.css delete mode 100644 packages/backend/src/server/web/cli.js delete mode 100644 packages/backend/src/server/web/feed.ts delete mode 100644 packages/backend/src/server/web/index.ts delete mode 100644 packages/backend/src/server/web/manifest.json delete mode 100644 packages/backend/src/server/web/manifest.ts delete mode 100644 packages/backend/src/server/web/style.css delete mode 100644 packages/backend/src/server/web/url-preview.ts delete mode 100644 packages/backend/src/server/web/views/base.pug delete mode 100644 packages/backend/src/server/web/views/bios.pug delete mode 100644 packages/backend/src/server/web/views/channel.pug delete mode 100644 packages/backend/src/server/web/views/cli.pug delete mode 100644 packages/backend/src/server/web/views/clip.pug delete mode 100644 packages/backend/src/server/web/views/flush.pug delete mode 100644 packages/backend/src/server/web/views/gallery-post.pug delete mode 100644 packages/backend/src/server/web/views/info-card.pug delete mode 100644 packages/backend/src/server/web/views/note.pug delete mode 100644 packages/backend/src/server/web/views/page.pug delete mode 100644 packages/backend/src/server/web/views/user.pug delete mode 100644 packages/backend/src/server/well-known.ts delete mode 100644 packages/backend/src/services/add-note-to-antenna.ts delete mode 100644 packages/backend/src/services/blocking/create.ts delete mode 100644 packages/backend/src/services/blocking/delete.ts delete mode 100644 packages/backend/src/services/chart/charts/active-users.ts delete mode 100644 packages/backend/src/services/chart/charts/ap-request.ts delete mode 100644 packages/backend/src/services/chart/charts/drive.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/active-users.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/ap-request.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/drive.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/federation.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/hashtag.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/instance.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/notes.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/per-user-drive.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/per-user-following.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/per-user-notes.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/per-user-reactions.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/test-grouped.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/test-intersection.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/test-unique.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/test.ts delete mode 100644 packages/backend/src/services/chart/charts/entities/users.ts delete mode 100644 packages/backend/src/services/chart/charts/federation.ts delete mode 100644 packages/backend/src/services/chart/charts/hashtag.ts delete mode 100644 packages/backend/src/services/chart/charts/instance.ts delete mode 100644 packages/backend/src/services/chart/charts/notes.ts delete mode 100644 packages/backend/src/services/chart/charts/per-user-drive.ts delete mode 100644 packages/backend/src/services/chart/charts/per-user-following.ts delete mode 100644 packages/backend/src/services/chart/charts/per-user-notes.ts delete mode 100644 packages/backend/src/services/chart/charts/per-user-reactions.ts delete mode 100644 packages/backend/src/services/chart/charts/test-grouped.ts delete mode 100644 packages/backend/src/services/chart/charts/test-intersection.ts delete mode 100644 packages/backend/src/services/chart/charts/test-unique.ts delete mode 100644 packages/backend/src/services/chart/charts/test.ts delete mode 100644 packages/backend/src/services/chart/charts/users.ts delete mode 100644 packages/backend/src/services/chart/core.ts delete mode 100644 packages/backend/src/services/chart/entities.ts delete mode 100644 packages/backend/src/services/chart/index.ts delete mode 100644 packages/backend/src/services/create-notification.ts delete mode 100644 packages/backend/src/services/create-system-user.ts delete mode 100644 packages/backend/src/services/delete-account.ts delete mode 100644 packages/backend/src/services/detect-sensitive.ts delete mode 100644 packages/backend/src/services/drive/add-file.ts delete mode 100644 packages/backend/src/services/drive/delete-file.ts delete mode 100644 packages/backend/src/services/drive/generate-video-thumbnail.ts delete mode 100644 packages/backend/src/services/drive/image-processor.ts delete mode 100644 packages/backend/src/services/drive/internal-storage.ts delete mode 100644 packages/backend/src/services/drive/logger.ts delete mode 100644 packages/backend/src/services/drive/s3.ts delete mode 100644 packages/backend/src/services/drive/upload-from-url.ts delete mode 100644 packages/backend/src/services/fetch-instance-metadata.ts delete mode 100644 packages/backend/src/services/following/create.ts delete mode 100644 packages/backend/src/services/following/delete.ts delete mode 100644 packages/backend/src/services/following/reject.ts delete mode 100644 packages/backend/src/services/following/requests/accept-all.ts delete mode 100644 packages/backend/src/services/following/requests/accept.ts delete mode 100644 packages/backend/src/services/following/requests/cancel.ts delete mode 100644 packages/backend/src/services/following/requests/create.ts delete mode 100644 packages/backend/src/services/i/pin.ts delete mode 100644 packages/backend/src/services/i/update.ts delete mode 100644 packages/backend/src/services/insert-moderation-log.ts delete mode 100644 packages/backend/src/services/instance-actor.ts delete mode 100644 packages/backend/src/services/logger.ts delete mode 100644 packages/backend/src/services/messages/create.ts delete mode 100644 packages/backend/src/services/messages/delete.ts delete mode 100644 packages/backend/src/services/note/create.ts delete mode 100644 packages/backend/src/services/note/delete.ts delete mode 100644 packages/backend/src/services/note/polls/update.ts delete mode 100644 packages/backend/src/services/note/polls/vote.ts delete mode 100644 packages/backend/src/services/note/reaction/create.ts delete mode 100644 packages/backend/src/services/note/reaction/delete.ts delete mode 100644 packages/backend/src/services/note/read.ts delete mode 100644 packages/backend/src/services/note/unread.ts delete mode 100644 packages/backend/src/services/note/unwatch.ts delete mode 100644 packages/backend/src/services/note/watch.ts delete mode 100644 packages/backend/src/services/push-notification.ts delete mode 100644 packages/backend/src/services/register-or-fetch-instance-doc.ts delete mode 100644 packages/backend/src/services/relay.ts delete mode 100644 packages/backend/src/services/send-email-notification.ts delete mode 100644 packages/backend/src/services/send-email.ts delete mode 100644 packages/backend/src/services/stream.ts delete mode 100644 packages/backend/src/services/suspend-user.ts delete mode 100644 packages/backend/src/services/unsuspend-user.ts delete mode 100644 packages/backend/src/services/update-hashtag.ts delete mode 100644 packages/backend/src/services/user-cache.ts delete mode 100644 packages/backend/src/services/user-list/push.ts delete mode 100644 packages/backend/src/services/validate-email-for-account.ts delete mode 100644 packages/backend/src/types.ts delete mode 100644 packages/backend/test/activitypub.ts delete mode 100644 packages/backend/test/ap-request.ts delete mode 100644 packages/backend/test/api-visibility.ts delete mode 100644 packages/backend/test/api.ts delete mode 100644 packages/backend/test/block.ts delete mode 100644 packages/backend/test/chart.ts delete mode 100644 packages/backend/test/docker-compose.yml delete mode 100644 packages/backend/test/endpoints.ts delete mode 100644 packages/backend/test/extract-mentions.ts delete mode 100644 packages/backend/test/fetch-resource.ts delete mode 100644 packages/backend/test/ff-visibility.ts delete mode 100644 packages/backend/test/get-file-info.ts delete mode 100644 packages/backend/test/loader.js delete mode 100644 packages/backend/test/mfm.ts delete mode 100644 packages/backend/test/misc/mock-resolver.ts delete mode 100644 packages/backend/test/mute.ts delete mode 100644 packages/backend/test/note.ts delete mode 100644 packages/backend/test/prelude/maybe.ts delete mode 100644 packages/backend/test/prelude/url.ts delete mode 100644 packages/backend/test/reaction-lib.ts delete mode 100644 packages/backend/test/resources/25000x25000.png delete mode 100644 packages/backend/test/resources/Lenna.jpg delete mode 100644 packages/backend/test/resources/Lenna.png delete mode 100644 packages/backend/test/resources/anime.gif delete mode 100644 packages/backend/test/resources/anime.png delete mode 100644 packages/backend/test/resources/emptyfile delete mode 100644 packages/backend/test/resources/image.svg delete mode 100644 packages/backend/test/resources/rotate.jpg delete mode 100644 packages/backend/test/resources/with-alpha.png delete mode 100644 packages/backend/test/resources/with-xml-def.svg delete mode 100644 packages/backend/test/streaming.ts delete mode 100644 packages/backend/test/thread-mute.ts delete mode 100644 packages/backend/test/tsconfig.json delete mode 100644 packages/backend/test/user-notes.ts delete mode 100644 packages/backend/test/utils.ts delete mode 100644 packages/backend/tsconfig.json diff --git a/packages/backend/.gitignore b/packages/backend/.gitignore new file mode 100644 index 0000000000..c4653682cf --- /dev/null +++ b/packages/backend/.gitignore @@ -0,0 +1,7 @@ +# +*.profraw +*.profdata + +# rust build dir +target/ + diff --git a/packages/backend/.mocharc.json b/packages/backend/.mocharc.json deleted file mode 100644 index f836f9e900..0000000000 --- a/packages/backend/.mocharc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extension": ["ts","js","cjs","mjs"], - "node-option": [ - "experimental-specifier-resolution=node", - "loader=./test/loader.js" - ], - "slow": 1000, - "timeout": 30000, - "exit": true -} diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc deleted file mode 100644 index 39e112ff7c..0000000000 --- a/packages/backend/.swcrc +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/swcrc", - "jsc": { - "parser": { - "syntax": "typescript", - "dynamicImport": true, - "decorators": true - }, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true - }, - "experimental": { - "keepImportAssertions": true - }, - "baseUrl": ".", - "paths": { - "@/*": [ - "./src/*" - ] - }, - "target": "es2022" - }, - "minify": false -} diff --git a/packages/backend/.vim/coc-settings.json b/packages/backend/.vim/coc-settings.json new file mode 100644 index 0000000000..b5c02503fb --- /dev/null +++ b/packages/backend/.vim/coc-settings.json @@ -0,0 +1,5 @@ +{ + "workspace.workspaceFolderCheckCwd": false, + "rust-analyzer.check.command": "clippy", + "rust-analyzer.check.extraArgs": "--profile test" +} diff --git a/packages/backend/Cargo.lock b/packages/backend/Cargo.lock new file mode 100644 index 0000000000..76277eac5c --- /dev/null +++ b/packages/backend/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "backend" +version = "0.0.0" +dependencies = [ + "server", +] + +[[package]] +name = "server" +version = "0.1.0" diff --git a/packages/backend/Cargo.toml b/packages/backend/Cargo.toml new file mode 100644 index 0000000000..893418cbcd --- /dev/null +++ b/packages/backend/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "backend" +version = "0.0.0" +edition = "2021" +default-run = "backend" + +[workspace] + +members = ["crates/*"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +server = { path = "crates/server" } + +[dev-dependencies] diff --git a/packages/backend/crates/server/Cargo.toml b/packages/backend/crates/server/Cargo.toml new file mode 100644 index 0000000000..dd022ab514 --- /dev/null +++ b/packages/backend/crates/server/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/packages/backend/crates/server/src/lib.rs b/packages/backend/crates/server/src/lib.rs new file mode 100644 index 0000000000..9771df5f2e --- /dev/null +++ b/packages/backend/crates/server/src/lib.rs @@ -0,0 +1,15 @@ +pub fn add(left: usize, right: usize) -> usize { + todo!(); + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/packages/backend/jsconfig.json b/packages/backend/jsconfig.json deleted file mode 100644 index 1230aadd12..0000000000 --- a/packages/backend/jsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "allowSyntheticDefaultImports": true - }, - "exclude": [ - "node_modules", - "jspm_packages", - "tmp", - "temp" - ] -} diff --git a/packages/backend/native-utils/.cargo/config.toml b/packages/backend/native-utils/.cargo/config.toml deleted file mode 100644 index 7ede30ee04..0000000000 --- a/packages/backend/native-utils/.cargo/config.toml +++ /dev/null @@ -1,3 +0,0 @@ -[target.aarch64-unknown-linux-musl] -linker = "aarch64-linux-musl-gcc" -rustflags = ["-C", "target-feature=-crt-static"] \ No newline at end of file diff --git a/packages/backend/native-utils/.gitignore b/packages/backend/native-utils/.gitignore deleted file mode 100644 index 78b75d55ad..0000000000 --- a/packages/backend/native-utils/.gitignore +++ /dev/null @@ -1,200 +0,0 @@ -# Created by https://www.toptal.com/developers/gitignore/api/node -# Edit at https://www.toptal.com/developers/gitignore?templates=node - -### Node ### -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# End of https://www.toptal.com/developers/gitignore/api/node - -# Created by https://www.toptal.com/developers/gitignore/api/macos -# Edit at https://www.toptal.com/developers/gitignore?templates=macos - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### macOS Patch ### -# iCloud generated files -*.icloud - -# End of https://www.toptal.com/developers/gitignore/api/macos - -# Created by https://www.toptal.com/developers/gitignore/api/windows -# Edit at https://www.toptal.com/developers/gitignore?templates=windows - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# End of https://www.toptal.com/developers/gitignore/api/windows - -# napi-rs generated files -built/ - -#Added by cargo - -/target -Cargo.lock - -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -*.node diff --git a/packages/backend/native-utils/.npmignore b/packages/backend/native-utils/.npmignore deleted file mode 100644 index ec144db2a7..0000000000 --- a/packages/backend/native-utils/.npmignore +++ /dev/null @@ -1,13 +0,0 @@ -target -Cargo.lock -.cargo -.github -npm -.eslintrc -.prettierignore -rustfmt.toml -yarn.lock -*.node -.yarn -__test__ -renovate.json diff --git a/packages/backend/native-utils/Cargo.toml b/packages/backend/native-utils/Cargo.toml deleted file mode 100644 index 4f7fb4c39a..0000000000 --- a/packages/backend/native-utils/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -edition = "2021" -name = "native-utils" -version = "0.0.0" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix -napi = { version = "2.12.0", default-features = false, features = ["napi4"] } -napi-derive = "2.12.0" - -[build-dependencies] -napi-build = "2.0.1" - -[profile.release] -lto = true diff --git a/packages/backend/native-utils/__test__/index.spec.mjs b/packages/backend/native-utils/__test__/index.spec.mjs deleted file mode 100644 index 0d41e012dd..0000000000 --- a/packages/backend/native-utils/__test__/index.spec.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import test from "ava"; - -import { sum } from "../index.js"; - -test("sum from native", (t) => { - t.is(sum(1, 2), 3); -}); diff --git a/packages/backend/native-utils/build.rs b/packages/backend/native-utils/build.rs deleted file mode 100644 index 1f866b6a3c..0000000000 --- a/packages/backend/native-utils/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -extern crate napi_build; - -fn main() { - napi_build::setup(); -} diff --git a/packages/backend/native-utils/npm/android-arm-eabi/README.md b/packages/backend/native-utils/npm/android-arm-eabi/README.md deleted file mode 100644 index 10199cb8ec..0000000000 --- a/packages/backend/native-utils/npm/android-arm-eabi/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-android-arm-eabi` - -This is the **armv7-linux-androideabi** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/android-arm-eabi/package.json b/packages/backend/native-utils/npm/android-arm-eabi/package.json deleted file mode 100644 index b4404c410a..0000000000 --- a/packages/backend/native-utils/npm/android-arm-eabi/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "native-utils-android-arm-eabi", - "version": "0.0.0", - "os": [ - "android" - ], - "cpu": [ - "arm" - ], - "main": "native-utils.android-arm-eabi.node", - "files": [ - "native-utils.android-arm-eabi.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/android-arm64/README.md b/packages/backend/native-utils/npm/android-arm64/README.md deleted file mode 100644 index c32c2fe710..0000000000 --- a/packages/backend/native-utils/npm/android-arm64/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-android-arm64` - -This is the **aarch64-linux-android** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/android-arm64/package.json b/packages/backend/native-utils/npm/android-arm64/package.json deleted file mode 100644 index 9050ef37bd..0000000000 --- a/packages/backend/native-utils/npm/android-arm64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "native-utils-android-arm64", - "version": "0.0.0", - "os": [ - "android" - ], - "cpu": [ - "arm64" - ], - "main": "native-utils.android-arm64.node", - "files": [ - "native-utils.android-arm64.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/darwin-arm64/README.md b/packages/backend/native-utils/npm/darwin-arm64/README.md deleted file mode 100644 index 8703902223..0000000000 --- a/packages/backend/native-utils/npm/darwin-arm64/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-darwin-arm64` - -This is the **aarch64-apple-darwin** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/darwin-arm64/package.json b/packages/backend/native-utils/npm/darwin-arm64/package.json deleted file mode 100644 index a7fcef289f..0000000000 --- a/packages/backend/native-utils/npm/darwin-arm64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "native-utils-darwin-arm64", - "version": "0.0.0", - "os": [ - "darwin" - ], - "cpu": [ - "arm64" - ], - "main": "native-utils.darwin-arm64.node", - "files": [ - "native-utils.darwin-arm64.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/darwin-universal/README.md b/packages/backend/native-utils/npm/darwin-universal/README.md deleted file mode 100644 index 098bb35906..0000000000 --- a/packages/backend/native-utils/npm/darwin-universal/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-darwin-universal` - -This is the **universal-apple-darwin** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/darwin-universal/package.json b/packages/backend/native-utils/npm/darwin-universal/package.json deleted file mode 100644 index a46061d421..0000000000 --- a/packages/backend/native-utils/npm/darwin-universal/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "native-utils-darwin-universal", - "version": "0.0.0", - "os": [ - "darwin" - ], - "main": "native-utils.darwin-universal.node", - "files": [ - "native-utils.darwin-universal.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/darwin-x64/README.md b/packages/backend/native-utils/npm/darwin-x64/README.md deleted file mode 100644 index 0acf363352..0000000000 --- a/packages/backend/native-utils/npm/darwin-x64/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-darwin-x64` - -This is the **x86_64-apple-darwin** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/darwin-x64/package.json b/packages/backend/native-utils/npm/darwin-x64/package.json deleted file mode 100644 index 6bbcf1d232..0000000000 --- a/packages/backend/native-utils/npm/darwin-x64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "native-utils-darwin-x64", - "version": "0.0.0", - "os": [ - "darwin" - ], - "cpu": [ - "x64" - ], - "main": "native-utils.darwin-x64.node", - "files": [ - "native-utils.darwin-x64.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/freebsd-x64/README.md b/packages/backend/native-utils/npm/freebsd-x64/README.md deleted file mode 100644 index 2b74996de7..0000000000 --- a/packages/backend/native-utils/npm/freebsd-x64/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-freebsd-x64` - -This is the **x86_64-unknown-freebsd** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/freebsd-x64/package.json b/packages/backend/native-utils/npm/freebsd-x64/package.json deleted file mode 100644 index 654b8abf38..0000000000 --- a/packages/backend/native-utils/npm/freebsd-x64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "native-utils-freebsd-x64", - "version": "0.0.0", - "os": [ - "freebsd" - ], - "cpu": [ - "x64" - ], - "main": "native-utils.freebsd-x64.node", - "files": [ - "native-utils.freebsd-x64.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/linux-arm-gnueabihf/README.md b/packages/backend/native-utils/npm/linux-arm-gnueabihf/README.md deleted file mode 100644 index 2203036de0..0000000000 --- a/packages/backend/native-utils/npm/linux-arm-gnueabihf/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-linux-arm-gnueabihf` - -This is the **armv7-unknown-linux-gnueabihf** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/linux-arm-gnueabihf/package.json b/packages/backend/native-utils/npm/linux-arm-gnueabihf/package.json deleted file mode 100644 index 1e206c078d..0000000000 --- a/packages/backend/native-utils/npm/linux-arm-gnueabihf/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "native-utils-linux-arm-gnueabihf", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "arm" - ], - "main": "native-utils.linux-arm-gnueabihf.node", - "files": [ - "native-utils.linux-arm-gnueabihf.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/linux-arm64-gnu/README.md b/packages/backend/native-utils/npm/linux-arm64-gnu/README.md deleted file mode 100644 index ad3a9333f5..0000000000 --- a/packages/backend/native-utils/npm/linux-arm64-gnu/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-linux-arm64-gnu` - -This is the **aarch64-unknown-linux-gnu** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/linux-arm64-gnu/package.json b/packages/backend/native-utils/npm/linux-arm64-gnu/package.json deleted file mode 100644 index aa0b2a805f..0000000000 --- a/packages/backend/native-utils/npm/linux-arm64-gnu/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "native-utils-linux-arm64-gnu", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "arm64" - ], - "main": "native-utils.linux-arm64-gnu.node", - "files": [ - "native-utils.linux-arm64-gnu.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "libc": [ - "glibc" - ] -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/linux-arm64-musl/README.md b/packages/backend/native-utils/npm/linux-arm64-musl/README.md deleted file mode 100644 index df282532ff..0000000000 --- a/packages/backend/native-utils/npm/linux-arm64-musl/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-linux-arm64-musl` - -This is the **aarch64-unknown-linux-musl** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/linux-arm64-musl/package.json b/packages/backend/native-utils/npm/linux-arm64-musl/package.json deleted file mode 100644 index 99e9387ee6..0000000000 --- a/packages/backend/native-utils/npm/linux-arm64-musl/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "native-utils-linux-arm64-musl", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "arm64" - ], - "main": "native-utils.linux-arm64-musl.node", - "files": [ - "native-utils.linux-arm64-musl.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "libc": [ - "musl" - ] -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/linux-x64-gnu/README.md b/packages/backend/native-utils/npm/linux-x64-gnu/README.md deleted file mode 100644 index 52eea85aab..0000000000 --- a/packages/backend/native-utils/npm/linux-x64-gnu/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-linux-x64-gnu` - -This is the **x86_64-unknown-linux-gnu** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/linux-x64-gnu/package.json b/packages/backend/native-utils/npm/linux-x64-gnu/package.json deleted file mode 100644 index f99a5f664e..0000000000 --- a/packages/backend/native-utils/npm/linux-x64-gnu/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "native-utils-linux-x64-gnu", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "x64" - ], - "main": "native-utils.linux-x64-gnu.node", - "files": [ - "native-utils.linux-x64-gnu.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "libc": [ - "glibc" - ] -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/linux-x64-musl/README.md b/packages/backend/native-utils/npm/linux-x64-musl/README.md deleted file mode 100644 index 6664b23783..0000000000 --- a/packages/backend/native-utils/npm/linux-x64-musl/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-linux-x64-musl` - -This is the **x86_64-unknown-linux-musl** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/linux-x64-musl/package.json b/packages/backend/native-utils/npm/linux-x64-musl/package.json deleted file mode 100644 index 56b520fff4..0000000000 --- a/packages/backend/native-utils/npm/linux-x64-musl/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "native-utils-linux-x64-musl", - "version": "0.0.0", - "os": [ - "linux" - ], - "cpu": [ - "x64" - ], - "main": "native-utils.linux-x64-musl.node", - "files": [ - "native-utils.linux-x64-musl.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "libc": [ - "musl" - ] -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/win32-arm64-msvc/README.md b/packages/backend/native-utils/npm/win32-arm64-msvc/README.md deleted file mode 100644 index 7aec7e0a55..0000000000 --- a/packages/backend/native-utils/npm/win32-arm64-msvc/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-win32-arm64-msvc` - -This is the **aarch64-pc-windows-msvc** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/win32-arm64-msvc/package.json b/packages/backend/native-utils/npm/win32-arm64-msvc/package.json deleted file mode 100644 index 865a771052..0000000000 --- a/packages/backend/native-utils/npm/win32-arm64-msvc/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "native-utils-win32-arm64-msvc", - "version": "0.0.0", - "os": [ - "win32" - ], - "cpu": [ - "arm64" - ], - "main": "native-utils.win32-arm64-msvc.node", - "files": [ - "native-utils.win32-arm64-msvc.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/win32-ia32-msvc/README.md b/packages/backend/native-utils/npm/win32-ia32-msvc/README.md deleted file mode 100644 index 690de1975d..0000000000 --- a/packages/backend/native-utils/npm/win32-ia32-msvc/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-win32-ia32-msvc` - -This is the **i686-pc-windows-msvc** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/win32-ia32-msvc/package.json b/packages/backend/native-utils/npm/win32-ia32-msvc/package.json deleted file mode 100644 index 994eff12fd..0000000000 --- a/packages/backend/native-utils/npm/win32-ia32-msvc/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "native-utils-win32-ia32-msvc", - "version": "0.0.0", - "os": [ - "win32" - ], - "cpu": [ - "ia32" - ], - "main": "native-utils.win32-ia32-msvc.node", - "files": [ - "native-utils.win32-ia32-msvc.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/npm/win32-x64-msvc/README.md b/packages/backend/native-utils/npm/win32-x64-msvc/README.md deleted file mode 100644 index e34a5ff172..0000000000 --- a/packages/backend/native-utils/npm/win32-x64-msvc/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `native-utils-win32-x64-msvc` - -This is the **x86_64-pc-windows-msvc** binary for `native-utils` diff --git a/packages/backend/native-utils/npm/win32-x64-msvc/package.json b/packages/backend/native-utils/npm/win32-x64-msvc/package.json deleted file mode 100644 index 33b259b132..0000000000 --- a/packages/backend/native-utils/npm/win32-x64-msvc/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "native-utils-win32-x64-msvc", - "version": "0.0.0", - "os": [ - "win32" - ], - "cpu": [ - "x64" - ], - "main": "native-utils.win32-x64-msvc.node", - "files": [ - "native-utils.win32-x64-msvc.node" - ], - "license": "MIT", - "engines": { - "node": ">= 10" - } -} \ No newline at end of file diff --git a/packages/backend/native-utils/package.json b/packages/backend/native-utils/package.json deleted file mode 100644 index 787d1bd89f..0000000000 --- a/packages/backend/native-utils/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "native-utils", - "version": "0.0.0", - "main": "built/index.js", - "types": "built/index.d.ts", - "napi": { - "name": "native-utils", - "triples": { - "additional": [ - "aarch64-apple-darwin", - "aarch64-linux-android", - "aarch64-unknown-linux-gnu", - "aarch64-unknown-linux-musl", - "aarch64-pc-windows-msvc", - "armv7-unknown-linux-gnueabihf", - "x86_64-unknown-linux-musl", - "x86_64-unknown-freebsd", - "i686-pc-windows-msvc", - "armv7-linux-androideabi", - "universal-apple-darwin" - ] - } - }, - "license": "MIT", - "devDependencies": { - "@napi-rs/cli": "^2.15.0", - "ava": "^5.1.1" - }, - "ava": { - "timeout": "3m" - }, - "engines": { - "node": ">= 10" - }, - "scripts": { - "artifacts": "napi artifacts", - "build": "napi build --platform --release ./built/", - "build:debug": "napi build --platform", - "prepublishOnly": "napi prepublish -t npm", - "test": "ava", - "universal": "napi universal", - "version": "napi version" - } -} diff --git a/packages/backend/native-utils/rustfmt.toml b/packages/backend/native-utils/rustfmt.toml deleted file mode 100644 index cab5731eda..0000000000 --- a/packages/backend/native-utils/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -tab_spaces = 2 -edition = "2021" diff --git a/packages/backend/native-utils/src/lib.rs b/packages/backend/native-utils/src/lib.rs deleted file mode 100644 index bc5b9fc7cf..0000000000 --- a/packages/backend/native-utils/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ - -pub mod mastodon_api; diff --git a/packages/backend/native-utils/src/mastodon_api.rs b/packages/backend/native-utils/src/mastodon_api.rs deleted file mode 100644 index 36b4eb9849..0000000000 --- a/packages/backend/native-utils/src/mastodon_api.rs +++ /dev/null @@ -1,70 +0,0 @@ -use napi::{bindgen_prelude::*, Error, Status}; -use napi_derive::napi; - -static CHAR_COLLECTION: &str = "0123456789abcdefghijklmnopqrstuvwxyz"; - -// -- NAPI exports -- - -#[napi] -pub enum IdConvertType { - MastodonId, - CalckeyId, -} - -#[napi] -pub fn convert_id(in_id: String, id_convert_type: IdConvertType) -> napi::Result { - use IdConvertType::*; - match id_convert_type { - MastodonId => { - let mut out: i64 = 0; - for (i, c) in in_id.to_lowercase().chars().rev().enumerate() { - out += num_from_char(c)? as i64 * 36_i64.pow(i as u32); - } - - Ok(out.to_string()) - } - CalckeyId => { - let mut input: i64 = match in_id.parse() { - Ok(s) => s, - Err(_) => { - return Err(Error::new( - Status::InvalidArg, - "Unable to parse ID as MasstodonId", - )) - } - }; - let mut out = String::new(); - - while input != 0 { - out.insert(0, char_from_num((input % 36) as u8)?); - input /= 36; - } - - Ok(out) - } - } -} - -// -- end -- - -#[inline(always)] -fn num_from_char(character: char) -> napi::Result { - for (i, c) in CHAR_COLLECTION.chars().enumerate() { - if c == character { - return Ok(i as u8); - } - } - - Err(Error::new( - Status::InvalidArg, - "Invalid character in parsed base36 id", - )) -} - -#[inline(always)] -fn char_from_num(number: u8) -> napi::Result { - CHAR_COLLECTION - .chars() - .nth(number as usize) - .ok_or(Error::from_status(Status::Unknown)) -} diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js deleted file mode 100644 index 5f85cead8a..0000000000 --- a/packages/backend/ormconfig.js +++ /dev/null @@ -1,15 +0,0 @@ -import { DataSource } from "typeorm"; -import config from "./built/config/index.js"; -import { entities } from "./built/db/postgre.js"; - -export default new DataSource({ - type: "postgres", - host: config.db.host, - port: config.db.port, - username: config.db.user, - password: config.db.pass, - database: config.db.db, - extra: config.db.extra, - entities: entities, - migrations: ["migration/*.js"], -}); diff --git a/packages/backend/package.json b/packages/backend/package.json index 442ef5e187..8e3c70e1ab 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,197 +1,15 @@ { "name": "backend", - "main": "./index.js", "private": true, "type": "module", "scripts": { - "start": "pnpm node ./built/index.js", + "start": "cargo run --profile $( [ ${NODE_ENV:=dev} = 'production' ] && echo 'release' || echo $NODE_ENV )", "start:test": "NODE_ENV=test pnpm node ./built/index.js", - "migrate": "typeorm migration:run -d ormconfig.js", + "check": "cargo check", + "migrate": "cargo run --bin migrate", + "build": "cargo build --profile $( [ ${NODE_ENV:=dev} = 'production' ] && echo 'release' || echo $NODE_ENV )", "revertmigration": "typeorm migration:revert -d ormconfig.js", - "check:connect": "node ./check_connect.js", - "build": "napi build --platform --release --cargo-cwd native-utils ./native-utils/built/ && pnpm swc src -d built -D", - "watch": "pnpm swc src -d built -D -w", - "lint": "pnpm rome check \"src/**/*.ts\"", - "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", - "test": "pnpm run mocha" - }, - "resolutions": { - "chokidar": "^3.3.1" - }, - "optionalDependencies": { - "@swc/core-android-arm64": "1.3.11", - "@tensorflow/tfjs-node": "3.21.1" - }, - "dependencies": { - "@bull-board/api": "^4.6.4", - "@bull-board/koa": "^4.6.4", - "@bull-board/ui": "^4.6.4", - "@calckey/megalodon": "5.1.24", - "@discordapp/twemoji": "14.0.2", - "@elastic/elasticsearch": "7.17.0", - "@koa/cors": "3.4.3", - "@koa/multer": "3.0.0", - "@koa/router": "9.0.1", - "@peertube/http-signature": "1.7.0", - "@redocly/openapi-core": "1.0.0-beta.120", - "@sinonjs/fake-timers": "9.1.2", - "@syuilo/aiscript": "0.11.1", - "@tensorflow/tfjs": "^4.2.0", - "ajv": "8.11.2", - "archiver": "5.3.1", - "argon2": "^0.30.3", - "autobind-decorator": "2.4.0", - "autolinker": "4.0.0", - "autwh": "0.1.0", - "aws-sdk": "2.1277.0", - "axios": "^1.3.2", - "bcryptjs": "2.4.3", - "blurhash": "1.1.5", - "bull": "4.10.2", - "cacheable-lookup": "7.0.0", - "calckey-js": "workspace:*", - "cbor": "8.1.0", - "chalk": "5.2.0", - "chalk-template": "0.4.0", - "chokidar": "3.5.3", - "cli-highlight": "2.1.11", - "color-convert": "2.0.1", - "content-disposition": "0.5.4", - "date-fns": "2.29.3", - "deep-email-validator": "0.1.21", - "escape-regexp": "0.0.1", - "feed": "4.2.2", - "file-type": "17.1.6", - "fluent-ffmpeg": "2.1.2", - "got": "12.5.3", - "hpagent": "0.1.2", - "ioredis": "5.2.4", - "ip-cidr": "3.0.11", - "is-svg": "4.3.2", - "js-yaml": "4.1.0", - "jsdom": "20.0.3", - "jsonld": "6.0.0", - "jsrsasign": "10.6.1", - "koa": "2.13.4", - "koa-body": "^6.0.1", - "koa-bodyparser": "4.3.0", - "koa-json-body": "5.3.0", - "koa-logger": "3.2.1", - "koa-mount": "4.0.0", - "koa-send": "5.0.1", - "koa-slow": "2.1.0", - "koa-views": "7.0.2", - "mfm-js": "0.23.2", - "mime-types": "2.1.35", - "multer": "1.4.4-lts.1", - "native-utils": "link:native-utils", - "nested-property": "4.0.0", - "node-fetch": "3.3.0", - "nodemailer": "6.8.0", - "nsfwjs": "2.4.2", - "oauth": "^0.10.0", - "os-utils": "0.0.14", - "parse5": "7.1.2", - "pg": "8.8.0", - "private-ip": "2.3.4", - "probe-image-size": "7.2.3", - "promise-limit": "2.7.0", - "punycode": "2.1.1", - "pureimage": "0.3.15", - "qrcode": "1.5.1", - "qs": "6.9.7", - "random-seed": "0.3.0", - "ratelimiter": "3.4.1", - "re2": "1.18.0", - "redis-lock": "0.1.4", - "reflect-metadata": "0.1.13", - "rename": "1.0.4", - "rndstr": "1.0.0", - "rss-parser": "3.12.0", - "sanitize-html": "2.8.1", - "seedrandom": "^3.0.5", - "semver": "7.3.8", - "sharp": "0.31.3", - "sonic-channel": "^1.3.1", - "speakeasy": "2.0.0", - "stringz": "2.1.0", - "summaly": "github:misskey-dev/summaly", - "syslog-pro": "1.0.0", - "systeminformation": "5.16.9", - "tesseract.js": "^3.0.3", - "tinycolor2": "1.5.2", - "tmp": "0.2.1", - "twemoji-parser": "14.0.0", - "typeorm": "0.3.11", - "ulid": "2.3.0", - "unzipper": "0.10.11", - "uuid": "9.0.0", - "web-push": "3.5.0", - "websocket": "1.0.34", - "xev": "3.0.2" - }, - "devDependencies": { - "@swc/cli": "^0.1.62", - "@swc/core": "^1.3.50", - "@types/bcryptjs": "2.4.2", - "@types/bull": "3.15.9", - "@types/cbor": "6.0.0", - "@types/escape-regexp": "0.0.1", - "@types/fluent-ffmpeg": "2.1.20", - "@types/js-yaml": "4.0.5", - "@types/jsdom": "20.0.1", - "@types/jsonld": "1.5.8", - "@types/jsrsasign": "10.5.4", - "@types/koa": "2.13.5", - "@types/koa-bodyparser": "4.3.10", - "@types/koa-cors": "0.0.2", - "@types/koa-favicon": "2.0.21", - "@types/koa-logger": "3.1.2", - "@types/koa-mount": "4.0.2", - "@types/koa-send": "4.1.3", - "@types/koa-views": "7.0.0", - "@types/koa__cors": "3.3.0", - "@types/koa__multer": "2.0.4", - "@types/koa__router": "8.0.11", - "@types/mocha": "9.1.1", - "@types/node": "18.11.18", - "@types/node-fetch": "3.0.3", - "@types/nodemailer": "6.4.7", - "@types/oauth": "0.9.1", - "@types/pug": "2.0.6", - "@types/punycode": "2.1.0", - "@types/qrcode": "1.5.0", - "@types/qs": "6.9.7", - "@types/random-seed": "0.3.3", - "@types/ratelimiter": "3.4.4", - "@types/redis": "4.0.11", - "@types/rename": "1.0.4", - "@types/sanitize-html": "2.8.0", - "@types/semver": "7.3.13", - "@types/sharp": "0.31.1", - "@types/sinonjs__fake-timers": "8.1.2", - "@types/speakeasy": "2.0.7", - "@types/tinycolor2": "1.4.3", - "@types/tmp": "0.2.3", - "@types/uuid": "8.3.4", - "@types/web-push": "3.3.2", - "@types/websocket": "1.0.5", - "@types/ws": "8.5.3", - "autobind-decorator": "2.4.0", - "cross-env": "7.0.3", - "eslint": "^8.31.0", - "execa": "6.1.0", - "json5": "2.2.3", - "json5-loader": "4.0.1", - "mocha": "10.2.0", - "pug": "3.0.2", - "strict-event-emitter-types": "2.0.0", - "swc-loader": "^0.2.3", - "ts-loader": "9.4.2", - "ts-node": "10.9.1", - "tsconfig-paths": "4.1.2", - "typescript": "4.9.4", - "webpack": "^5.75.0", - "ws": "8.11.0" + "lint": "cargo check", + "test": "cargo test --workspace" } } diff --git a/packages/backend/rust-toolchain b/packages/backend/rust-toolchain new file mode 100644 index 0000000000..bf867e0ae5 --- /dev/null +++ b/packages/backend/rust-toolchain @@ -0,0 +1 @@ +nightly diff --git a/packages/backend/rustfmt.toml b/packages/backend/rustfmt.toml new file mode 100644 index 0000000000..b196eaa2dc --- /dev/null +++ b/packages/backend/rustfmt.toml @@ -0,0 +1 @@ +tab_spaces = 2 diff --git a/packages/backend/src/@types/hcaptcha.d.ts b/packages/backend/src/@types/hcaptcha.d.ts deleted file mode 100644 index 21f65c678c..0000000000 --- a/packages/backend/src/@types/hcaptcha.d.ts +++ /dev/null @@ -1,14 +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/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts deleted file mode 100644 index 3bfece8cbf..0000000000 --- a/packages/backend/src/@types/http-signature.d.ts +++ /dev/null @@ -1,98 +0,0 @@ -declare module "@peertube/http-signature" { - import type { IncomingMessage, ClientRequest } from "node:http"; - - interface ISignature { - keyId: string; - algorithm: string; - headers: string[]; - signature: string; - } - - interface IOptions { - headers?: string[]; - algorithm?: string; - strict?: boolean; - authorizationHeaderName?: string; - } - - interface IParseRequestOptions extends IOptions { - clockSkew?: number; - } - - interface IParsedSignature { - scheme: string; - params: ISignature; - signingString: string; - algorithm: string; - keyId: string; - } - - type RequestSignerConstructorOptions = - | IRequestSignerConstructorOptionsFromProperties - | IRequestSignerConstructorOptionsFromFunction; - - interface IRequestSignerConstructorOptionsFromProperties { - keyId: string; - key: string | Buffer; - algorithm?: string; - } - - interface IRequestSignerConstructorOptionsFromFunction { - sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void; - } - - class RequestSigner { - constructor(options: RequestSignerConstructorOptions); - - public writeHeader(header: string, value: string): string; - - public writeDateHeader(): string; - - public writeTarget(method: string, path: string): void; - - public sign(cb: (err: any, authz: string) => void): void; - } - - interface ISignRequestOptions extends IOptions { - keyId: string; - key: string; - httpVersion?: string; - } - - export function parse( - request: IncomingMessage, - options?: IParseRequestOptions, - ): IParsedSignature; - export function parseRequest( - request: IncomingMessage, - options?: IParseRequestOptions, - ): IParsedSignature; - - export function sign( - request: ClientRequest, - options: ISignRequestOptions, - ): boolean; - export function signRequest( - request: ClientRequest, - options: ISignRequestOptions, - ): boolean; - export function createSigner(): RequestSigner; - export function isSigner(obj: any): obj is RequestSigner; - - export function sshKeyToPEM(key: string): string; - export function sshKeyFingerprint(key: string): string; - export function pemToRsaSSHKey(pem: string, comment: string): string; - - export function verify( - parsedSignature: IParsedSignature, - pubkey: string | Buffer, - ): boolean; - export function verifySignature( - parsedSignature: IParsedSignature, - pubkey: string | Buffer, - ): boolean; - export function verifyHMAC( - parsedSignature: IParsedSignature, - secret: string, - ): boolean; -} diff --git a/packages/backend/src/@types/koa-json-body.d.ts b/packages/backend/src/@types/koa-json-body.d.ts deleted file mode 100644 index e5282d81bf..0000000000 --- a/packages/backend/src/@types/koa-json-body.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare module "koa-json-body" { - import type { Middleware } from "koa"; - - interface IKoaJsonBodyOptions { - strict: boolean; - limit: string; - fallback: boolean; - } - - function koaJsonBody(opt?: IKoaJsonBodyOptions): Middleware; - - namespace koaJsonBody {} // Hack - - export = koaJsonBody; -} diff --git a/packages/backend/src/@types/koa-remove-trailing-slashes/index.d.ts b/packages/backend/src/@types/koa-remove-trailing-slashes/index.d.ts deleted file mode 100644 index 429d1d53e0..0000000000 --- a/packages/backend/src/@types/koa-remove-trailing-slashes/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module "koa-remove-trailing-slashes"; diff --git a/packages/backend/src/@types/koa-slow.d.ts b/packages/backend/src/@types/koa-slow.d.ts deleted file mode 100644 index e24be51e2a..0000000000 --- a/packages/backend/src/@types/koa-slow.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module "koa-slow" { - import type { Middleware } from "koa"; - - interface ISlowOptions { - url?: RegExp; - delay?: number; - } - - function slow(options?: ISlowOptions): Middleware; - - namespace slow {} // Hack - - export = slow; -} diff --git a/packages/backend/src/@types/os-utils.d.ts b/packages/backend/src/@types/os-utils.d.ts deleted file mode 100644 index 504096ae2b..0000000000 --- a/packages/backend/src/@types/os-utils.d.ts +++ /dev/null @@ -1,33 +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/packages/backend/src/@types/package.json.d.ts b/packages/backend/src/@types/package.json.d.ts deleted file mode 100644 index d8ec636446..0000000000 --- a/packages/backend/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/packages/backend/src/@types/probe-image-size.d.ts b/packages/backend/src/@types/probe-image-size.d.ts deleted file mode 100644 index 4ed13df7fa..0000000000 --- a/packages/backend/src/@types/probe-image-size.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -declare module "probe-image-size" { - import type { ReadStream } from "node: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/packages/backend/src/bin/migrate.rs b/packages/backend/src/bin/migrate.rs new file mode 100644 index 0000000000..299db6482a --- /dev/null +++ b/packages/backend/src/bin/migrate.rs @@ -0,0 +1,4 @@ + +fn main() { + todo!(); +} diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts deleted file mode 100644 index e4f2ed7b1b..0000000000 --- a/packages/backend/src/boot/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -import cluster from "node:cluster"; -import chalk from "chalk"; -import Xev from "xev"; - -import Logger from "@/services/logger.js"; -import { envOption } from "../env.js"; - -// for typeorm -import "reflect-metadata"; -import { masterMain } from "./master.js"; -import { workerMain } from "./worker.js"; -import os from "node:os"; - -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 = `Calckey (${cluster.isPrimary ? "master" : "worker"})`; - - if (cluster.isPrimary || envOption.disableClustering) { - await masterMain(); - if (cluster.isPrimary) { - ev.mount(); - } - } - - if (cluster.isWorker || envOption.disableClustering) { - await workerMain(); - } - - if (cluster.isPrimary) { - // Leave the master process with a marginally lower priority but not too low. - os.setPriority(2); - } - if (cluster.isWorker) { - // Set workers to a much lower priority so that the master process will be - // able to respond to api calls even if the workers gank everything. - os.setPriority(10); - } - - // For when Calckey is started in a child process during unit testing. - // Otherwise, process.send cannot be used, so start it. - if (process.send) { - process.send("ok"); - } -} - -//#region Events - -// Listen new workers -cluster.on("fork", (worker) => { - clusterLogger.debug(`Process forked: [${worker.id}]`); -}); - -// Listen online workers -cluster.on("online", (worker) => { - clusterLogger.debug(`Process is now online: [${worker.id}]`); -}); - -// Listen for dying workers -cluster.on("exit", (worker) => { - // Replace the dead worker, - // we're not sentimental - clusterLogger.error(chalk.red(`[${worker.id}] died :(`)); - cluster.fork(); -}); - -// Display detail of unhandled promise rejection -if (!envOption.quiet) { - process.on("unhandledRejection", console.dir); -} - -// Display detail of uncaught exception -process.on("uncaughtException", (err) => { - try { - logger.error(err); - } catch {} -}); - -// Dying away... -process.on("exit", (code) => { - logger.info(`The process is going to exit with code ${code}`); -}); - -//#endregion diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts deleted file mode 100644 index 193f02429d..0000000000 --- a/packages/backend/src/boot/master.ts +++ /dev/null @@ -1,189 +0,0 @@ -import * as fs from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; -import * as os from "node:os"; -import cluster from "node:cluster"; -import chalk from "chalk"; -import chalkTemplate from "chalk-template"; -import semver from "semver"; - -import Logger from "@/services/logger.js"; -import loadConfig from "@/config/load.js"; -import type { Config } from "@/config/types.js"; -import { lessThan } from "@/prelude/array.js"; -import { envOption } from "../env.js"; -import { showMachineInfo } from "@/misc/show-machine-info.js"; -import { db, initDb } from "../db/postgre.js"; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const meta = JSON.parse( - fs.readFileSync(`${_dirname}/../../../../built/meta.json`, "utf-8"), -); - -const logger = new Logger("core", "cyan"); -const bootLogger = logger.createSubLogger("boot", "magenta", false); - -const themeColor = chalk.hex("#31748f"); - -function greet() { - if (!envOption.quiet) { - //#region Calckey logo - const v = `v${meta.version}`; - console.log(themeColor(" ___ _ _ ")); - console.log(themeColor(" / __\\__ _| | ___| | _____ _ _ ")); - console.log(themeColor(" / / / _` | |/ __| |/ / _ | | |")); - console.log(themeColor("/ /__| (_| | | (__| < __/ |_| |")); - console.log(themeColor("\\____/\\__,_|_|\\___|_|\\_\\___|\\__, |")); - console.log(themeColor(" (___/ ")); - //#endregion - - console.log( - " Calckey is an open-source decentralized microblogging platform.", - ); - console.log( - chalk.rgb( - 255, - 136, - 0, - )( - " If you like Calckey, please consider starring or contributing to the repo. https://codeberg.org/calckey/calckey", - ), - ); - - console.log(""); - console.log( - chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`, - ); - } - - bootLogger.info("Welcome to Calckey!"); - bootLogger.info(`Calckey v${meta.version}`, null, true); -} - -/** - * Init master process - */ -export async function masterMain() { - let config!: Config; - - // initialize app - try { - greet(); - showEnvironment(); - await showMachineInfo(bootLogger); - showNodejsVersion(); - config = loadConfigBoot(); - await connectDb(); - } catch (e) { - bootLogger.error("Fatal error occurred during initialization", null, true); - process.exit(1); - } - - bootLogger.succ("Calckey initialized"); - - if (!envOption.disableClustering) { - await spawnWorkers(config.clusterLimit); - } - - bootLogger.succ( - `Now listening on port ${config.port} on ${config.url}`, - null, - true, - ); - - if (!envOption.noDaemons) { - import("../daemons/server-stats.js").then((x) => x.default()); - import("../daemons/queue-stats.js").then((x) => x.default()); - import("../daemons/janitor.js").then((x) => x.default()); - } -} - -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); - } -} - -function showNodejsVersion(): void { - const nodejsLogger = bootLogger.createSubLogger("nodejs"); - - nodejsLogger.info(`Version ${process.version} detected.`); - - const minVersion = fs - .readFileSync(`${_dirname}/../../../../.node-version`, "utf-8") - .trim(); - if (semver.lt(process.version, minVersion)) { - nodejsLogger.error(`At least Node.js ${minVersion} required!`); - process.exit(1); - } -} - -function loadConfigBoot(): Config { - const configLogger = bootLogger.createSubLogger("config"); - let config; - - try { - config = loadConfig(); - } catch (exception) { - if (exception.code === "ENOENT") { - configLogger.error("Configuration file not found", null, true); - process.exit(1); - } else if (e instanceof Error) { - configLogger.error(e.message); - 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 db - .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 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 === "listenFailed") { - bootLogger.error("The server Listen failed due to the previous error."); - process.exit(1); - } - if (message !== "ready") return; - res(); - }); - }); -} diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts deleted file mode 100644 index 70442b096e..0000000000 --- a/packages/backend/src/boot/worker.ts +++ /dev/null @@ -1,20 +0,0 @@ -import cluster from "node:cluster"; -import { initDb } from "../db/postgre.js"; - -/** - * Init worker process - */ -export async function workerMain() { - await initDb(); - - // start server - await import("../server/index.js").then((x) => x.default()); - - // start job queue - import("../queue/index.js").then((x) => x.default()); - - if (cluster.isWorker) { - // Send a 'ready' message to parent process - process.send!("ready"); - } -} diff --git a/packages/backend/src/config/index.ts b/packages/backend/src/config/index.ts deleted file mode 100644 index ae197b09ca..0000000000 --- a/packages/backend/src/config/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import load from "./load.js"; - -export default load(); diff --git a/packages/backend/src/config/load.ts b/packages/backend/src/config/load.ts deleted file mode 100644 index 9b8ee5edbb..0000000000 --- a/packages/backend/src/config/load.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Config loader - */ - -import * as fs from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; -import * as yaml from "js-yaml"; -import type { Source, Mixin } from "./types.js"; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -/** - * Path of configuration directory - */ -const dir = `${_dirname}/../../../../.config`; - -/** - * Path of configuration file - */ -const path = - process.env.NODE_ENV === "test" ? `${dir}/test.yml` : `${dir}/default.yml`; - -export default function load() { - const meta = JSON.parse( - fs.readFileSync(`${_dirname}/../../../../built/meta.json`, "utf-8"), - ); - const clientManifest = JSON.parse( - fs.readFileSync( - `${_dirname}/../../../../built/_client_dist_/manifest.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 = `Calckey/${meta.version} (${config.url})`; - mixin.clientEntry = clientManifest["src/init.ts"]; - - 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 new Error(`url="${url}" is not a valid URL.`); - } -} diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts deleted file mode 100644 index a7cdc89cf2..0000000000 --- a/packages/backend/src/config/types.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * ユーザーが設定する必要のある情報 - */ -export type Source = { - repository_url?: string; - feedback_url?: string; - url: string; - port: number; - disableHsts?: boolean; - db: { - host: string; - port: number; - db: string; - user: string; - pass: string; - disableCache?: boolean; - extra?: { [x: string]: string }; - }; - redis: { - host: string; - port: number; - family?: number; - pass: string; - db?: number; - prefix?: string; - }; - elasticsearch: { - host: string; - port: number; - ssl?: boolean; - user?: string; - pass?: string; - index?: string; - }; - sonic: { - host: string; - port: number; - auth?: string; - collection?: string; - bucket?: 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; - proxyRemoteFiles?: boolean; - - twa: { - nameSpace?: string; - packageName?: string; - sha256CertFingerprints?: string[]; - }; - - // Managed hosting stuff - maxUserSignups?: number; - isManagedHosting?: boolean; - maxNoteLength?: number; - maxCaptionLength?: number; - deepl: { - managed?: boolean; - authKey?: string; - isPro?: boolean; - }; - email: { - managed?: boolean; - address?: string; - host?: string; - port?: number; - user?: string; - pass?: string; - useImplicitSslTls?: boolean; - }; - objectStorage: { - managed?: boolean; - baseUrl?: string; - bucket?: string; - prefix?: string; - endpoint?: string; - region?: string; - accessKey?: string; - secretKey?: string; - useSsl?: boolean; - connnectOverProxy?: boolean; - setPublicReadOnUpload?: boolean; - s3ForcePathStyle?: boolean; - }; - summalyProxyUrl?: string; -}; - -/** - * Misskeyが自動的に(ユーザーが設定した情報から推論して)設定する情報 - */ -export type Mixin = { - version: string; - host: string; - hostname: string; - scheme: string; - wsScheme: string; - apiUrl: string; - wsUrl: string; - authUrl: string; - driveUrl: string; - userAgent: string; - clientEntry: string; -}; - -export type Config = Source & Mixin; diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts deleted file mode 100644 index 7e8f96444e..0000000000 --- a/packages/backend/src/const.ts +++ /dev/null @@ -1,71 +0,0 @@ -import config from "@/config/index.js"; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; - -export const MAX_NOTE_TEXT_LENGTH = - config.maxNoteLength != null ? config.maxNoteLength : 3000; // <- should we increase this? -export const MAX_CAPTION_TEXT_LENGTH = Math.min( - config.maxCaptionLength ?? 1500, - DB_MAX_IMAGE_COMMENT_LENGTH, -); - -export const SECOND = 1000; -export const SEC = 1000; // why do we need this duplicate here? -export const MINUTE = 60 * SEC; -export const MIN = 60 * SEC; // why do we need this duplicate here? -export const HOUR = 60 * MIN; -export const DAY = 24 * HOUR; - -export const USER_ONLINE_THRESHOLD = 10 * MINUTE; -export const USER_ACTIVE_THRESHOLD = 3 * DAY; - -// List of file types allowed to be viewed directly in the browser -// Anything not included here will be responded as application/octet-stream -// SVG is not allowed because it generates XSS <- we need to fix this and later allow it to be viewed directly -export const FILE_TYPE_BROWSERSAFE = [ - // Images - "image/png", - "image/gif", // TODO: deprecated, but still used by old notes, new gifs should be converted to webp in the future - "image/jpeg", - "image/webp", // TODO: make this the default image format - "image/apng", - "image/bmp", - "image/tiff", - "image/x-icon", - "image/avif", // not as good supported now, but its good to introduce initial support for the future - - // OggS - "audio/opus", - "video/ogg", - "audio/ogg", - "application/ogg", - - // ISO/IEC base media file format - "video/quicktime", - "video/mp4", // TODO: we need to check for av1 later - "video/vnd.avi", // also av1 - "audio/mp4", - "video/x-m4v", - "audio/x-m4a", - "video/3gpp", - "video/3gpp2", - "video/3gp2", - "audio/3gpp", - "audio/3gpp2", - "audio/3gp2", - - "video/mpeg", - "audio/mpeg", - - "video/webm", - "audio/webm", - - "audio/aac", - "audio/x-flac", - "audio/flac", - "audio/vnd.wave", -]; -/* -https://github.com/sindresorhus/file-type/blob/main/supported.js -https://github.com/sindresorhus/file-type/blob/main/core.js -https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers -*/ diff --git a/packages/backend/src/daemons/janitor.ts b/packages/backend/src/daemons/janitor.ts deleted file mode 100644 index 2050d54d4c..0000000000 --- a/packages/backend/src/daemons/janitor.ts +++ /dev/null @@ -1,20 +0,0 @@ -// TODO: 消したい - -const interval = 30 * 60 * 1000; -import { AttestationChallenges } from "@/models/index.js"; -import { LessThan } from "typeorm"; - -/** - * Clean up database occasionally - */ -export default function () { - async function tick() { - await AttestationChallenges.delete({ - createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)), - }); - } - - tick(); - - setInterval(tick, interval); -} diff --git a/packages/backend/src/daemons/queue-stats.ts b/packages/backend/src/daemons/queue-stats.ts deleted file mode 100644 index 381b52a916..0000000000 --- a/packages/backend/src/daemons/queue-stats.ts +++ /dev/null @@ -1,60 +0,0 @@ -import Xev from "xev"; -import { deliverQueue, inboxQueue } from "../queue/queues.js"; - -const ev = new Xev(); - -const interval = 10000; - -/** - * Report queue stats regularly - */ -export default function () { - const log = [] as any[]; - - ev.on("requestQueueStatsLog", (x) => { - ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50)); - }); - - let activeDeliverJobs = 0; - let activeInboxJobs = 0; - - deliverQueue.on("global:active", () => { - activeDeliverJobs++; - }); - - inboxQueue.on("global:active", () => { - activeInboxJobs++; - }); - - async function tick() { - const deliverJobCounts = await deliverQueue.getJobCounts(); - const inboxJobCounts = await inboxQueue.getJobCounts(); - - const stats = { - deliver: { - activeSincePrevTick: activeDeliverJobs, - active: deliverJobCounts.active, - waiting: deliverJobCounts.waiting, - delayed: deliverJobCounts.delayed, - }, - inbox: { - activeSincePrevTick: activeInboxJobs, - active: inboxJobCounts.active, - waiting: inboxJobCounts.waiting, - delayed: inboxJobCounts.delayed, - }, - }; - - ev.emit("queueStats", stats); - - log.unshift(stats); - if (log.length > 200) log.pop(); - - activeDeliverJobs = 0; - activeInboxJobs = 0; - } - - tick(); - - setInterval(tick, interval); -} diff --git a/packages/backend/src/daemons/server-stats.ts b/packages/backend/src/daemons/server-stats.ts deleted file mode 100644 index b0bf1288fd..0000000000 --- a/packages/backend/src/daemons/server-stats.ts +++ /dev/null @@ -1,79 +0,0 @@ -import 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 ?? 0)), - w: round(Math.max(0, fsStats.wIO_sec ?? 0)), - }, - }; - ev.emit("serverStats", stats); - log.unshift(stats); - if (log.length > 200) log.pop(); - } - - tick(); - - setInterval(tick, interval); -} - -// CPU STAT -function cpuUsage(): Promise { - return new Promise((res, rej) => { - osUtils.cpuUsage((cpuUsage) => { - res(cpuUsage); - }); - }); -} - -// MEMORY STAT -async function mem() { - const data = await si.mem(); - return data; -} - -// NETWORK STAT -async function net() { - const iface = await si.networkInterfaceDefault(); - const data = await si.networkStats(iface); - return data[0]; -} - -// FS STAT -async function fs() { - const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 })); - return data || { rIO_sec: 0, wIO_sec: 0 }; -} diff --git a/packages/backend/src/db/elasticsearch.ts b/packages/backend/src/db/elasticsearch.ts deleted file mode 100644 index 2640e7f918..0000000000 --- a/packages/backend/src/db/elasticsearch.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as elasticsearch from "@elastic/elasticsearch"; -import config from "@/config/index.js"; - -const index = { - settings: { - analysis: { - analyzer: { - ngram: { - tokenizer: "ngram", - }, - }, - }, - }, - mappings: { - properties: { - text: { - type: "text", - index: true, - analyzer: "ngram", - }, - userId: { - type: "keyword", - index: true, - }, - userHost: { - type: "keyword", - index: true, - }, - }, - }, -}; - -// Init ElasticSearch connection -const client = config.elasticsearch - ? new elasticsearch.Client({ - node: `${config.elasticsearch.ssl ? "https://" : "http://"}${ - config.elasticsearch.host - }:${config.elasticsearch.port}`, - auth: - config.elasticsearch.user && config.elasticsearch.pass - ? { - username: config.elasticsearch.user, - password: config.elasticsearch.pass, - } - : undefined, - pingTimeout: 30000, - }) - : null; - -if (client) { - client.indices - .exists({ - index: config.elasticsearch.index || "misskey_note", - }) - .then((exist) => { - if (!exist.body) { - client.indices.create({ - index: config.elasticsearch.index || "misskey_note", - body: index, - }); - } - }); -} - -export default client; diff --git a/packages/backend/src/db/logger.ts b/packages/backend/src/db/logger.ts deleted file mode 100644 index 28ec65dd24..0000000000 --- a/packages/backend/src/db/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from "@/services/logger.js"; - -export const dbLogger = new Logger("db"); diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts deleted file mode 100644 index bdeb910e84..0000000000 --- a/packages/backend/src/db/postgre.ts +++ /dev/null @@ -1,262 +0,0 @@ -// https://github.com/typeorm/typeorm/issues/2400 -import pg from "pg"; -pg.types.setTypeParser(20, Number); - -import type { Logger } from "typeorm"; -import { DataSource } from "typeorm"; -import * as highlight from "cli-highlight"; -import config from "@/config/index.js"; - -import { User } from "@/models/entities/user.js"; -import { DriveFile } from "@/models/entities/drive-file.js"; -import { DriveFolder } from "@/models/entities/drive-folder.js"; -import { AccessToken } from "@/models/entities/access-token.js"; -import { App } from "@/models/entities/app.js"; -import { PollVote } from "@/models/entities/poll-vote.js"; -import { Note } from "@/models/entities/note.js"; -import { NoteReaction } from "@/models/entities/note-reaction.js"; -import { NoteWatching } from "@/models/entities/note-watching.js"; -import { NoteThreadMuting } from "@/models/entities/note-thread-muting.js"; -import { NoteUnread } from "@/models/entities/note-unread.js"; -import { Notification } from "@/models/entities/notification.js"; -import { Meta } from "@/models/entities/meta.js"; -import { Following } from "@/models/entities/following.js"; -import { Instance } from "@/models/entities/instance.js"; -import { Muting } from "@/models/entities/muting.js"; -import { RenoteMuting } from "@/models/entities/renote-muting.js"; -import { SwSubscription } from "@/models/entities/sw-subscription.js"; -import { Blocking } from "@/models/entities/blocking.js"; -import { UserList } from "@/models/entities/user-list.js"; -import { UserListJoining } from "@/models/entities/user-list-joining.js"; -import { UserGroup } from "@/models/entities/user-group.js"; -import { UserGroupJoining } from "@/models/entities/user-group-joining.js"; -import { UserGroupInvitation } from "@/models/entities/user-group-invitation.js"; -import { Hashtag } from "@/models/entities/hashtag.js"; -import { NoteFavorite } from "@/models/entities/note-favorite.js"; -import { AbuseUserReport } from "@/models/entities/abuse-user-report.js"; -import { RegistrationTicket } from "@/models/entities/registration-tickets.js"; -import { MessagingMessage } from "@/models/entities/messaging-message.js"; -import { Signin } from "@/models/entities/signin.js"; -import { AuthSession } from "@/models/entities/auth-session.js"; -import { FollowRequest } from "@/models/entities/follow-request.js"; -import { Emoji } from "@/models/entities/emoji.js"; -import { UserNotePining } from "@/models/entities/user-note-pining.js"; -import { Poll } from "@/models/entities/poll.js"; -import { UserKeypair } from "@/models/entities/user-keypair.js"; -import { UserPublickey } from "@/models/entities/user-publickey.js"; -import { UserProfile } from "@/models/entities/user-profile.js"; -import { UserSecurityKey } from "@/models/entities/user-security-key.js"; -import { AttestationChallenge } from "@/models/entities/attestation-challenge.js"; -import { Page } from "@/models/entities/page.js"; -import { PageLike } from "@/models/entities/page-like.js"; -import { GalleryPost } from "@/models/entities/gallery-post.js"; -import { GalleryLike } from "@/models/entities/gallery-like.js"; -import { ModerationLog } from "@/models/entities/moderation-log.js"; -import { UsedUsername } from "@/models/entities/used-username.js"; -import { Announcement } from "@/models/entities/announcement.js"; -import { AnnouncementRead } from "@/models/entities/announcement-read.js"; -import { Clip } from "@/models/entities/clip.js"; -import { ClipNote } from "@/models/entities/clip-note.js"; -import { Antenna } from "@/models/entities/antenna.js"; -import { AntennaNote } from "@/models/entities/antenna-note.js"; -import { PromoNote } from "@/models/entities/promo-note.js"; -import { PromoRead } from "@/models/entities/promo-read.js"; -import { Relay } from "@/models/entities/relay.js"; -import { MutedNote } from "@/models/entities/muted-note.js"; -import { Channel } from "@/models/entities/channel.js"; -import { ChannelFollowing } from "@/models/entities/channel-following.js"; -import { ChannelNotePining } from "@/models/entities/channel-note-pining.js"; -import { RegistryItem } from "@/models/entities/registry-item.js"; -import { Ad } from "@/models/entities/ad.js"; -import { PasswordResetRequest } from "@/models/entities/password-reset-request.js"; -import { UserPending } from "@/models/entities/user-pending.js"; -import { Webhook } from "@/models/entities/webhook.js"; -import { UserIp } from "@/models/entities/user-ip.js"; - -import { entities as charts } from "@/services/chart/entities.js"; -import { envOption } from "../env.js"; -import { dbLogger } from "./logger.js"; -import { redisClient } from "./redis.js"; - -const sqlLogger = dbLogger.createSubLogger("sql", "gray", false); - -class MyCustomLogger implements Logger { - private highlight(sql: string) { - return highlight.highlight(sql, { - language: "sql", - ignoreIllegals: true, - }); - } - - public logQuery(query: string, parameters?: any[]) { - sqlLogger.info(this.highlight(query).substring(0, 100)); - } - - 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, - RenoteMuting, - 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, - Relay, - MutedNote, - Channel, - ChannelFollowing, - ChannelNotePining, - RegistryItem, - Ad, - PasswordResetRequest, - UserPending, - Webhook, - UserIp, - ...charts, -]; - -const log = process.env.NODE_ENV !== "production"; - -export const db = new DataSource({ - type: "postgres", - host: config.db.host, - port: config.db.port, - username: config.db.user, - password: config.db.pass, - database: config.db.db, - extra: { - statement_timeout: 1000 * 10, - ...config.db.extra, - }, - synchronize: process.env.NODE_ENV === "test", - dropSchema: process.env.NODE_ENV === "test", - cache: !config.db.disableCache - ? { - type: "ioredis", - options: { - host: config.redis.host, - port: config.redis.port, - family: config.redis.family == null ? 0 : config.redis.family, - password: config.redis.pass, - keyPrefix: `${config.redis.prefix}:query:`, - db: config.redis.db || 0, - }, - } - : false, - logging: log, - logger: log ? new MyCustomLogger() : undefined, - maxQueryExecutionTime: 300, - entities: entities, - migrations: ["../../migration/*.js"], -}); - -export async function initDb(force = false) { - if (force) { - if (db.isInitialized) { - await db.destroy(); - } - await db.initialize(); - return; - } - - if (db.isInitialized) { - // nop - } else { - await db.initialize(); - } -} - -export async function resetDb() { - const reset = async () => { - await redisClient.flushdb(); - const tables = await db.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';`); - for (const table of tables) { - await db.query(`DELETE FROM "${table.table}" CASCADE`); - } - }; - - for (let i = 1; i <= 3; i++) { - try { - await reset(); - } catch (e) { - if (i === 3) { - throw e; - } else { - await new Promise((resolve) => setTimeout(resolve, 1000)); - continue; - } - } - break; - } -} diff --git a/packages/backend/src/db/redis.ts b/packages/backend/src/db/redis.ts deleted file mode 100644 index 6ad3de386f..0000000000 --- a/packages/backend/src/db/redis.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Redis from "ioredis"; -import config from "@/config/index.js"; - -export function createConnection() { - return new Redis({ - port: config.redis.port, - host: config.redis.host, - family: config.redis.family == null ? 0 : config.redis.family, - password: config.redis.pass, - keyPrefix: `${config.redis.prefix}:`, - db: config.redis.db || 0, - }); -} - -export const subscriber = createConnection(); -subscriber.subscribe(config.host); - -export const redisClient = createConnection(); diff --git a/packages/backend/src/db/sonic.ts b/packages/backend/src/db/sonic.ts deleted file mode 100644 index 590c479247..0000000000 --- a/packages/backend/src/db/sonic.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as SonicChannel from "sonic-channel"; -import { dbLogger } from "./logger.js"; - -import config from "@/config/index.js"; - -const logger = dbLogger.createSubLogger("sonic", "gray", false); - -logger.info("Connecting to Sonic"); - -const handlers = (type: string): SonicChannel.Handlers => ({ - connected: () => { - logger.succ(`Connected to Sonic ${type}`); - }, - disconnected: (error) => { - logger.warn(`Disconnected from Sonic ${type}, error: ${error}`); - }, - error: (error) => { - logger.warn(`Sonic ${type} error: ${error}`); - }, - retrying: () => { - logger.info(`Sonic ${type} retrying`); - }, - timeout: () => { - logger.warn(`Sonic ${type} timeout`); - }, -}); - -const hasConfig = - config.sonic && (config.sonic.host || config.sonic.port || config.sonic.auth); - -const host = hasConfig ? config.sonic.host ?? "localhost" : ""; -const port = hasConfig ? config.sonic.port ?? 1491 : 0; -const auth = hasConfig ? config.sonic.auth ?? "SecretPassword" : ""; -const collection = hasConfig ? config.sonic.collection ?? "main" : ""; -const bucket = hasConfig ? config.sonic.bucket ?? "default" : ""; - -export default hasConfig - ? { - search: new SonicChannel.Search({ host, port, auth }).connect( - handlers("search"), - ), - ingest: new SonicChannel.Ingest({ host, port, auth }).connect( - handlers("ingest"), - ), - - collection, - bucket, - } - : null; diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts deleted file mode 100644 index a788a0fba2..0000000000 --- a/packages/backend/src/env.ts +++ /dev/null @@ -1,25 +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/packages/backend/src/global.d.ts b/packages/backend/src/global.d.ts deleted file mode 100644 index 503e26eb60..0000000000 --- a/packages/backend/src/global.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -// rome-ignore lint/suspicious/noExplicitAny: i have no idea -type FIXME = any; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts deleted file mode 100644 index 278f630f70..0000000000 --- a/packages/backend/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Misskey Entry Point! - */ - -import { EventEmitter } from "node:events"; -import boot from "./boot/index.js"; - -Error.stackTraceLimit = Infinity; -EventEmitter.defaultMaxListeners = 128; - -boot().catch((err) => { - console.error(err); -}); diff --git a/packages/backend/src/main.rs b/packages/backend/src/main.rs new file mode 100644 index 0000000000..336d023394 --- /dev/null +++ b/packages/backend/src/main.rs @@ -0,0 +1,5 @@ + + +fn main() { + +} diff --git a/packages/backend/src/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts deleted file mode 100644 index 7c956e9058..0000000000 --- a/packages/backend/src/mfm/from-html.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { URL } from "node:url"; -import * as parse5 from "parse5"; -import * as TreeAdapter from "../../node_modules/parse5/dist/tree-adapters/default.js"; - -const treeAdapter = TreeAdapter.defaultTreeAdapter; - -const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; -const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; - -export function fromHtml(html: string, hashtagNames?: string[]): string { - // some AP servers like Pixelfed use br tags as well as newlines - html = html.replace(/\r?\n/gi, "\n"); - - const dom = parse5.parseFragment(html); - - let text = ""; - - for (const n of dom.childNodes) { - analyze(n); - } - - return text.trim(); - - function getText(node: TreeAdapter.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: TreeAdapter.ChildNode[]): void { - if (childNodes) { - for (const n of childNodes) { - analyze(n); - } - } - } - - function analyze(node: TreeAdapter.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?.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 += "\n> ";
-					text += t.split("\n").join("\n> ");
-				}
-				break;
-			}
-
-			case "p":
-			case "h2":
-			case "h3":
-			case "h4":
-			case "h5":
-			case "h6": {
-				text += "\n\n";
-				appendChildren(node.childNodes);
-				break;
-			}
-
-			// other block elements
-			case "div":
-			case "header":
-			case "footer":
-			case "article":
-			case "li":
-			case "dt":
-			case "dd": {
-				text += "\n";
-				appendChildren(node.childNodes);
-				break;
-			}
-
-			default: {
-				// includes inline elements
-				appendChildren(node.childNodes);
-				break;
-			}
-		}
-	}
-}
diff --git a/packages/backend/src/mfm/to-html.ts b/packages/backend/src/mfm/to-html.ts
deleted file mode 100644
index 8d8a4a8889..0000000000
--- a/packages/backend/src/mfm/to-html.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { JSDOM } from "jsdom";
-import type * as mfm from "mfm-js";
-import config from "@/config/index.js";
-import { intersperse } from "@/prelude/array.js";
-import type { IMentionedRemoteUsers } from "@/models/entities/note.js";
-
-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 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://search.annoyingorange.xyz/search?q=${node.props.query}`;
-			a.textContent = node.props.content;
-			return a;
-		},
-
-		plain(node) {
-			const el = doc.createElement("span");
-			appendChildren(node.children, el);
-			return el;
-		},
-	};
-
-	appendChildren(nodes, doc.body);
-
-	return `

${doc.body.innerHTML}

`; -} diff --git a/packages/backend/src/misc/acct.ts b/packages/backend/src/misc/acct.ts deleted file mode 100644 index 5b7767a106..0000000000 --- a/packages/backend/src/misc/acct.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type Acct = { - username: string; - host: string | null; -}; - -export function parse(acct: string): Acct { - if (acct.startsWith("@")) acct = acct.substr(1); - const split = acct.split("@", 2); - return { username: split[0], host: split[1] || null }; -} - -export function toString(acct: Acct): string { - return acct.host == null ? acct.username : `${acct.username}@${acct.host}`; -} diff --git a/packages/backend/src/misc/antenna-cache.ts b/packages/backend/src/misc/antenna-cache.ts deleted file mode 100644 index 7f199c3967..0000000000 --- a/packages/backend/src/misc/antenna-cache.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Antennas } from "@/models/index.js"; -import type { Antenna } from "@/models/entities/antenna.js"; -import { subscriber } from "@/db/redis.js"; - -let antennasFetched = false; -let antennas: Antenna[] = []; - -export async function getAntennas() { - if (!antennasFetched) { - antennas = await Antennas.find(); - antennasFetched = true; - } - - return antennas; -} - -subscriber.on("message", async (_, data) => { - const obj = JSON.parse(data); - - if (obj.channel === "internal") { - const { type, body } = obj.message; - switch (type) { - case "antennaCreated": - antennas.push(body); - break; - case "antennaUpdated": - antennas[antennas.findIndex((a) => a.id === body.id)] = body; - break; - case "antennaDeleted": - antennas = antennas.filter((a) => a.id !== body.id); - break; - default: - break; - } - } -}); diff --git a/packages/backend/src/misc/api-permissions.ts b/packages/backend/src/misc/api-permissions.ts deleted file mode 100644 index 9e040262f1..0000000000 --- a/packages/backend/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/packages/backend/src/misc/app-lock.ts b/packages/backend/src/misc/app-lock.ts deleted file mode 100644 index 05bcf54244..0000000000 --- a/packages/backend/src/misc/app-lock.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { redisClient } from "../db/redis.js"; -import { promisify } from "node:util"; -import redisLock from "redis-lock"; - -/** - * Retry delay (ms) for lock acquisition - */ -const retryDelay = 100; - -const lock: (key: string, timeout?: number) => Promise<() => void> = redisClient - ? promisify(redisLock(redisClient, retryDelay)) - : async () => () => {}; - -/** - * Get AP Object lock - * @param uri AP object ID - * @param timeout Lock timeout (ms), The timeout releases previous lock. - * @returns Unlock function - */ -export function getApLock(uri: string, timeout = 30 * 1000) { - return lock(`ap-object:${uri}`, timeout); -} - -export function getFetchInstanceMetadataLock( - host: string, - timeout = 30 * 1000, -) { - return lock(`instance:${host}`, timeout); -} - -export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) { - return lock(`chart-insert:${lockKey}`, timeout); -} diff --git a/packages/backend/src/misc/before-shutdown.ts b/packages/backend/src/misc/before-shutdown.ts deleted file mode 100644 index 0820418356..0000000000 --- a/packages/backend/src/misc/before-shutdown.ts +++ /dev/null @@ -1,103 +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: ((signalOrEvent: string) => void)[] = []; - -/** - * 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: string[], - fn: (signalOrEvent: string) => void, -) => { - 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: number) => () => { - 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: string) { - 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) { - if (err instanceof Error) { - 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: () => void) { - shutdownListeners.push(listener); - return listener; -} - -// Register shutdown callback that kills the process after `SHUTDOWN_TIMEOUT` milliseconds -// This prevents custom shutdown handlers from hanging the process indefinitely -processOnce(SHUTDOWN_SIGNALS, forceExitAfter(SHUTDOWN_TIMEOUT)); - -// Register process shutdown callback -// Will listen to incoming signal events and execute all registered handlers in the stack -processOnce(SHUTDOWN_SIGNALS, shutdownHandler); diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts deleted file mode 100644 index 9abebc91cb..0000000000 --- a/packages/backend/src/misc/cache.ts +++ /dev/null @@ -1,88 +0,0 @@ -export class Cache { - public 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); - } - - /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします - */ - public async fetch( - key: string | null, - fetcher: () => Promise, - validator?: (cachedValue: T) => boolean, - ): Promise { - const cachedValue = this.get(key); - if (cachedValue !== undefined) { - if (validator) { - if (validator(cachedValue)) { - // Cache HIT - return cachedValue; - } - } else { - // Cache HIT - return cachedValue; - } - } - - // Cache MISS - const value = await fetcher(); - this.set(key, value); - return value; - } - - /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします - */ - public async fetchMaybe( - key: string | null, - fetcher: () => Promise, - validator?: (cachedValue: T) => boolean, - ): Promise { - const cachedValue = this.get(key); - if (cachedValue !== undefined) { - if (validator) { - if (validator(cachedValue)) { - // Cache HIT - return cachedValue; - } - } else { - // Cache HIT - return cachedValue; - } - } - - // Cache MISS - const value = await fetcher(); - if (value !== undefined) { - this.set(key, value); - } - return value; - } -} diff --git a/packages/backend/src/misc/captcha.ts b/packages/backend/src/misc/captcha.ts deleted file mode 100644 index 8ea4abedb6..0000000000 --- a/packages/backend/src/misc/captcha.ts +++ /dev/null @@ -1,73 +0,0 @@ -import fetch from "node-fetch"; -import { URLSearchParams } from "node:url"; -import { getAgentByUrl } from "./fetch.js"; -import config from "@/config/index.js"; - -export async function verifyRecaptcha(secret: string, response: string) { - const result = await getCaptchaResponse( - "https://www.recaptcha.net/recaptcha/api/siteverify", - secret, - response, - ).catch((e) => { - throw new Error(`recaptcha-request-failed: ${e.message}`); - }); - - if (result.success !== true) { - const errorCodes = result["error-codes"] - ? result["error-codes"]?.join(", ") - : ""; - throw new Error(`recaptcha-failed: ${errorCodes}`); - } -} - -export async function verifyHcaptcha(secret: string, response: string) { - const result = await getCaptchaResponse( - "https://hcaptcha.com/siteverify", - secret, - response, - ).catch((e) => { - throw new Error(`hcaptcha-request-failed: ${e.message}`); - }); - - if (result.success !== true) { - const errorCodes = result["error-codes"] - ? result["error-codes"]?.join(", ") - : ""; - throw new Error(`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, - }, - // TODO - //timeout: 10 * 1000, - agent: getAgentByUrl, - }).catch((e) => { - throw new Error(`${e.message || e}`); - }); - - if (!res.ok) { - throw new Error(`${res.status}`); - } - - return (await res.json()) as CaptchaResponse; -} diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts deleted file mode 100644 index adcdd190f8..0000000000 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { Antenna } from "@/models/entities/antenna.js"; -import type { Note } from "@/models/entities/note.js"; -import type { User } from "@/models/entities/user.js"; -import { - UserListJoinings, - UserGroupJoinings, - Blockings, -} from "@/models/index.js"; -import { getFullApAccount } from "./convert-host.js"; -import * as Acct from "@/misc/acct.js"; -import type { Packed } from "./schema.js"; -import { Cache } from "./cache.js"; - -const blockingCache = new Cache(1000 * 60 * 5); - -// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている - -/** - * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい - */ -export async function checkHitAntenna( - antenna: Antenna, - note: Note | Packed<"Note">, - noteUser: { id: User["id"]; username: string; host: string | null }, - noteUserFollowers?: User["id"][], - antennaUserFollowing?: User["id"][], -): Promise { - if (note.visibility === "specified") return false; - - // アンテナ作成者がノート作成者にブロックされていたらスキップ - const blockings = await blockingCache.fetch(noteUser.id, () => - Blockings.findBy({ blockerId: noteUser.id }).then((res) => - res.map((x) => x.blockeeId), - ), - ); - if (blockings.some((blocking) => blocking === antenna.userId)) 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.findBy({ - userListId: antenna.userListId!, - }) - ).map((x) => x.userId); - - if (!listUsers.includes(note.userId)) return false; - } else if (antenna.src === "group") { - const joining = await UserGroupJoinings.findOneByOrFail({ - id: antenna.userGroupJoiningId!, - }); - - const groupUsers = ( - await UserGroupJoinings.findBy({ - userGroupId: joining.userGroupId, - }) - ).map((x) => x.userId); - - if (!groupUsers.includes(note.userId)) return false; - } else if (antenna.src === "users") { - const accts = antenna.users.map((x) => { - const { username, host } = Acct.parse(x); - return getFullApAccount(username, host).toLowerCase(); - }); - if ( - !accts.includes( - getFullApAccount(noteUser.username, noteUser.host).toLowerCase(), - ) - ) - return false; - } else if (antenna.src === "instances") { - const instances = antenna.instances - .filter((x) => x !== "") - .map((host) => { - return host.toLowerCase(); - }); - if (!instances.includes(noteUser.host?.toLowerCase() ?? "")) return false; - } - - const keywords = antenna.keywords - // Clean up - .map((xs) => xs.filter((x) => x !== "")) - .filter((xs) => xs.length > 0); - - if (keywords.length > 0) { - if (note.text == null) return false; - - const matched = keywords.some((and) => - and.every((keyword) => - antenna.caseSensitive - ? note.text!.includes(keyword) - : note.text!.toLowerCase().includes(keyword.toLowerCase()), - ), - ); - - if (!matched) return false; - } - - const excludeKeywords = antenna.excludeKeywords - // Clean up - .map((xs) => xs.filter((x) => x !== "")) - .filter((xs) => xs.length > 0); - - if (excludeKeywords.length > 0) { - if (note.text == null) return false; - - const matched = excludeKeywords.some((and) => - and.every((keyword) => - antenna.caseSensitive - ? note.text!.includes(keyword) - : note.text!.toLowerCase().includes(keyword.toLowerCase()), - ), - ); - - if (matched) return false; - } - - if (antenna.withFile) { - if (note.fileIds && note.fileIds.length === 0) return false; - } - - // TODO: eval expression - - return true; -} diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts deleted file mode 100644 index 53193d851a..0000000000 --- a/packages/backend/src/misc/check-word-mute.ts +++ /dev/null @@ -1,78 +0,0 @@ -import RE2 from "re2"; -import type { Note } from "@/models/entities/note.js"; -import type { User } from "@/models/entities/user.js"; - -type NoteLike = { - userId: Note["userId"]; - text: Note["text"]; - cw?: Note["cw"]; -}; - -type UserLike = { - id: User["id"]; -}; - -export type Muted = { - muted: boolean; - matched: string[]; -}; - -const NotMuted = { muted: false, matched: [] }; - -function escapeRegExp(x: string) { - return x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string -} - -export async function getWordMute( - note: NoteLike, - me: UserLike | null | undefined, - mutedWords: Array, -): Promise { - // 自分自身 - if (me && note.userId === me.id) { - return NotMuted; - } - - if (mutedWords.length > 0) { - const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim(); - - if (text === "") { - return NotMuted; - } - - for (const mutePattern of mutedWords) { - let mute: RE2; - let matched: string[]; - if (Array.isArray(mutePattern)) { - matched = mutePattern.filter((keyword) => keyword !== ""); - - if (matched.length === 0) { - continue; - } - mute = new RE2( - `\\b${matched.map(escapeRegExp).join("\\b.*\\b")}\\b`, - "g", - ); - } else { - const regexp = mutePattern.match(/^\/(.+)\/(.*)$/); - // This should never happen due to input sanitisation. - if (!regexp) { - console.warn(`Found invalid regex in word mutes: ${mutePattern}`); - continue; - } - mute = new RE2(regexp[1], regexp[2]); - matched = [mutePattern]; - } - - try { - if (mute.test(text)) { - return { muted: true, matched }; - } - } catch (err) { - // This should never happen due to input sanitisation. - } - } - } - - return NotMuted; -} diff --git a/packages/backend/src/misc/clone.ts b/packages/backend/src/misc/clone.ts deleted file mode 100644 index 4322e2e28f..0000000000 --- a/packages/backend/src/misc/clone.ts +++ /dev/null @@ -1,24 +0,0 @@ -// structredCloneが遅いため -// SEE: http://var.blog.jp/archives/86038606.html - -type Cloneable = - | string - | number - | boolean - | null - | { [key: string]: Cloneable } - | Cloneable[]; - -export function deepClone(x: T): T { - if (typeof x === "object") { - if (x === null) return x; - if (Array.isArray(x)) return x.map(deepClone) as T; - const obj = {} as Record; - for (const [k, v] of Object.entries(x)) { - obj[k] = deepClone(v); - } - return obj as T; - } else { - return x; - } -} diff --git a/packages/backend/src/misc/content-disposition.ts b/packages/backend/src/misc/content-disposition.ts deleted file mode 100644 index 25d6f58177..0000000000 --- a/packages/backend/src/misc/content-disposition.ts +++ /dev/null @@ -1,9 +0,0 @@ -import cd from "content-disposition"; - -export function contentDisposition( - type: "inline" | "attachment", - filename: string, -): string { - const fallback = filename.replace(/[^\w.-]/g, "_"); - return cd(filename, { type, fallback }); -} diff --git a/packages/backend/src/misc/convert-host.ts b/packages/backend/src/misc/convert-host.ts deleted file mode 100644 index 856ce3c127..0000000000 --- a/packages/backend/src/misc/convert-host.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { URL } from "node:url"; -import config from "@/config/index.js"; -import { toASCII } from "punycode"; - -export function getFullApAccount(username: string, host: string | null) { - return host - ? `${username}@${toPuny(host)}` - : `${username}@${toPuny(config.host)}`; -} - -export function isSelfHost(host: string) { - if (host == null) return true; - return toPuny(config.host) === toPuny(host); -} - -export function extractDbHost(uri: string) { - const url = new URL(uri); - return toPuny(url.hostname); -} - -export function toPuny(host: string) { - return toASCII(host.toLowerCase()); -} - -export function toPunyNullable(host: string | null | undefined): string | null { - if (host == null) return null; - return toASCII(host.toLowerCase()); -} diff --git a/packages/backend/src/misc/count-same-renotes.ts b/packages/backend/src/misc/count-same-renotes.ts deleted file mode 100644 index 45a6c1d35a..0000000000 --- a/packages/backend/src/misc/count-same-renotes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Notes } from "@/models/index.js"; - -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/packages/backend/src/misc/create-temp.ts b/packages/backend/src/misc/create-temp.ts deleted file mode 100644 index 16c85ee7bd..0000000000 --- a/packages/backend/src/misc/create-temp.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as tmp from "tmp"; - -export function createTemp(): Promise<[string, () => void]> { - return new Promise<[string, () => void]>((res, rej) => { - tmp.file((e, path, fd, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }); - }); -} - -export function createTempDir(): Promise<[string, () => void]> { - return new Promise<[string, () => void]>((res, rej) => { - tmp.dir( - { - unsafeCleanup: true, - }, - (e, path, cleanup) => { - if (e) return rej(e); - res([path, cleanup]); - }, - ); - }); -} diff --git a/packages/backend/src/misc/detect-url-mime.ts b/packages/backend/src/misc/detect-url-mime.ts deleted file mode 100644 index 9f0e4325d9..0000000000 --- a/packages/backend/src/misc/detect-url-mime.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createTemp } from "./create-temp.js"; -import { downloadUrl } from "./download-url.js"; -import { detectType } from "./get-file-info.js"; - -export async function detectUrlMime(url: string) { - const [path, cleanup] = await createTemp(); - - try { - await downloadUrl(url, path); - const { mime } = await detectType(path); - return mime; - } finally { - cleanup(); - } -} diff --git a/packages/backend/src/misc/download-text-file.ts b/packages/backend/src/misc/download-text-file.ts deleted file mode 100644 index 9d3821b20f..0000000000 --- a/packages/backend/src/misc/download-text-file.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as fs from "node:fs"; -import * as util from "node:util"; -import Logger from "@/services/logger.js"; -import { createTemp } from "./create-temp.js"; -import { downloadUrl } from "./download-url.js"; - -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/packages/backend/src/misc/download-url.ts b/packages/backend/src/misc/download-url.ts deleted file mode 100644 index 7fafb635ba..0000000000 --- a/packages/backend/src/misc/download-url.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as fs from "node:fs"; -import * as stream from "node:stream"; -import * as util from "node:util"; -import got, * as Got from "got"; -import { httpAgent, httpsAgent, StatusError } from "./fetch.js"; -import config from "@/config/index.js"; -import chalk from "chalk"; -import Logger from "@/services/logger.js"; -import IPCIDR from "ip-cidr"; -import PrivateIp from "private-ip"; - -const pipeline = util.promisify(stream.pipeline); - -export async function downloadUrl(url: string, path: string): Promise { - 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: { - limit: 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): boolean { - for (const net of config.allowedPrivateNetworks || []) { - const cidr = new IPCIDR(net); - if (cidr.contains(ip)) { - return false; - } - } - - return PrivateIp(ip); -} diff --git a/packages/backend/src/misc/emoji-regex.ts b/packages/backend/src/misc/emoji-regex.ts deleted file mode 100644 index 08b44788de..0000000000 --- a/packages/backend/src/misc/emoji-regex.ts +++ /dev/null @@ -1,5 +0,0 @@ -import twemoji from "twemoji-parser/dist/lib/regex.js"; -const twemojiRegex = twemoji.default; - -export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); -export const emojiRegexAtStartToEnd = new RegExp(`^(${twemojiRegex.source})$`); diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts deleted file mode 100644 index 7de32e6d60..0000000000 --- a/packages/backend/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.js"; - -export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { - const emojiNodes = mfm.extract(nodes, (node) => { - return node.type === "emojiCode" && node.props.name.length <= 100; - }); - - return unique(emojiNodes.map((x) => x.props.name)); -} diff --git a/packages/backend/src/misc/extract-hashtags.ts b/packages/backend/src/misc/extract-hashtags.ts deleted file mode 100644 index 826e36221b..0000000000 --- a/packages/backend/src/misc/extract-hashtags.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as mfm from "mfm-js"; -import { unique } from "@/prelude/array.js"; - -export function extractHashtags(nodes: mfm.MfmNode[]): string[] { - const hashtagNodes = mfm.extract(nodes, (node) => node.type === "hashtag"); - const hashtags = unique(hashtagNodes.map((x) => x.props.hashtag)); - - return hashtags; -} diff --git a/packages/backend/src/misc/extract-mentions.ts b/packages/backend/src/misc/extract-mentions.ts deleted file mode 100644 index 259f78e576..0000000000 --- a/packages/backend/src/misc/extract-mentions.ts +++ /dev/null @@ -1,13 +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/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts deleted file mode 100644 index 32c45813ca..0000000000 --- a/packages/backend/src/misc/fetch-meta.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Meta } from "@/models/entities/meta.js"; - -let cache: Meta; - -export async function fetchMeta(noCache = false): Promise { - if (!noCache && cache) return cache; - - return await db.transaction(async (transactionalEntityManager) => { - // New IDs are prioritized because multiple records may have been created due to past bugs. - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: "DESC", - }, - }); - - const meta = metas[0]; - - if (meta) { - cache = meta; - return meta; - } else { - // If fetchMeta is called at the same time when meta is empty, this part may be called at the same time, so use fail-safe upsert. - const saved = await transactionalEntityManager - .upsert( - Meta, - { - id: "x", - }, - ["id"], - ) - .then((x) => - transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]), - ); - - cache = saved; - return saved; - } - }); -} - -setInterval(() => { - fetchMeta(true).then((meta) => { - cache = meta; - }); -}, 1000 * 10); diff --git a/packages/backend/src/misc/fetch-proxy-account.ts b/packages/backend/src/misc/fetch-proxy-account.ts deleted file mode 100644 index a277db6fb9..0000000000 --- a/packages/backend/src/misc/fetch-proxy-account.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { fetchMeta } from "./fetch-meta.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; - -export async function fetchProxyAccount(): Promise { - const meta = await fetchMeta(); - if (meta.proxyAccountId == null) return null; - return (await Users.findOneByOrFail({ - id: meta.proxyAccountId, - })) as ILocalUser; -} diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts deleted file mode 100644 index 0e673ba3a8..0000000000 --- a/packages/backend/src/misc/fetch.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as http from "node:http"; -import * as https from "node:https"; -import type { URL } from "node:url"; -import CacheableLookup from "cacheable-lookup"; -import fetch from "node-fetch"; -import { HttpProxyAgent, HttpsProxyAgent } from "hpagent"; -import config from "@/config/index.js"; - -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/packages/backend/src/misc/gen-id.ts b/packages/backend/src/misc/gen-id.ts deleted file mode 100644 index b7cc0965a1..0000000000 --- a/packages/backend/src/misc/gen-id.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ulid } from "ulid"; -import { genAid } from "./id/aid.js"; -import { genMeid } from "./id/meid.js"; -import { genMeidg } from "./id/meidg.js"; -import { genObjectId } from "./id/object-id.js"; -import config from "@/config/index.js"; - -const metohd = config.id.toLowerCase(); - -export function genId(date?: Date): string { - if (!date || date > new Date()) date = new Date(); - - switch (metohd) { - case "aid": - return genAid(date); - case "meid": - return genMeid(date); - case "meidg": - return genMeidg(date); - case "ulid": - return ulid(date.getTime()); - case "objectid": - return genObjectId(date); - default: - throw new Error("unrecognized id generation method"); - } -} diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts deleted file mode 100644 index 1e51dfe2ac..0000000000 --- a/packages/backend/src/misc/gen-identicon.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Identicon generator - * https://en.wikipedia.org/wiki/Identicon - */ - -import type { WriteStream } from "node:fs"; -import * as p from "pureimage"; -import gen from "random-seed"; - -const size = 128; // px -const n = 5; // resolution -const margin = size / 4; -const colors = [ - ["#eb6f92", "#b4637a"], - ["#f6c177", "#ea9d34"], - ["#ebbcba", "#d7827e"], - ["#9ccfd8", "#56949f"], - ["#c4a7e7", "#907aa9"], - ["#eb6f92", "#f6c177"], - ["#eb6f92", "#ebbcba"], - ["#eb6f92", "#31748f"], - ["#eb6f92", "#9ccfd8"], - ["#eb6f92", "#c4a7e7"], - ["#f6c177", "#eb6f92"], - ["#f6c177", "#ebbcba"], - ["#f6c177", "#31748f"], - ["#f6c177", "#9ccfd8"], - ["#f6c177", "#c4a7e7"], - ["#ebbcba", "#eb6f92"], - ["#ebbcba", "#f6c177"], - ["#ebbcba", "#31748f"], - ["#ebbcba", "#9ccfd8"], - ["#ebbcba", "#c4a7e7"], - ["#31748f", "#eb6f92"], - ["#31748f", "#f6c177"], - ["#31748f", "#ebbcba"], - ["#31748f", "#9ccfd8"], - ["#31748f", "#c4a7e7"], - ["#9ccfd8", "#eb6f92"], - ["#9ccfd8", "#f6c177"], - ["#9ccfd8", "#ebbcba"], - ["#9ccfd8", "#31748f"], - ["#9ccfd8", "#c4a7e7"], - ["#c4a7e7", "#eb6f92"], - ["#c4a7e7", "#f6c177"], - ["#c4a7e7", "#ebbcba"], - ["#c4a7e7", "#31748f"], - ["#c4a7e7", "#9ccfd8"], -]; - -const actualSize = size - margin * 2; -const cellSize = actualSize / n; -const sideN = Math.floor(n / 2); - -/** - * Generate buffer of an identicon by seed - */ -export function genIdenticon(seed: string, stream: WriteStream): Promise { - const rand = gen.create(seed); - const canvas = p.make(size, size, undefined); - const ctx = canvas.getContext("2d"); - - const bgColors = colors[rand(colors.length)]; - - const bg = ctx.createLinearGradient(0, 0, size, size); - bg.addColorStop(0, bgColors[0]); - bg.addColorStop(1, bgColors[1]); - - ctx.fillStyle = bg; - ctx.beginPath(); - ctx.fillRect(0, 0, size, size); - - ctx.fillStyle = "#ffffff"; - - // 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); - - for (let x = 0; x < side.length; x++) { - for (let y = 0; y < side[x].length; y++) { - side[x][y] = rand(3) === 0; - } - } - - for (let i = 0; i < center.length; i++) { - center[i] = rand(3) === 0; - } - - // Draw - for (let x = 0; x < n; x++) { - for (let y = 0; y < n; y++) { - const isXCenter = x === (n - 1) / 2; - if (isXCenter && !center[y]) continue; - - const isLeftSide = x < (n - 1) / 2; - if (isLeftSide && !side[x][y]) continue; - - const isRightSide = x > (n - 1) / 2; - if (isRightSide && !side[sideN - (x - sideN)][y]) continue; - - const actualX = margin + cellSize * x; - const actualY = margin + cellSize * y; - ctx.beginPath(); - ctx.fillRect(actualX, actualY, cellSize, cellSize); - } - } - - return p.encodePNGToStream(canvas, stream); -} diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts deleted file mode 100644 index 8ae4175e30..0000000000 --- a/packages/backend/src/misc/gen-key-pair.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as crypto from "node:crypto"; -import * as util from "node:util"; - -const generateKeyPair = util.promisify(crypto.generateKeyPair); - -export async function genRsaKeyPair(modulusLength = 2048) { - return await generateKeyPair("rsa", { - modulusLength, - publicKeyEncoding: { - type: "spki", - format: "pem", - }, - privateKeyEncoding: { - type: "pkcs8", - format: "pem", - cipher: undefined, - passphrase: undefined, - }, - }); -} - -export async function genEcKeyPair( - namedCurve: - | "prime256v1" - | "secp384r1" - | "secp521r1" - | "curve25519" = "prime256v1", -) { - return await generateKeyPair("ec", { - namedCurve, - publicKeyEncoding: { - type: "spki", - format: "pem", - }, - privateKeyEncoding: { - type: "pkcs8", - format: "pem", - cipher: undefined, - passphrase: undefined, - }, - }); -} diff --git a/packages/backend/src/misc/get-file-info.ts b/packages/backend/src/misc/get-file-info.ts deleted file mode 100644 index a63de286ea..0000000000 --- a/packages/backend/src/misc/get-file-info.ts +++ /dev/null @@ -1,443 +0,0 @@ -import * as fs from "node:fs"; -import * as crypto from "node:crypto"; -import { join } from "node:path"; -import * as stream from "node:stream"; -import * as util from "node:util"; -import { FSWatcher } from "chokidar"; -import { fileTypeFromFile } from "file-type"; -import FFmpeg from "fluent-ffmpeg"; -import isSvg from "is-svg"; -import probeImageSize from "probe-image-size"; -import { type predictionType } from "nsfwjs"; -import sharp from "sharp"; -import { encode } from "blurhash"; -import { detectSensitive } from "@/services/detect-sensitive.js"; -import { createTempDir } from "./create-temp.js"; - -const pipeline = util.promisify(stream.pipeline); - -export type FileInfo = { - size: number; - md5: string; - type: { - mime: string; - ext: string | null; - }; - width?: number; - height?: number; - orientation?: number; - blurhash?: string; - sensitive: boolean; - porn: boolean; - 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, - opts: { - skipSensitiveDetection: boolean; - sensitiveThreshold?: number; - sensitiveThresholdForPorn?: number; - enableSensitiveMediaDetectionForVideos?: boolean; - }, -): 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; - let orientation: number | undefined; - - if ( - [ - "image/jpeg", - "image/gif", - "image/png", - "image/apng", - "image/webp", - "image/bmp", - "image/tiff", - "image/svg+xml", - "image/vnd.adobe.photoshop", - "image/avif", - ].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; - orientation = imageSize.orientation; - - // 制限を超えている画像は 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", - "image/avif", - ].includes(type.mime) - ) { - blurhash = await getBlurhash(path).catch((e) => { - warnings.push(`getBlurhash failed: ${e}`); - return undefined; - }); - } - - let sensitive = false; - let porn = false; - - if (!opts.skipSensitiveDetection) { - await detectSensitivity( - path, - type.mime, - opts.sensitiveThreshold ?? 0.5, - opts.sensitiveThresholdForPorn ?? 0.75, - opts.enableSensitiveMediaDetectionForVideos ?? false, - ).then( - (value) => { - [sensitive, porn] = value; - }, - (error) => { - warnings.push(`detectSensitivity failed: ${error}`); - }, - ); - } - - return { - size, - md5, - type, - width, - height, - orientation, - blurhash, - sensitive, - porn, - warnings, - }; -} - -async function detectSensitivity( - source: string, - mime: string, - sensitiveThreshold: number, - sensitiveThresholdForPorn: number, - analyzeVideo: boolean, -): Promise<[sensitive: boolean, porn: boolean]> { - let sensitive = false; - let porn = false; - - function judgePrediction( - result: readonly predictionType[], - ): [sensitive: boolean, porn: boolean] { - let sensitive = false; - let porn = false; - - if ( - (result.find((x) => x.className === "Sexy")?.probability ?? 0) > - sensitiveThreshold - ) - sensitive = true; - if ( - (result.find((x) => x.className === "Hentai")?.probability ?? 0) > - sensitiveThreshold - ) - sensitive = true; - if ( - (result.find((x) => x.className === "Porn")?.probability ?? 0) > - sensitiveThreshold - ) - sensitive = true; - - if ( - (result.find((x) => x.className === "Porn")?.probability ?? 0) > - sensitiveThresholdForPorn - ) - porn = true; - - return [sensitive, porn]; - } - - if (["image/jpeg", "image/png", "image/webp"].includes(mime)) { - const result = await detectSensitive(source); - if (result) { - [sensitive, porn] = judgePrediction(result); - } - } else if ( - analyzeVideo && - (mime === "image/apng" || mime.startsWith("video/")) - ) { - const [outDir, disposeOutDir] = await createTempDir(); - try { - const command = FFmpeg() - .input(source) - .inputOptions([ - "-skip_frame", - "nokey", // 可能ならキーフレームのみを取得してほしいとする(そうなるとは限らない) - "-lowres", - "3", // 元の画質でデコードする必要はないので 1/8 画質でデコードしてもよいとする(そうなるとは限らない) - ]) - .noAudio() - .videoFilters([ - { - filter: "select", // フレームのフィルタリング - options: { - e: "eq(pict_type,PICT_TYPE_I)", // I-Frame のみをフィルタする(VP9 とかはデコードしてみないとわからないっぽい) - }, - }, - { - filter: "blackframe", // 暗いフレームの検出 - options: { - amount: "0", // 暗さに関わらず全てのフレームで測定値を取る - }, - }, - { - filter: "metadata", - options: { - mode: "select", // フレーム選択モード - key: "lavfi.blackframe.pblack", // フレームにおける暗部の百分率(前のフィルタからのメタデータを参照する) - value: "50", - function: "less", // 50% 未満のフレームを選択する(50% 以上暗部があるフレームだと誤検知を招くかもしれないので) - }, - }, - { - filter: "scale", - options: { - w: 299, - h: 299, - }, - }, - ]) - .format("image2") - .output(join(outDir, "%d.png")) - .outputOptions(["-vsync", "0"]); // 可変フレームレートにすることで穴埋めをさせない - const results: ReturnType[] = []; - let frameIndex = 0; - let targetIndex = 0; - let nextIndex = 1; - for await (const path of asyncIterateFrames(outDir, command)) { - try { - const index = frameIndex++; - if (index !== targetIndex) { - continue; - } - targetIndex = nextIndex; - nextIndex += index; // fibonacci sequence によってフレーム数制限を掛ける - const result = await detectSensitive(path); - if (result) { - results.push(judgePrediction(result)); - } - } finally { - fs.promises.unlink(path); - } - } - sensitive = - results.filter((x) => x[0]).length >= - Math.ceil(results.length * sensitiveThreshold); - porn = - results.filter((x) => x[1]).length >= - Math.ceil(results.length * sensitiveThresholdForPorn); - } finally { - disposeOutDir(); - } - } - - return [sensitive, porn]; -} - -async function* asyncIterateFrames( - cwd: string, - command: FFmpeg.FfmpegCommand, -): AsyncGenerator { - const watcher = new FSWatcher({ - cwd, - disableGlobbing: true, - }); - let finished = false; - command.once("end", () => { - finished = true; - watcher.close(); - }); - command.run(); - for (let i = 1; true; i++) { - const current = `${i}.png`; - const next = `${i + 1}.png`; - const framePath = join(cwd, current); - if (await exists(join(cwd, next))) { - yield framePath; - } else if (!finished) { - watcher.add(next); - await new Promise((resolve, reject) => { - watcher.on("add", function onAdd(path) { - if (path === next) { - // 次フレームの書き出しが始まっているなら、現在フレームの書き出しは終わっている - watcher.unwatch(current); - watcher.off("add", onAdd); - resolve(); - } - }); - command.once("end", resolve); // 全てのフレームを処理し終わったなら、最終フレームである現在フレームの書き出しは終わっている - command.once("error", reject); - }); - yield framePath; - } else if (await exists(framePath)) { - yield framePath; - } else { - return; - } - } -} - -function exists(path: string): Promise { - return fs.promises.access(path).then( - () => true, - () => false, - ); -} - -/** - * Detect MIME Type and extension - */ -export async function detectType(path: string): Promise<{ - mime: string; - ext: string | null; -}> { - // Check 0 byte - const fileSize = await getFileSize(path); - if (fileSize === 0) { - return TYPE_OCTET_STREAM; - } - - const type = await fileTypeFromFile(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; - orientation?: number; -}> { - 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/packages/backend/src/misc/get-ip-hash.ts b/packages/backend/src/misc/get-ip-hash.ts deleted file mode 100644 index 3bafaee5df..0000000000 --- a/packages/backend/src/misc/get-ip-hash.ts +++ /dev/null @@ -1,14 +0,0 @@ -import IPCIDR from "ip-cidr"; - -export function getIpHash(ip: string) { - try { - // because a single person may control many IPv6 addresses, - // only a /64 subnet prefix of any IP will be taken into account. - // (this means for IPv4 the entire address is used) - const prefix = IPCIDR.createAddress(ip).mask(64); - return `ip-${BigInt(`0b${prefix}`).toString(36)}`; - } catch (e) { - const prefix = IPCIDR.createAddress(ip.replace(/:[0-9]+$/, "")).mask(64); - return `ip-${BigInt(`0b${prefix}`).toString(36)}`; - } -} diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts deleted file mode 100644 index 446e3fc140..0000000000 --- a/packages/backend/src/misc/get-note-summary.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Packed } from "./schema.js"; - -/** - * 投稿を表す文字列を取得します。 - * @param {*} note (packされた)投稿 - */ -export const getNoteSummary = (note: Packed<"Note">): string => { - if (note.deletedAt) { - return "❌"; - } - - let summary = ""; - - // 本文 - if (note.cw != null) { - summary += note.cw; - } else { - summary += note.text ? note.text : ""; - } - - // ファイルが添付されているとき - if ((note.files || []).length !== 0) { - summary += ` (📎${note.files!.length})`; - } - - // 投票が添付されているとき - if (note.poll) { - summary += " (📊)"; - } - - /* - // 返信のとき - if (note.replyId) { - if (note.reply) { - summary += `\n\nRE: ${getNoteSummary(note.reply)}`; - } else { - summary += '\n\nRE: ...'; - } - } - - // Renoteのとき - if (note.renoteId) { - if (note.renote) { - summary += `\n\nRN: ${getNoteSummary(note.renote)}`; - } else { - summary += '\n\nRN: ...'; - } - } - */ - - return summary.trim(); -}; diff --git a/packages/backend/src/misc/get-reaction-emoji.ts b/packages/backend/src/misc/get-reaction-emoji.ts deleted file mode 100644 index 71521c4ae8..0000000000 --- a/packages/backend/src/misc/get-reaction-emoji.ts +++ /dev/null @@ -1,28 +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/packages/backend/src/misc/hard-limits.ts b/packages/backend/src/misc/hard-limits.ts deleted file mode 100644 index 51d2c0f5d2..0000000000 --- a/packages/backend/src/misc/hard-limits.ts +++ /dev/null @@ -1,13 +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 = 8192; diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts deleted file mode 100644 index 742bdb0f69..0000000000 --- a/packages/backend/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/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts deleted file mode 100644 index a12360360b..0000000000 --- a/packages/backend/src/misc/id/aid.ts +++ /dev/null @@ -1,25 +0,0 @@ -// AID -// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列] - -import * as crypto from "node:crypto"; - -const TIME2000 = 946684800000; -let counter = crypto.randomBytes(2).readUInt16LE(0); - -function getTime(time: number) { - time = time - TIME2000; - if (time < 0) time = 0; - - return time.toString(36).padStart(8, "0"); -} - -function getNoise() { - return counter.toString(36).padStart(2, "0").slice(-2); -} - -export function genAid(date: Date): string { - const t = date.getTime(); - if (isNaN(t)) throw "Failed to create AID: Invalid Date"; - counter++; - return getTime(t) + getNoise(); -} diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts deleted file mode 100644 index ee78eb8d14..0000000000 --- a/packages/backend/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/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts deleted file mode 100644 index 4fd39a8b41..0000000000 --- a/packages/backend/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/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts deleted file mode 100644 index 45822f0acc..0000000000 --- a/packages/backend/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/packages/backend/src/misc/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts deleted file mode 100644 index be6eb5bd8d..0000000000 --- a/packages/backend/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/packages/backend/src/misc/is-duplicate-key-value-error.ts b/packages/backend/src/misc/is-duplicate-key-value-error.ts deleted file mode 100644 index 18d22bb77c..0000000000 --- a/packages/backend/src/misc/is-duplicate-key-value-error.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isDuplicateKeyValueError(e: unknown | Error): boolean { - return (e as Error).message?.startsWith("duplicate key value"); -} diff --git a/packages/backend/src/misc/is-instance-muted.ts b/packages/backend/src/misc/is-instance-muted.ts deleted file mode 100644 index 1547d4555c..0000000000 --- a/packages/backend/src/misc/is-instance-muted.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Packed } from "./schema.js"; - -export function isInstanceMuted( - note: Packed<"Note">, - mutedInstances: Set, -): boolean { - if (mutedInstances.has(note?.user?.host ?? "")) return true; - if (mutedInstances.has(note?.reply?.user?.host ?? "")) return true; - if (mutedInstances.has(note?.renote?.user?.host ?? "")) return true; - - return false; -} - -export function isUserFromMutedInstance( - notif: Packed<"Notification">, - mutedInstances: Set, -): boolean { - if (mutedInstances.has(notif?.user?.host ?? "")) return true; - - return false; -} diff --git a/packages/backend/src/misc/is-mime-image.ts b/packages/backend/src/misc/is-mime-image.ts deleted file mode 100644 index a8ba62ec20..0000000000 --- a/packages/backend/src/misc/is-mime-image.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { FILE_TYPE_BROWSERSAFE } from "@/const.js"; - -const dictionary = { - "safe-file": FILE_TYPE_BROWSERSAFE, - "sharp-convertible-image": [ - "image/jpeg", - "image/png", - "image/gif", - "image/apng", - "image/vnd.mozilla.apng", - "image/webp", - "image/svg+xml", - "image/avif", - ], -}; - -export const isMimeImage = ( - mime: string, - type: keyof typeof dictionary, -): boolean => dictionary[type].includes(mime); diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts deleted file mode 100644 index fe83a56a55..0000000000 --- a/packages/backend/src/misc/is-quote.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Note } from "@/models/entities/note.js"; - -export default function (note: Note): boolean { - return ( - note.renoteId != null && - (note.text != null || - note.hasPoll || - (note.fileIds != null && note.fileIds.length > 0)) - ); -} diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts deleted file mode 100644 index 64591cfef3..0000000000 --- a/packages/backend/src/misc/is-user-related.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function isUserRelated(note: any, ids: Set): boolean { - if (ids.has(note.userId)) return true; // note author is muted - if (note.mentions?.some((user: string) => ids.has(user))) return true; // any of mentioned users are muted - if (note.reply && isUserRelated(note.reply, ids)) return true; // also check reply target - if (note.renote && isUserRelated(note.renote, ids)) return true; // also check renote target - return false; -} diff --git a/packages/backend/src/misc/keypair-store.ts b/packages/backend/src/misc/keypair-store.ts deleted file mode 100644 index 4551bfd988..0000000000 --- a/packages/backend/src/misc/keypair-store.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { UserKeypairs } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import type { UserKeypair } from "@/models/entities/user-keypair.js"; -import { Cache } from "./cache.js"; - -const cache = new Cache(Infinity); - -export async function getUserKeypair(userId: User["id"]): Promise { - return await cache.fetch(userId, () => - UserKeypairs.findOneByOrFail({ userId: userId }), - ); -} diff --git a/packages/backend/src/misc/langmap.ts b/packages/backend/src/misc/langmap.ts deleted file mode 100644 index 106130d3c3..0000000000 --- a/packages/backend/src/misc/langmap.ts +++ /dev/null @@ -1,666 +0,0 @@ -// TODO: sharedに置いてフロントエンドのと統合したい -export const langmap = { - ach: { - nativeName: "Lwo", - }, - ady: { - nativeName: "Адыгэбзэ", - }, - af: { - nativeName: "Afrikaans", - }, - "af-NA": { - nativeName: "Afrikaans (Namibia)", - }, - "af-ZA": { - nativeName: "Afrikaans (South Africa)", - }, - ak: { - nativeName: "Tɕɥi", - }, - ar: { - nativeName: "العربية", - }, - "ar-AR": { - nativeName: "العربية", - }, - "ar-MA": { - nativeName: "العربية", - }, - "ar-SA": { - nativeName: "العربية (السعودية)", - }, - "ay-BO": { - nativeName: "Aymar aru", - }, - az: { - nativeName: "Azərbaycan dili", - }, - "az-AZ": { - nativeName: "Azərbaycan dili", - }, - "be-BY": { - nativeName: "Беларуская", - }, - bg: { - nativeName: "Български", - }, - "bg-BG": { - nativeName: "Български", - }, - bn: { - nativeName: "বাংলা", - }, - "bn-IN": { - nativeName: "বাংলা (ভারত)", - }, - "bn-BD": { - nativeName: "বাংলা(বাংলাদেশ)", - }, - br: { - nativeName: "Brezhoneg", - }, - "bs-BA": { - nativeName: "Bosanski", - }, - ca: { - nativeName: "Català", - }, - "ca-ES": { - nativeName: "Català", - }, - cak: { - nativeName: "Maya Kaqchikel", - }, - "ck-US": { - nativeName: "ᏣᎳᎩ (tsalagi)", - }, - cs: { - nativeName: "Čeština", - }, - "cs-CZ": { - nativeName: "Čeština", - }, - cy: { - nativeName: "Cymraeg", - }, - "cy-GB": { - nativeName: "Cymraeg", - }, - da: { - nativeName: "Dansk", - }, - "da-DK": { - nativeName: "Dansk", - }, - de: { - nativeName: "Deutsch", - }, - "de-AT": { - nativeName: "Deutsch (Österreich)", - }, - "de-DE": { - nativeName: "Deutsch (Deutschland)", - }, - "de-CH": { - nativeName: "Deutsch (Schweiz)", - }, - dsb: { - nativeName: "Dolnoserbšćina", - }, - el: { - nativeName: "Ελληνικά", - }, - "el-GR": { - nativeName: "Ελληνικά", - }, - en: { - nativeName: "English", - }, - "en-GB": { - nativeName: "English (UK)", - }, - "en-AU": { - nativeName: "English (Australia)", - }, - "en-CA": { - nativeName: "English (Canada)", - }, - "en-IE": { - nativeName: "English (Ireland)", - }, - "en-IN": { - nativeName: "English (India)", - }, - "en-PI": { - nativeName: "English (Pirate)", - }, - "en-SG": { - nativeName: "English (Singapore)", - }, - "en-UD": { - nativeName: "English (Upside Down)", - }, - "en-US": { - nativeName: "English (US)", - }, - "en-ZA": { - nativeName: "English (South Africa)", - }, - "en@pirate": { - nativeName: "English (Pirate)", - }, - eo: { - nativeName: "Esperanto", - }, - "eo-EO": { - nativeName: "Esperanto", - }, - es: { - nativeName: "Español", - }, - "es-AR": { - nativeName: "Español (Argentine)", - }, - "es-419": { - nativeName: "Español (Latinoamérica)", - }, - "es-CL": { - nativeName: "Español (Chile)", - }, - "es-CO": { - nativeName: "Español (Colombia)", - }, - "es-EC": { - nativeName: "Español (Ecuador)", - }, - "es-ES": { - nativeName: "Español (España)", - }, - "es-LA": { - nativeName: "Español (Latinoamérica)", - }, - "es-NI": { - nativeName: "Español (Nicaragua)", - }, - "es-MX": { - nativeName: "Español (México)", - }, - "es-US": { - nativeName: "Español (Estados Unidos)", - }, - "es-VE": { - nativeName: "Español (Venezuela)", - }, - et: { - nativeName: "eesti keel", - }, - "et-EE": { - nativeName: "Eesti (Estonia)", - }, - eu: { - nativeName: "Euskara", - }, - "eu-ES": { - nativeName: "Euskara", - }, - fa: { - nativeName: "فارسی", - }, - "fa-IR": { - nativeName: "فارسی", - }, - "fb-LT": { - nativeName: "Leet Speak", - }, - ff: { - nativeName: "Fulah", - }, - fi: { - nativeName: "Suomi", - }, - "fi-FI": { - nativeName: "Suomi", - }, - fo: { - nativeName: "Føroyskt", - }, - "fo-FO": { - nativeName: "Føroyskt (Færeyjar)", - }, - fr: { - nativeName: "Français", - }, - "fr-CA": { - nativeName: "Français (Canada)", - }, - "fr-FR": { - nativeName: "Français (France)", - }, - "fr-BE": { - nativeName: "Français (Belgique)", - }, - "fr-CH": { - nativeName: "Français (Suisse)", - }, - "fy-NL": { - nativeName: "Frysk", - }, - ga: { - nativeName: "Gaeilge", - }, - "ga-IE": { - nativeName: "Gaeilge", - }, - gd: { - nativeName: "Gàidhlig", - }, - gl: { - nativeName: "Galego", - }, - "gl-ES": { - nativeName: "Galego", - }, - "gn-PY": { - nativeName: "Avañe'ẽ", - }, - "gu-IN": { - nativeName: "ગુજરાતી", - }, - gv: { - nativeName: "Gaelg", - }, - "gx-GR": { - nativeName: "Ἑλληνική ἀρχαία", - }, - he: { - nativeName: "עברית‏", - }, - "he-IL": { - nativeName: "עברית‏", - }, - hi: { - nativeName: "हिन्दी", - }, - "hi-IN": { - nativeName: "हिन्दी", - }, - hr: { - nativeName: "Hrvatski", - }, - "hr-HR": { - nativeName: "Hrvatski", - }, - hsb: { - nativeName: "Hornjoserbšćina", - }, - ht: { - nativeName: "Kreyòl", - }, - hu: { - nativeName: "Magyar", - }, - "hu-HU": { - nativeName: "Magyar", - }, - hy: { - nativeName: "Հայերեն", - }, - "hy-AM": { - nativeName: "Հայերեն (Հայաստան)", - }, - id: { - nativeName: "Bahasa Indonesia", - }, - "id-ID": { - nativeName: "Bahasa Indonesia", - }, - is: { - nativeName: "Íslenska", - }, - "is-IS": { - nativeName: "Íslenska (Iceland)", - }, - it: { - nativeName: "Italiano", - }, - "it-IT": { - nativeName: "Italiano", - }, - ja: { - nativeName: "日本語", - }, - "ja-JP": { - nativeName: "日本語 (日本)", - }, - "jv-ID": { - nativeName: "Basa Jawa", - }, - "ka-GE": { - nativeName: "ქართული", - }, - "kk-KZ": { - nativeName: "Қазақша", - }, - km: { - nativeName: "ភាសាខ្មែរ", - }, - kl: { - nativeName: "kalaallisut", - }, - "km-KH": { - nativeName: "ភាសាខ្មែរ", - }, - kab: { - nativeName: "Taqbaylit", - }, - kn: { - nativeName: "ಕನ್ನಡ", - }, - "kn-IN": { - nativeName: "ಕನ್ನಡ (India)", - }, - ko: { - nativeName: "한국어", - }, - "ko-KR": { - nativeName: "한국어 (한국)", - }, - "ku-TR": { - nativeName: "Kurdî", - }, - kw: { - nativeName: "Kernewek", - }, - la: { - nativeName: "Latin", - }, - "la-VA": { - nativeName: "Latin", - }, - lb: { - nativeName: "Lëtzebuergesch", - }, - "li-NL": { - nativeName: "Lèmbörgs", - }, - lt: { - nativeName: "Lietuvių", - }, - "lt-LT": { - nativeName: "Lietuvių", - }, - lv: { - nativeName: "Latviešu", - }, - "lv-LV": { - nativeName: "Latviešu", - }, - mai: { - nativeName: "मैथिली, মৈথিলী", - }, - "mg-MG": { - nativeName: "Malagasy", - }, - mk: { - nativeName: "Македонски", - }, - "mk-MK": { - nativeName: "Македонски (Македонски)", - }, - ml: { - nativeName: "മലയാളം", - }, - "ml-IN": { - nativeName: "മലയാളം", - }, - "mn-MN": { - nativeName: "Монгол", - }, - mr: { - nativeName: "मराठी", - }, - "mr-IN": { - nativeName: "मराठी", - }, - ms: { - nativeName: "Bahasa Melayu", - }, - "ms-MY": { - nativeName: "Bahasa Melayu", - }, - mt: { - nativeName: "Malti", - }, - "mt-MT": { - nativeName: "Malti", - }, - my: { - nativeName: "ဗမာစကာ", - }, - no: { - nativeName: "Norsk", - }, - nb: { - nativeName: "Norsk (bokmål)", - }, - "nb-NO": { - nativeName: "Norsk (bokmål)", - }, - ne: { - nativeName: "नेपाली", - }, - "ne-NP": { - nativeName: "नेपाली", - }, - nl: { - nativeName: "Nederlands", - }, - "nl-BE": { - nativeName: "Nederlands (België)", - }, - "nl-NL": { - nativeName: "Nederlands (Nederland)", - }, - "nn-NO": { - nativeName: "Norsk (nynorsk)", - }, - oc: { - nativeName: "Occitan", - }, - "or-IN": { - nativeName: "ଓଡ଼ିଆ", - }, - pa: { - nativeName: "ਪੰਜਾਬੀ", - }, - "pa-IN": { - nativeName: "ਪੰਜਾਬੀ (ਭਾਰਤ ਨੂੰ)", - }, - pl: { - nativeName: "Polski", - }, - "pl-PL": { - nativeName: "Polski", - }, - "ps-AF": { - nativeName: "پښتو", - }, - pt: { - nativeName: "Português", - }, - "pt-BR": { - nativeName: "Português (Brasil)", - }, - "pt-PT": { - nativeName: "Português (Portugal)", - }, - "qu-PE": { - nativeName: "Qhichwa", - }, - "rm-CH": { - nativeName: "Rumantsch", - }, - ro: { - nativeName: "Română", - }, - "ro-RO": { - nativeName: "Română", - }, - ru: { - nativeName: "Русский", - }, - "ru-RU": { - nativeName: "Русский", - }, - "sa-IN": { - nativeName: "संस्कृतम्", - }, - "se-NO": { - nativeName: "Davvisámegiella", - }, - sh: { - nativeName: "српскохрватски", - }, - "si-LK": { - nativeName: "සිංහල", - }, - sk: { - nativeName: "Slovenčina", - }, - "sk-SK": { - nativeName: "Slovenčina (Slovakia)", - }, - sl: { - nativeName: "Slovenščina", - }, - "sl-SI": { - nativeName: "Slovenščina", - }, - "so-SO": { - nativeName: "Soomaaliga", - }, - sq: { - nativeName: "Shqip", - }, - "sq-AL": { - nativeName: "Shqip", - }, - sr: { - nativeName: "Српски", - }, - "sr-RS": { - nativeName: "Српски (Serbia)", - }, - su: { - nativeName: "Basa Sunda", - }, - sv: { - nativeName: "Svenska", - }, - "sv-SE": { - nativeName: "Svenska", - }, - sw: { - nativeName: "Kiswahili", - }, - "sw-KE": { - nativeName: "Kiswahili", - }, - ta: { - nativeName: "தமிழ்", - }, - "ta-IN": { - nativeName: "தமிழ்", - }, - te: { - nativeName: "తెలుగు", - }, - "te-IN": { - nativeName: "తెలుగు", - }, - tg: { - nativeName: "забо́ни тоҷикӣ́", - }, - "tg-TJ": { - nativeName: "тоҷикӣ", - }, - th: { - nativeName: "ภาษาไทย", - }, - "th-TH": { - nativeName: "ภาษาไทย (ประเทศไทย)", - }, - fil: { - nativeName: "Filipino", - }, - tlh: { - nativeName: "tlhIngan-Hol", - }, - tr: { - nativeName: "Türkçe", - }, - "tr-TR": { - nativeName: "Türkçe", - }, - "tt-RU": { - nativeName: "татарча", - }, - uk: { - nativeName: "Українська", - }, - "uk-UA": { - nativeName: "Українська", - }, - ur: { - nativeName: "اردو", - }, - "ur-PK": { - nativeName: "اردو", - }, - uz: { - nativeName: "O'zbek", - }, - "uz-UZ": { - nativeName: "O'zbek", - }, - vi: { - nativeName: "Tiếng Việt", - }, - "vi-VN": { - nativeName: "Tiếng Việt", - }, - "xh-ZA": { - nativeName: "isiXhosa", - }, - yi: { - nativeName: "ייִדיש", - }, - "yi-DE": { - nativeName: "ייִדיש (German)", - }, - zh: { - nativeName: "中文", - }, - "zh-Hans": { - nativeName: "中文简体", - }, - "zh-Hant": { - nativeName: "中文繁體", - }, - "zh-CN": { - nativeName: "中文(中国大陆)", - }, - "zh-HK": { - nativeName: "中文(香港)", - }, - "zh-SG": { - nativeName: "中文(新加坡)", - }, - "zh-TW": { - nativeName: "中文(台灣)", - }, - "zu-ZA": { - nativeName: "isiZulu", - }, -}; diff --git a/packages/backend/src/misc/normalize-for-search.ts b/packages/backend/src/misc/normalize-for-search.ts deleted file mode 100644 index 6882a1243e..0000000000 --- a/packages/backend/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/packages/backend/src/misc/nyaize.ts b/packages/backend/src/misc/nyaize.ts deleted file mode 100644 index b85f1d918e..0000000000 --- a/packages/backend/src/misc/nyaize.ts +++ /dev/null @@ -1,21 +0,0 @@ -export function nyaize(text: string): string { - return ( - text - // ja-JP - .replaceAll("な", "にゃ") - .replaceAll("ナ", "ニャ") - .replaceAll("ナ", "ニャ") - // en-US - .replace(/(?<=n)a/gi, (x) => (x === "A" ? "YA" : "ya")) - .replace(/(?<=morn)ing/gi, (x) => (x === "ING" ? "YAN" : "yan")) - .replace(/(?<=every)one/gi, (x) => (x === "ONE" ? "NYAN" : "nyan")) - // ko-KR - .replace(/[나-낳]/g, (match) => - String.fromCharCode( - match.charCodeAt(0)! + "냐".charCodeAt(0) - "나".charCodeAt(0), - ), - ) - .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, "다냥") - .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, "냥") - ); -} diff --git a/packages/backend/src/misc/password.ts b/packages/backend/src/misc/password.ts deleted file mode 100644 index c63f89f5c9..0000000000 --- a/packages/backend/src/misc/password.ts +++ /dev/null @@ -1,20 +0,0 @@ -import bcrypt from "bcryptjs"; -import * as argon2 from "argon2"; - -export async function hashPassword(password: string): Promise { - return argon2.hash(password); -} - -export async function comparePassword( - password: string, - hash: string, -): Promise { - if (isOldAlgorithm(hash)) return bcrypt.compare(password, hash); - - return argon2.verify(hash, password); -} - -export function isOldAlgorithm(hash: string): boolean { - // bcrypt hashes start with $2[ab]$ - return hash.startsWith("$2"); -} diff --git a/packages/backend/src/misc/populate-emojis.ts b/packages/backend/src/misc/populate-emojis.ts deleted file mode 100644 index 3f20f9f10d..0000000000 --- a/packages/backend/src/misc/populate-emojis.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { In, IsNull } from "typeorm"; -import { Emojis } from "@/models/index.js"; -import type { Emoji } from "@/models/entities/emoji.js"; -import type { Note } from "@/models/entities/note.js"; -import { Cache } from "./cache.js"; -import { isSelfHost, toPunyNullable } from "./convert-host.js"; -import { decodeReaction } from "./reaction-lib.js"; -import config from "@/config/index.js"; -import { query } from "@/prelude/url.js"; - -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.findOneBy({ - name, - host: host ?? IsNull(), - })) || null; - - const emoji = await cache.fetch(`${name} ${host}`, queryOrNull); - - if (emoji == null) return null; - - const isLocal = emoji.host == null; - const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため - const url = isLocal - ? emojiUrl - : `${config.url}/proxy/${encodeURIComponent( - new URL(emojiUrl).pathname, - )}?${query({ url: emojiUrl })}`; - - 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 ?? IsNull(), - }); - } - const _emojis = - emojisQuery.length > 0 - ? await Emojis.find({ - where: emojisQuery, - select: ["name", "host", "originalUrl", "publicUrl"], - }) - : []; - for (const emoji of _emojis) { - cache.set(`${emoji.name} ${emoji.host}`, emoji); - } -} diff --git a/packages/backend/src/misc/post.ts b/packages/backend/src/misc/post.ts deleted file mode 100644 index 90f4f75283..0000000000 --- a/packages/backend/src/misc/post.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type Post = { - text: string | null; - cw: string | null; - localOnly: boolean; - createdAt: Date; -}; - -export function parse(acct: any): Post { - return { - text: acct.text, - cw: acct.cw, - localOnly: acct.localOnly, - createdAt: new Date(acct.createdAt), - }; -} - -export function toJson(acct: Post): string { - return { text: acct.text, cw: acct.cw, localOnly: acct.localOnly }.toString(); -} diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts deleted file mode 100644 index e25b2d6614..0000000000 --- a/packages/backend/src/misc/reaction-lib.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { emojiRegex } from "./emoji-regex.js"; -import { fetchMeta } from "./fetch-meta.js"; -import { Emojis } from "@/models/index.js"; -import { toPunyNullable } from "./convert-host.js"; -import { IsNull } from "typeorm"; - -const legacies = new Map([ - ["like", "👍"], - ["love", "❤️"], - ["laugh", "😆"], - ["hmm", "🤔"], - ["surprise", "😮"], - ["congrats", "🎉"], - ["angry", "💢"], - ["confused", "😥"], - ["rip", "😇"], - ["pudding", "🍮"], - ["star", "⭐"], -]); - -export async function getFallbackReaction() { - const meta = await fetchMeta(); - return meta.defaultReaction; -} - -export function convertLegacyReactions(reactions: Record) { - const _reactions = new Map(); - const decodedReactions = new Map(); - - for (const reaction in reactions) { - if (reactions[reaction] <= 0) continue; - - let decodedReaction; - if (decodedReactions.has(reaction)) { - decodedReaction = decodedReactions.get(reaction); - } else { - decodedReaction = decodeReaction(reaction); - decodedReactions.set(reaction, decodedReaction); - } - - let emoji = legacies.get(decodedReaction.reaction); - if (emoji) { - _reactions.set(emoji, (_reactions.get(emoji) || 0) + reactions[reaction]); - } else { - _reactions.set( - reaction, - (_reactions.get(reaction) || 0) + reactions[reaction], - ); - } - } - - const _reactions2 = new Map(); - for (const [reaction, count] of _reactions) { - const decodedReaction = decodedReactions.get(reaction); - _reactions2.set(decodedReaction.reaction, count); - } - - return Object.fromEntries(_reactions2); -} - -export async function toDbReaction( - reaction?: string | null, - reacterHost?: string | null, -): Promise { - if (!reaction) return await getFallbackReaction(); - - reacterHost = toPunyNullable(reacterHost); - - // Convert string-type reactions to unicode - const emoji = legacies.get(reaction) || (reaction === "♥️" ? "❤️" : null); - if (emoji) return emoji; - - // Allow unicode reactions - const match = emojiRegex.exec(reaction); - if (match) { - const unicode = match[0]; - return unicode; - } - - const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); - if (custom) { - const name = custom[1]; - const emoji = await Emojis.findOneBy({ - host: reacterHost || IsNull(), - 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 { - const decoded = decodeReaction(reaction).reaction; - if (legacies.has(decoded)) return legacies.get(decoded)!; - return decoded; -} diff --git a/packages/backend/src/misc/safe-for-sql.ts b/packages/backend/src/misc/safe-for-sql.ts deleted file mode 100644 index 02eb7f0a26..0000000000 --- a/packages/backend/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/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts deleted file mode 100644 index 7eaeb92e04..0000000000 --- a/packages/backend/src/misc/schema.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { - packedUserLiteSchema, - packedUserDetailedNotMeOnlySchema, - packedMeDetailedOnlySchema, - packedUserDetailedNotMeSchema, - packedMeDetailedSchema, - packedUserDetailedSchema, - packedUserSchema, -} from "@/models/schema/user.js"; -import { packedNoteSchema } from "@/models/schema/note.js"; -import { packedUserListSchema } from "@/models/schema/user-list.js"; -import { packedAppSchema } from "@/models/schema/app.js"; -import { packedMessagingMessageSchema } from "@/models/schema/messaging-message.js"; -import { packedNotificationSchema } from "@/models/schema/notification.js"; -import { packedDriveFileSchema } from "@/models/schema/drive-file.js"; -import { packedDriveFolderSchema } from "@/models/schema/drive-folder.js"; -import { packedFollowingSchema } from "@/models/schema/following.js"; -import { packedMutingSchema } from "@/models/schema/muting.js"; -import { packedRenoteMutingSchema } from "@/models/schema/renote-muting.js"; -import { packedBlockingSchema } from "@/models/schema/blocking.js"; -import { packedNoteReactionSchema } from "@/models/schema/note-reaction.js"; -import { packedHashtagSchema } from "@/models/schema/hashtag.js"; -import { packedPageSchema } from "@/models/schema/page.js"; -import { packedUserGroupSchema } from "@/models/schema/user-group.js"; -import { packedNoteFavoriteSchema } from "@/models/schema/note-favorite.js"; -import { packedChannelSchema } from "@/models/schema/channel.js"; -import { packedAntennaSchema } from "@/models/schema/antenna.js"; -import { packedClipSchema } from "@/models/schema/clip.js"; -import { packedFederationInstanceSchema } from "@/models/schema/federation-instance.js"; -import { packedQueueCountSchema } from "@/models/schema/queue.js"; -import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js"; -import { packedEmojiSchema } from "@/models/schema/emoji.js"; - -export const refs = { - UserLite: packedUserLiteSchema, - UserDetailedNotMeOnly: packedUserDetailedNotMeOnlySchema, - MeDetailedOnly: packedMeDetailedOnlySchema, - UserDetailedNotMe: packedUserDetailedNotMeSchema, - MeDetailed: packedMeDetailedSchema, - UserDetailed: packedUserDetailedSchema, - 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, - RenoteMuting: packedRenoteMutingSchema, - Blocking: packedBlockingSchema, - Hashtag: packedHashtagSchema, - Page: packedPageSchema, - Channel: packedChannelSchema, - QueueCount: packedQueueCountSchema, - Antenna: packedAntennaSchema, - Clip: packedClipSchema, - FederationInstance: packedFederationInstanceSchema, - GalleryPost: packedGalleryPostSchema, - Emoji: packedEmojiSchema, -}; - -export type Packed = SchemaType; - -type TypeStringef = - | "null" - | "boolean" - | "integer" - | "number" - | "string" - | "array" - | "object" - | "any"; -type StringDefToType = T extends "null" - ? null - : T extends "boolean" - ? boolean - : T extends "integer" - ? number - : T extends "number" - ? number - : T extends "string" - ? string | Date - : T extends "array" - ? ReadonlyArray - : T extends "object" - ? Record - : any; - -// https://swagger.io/specification/?sbsearch=optional#schema-object -type OfSchema = { - readonly anyOf?: ReadonlyArray; - readonly oneOf?: ReadonlyArray; - readonly allOf?: ReadonlyArray; -}; - -export interface Schema extends OfSchema { - readonly type?: TypeStringef; - readonly nullable?: boolean; - readonly optional?: boolean; - readonly items?: Schema; - readonly properties?: Obj; - readonly required?: ReadonlyArray< - Extract, string> - >; - readonly description?: string; - readonly example?: any; - readonly format?: string; - readonly ref?: keyof typeof refs; - readonly enum?: ReadonlyArray; - readonly default?: - | (this["type"] extends TypeStringef ? StringDefToType : any) - | null; - readonly maxLength?: number; - readonly minLength?: number; - readonly maximum?: number; - readonly minimum?: number; - readonly pattern?: string; -} - -type RequiredPropertyNames = { - [K in keyof s]: // K is not optional - s[K]["optional"] extends false - ? K - : // K has default value - s[K]["default"] extends - | null - | string - | number - | boolean - | Record - ? K - : never; -}[keyof s]; - -export type Obj = Record; - -// https://github.com/misskey-dev/misskey/issues/8535 -// To avoid excessive stack depth error, -// deceive TypeScript with UnionToIntersection (or more precisely, `infer` expression within it). -export type ObjType< - s extends Obj, - RequiredProps extends keyof s, -> = UnionToIntersection< - { - -readonly [R in RequiredPropertyNames]-?: SchemaType; - } & { - -readonly [R in RequiredProps]-?: SchemaType; - } & { - -readonly [P in keyof s]?: SchemaType; - } ->; - -type NullOrUndefined

= - | (p["nullable"] extends true ? null : never) - | (p["optional"] extends true ? undefined : never) - | T; - -// https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -// Get intersection from union -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( - k: infer I, -) => void - ? I - : never; - -// https://github.com/misskey-dev/misskey/pull/8144#discussion_r785287552 -// To get union, we use `Foo extends any ? Hoge : never` -type UnionSchemaType< - a extends readonly any[], - X extends Schema = a[number], -> = X extends any ? SchemaType : never; -type ArrayUnion = T extends any ? Array : never; - -export type SchemaTypeDef

= p["type"] extends "null" - ? null - : p["type"] extends "integer" - ? number - : p["type"] extends "number" - ? number - : p["type"] extends "string" - ? p["enum"] extends readonly string[] - ? p["enum"][number] - : p["format"] extends "date-time" - ? string - : // Dateにする?? - string - : p["type"] extends "boolean" - ? boolean - : p["type"] extends "object" - ? p["ref"] extends keyof typeof refs - ? Packed - : p["properties"] extends NonNullable - ? ObjType[number]> - : p["anyOf"] extends ReadonlyArray - ? UnionSchemaType & - Partial>> - : p["allOf"] extends ReadonlyArray - ? UnionToIntersection> - : any - : p["type"] extends "array" - ? p["items"] extends OfSchema - ? p["items"]["anyOf"] extends ReadonlyArray - ? UnionSchemaType>[] - : p["items"]["oneOf"] extends ReadonlyArray - ? ArrayUnion>> - : p["items"]["allOf"] extends ReadonlyArray - ? UnionToIntersection>>[] - : never - : p["items"] extends NonNullable - ? SchemaTypeDef[] - : any[] - : p["oneOf"] extends ReadonlyArray - ? UnionSchemaType - : any; - -export type SchemaType

= NullOrUndefined>; diff --git a/packages/backend/src/misc/secure-rndstr.ts b/packages/backend/src/misc/secure-rndstr.ts deleted file mode 100644 index 7f5754e1c5..0000000000 --- a/packages/backend/src/misc/secure-rndstr.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as crypto from "node:crypto"; - -const L_CHARS = "0123456789abcdefghijklmnopqrstuvwxyz"; -const LU_CHARS = - "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; - -export function secureRndstr(length = 32, useLU = true): string { - const chars = useLU ? LU_CHARS : L_CHARS; - const chars_len = chars.length; - - let str = ""; - - for (let i = 0; i < length; i++) { - let rand = Math.floor( - (crypto.randomBytes(1).readUInt8(0) / 0xff) * chars_len, - ); - if (rand === chars_len) { - rand = chars_len - 1; - } - str += chars.charAt(rand); - } - - return str; -} diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts deleted file mode 100644 index 6e46232428..0000000000 --- a/packages/backend/src/misc/should-block-instance.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; -import type { Instance } from "@/models/entities/instance.js"; -import type { Meta } from "@/models/entities/meta.js"; - -/** - * Returns whether a specific host (punycoded) should be blocked. - * - * @param host punycoded instance host - * @param meta a resolved Meta table - * @returns whether the given host should be blocked - */ -export async function shouldBlockInstance( - host: Instance["host"], - meta?: Meta, -): Promise { - const { blockedHosts } = meta ?? (await fetchMeta()); - return blockedHosts.some( - (blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`), - ); -} diff --git a/packages/backend/src/misc/show-machine-info.ts b/packages/backend/src/misc/show-machine-info.ts deleted file mode 100644 index d3a28cbd37..0000000000 --- a/packages/backend/src/misc/show-machine-info.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as os from "node:os"; -import sysUtils from "systeminformation"; -import type Logger from "@/services/logger.js"; - -export async function showMachineInfo(parentLogger: Logger) { - const logger = parentLogger.createSubLogger("machine"); - logger.debug(`Hostname: ${os.hostname()}`); - logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`); - const mem = await sysUtils.mem(); - const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1); - const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1); - logger.debug( - `CPU: ${ - os.cpus().length - } core MEM: ${totalmem}GB (available: ${availmem}GB)`, - ); -} diff --git a/packages/backend/src/misc/skipped-instances.ts b/packages/backend/src/misc/skipped-instances.ts deleted file mode 100644 index 785393022a..0000000000 --- a/packages/backend/src/misc/skipped-instances.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Instances } from "@/models/index.js"; -import type { Instance } from "@/models/entities/instance.js"; -import { DAY } from "@/const.js"; -import { shouldBlockInstance } from "./should-block-instance.js"; - -// Threshold from last contact after which an instance will be considered -// "dead" and should no longer get activities delivered to it. -const deadThreshold = 7 * DAY; - -/** - * Returns the subset of hosts which should be skipped. - * - * @param hosts array of punycoded instance hosts - * @returns array of punycoed instance hosts that should be skipped (subset of hosts parameter) - */ -export async function skippedInstances( - hosts: Instance["host"][], -): Promise { - // first check for blocked instances since that info may already be in memory - const meta = await fetchMeta(); - const shouldSkip = await Promise.all( - hosts.map((host) => shouldBlockInstance(host, meta)), - ); - const skipped = hosts.filter((_, i) => shouldSkip[i]); - - // if possible return early and skip accessing the database - if (skipped.length === hosts.length) return hosts; - - const deadTime = new Date(Date.now() - deadThreshold); - - return skipped.concat( - await Instances.createQueryBuilder("instance") - .where("instance.host in (:...hosts)", { - // don't check hosts again that we already know are suspended - // also avoids adding duplicates to the list - hosts: hosts.filter((host) => !skipped.includes(host)), - }) - .andWhere( - new Brackets((qb) => { - qb.where("instance.isSuspended"); - }), - ) - .select("host") - .getRawMany(), - ); -} - -/** - * Returns whether a specific host (punycoded) should be skipped. - * Convenience wrapper around skippedInstances which should only be used if there is a single host to check. - * If you have multiple hosts, consider using skippedInstances instead to do a bulk check. - * - * @param host punycoded instance host - * @returns whether the given host should be skipped - */ -export async function shouldSkipInstance( - host: Instance["host"], -): Promise { - const skipped = await skippedInstances([host]); - return skipped.length > 0; -} diff --git a/packages/backend/src/misc/truncate.ts b/packages/backend/src/misc/truncate.ts deleted file mode 100644 index 6bc58941a2..0000000000 --- a/packages/backend/src/misc/truncate.ts +++ /dev/null @@ -1,17 +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/packages/backend/src/misc/webhook-cache.ts b/packages/backend/src/misc/webhook-cache.ts deleted file mode 100644 index 1eda5eaecd..0000000000 --- a/packages/backend/src/misc/webhook-cache.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Webhooks } from "@/models/index.js"; -import type { Webhook } from "@/models/entities/webhook.js"; -import { subscriber } from "@/db/redis.js"; - -let webhooksFetched = false; -let webhooks: Webhook[] = []; - -export async function getActiveWebhooks() { - if (!webhooksFetched) { - webhooks = await Webhooks.findBy({ - active: true, - }); - webhooksFetched = true; - } - - return webhooks; -} - -subscriber.on("message", async (_, data) => { - const obj = JSON.parse(data); - - if (obj.channel === "internal") { - const { type, body } = obj.message; - switch (type) { - case "webhookCreated": - if (body.active) { - webhooks.push(body); - } - break; - case "webhookUpdated": - if (body.active) { - const i = webhooks.findIndex((a) => a.id === body.id); - if (i > -1) { - webhooks[i] = body; - } else { - webhooks.push(body); - } - } else { - webhooks = webhooks.filter((a) => a.id !== body.id); - } - break; - case "webhookDeleted": - webhooks = webhooks.filter((a) => a.id !== body.id); - break; - default: - break; - } - } -}); diff --git a/packages/backend/src/models/entities/abuse-user-report.ts b/packages/backend/src/models/entities/abuse-user-report.ts deleted file mode 100644 index 655fdd3ca6..0000000000 --- a/packages/backend/src/models/entities/abuse-user-report.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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('boolean', { - default: false - }) - public forwarded: boolean; - - @Column('varchar', { - length: 2048, - }) - public comment: string; - - //#region Denormalized fields - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public targetUserHost: string | null; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public reporterHost: string | null; - //#endregion -} diff --git a/packages/backend/src/models/entities/access-token.ts b/packages/backend/src/models/entities/access-token.ts deleted file mode 100644 index 83d7bbda86..0000000000 --- a/packages/backend/src/models/entities/access-token.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - Entity, - PrimaryColumn, - Index, - Column, - ManyToOne, - JoinColumn, -} from "typeorm"; -import { User } from "./user.js"; -import { App } from "./app.js"; -import { id } from "../id.js"; - -@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, - }) - public lastUsedAt: Date | null; - - @Index() - @Column('varchar', { - length: 128, - }) - public token: string; - - @Index() - @Column('varchar', { - length: 128, - nullable: true, - }) - 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, - }) - public appId: App["id"] | null; - - @ManyToOne(type => App, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public app: App | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public name: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public description: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public iconUrl: string | null; - - @Column('varchar', { - length: 64, array: true, - default: '{}', - }) - public permission: string[]; - - @Column('boolean', { - default: false, - }) - public fetched: boolean; -} diff --git a/packages/backend/src/models/entities/ad.ts b/packages/backend/src/models/entities/ad.ts deleted file mode 100644 index fa42973652..0000000000 --- a/packages/backend/src/models/entities/ad.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Entity, Index, Column, PrimaryColumn } from "typeorm"; -import { id } from "../id.js"; - -@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/packages/backend/src/models/entities/announcement-read.ts b/packages/backend/src/models/entities/announcement-read.ts deleted file mode 100644 index 87d0f0e9ed..0000000000 --- a/packages/backend/src/models/entities/announcement-read.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { Announcement } from "./announcement.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'announcementId'], { unique: true }) -export class AnnouncementRead { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the AnnouncementRead.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public announcementId: Announcement["id"]; - - @ManyToOne(type => Announcement, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public announcement: Announcement | null; -} diff --git a/packages/backend/src/models/entities/announcement.ts b/packages/backend/src/models/entities/announcement.ts deleted file mode 100644 index 9d45af0149..0000000000 --- a/packages/backend/src/models/entities/announcement.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Entity, Index, Column, PrimaryColumn } from "typeorm"; -import { id } from "../id.js"; - -@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/packages/backend/src/models/entities/antenna-note.ts b/packages/backend/src/models/entities/antenna-note.ts deleted file mode 100644 index c47c796bbf..0000000000 --- a/packages/backend/src/models/entities/antenna-note.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - Entity, - Index, - JoinColumn, - Column, - ManyToOne, - PrimaryColumn, -} from "typeorm"; -import { Note } from "./note.js"; -import { Antenna } from "./antenna.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['noteId', 'antennaId'], { unique: true }) -export class AntennaNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The antenna ID.', - }) - public antennaId: Antenna["id"]; - - @ManyToOne(type => Antenna, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public antenna: Antenna | null; - - @Index() - @Column('boolean', { - default: false, - }) - public read: boolean; -} diff --git a/packages/backend/src/models/entities/antenna.ts b/packages/backend/src/models/entities/antenna.ts deleted file mode 100644 index c653b2a051..0000000000 --- a/packages/backend/src/models/entities/antenna.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; -import { UserList } from "./user-list.js"; -import { UserGroupJoining } from "./user-group-joining.js"; - -@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', 'instances'] }) - public src: "home" | "all" | "users" | "list" | "group" | "instances"; - - @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 instances: string[]; - - @Column('jsonb', { - default: [], - }) - public keywords: string[][]; - - @Column('jsonb', { - default: [], - }) - public excludeKeywords: string[][]; - - @Column('boolean', { - default: false, - }) - public caseSensitive: boolean; - - @Column('boolean', { - default: false, - }) - public withReplies: boolean; - - @Column('boolean') - public withFile: boolean; - - @Column('varchar', { - length: 2048, nullable: true, - }) - public expression: string | null; - - @Column('boolean') - public notify: boolean; -} diff --git a/packages/backend/src/models/entities/app.ts b/packages/backend/src/models/entities/app.ts deleted file mode 100644 index bb33eede4f..0000000000 --- a/packages/backend/src/models/entities/app.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Entity, PrimaryColumn, Column, Index, ManyToOne } from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -export class App { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the App.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The owner ID.', - }) - public userId: User["id"] | null; - - @ManyToOne(type => User, { - onDelete: 'SET NULL', - nullable: true, - }) - public user: User | null; - - @Index() - @Column('varchar', { - length: 64, - comment: 'The secret key of the App.', - }) - public secret: string; - - @Column('varchar', { - length: 128, - comment: 'The name of the App.', - }) - public name: string; - - @Column('varchar', { - length: 512, - comment: 'The description of the App.', - }) - public description: string; - - @Column('varchar', { - length: 64, array: true, - comment: 'The permission of the App.', - }) - public permission: string[]; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'The callbackUrl of the App.', - }) - public callbackUrl: string | null; -} diff --git a/packages/backend/src/models/entities/attestation-challenge.ts b/packages/backend/src/models/entities/attestation-challenge.ts deleted file mode 100644 index 7a87d42be0..0000000000 --- a/packages/backend/src/models/entities/attestation-challenge.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - PrimaryColumn, - Entity, - JoinColumn, - Column, - ManyToOne, - Index, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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/packages/backend/src/models/entities/auth-session.ts b/packages/backend/src/models/entities/auth-session.ts deleted file mode 100644 index b762f84625..0000000000 --- a/packages/backend/src/models/entities/auth-session.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - Entity, - PrimaryColumn, - Index, - Column, - ManyToOne, - JoinColumn, -} from "typeorm"; -import { User } from "./user.js"; -import { App } from "./app.js"; -import { id } from "../id.js"; - -@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"] | null; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - nullable: true, - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public appId: App["id"]; - - @ManyToOne(type => App, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public app: App | null; -} diff --git a/packages/backend/src/models/entities/blocking.ts b/packages/backend/src/models/entities/blocking.ts deleted file mode 100644 index 3a44a4d656..0000000000 --- a/packages/backend/src/models/entities/blocking.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['blockerId', 'blockeeId'], { unique: true }) -export class Blocking { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Blocking.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The blockee user ID.', - }) - public blockeeId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public blockee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The blocker user ID.', - }) - public blockerId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public blocker: User | null; -} diff --git a/packages/backend/src/models/entities/channel-following.ts b/packages/backend/src/models/entities/channel-following.ts deleted file mode 100644 index 04ec193e19..0000000000 --- a/packages/backend/src/models/entities/channel-following.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; -import { Channel } from "./channel.js"; - -@Entity() -@Index(['followerId', 'followeeId'], { unique: true }) -export class ChannelFollowing { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the ChannelFollowing.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The followee channel ID.', - }) - public followeeId: Channel["id"]; - - @ManyToOne(type => Channel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followee: Channel | null; - - @Index() - @Column({ - ...id(), - comment: 'The follower user ID.', - }) - public followerId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public follower: User | null; -} diff --git a/packages/backend/src/models/entities/channel-note-pining.ts b/packages/backend/src/models/entities/channel-note-pining.ts deleted file mode 100644 index bd13f4ca39..0000000000 --- a/packages/backend/src/models/entities/channel-note-pining.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { Note } from "./note.js"; -import { Channel } from "./channel.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['channelId', 'noteId'], { unique: true }) -export class ChannelNotePining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the ChannelNotePining.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public channelId: Channel["id"]; - - @ManyToOne(type => Channel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public channel: Channel | null; - - @Column(id()) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/channel.ts b/packages/backend/src/models/entities/channel.ts deleted file mode 100644 index 7f9851dbf9..0000000000 --- a/packages/backend/src/models/entities/channel.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; -import { DriveFile } from "./drive-file.js"; - -@Entity() -export class Channel { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Channel.', - }) - public createdAt: Date; - - @Index() - @Column('timestamp with time zone', { - nullable: true, - }) - public lastNotedAt: Date | null; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The owner ID.', - }) - public userId: User["id"] | null; - - @ManyToOne(type => User, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - comment: 'The name of the Channel.', - }) - public name: string; - - @Column('varchar', { - length: 2048, nullable: true, - comment: 'The description of the Channel.', - }) - public description: string | null; - - @Column({ - ...id(), - nullable: true, - comment: 'The ID of banner Channel.', - }) - public bannerId: DriveFile["id"] | null; - - @ManyToOne(type => DriveFile, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public banner: DriveFile | null; - - @Index() - @Column('integer', { - default: 0, - comment: 'The count of notes.', - }) - public notesCount: number; - - @Index() - @Column('integer', { - default: 0, - comment: 'The count of users.', - }) - public usersCount: number; -} diff --git a/packages/backend/src/models/entities/clip-note.ts b/packages/backend/src/models/entities/clip-note.ts deleted file mode 100644 index bc51daaf4d..0000000000 --- a/packages/backend/src/models/entities/clip-note.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - Entity, - Index, - JoinColumn, - Column, - ManyToOne, - PrimaryColumn, -} from "typeorm"; -import { Note } from "./note.js"; -import { Clip } from "./clip.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['noteId', 'clipId'], { unique: true }) -export class ClipNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The clip ID.', - }) - public clipId: Clip["id"]; - - @ManyToOne(type => Clip, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public clip: Clip | null; -} diff --git a/packages/backend/src/models/entities/clip.ts b/packages/backend/src/models/entities/clip.ts deleted file mode 100644 index 10591cbeef..0000000000 --- a/packages/backend/src/models/entities/clip.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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, - comment: 'The description of the Clip.', - }) - public description: string | null; -} diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts deleted file mode 100644 index 32e19bc6ee..0000000000 --- a/packages/backend/src/models/entities/drive-file.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { id } from "../id.js"; -import { User } from "./user.js"; -import { DriveFolder } from "./drive-folder.js"; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; - -@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: DB_MAX_IMAGE_COMMENT_LENGTH, - 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; - orientation?: number; - avgColor?: string; - }; - - @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; - - @Column('varchar', { - length: 128, nullable: true, - }) - public webpublicType: 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; - - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the DriveFile is NSFW. (predict)', - }) - public maybeSensitive: boolean; - - @Index() - @Column('boolean', { - default: false, - }) - public maybePorn: boolean; - - /** - * 外部の(信頼されていない)URLへの直リンクか否か - */ - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the DriveFile is direct link to remote server.', - }) - public isLink: boolean; - - @Column('jsonb', { - default: {}, - nullable: true, - }) - public requestHeaders: Record | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public requestIp: string | null; -} diff --git a/packages/backend/src/models/entities/drive-folder.ts b/packages/backend/src/models/entities/drive-folder.ts deleted file mode 100644 index 77031ce4ea..0000000000 --- a/packages/backend/src/models/entities/drive-folder.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - JoinColumn, - ManyToOne, - Entity, - PrimaryColumn, - Index, - Column, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -export class DriveFolder { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the DriveFolder.', - }) - public createdAt: Date; - - @Column('varchar', { - length: 128, - comment: 'The name of the DriveFolder.', - }) - public name: string; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The owner ID.', - }) - public userId: User["id"] | null; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.', - }) - public parentId: DriveFolder["id"] | null; - - @ManyToOne(type => DriveFolder, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public parent: DriveFolder | null; -} diff --git a/packages/backend/src/models/entities/emoji.ts b/packages/backend/src/models/entities/emoji.ts deleted file mode 100644 index 2315686968..0000000000 --- a/packages/backend/src/models/entities/emoji.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column } from "typeorm"; -import { id } from "../id.js"; - -@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 originalUrl: string; - - @Column('varchar', { - length: 512, - default: '', - }) - public publicUrl: string; - - @Column('varchar', { - length: 512, nullable: true, - }) - public uri: string | null; - - // publicUrlの方のtypeが入る - @Column('varchar', { - length: 64, nullable: true, - }) - public type: string | null; - - @Column('varchar', { - array: true, length: 128, default: '{}', - }) - public aliases: string[]; - - @Column('varchar', { - length: 1024, nullable: true, - }) - public license: string | null; -} diff --git a/packages/backend/src/models/entities/follow-request.ts b/packages/backend/src/models/entities/follow-request.ts deleted file mode 100644 index 658fed5a5e..0000000000 --- a/packages/backend/src/models/entities/follow-request.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['followerId', 'followeeId'], { unique: true }) -export class FollowRequest { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the FollowRequest.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The followee user ID.', - }) - public followeeId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The follower user ID.', - }) - public followerId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public follower: User | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'id of Follow Activity.', - }) - public requestId: string | null; - - //#region Denormalized fields - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public followerHost: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followerInbox: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followerSharedInbox: string | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public followeeHost: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followeeInbox: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followeeSharedInbox: string | null; - //#endregion -} diff --git a/packages/backend/src/models/entities/following.ts b/packages/backend/src/models/entities/following.ts deleted file mode 100644 index 11f633fcd8..0000000000 --- a/packages/backend/src/models/entities/following.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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 - @Index() - @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; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public followeeHost: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followeeInbox: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: '[Denormalized]', - }) - public followeeSharedInbox: string | null; - //#endregion -} diff --git a/packages/backend/src/models/entities/gallery-like.ts b/packages/backend/src/models/entities/gallery-like.ts deleted file mode 100644 index e74e3c3ce5..0000000000 --- a/packages/backend/src/models/entities/gallery-like.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; -import { GalleryPost } from "./gallery-post.js"; - -@Entity() -@Index(['userId', 'postId'], { unique: true }) -export class GalleryLike { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public postId: GalleryPost["id"]; - - @ManyToOne(type => GalleryPost, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public post: GalleryPost | null; -} diff --git a/packages/backend/src/models/entities/gallery-post.ts b/packages/backend/src/models/entities/gallery-post.ts deleted file mode 100644 index a79bb88353..0000000000 --- a/packages/backend/src/models/entities/gallery-post.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - Entity, - Index, - JoinColumn, - Column, - PrimaryColumn, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; -import type { DriveFile } from "./drive-file.js"; - -@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/packages/backend/src/models/entities/hashtag.ts b/packages/backend/src/models/entities/hashtag.ts deleted file mode 100644 index 06fa004be4..0000000000 --- a/packages/backend/src/models/entities/hashtag.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Entity, PrimaryColumn, Index, Column } from "typeorm"; -import type { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -export class Hashtag { - @PrimaryColumn(id()) - public id: string; - - @Index({ unique: true }) - @Column('varchar', { - length: 128, - }) - public name: string; - - @Column({ - ...id(), - array: true, - }) - public mentionedUserIds: User["id"][]; - - @Index() - @Column('integer', { - default: 0, - }) - public mentionedUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public mentionedLocalUserIds: User["id"][]; - - @Index() - @Column('integer', { - default: 0, - }) - public mentionedLocalUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public mentionedRemoteUserIds: User["id"][]; - - @Index() - @Column('integer', { - default: 0, - }) - public mentionedRemoteUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public attachedUserIds: User["id"][]; - - @Index() - @Column('integer', { - default: 0, - }) - public attachedUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public attachedLocalUserIds: User["id"][]; - - @Index() - @Column('integer', { - default: 0, - }) - public attachedLocalUsersCount: number; - - @Column({ - ...id(), - array: true, - }) - public attachedRemoteUserIds: User["id"][]; - - @Index() - @Column('integer', { - default: 0, - }) - public attachedRemoteUsersCount: number; -} diff --git a/packages/backend/src/models/entities/instance.ts b/packages/backend/src/models/entities/instance.ts deleted file mode 100644 index 2b118455db..0000000000 --- a/packages/backend/src/models/entities/instance.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Entity, PrimaryColumn, Index, Column } from "typeorm"; -import { id } from "../id.js"; - -@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('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, - comment: 'The software of the Instance.', - }) - public softwareName: string | null; - - @Column('varchar', { - length: 64, nullable: true, - }) - public softwareVersion: string | null; - - @Column('boolean', { - nullable: true, - }) - public openRegistrations: boolean | null; - - @Column('varchar', { - length: 256, nullable: true, - }) - public name: string | null; - - @Column('varchar', { - length: 4096, nullable: true, - }) - public description: string | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public maintainerName: string | null; - - @Column('varchar', { - length: 256, nullable: true, - }) - public maintainerEmail: string | null; - - @Column('varchar', { - length: 256, nullable: true, - }) - public iconUrl: string | null; - - @Column('varchar', { - length: 256, nullable: true, - }) - public faviconUrl: string | null; - - @Column('varchar', { - length: 64, nullable: true, - }) - public themeColor: string | null; - - @Column('timestamp with time zone', { - nullable: true, - }) - public infoUpdatedAt: Date | null; -} diff --git a/packages/backend/src/models/entities/messaging-message.ts b/packages/backend/src/models/entities/messaging-message.ts deleted file mode 100644 index 9cf197fa3b..0000000000 --- a/packages/backend/src/models/entities/messaging-message.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { DriveFile } from "./drive-file.js"; -import { id } from "../id.js"; -import { UserGroup } from "./user-group.js"; - -@Entity() -export class MessagingMessage { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the MessagingMessage.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The sender user ID.', - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), nullable: true, - comment: 'The recipient user ID.', - }) - public recipientId: User["id"] | null; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public recipient: User | null; - - @Index() - @Column({ - ...id(), nullable: true, - comment: 'The recipient group ID.', - }) - public groupId: UserGroup["id"] | null; - - @ManyToOne(type => UserGroup, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public group: UserGroup | null; - - @Column('varchar', { - length: 4096, nullable: true, - }) - public text: string | null; - - @Column('boolean', { - default: false, - }) - public isRead: boolean; - - @Column('varchar', { - length: 512, nullable: true, - }) - public uri: string | null; - - @Column({ - ...id(), - array: true, default: '{}', - }) - public reads: User["id"][]; - - @Column({ - ...id(), - nullable: true, - }) - public fileId: DriveFile["id"] | null; - - @ManyToOne(type => DriveFile, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public file: DriveFile | null; -} diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts deleted file mode 100644 index 26a7c9c193..0000000000 --- a/packages/backend/src/models/entities/meta.ts +++ /dev/null @@ -1,502 +0,0 @@ -import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from "typeorm"; -import { id } from "../id.js"; -import { User } from "./user.js"; -import type { Clip } from "./clip.js"; - -@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: true, - }) - public disableRecommendedTimeline: boolean; - - @Column('boolean', { - default: false, - }) - public disableGlobalTimeline: boolean; - - @Column('varchar', { - length: 256, default: '⭐', - }) - public defaultReaction: string; - - @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 recommendedInstances: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public customMOTD: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public customSplashIcons: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public hiddenTags: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public blockedHosts: string[]; - - @Column('boolean', { - default: false, - }) - public secureMode: boolean; - - @Column('boolean', { - default: false, - }) - public privateMode: boolean; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public allowedHosts: string[]; - - @Column('varchar', { - length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-calckey}', - }) - public pinnedPages: string[]; - - @Column({ - ...id(), - nullable: true, - }) - public pinnedClipId: Clip["id"] | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public themeColor: string | 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({ - ...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('enum', { - enum: ['none', 'all', 'local', 'remote'], - default: 'none', - }) - public sensitiveMediaDetection: "none" | "all" | "local" | "remote"; - - @Column('enum', { - enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'], - default: 'medium', - }) - public sensitiveMediaDetectionSensitivity: - | "medium" - | "low" - | "high" - | "veryLow" - | "veryHigh"; - - @Column('boolean', { - default: false, - }) - public setSensitiveFlagAutomatically: boolean; - - @Column('boolean', { - default: false, - }) - public enableSensitiveMediaDetectionForVideos: boolean; - - @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('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://codeberg.org/calckey/calckey', - nullable: false, - }) - public repositoryUrl: string; - - @Column('varchar', { - length: 512, - default: 'https://codeberg.org/calckey/calckey/issues/new', - nullable: true, - }) - public feedbackUrl: string | null; - - @Column('varchar', { - length: 8192, - nullable: true, - }) - public defaultLightTheme: string | null; - - @Column('varchar', { - length: 8192, - nullable: true, - }) - public defaultDarkTheme: 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; - - @Column('boolean', { - default: false, - }) - public enableIpLogging: boolean; - - @Column('boolean', { - default: true, - }) - public enableActiveEmailValidation: boolean; -} diff --git a/packages/backend/src/models/entities/moderation-log.ts b/packages/backend/src/models/entities/moderation-log.ts deleted file mode 100644 index cc745e0d20..0000000000 --- a/packages/backend/src/models/entities/moderation-log.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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/packages/backend/src/models/entities/muted-note.ts b/packages/backend/src/models/entities/muted-note.ts deleted file mode 100644 index 11a6ae95d0..0000000000 --- a/packages/backend/src/models/entities/muted-note.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - Entity, - Index, - JoinColumn, - Column, - ManyToOne, - PrimaryColumn, -} from "typeorm"; -import { Note } from "./note.js"; -import { User } from "./user.js"; -import { id } from "../id.js"; -import { mutedNoteReasons } from "../../types.js"; - -@Entity() -@Index(['noteId', 'userId'], { unique: true }) -export class MutedNote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column({ - ...id(), - comment: 'The note ID.', - }) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - /** - * ミュートされた理由。 - */ - @Index() - @Column('enum', { - enum: mutedNoteReasons, - comment: 'The reason of the MutedNote.', - }) - public reason: typeof mutedNoteReasons[number]; -} diff --git a/packages/backend/src/models/entities/muting.ts b/packages/backend/src/models/entities/muting.ts deleted file mode 100644 index 561bcfb95f..0000000000 --- a/packages/backend/src/models/entities/muting.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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('timestamp with time zone', { - nullable: true, - }) - public expiresAt: Date | null; - - @Index() - @Column({ - ...id(), - comment: 'The mutee user ID.', - }) - public muteeId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public mutee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The muter user ID.', - }) - public muterId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public muter: User | null; -} diff --git a/packages/backend/src/models/entities/note-favorite.ts b/packages/backend/src/models/entities/note-favorite.ts deleted file mode 100644 index ab12d8b1b3..0000000000 --- a/packages/backend/src/models/entities/note-favorite.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { Note } from "./note.js"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteFavorite { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the NoteFavorite.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/note-reaction.ts b/packages/backend/src/models/entities/note-reaction.ts deleted file mode 100644 index 0e51c33b16..0000000000 --- a/packages/backend/src/models/entities/note-reaction.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { Note } from "./note.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteReaction { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the NoteReaction.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user?: User | null; - - @Index() - @Column(id()) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note?: Note | null; - - // TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため) - - @Column('varchar', { - length: 260, - }) - public reaction: string; -} diff --git a/packages/backend/src/models/entities/note-thread-muting.ts b/packages/backend/src/models/entities/note-thread-muting.ts deleted file mode 100644 index 2985b195f0..0000000000 --- a/packages/backend/src/models/entities/note-thread-muting.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { Note } from "./note.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'threadId'], { unique: true }) -export class NoteThreadMuting { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column('varchar', { - length: 256, - }) - public threadId: string; -} diff --git a/packages/backend/src/models/entities/note-unread.ts b/packages/backend/src/models/entities/note-unread.ts deleted file mode 100644 index d5bba72212..0000000000 --- a/packages/backend/src/models/entities/note-unread.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { Note } from "./note.js"; -import { id } from "../id.js"; -import type { Channel } from "./channel.js"; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteUnread { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - /** - * メンションか否か - */ - @Index() - @Column('boolean') - public isMentioned: boolean; - - /** - * ダイレクト投稿か否か - */ - @Index() - @Column('boolean') - public isSpecified: boolean; - - //#region Denormalized fields - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public noteUserId: User["id"]; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: '[Denormalized]', - }) - public noteChannelId: Channel["id"] | null; - //#endregion -} diff --git a/packages/backend/src/models/entities/note-watching.ts b/packages/backend/src/models/entities/note-watching.ts deleted file mode 100644 index 7ac3e8e297..0000000000 --- a/packages/backend/src/models/entities/note-watching.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { Note } from "./note.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteWatching { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the NoteWatching.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The watcher ID.', - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The target Note ID.', - }) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - //#region Denormalized fields - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public noteUserId: Note["userId"]; - //#endregion -} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts deleted file mode 100644 index fd6b170c0e..0000000000 --- a/packages/backend/src/models/entities/note.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { - Entity, - Index, - JoinColumn, - Column, - PrimaryColumn, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import type { DriveFile } from "./drive-file.js"; -import { id } from "../id.js"; -import { noteVisibilities } from "../../types.js"; -import { Channel } from "./channel.js"; - -@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('text', { - 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 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, - 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/packages/backend/src/models/entities/notification.ts b/packages/backend/src/models/entities/notification.ts deleted file mode 100644 index 2c55e988f1..0000000000 --- a/packages/backend/src/models/entities/notification.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { - Entity, - Index, - JoinColumn, - ManyToOne, - Column, - PrimaryColumn, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; -import { Note } from "./note.js"; -import { FollowRequest } from "./follow-request.js"; -import { UserGroupInvitation } from "./user-group-invitation.js"; -import { AccessToken } from "./access-token.js"; -import { notificationTypes } from "@/types.js"; - -@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; - - /** - * Notification Recipient ID - */ - @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; - - /** - * Notification sender (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; - - /** - * Notification types: - * follow - Follow request - * mention - User was referenced in a post. - * reply - A post that a user made (or was watching) has been replied to. - * renote - A post that a user made (or was watching) has been renoted. - * quote - A post that a user made (or was watching) has been quoted and renoted. - * reaction - (自分または自分がWatchしている)投稿にリアクションされた - * pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された - * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した - * receiveFollowRequest - フォローリクエストされた - * followRequestAccepted - A follow request has been accepted. - * groupInvited - グループに招待された - * app - App notifications. - */ - @Index() - @Column('enum', { - enum: notificationTypes, - comment: 'The type of the Notification.', - }) - public type: typeof notificationTypes[number]; - - /** - * Whether the notification was read. - */ - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the notification was 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; - - /** - * App notification body - */ - @Column('varchar', { - length: 2048, nullable: true, - }) - public customBody: string | null; - - /** - * App notification header - * (If omitted, it is expected to be displayed with the app name) - */ - @Column('varchar', { - length: 256, nullable: true, - }) - public customHeader: string | null; - - /** - * App notification icon (URL) - * (If omitted, it is expected to be displayed as an app icon) - */ - @Column('varchar', { - length: 1024, nullable: true, - }) - public customIcon: string | null; - - /** - * App notification app (token for) - */ - @Index() - @Column({ - ...id(), - nullable: true, - }) - public appAccessTokenId: AccessToken["id"] | null; - - @ManyToOne(type => AccessToken, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public appAccessToken: AccessToken | null; -} diff --git a/packages/backend/src/models/entities/page-like.ts b/packages/backend/src/models/entities/page-like.ts deleted file mode 100644 index 75f4dc49b0..0000000000 --- a/packages/backend/src/models/entities/page-like.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; -import { Page } from "./page.js"; - -@Entity() -@Index(['userId', 'pageId'], { unique: true }) -export class PageLike { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public pageId: Page["id"]; - - @ManyToOne(type => Page, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public page: Page | null; -} diff --git a/packages/backend/src/models/entities/page.ts b/packages/backend/src/models/entities/page.ts deleted file mode 100644 index 5fe9f52088..0000000000 --- a/packages/backend/src/models/entities/page.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - Entity, - Index, - JoinColumn, - Column, - PrimaryColumn, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; -import { DriveFile } from "./drive-file.js"; - -@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') - public isPublic: 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/packages/backend/src/models/entities/password-reset-request.ts b/packages/backend/src/models/entities/password-reset-request.ts deleted file mode 100644 index 4c681d4f70..0000000000 --- a/packages/backend/src/models/entities/password-reset-request.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - Column, - ManyToOne, - JoinColumn, -} from "typeorm"; -import { id } from "../id.js"; -import { User } from "./user.js"; - -@Entity() -export class PasswordResetRequest { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index({ unique: true }) - @Column('varchar', { - length: 256, - }) - public token: string; - - @Index() - @Column({ - ...id(), - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; -} diff --git a/packages/backend/src/models/entities/poll-vote.ts b/packages/backend/src/models/entities/poll-vote.ts deleted file mode 100644 index 0649951cf4..0000000000 --- a/packages/backend/src/models/entities/poll-vote.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { Note } from "./note.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'noteId', 'choice'], { unique: true }) -export class PollVote { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the PollVote.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Column('integer') - public choice: number; -} diff --git a/packages/backend/src/models/entities/poll.ts b/packages/backend/src/models/entities/poll.ts deleted file mode 100644 index 28a70b3c7b..0000000000 --- a/packages/backend/src/models/entities/poll.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - OneToOne, -} from "typeorm"; -import { id } from "../id.js"; -import { Note } from "./note.js"; -import type { User } from "./user.js"; -import { noteVisibilities } from "../../types.js"; - -@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: 256, 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/packages/backend/src/models/entities/promo-note.ts b/packages/backend/src/models/entities/promo-note.ts deleted file mode 100644 index 4daacd246a..0000000000 --- a/packages/backend/src/models/entities/promo-note.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - OneToOne, -} from "typeorm"; -import { Note } from "./note.js"; -import type { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -export class PromoNote { - @PrimaryColumn(id()) - public noteId: Note["id"]; - - @OneToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Column('timestamp with time zone') - public expiresAt: Date; - - //#region Denormalized fields - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public userId: User["id"]; - //#endregion -} diff --git a/packages/backend/src/models/entities/promo-read.ts b/packages/backend/src/models/entities/promo-read.ts deleted file mode 100644 index 5938bfde9d..0000000000 --- a/packages/backend/src/models/entities/promo-read.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { Note } from "./note.js"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class PromoRead { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the PromoRead.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/registration-tickets.ts b/packages/backend/src/models/entities/registration-tickets.ts deleted file mode 100644 index af785fbc0d..0000000000 --- a/packages/backend/src/models/entities/registration-tickets.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column } from "typeorm"; -import { id } from "../id.js"; - -@Entity() -export class RegistrationTicket { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index({ unique: true }) - @Column('varchar', { - length: 64, - }) - public code: string; -} diff --git a/packages/backend/src/models/entities/registry-item.ts b/packages/backend/src/models/entities/registry-item.ts deleted file mode 100644 index 655573883a..0000000000 --- a/packages/backend/src/models/entities/registry-item.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい -@Entity() -export class RegistryItem { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the RegistryItem.', - }) - public createdAt: Date; - - @Column('timestamp with time zone', { - comment: 'The updated date of the RegistryItem.', - }) - public updatedAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The owner ID.', - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 1024, - comment: 'The key of the RegistryItem.', - }) - public key: string; - - @Column('jsonb', { - default: {}, nullable: true, - comment: 'The value of the RegistryItem.', - }) - public value: any | null; - - @Index() - @Column('varchar', { - length: 1024, array: true, default: '{}', - }) - public scope: string[]; - - // サードパーティアプリに開放するときのためのカラム - @Index() - @Column('varchar', { - length: 512, nullable: true, - }) - public domain: string | null; -} diff --git a/packages/backend/src/models/entities/relay.ts b/packages/backend/src/models/entities/relay.ts deleted file mode 100644 index 82c0779ff8..0000000000 --- a/packages/backend/src/models/entities/relay.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column } from "typeorm"; -import { id } from "../id.js"; - -@Entity() -export class Relay { - @PrimaryColumn(id()) - public id: string; - - @Index({ unique: true }) - @Column('varchar', { - length: 512, nullable: false, - }) - public inbox: string; - - @Column('enum', { - enum: ['requesting', 'accepted', 'rejected'], - }) - public status: "requesting" | "accepted" | "rejected"; -} diff --git a/packages/backend/src/models/entities/renote-muting.ts b/packages/backend/src/models/entities/renote-muting.ts deleted file mode 100644 index 64ec7f583f..0000000000 --- a/packages/backend/src/models/entities/renote-muting.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { id } from "../id.js"; -import { User } from "./user.js"; - -@Entity() -@Index(["muterId", "muteeId"], { unique: true }) -export class RenoteMuting { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column("timestamp with time zone", { - comment: "The created date of the Muting.", - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: "The mutee user ID.", - }) - public muteeId: User["id"]; - - @ManyToOne(type => User, { - onDelete: "CASCADE", - }) - @JoinColumn() - public mutee: User | null; - - @Index() - @Column({ - ...id(), - comment: "The muter user ID.", - }) - public muterId: User["id"]; - - @ManyToOne(type => User, { - onDelete: "CASCADE", - }) - @JoinColumn() - public muter: User | null; -} diff --git a/packages/backend/src/models/entities/signin.ts b/packages/backend/src/models/entities/signin.ts deleted file mode 100644 index 7859918238..0000000000 --- a/packages/backend/src/models/entities/signin.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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/packages/backend/src/models/entities/sw-subscription.ts b/packages/backend/src/models/entities/sw-subscription.ts deleted file mode 100644 index 8f18688eab..0000000000 --- a/packages/backend/src/models/entities/sw-subscription.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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; - - @Column('boolean', { - default: false, - }) - public sendReadMessage: boolean; -} diff --git a/packages/backend/src/models/entities/used-username.ts b/packages/backend/src/models/entities/used-username.ts deleted file mode 100644 index a069205a5f..0000000000 --- a/packages/backend/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/packages/backend/src/models/entities/user-group-invitation.ts b/packages/backend/src/models/entities/user-group-invitation.ts deleted file mode 100644 index 8037b30e1b..0000000000 --- a/packages/backend/src/models/entities/user-group-invitation.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { UserGroup } from "./user-group.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'userGroupId'], { unique: true }) -export class UserGroupInvitation { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserGroupInvitation.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The group ID.', - }) - public userGroupId: UserGroup["id"]; - - @ManyToOne(type => UserGroup, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroup: UserGroup | null; -} diff --git a/packages/backend/src/models/entities/user-group-joining.ts b/packages/backend/src/models/entities/user-group-joining.ts deleted file mode 100644 index 6d503b274e..0000000000 --- a/packages/backend/src/models/entities/user-group-joining.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { UserGroup } from "./user-group.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'userGroupId'], { unique: true }) -export class UserGroupJoining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserGroupJoining.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The group ID.', - }) - public userGroupId: UserGroup["id"]; - - @ManyToOne(type => UserGroup, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroup: UserGroup | null; -} diff --git a/packages/backend/src/models/entities/user-group.ts b/packages/backend/src/models/entities/user-group.ts deleted file mode 100644 index 38e5af3346..0000000000 --- a/packages/backend/src/models/entities/user-group.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - Entity, - Index, - JoinColumn, - Column, - PrimaryColumn, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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/packages/backend/src/models/entities/user-ip.ts b/packages/backend/src/models/entities/user-ip.ts deleted file mode 100644 index 6b88d52216..0000000000 --- a/packages/backend/src/models/entities/user-ip.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, - PrimaryGeneratedColumn, -} from "typeorm"; -import { id } from "../id.js"; -import { Note } from "./note.js"; -import type { User } from "./user.js"; - -@Entity() -@Index(['userId', 'ip'], { unique: true }) -export class UserIp { - @PrimaryGeneratedColumn() - public id: string; - - @Column('timestamp with time zone', { - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User["id"]; - - @Column('varchar', { - length: 128, - }) - public ip: string; -} diff --git a/packages/backend/src/models/entities/user-keypair.ts b/packages/backend/src/models/entities/user-keypair.ts deleted file mode 100644 index 212e742b9e..0000000000 --- a/packages/backend/src/models/entities/user-keypair.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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/packages/backend/src/models/entities/user-list-joining.ts b/packages/backend/src/models/entities/user-list-joining.ts deleted file mode 100644 index e52fa7b399..0000000000 --- a/packages/backend/src/models/entities/user-list-joining.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { UserList } from "./user-list.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'userListId'], { unique: true }) -export class UserListJoining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserListJoining.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The list ID.', - }) - public userListId: UserList["id"]; - - @ManyToOne(type => UserList, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userList: UserList | null; -} diff --git a/packages/backend/src/models/entities/user-list.ts b/packages/backend/src/models/entities/user-list.ts deleted file mode 100644 index 7c43452308..0000000000 --- a/packages/backend/src/models/entities/user-list.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -export class UserList { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserList.', - }) - public createdAt: Date; - - @Index() - @Column({ - ...id(), - comment: 'The owner ID.', - }) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - comment: 'The name of the UserList.', - }) - public name: string; -} diff --git a/packages/backend/src/models/entities/user-note-pining.ts b/packages/backend/src/models/entities/user-note-pining.ts deleted file mode 100644 index dc6d61f7e5..0000000000 --- a/packages/backend/src/models/entities/user-note-pining.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { Note } from "./note.js"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class UserNotePining { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the UserNotePinings.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User["id"]; - - @ManyToOne(type => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note["id"]; - - @ManyToOne(type => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/entities/user-pending.ts b/packages/backend/src/models/entities/user-pending.ts deleted file mode 100644 index cac85d1c02..0000000000 --- a/packages/backend/src/models/entities/user-pending.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column } from "typeorm"; -import { id } from "../id.js"; - -@Entity() -export class UserPending { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone') - public createdAt: Date; - - @Index({ unique: true }) - @Column('varchar', { - length: 128, - }) - public code: string; - - @Column('varchar', { - length: 128, - }) - public username: string; - - @Column('varchar', { - length: 128, - }) - public email: string; - - @Column('varchar', { - length: 128, - }) - public password: string; -} diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts deleted file mode 100644 index cc3d238679..0000000000 --- a/packages/backend/src/models/entities/user-profile.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { - Entity, - Column, - Index, - OneToOne, - JoinColumn, - PrimaryColumn, -} from "typeorm"; -import { ffVisibility, notificationTypes } from "@/types.js"; -import { id } from "../id.js"; -import { User } from "./user.js"; -import { Page } from "./page.js"; - -// 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; - - @Column('varchar', { - length: 8192, default: '', - }) - public moderationNote: string | null; - - // TODO: そのうち消す - @Column('jsonb', { - default: {}, - comment: 'The client-specific data of the User.', - }) - public clientData: Record; - - // TODO: そのうち消す - @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 autoSensitive: 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('jsonb', { - default: [], - comment: 'List of instances muted by the user.', - }) - public mutedInstances: 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/packages/backend/src/models/entities/user-publickey.ts b/packages/backend/src/models/entities/user-publickey.ts deleted file mode 100644 index d1a9239d11..0000000000 --- a/packages/backend/src/models/entities/user-publickey.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - OneToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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/packages/backend/src/models/entities/user-security-key.ts b/packages/backend/src/models/entities/user-security-key.ts deleted file mode 100644 index 3b9d925d9e..0000000000 --- a/packages/backend/src/models/entities/user-security-key.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - PrimaryColumn, - Entity, - JoinColumn, - Column, - ManyToOne, - Index, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -@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/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts deleted file mode 100644 index c23f4f28d7..0000000000 --- a/packages/backend/src/models/entities/user.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { - Entity, - Column, - Index, - OneToOne, - JoinColumn, - PrimaryColumn, -} from "typeorm"; -import { id } from "../id.js"; -import { DriveFile } from "./drive-file.js"; - -@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('varchar', { - length: 512, - nullable: true, - comment: 'The URI of the new account of the User', - }) - public movedToUri: string | null; - - @Column('simple-array', { - nullable: true, - comment: 'URIs the user is known as too', - }) - public alsoKnownAs: string[] | null; - - @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('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: true, - comment: 'Whether to speak as a cat if isCat.', - }) - public speakAsCat: 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; - - @Column('boolean', { - default: false, - comment: 'Whether to show users replying to other users in the timeline.', - }) - public showTimelineReplies: boolean; - - @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; - - @Column('integer', { - nullable: true, - comment: 'Overrides user drive capacity limit', - }) - public driveCapacityOverrideMb: number | 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; -} - -export type CacheableLocalUser = ILocalUser; - -export type CacheableRemoteUser = IRemoteUser; - -export type CacheableUser = CacheableLocalUser | CacheableRemoteUser; diff --git a/packages/backend/src/models/entities/webhook.ts b/packages/backend/src/models/entities/webhook.ts deleted file mode 100644 index 5db51c3a3c..0000000000 --- a/packages/backend/src/models/entities/webhook.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - PrimaryColumn, - Entity, - Index, - JoinColumn, - Column, - ManyToOne, -} from "typeorm"; -import { User } from "./user.js"; -import { id } from "../id.js"; - -export const webhookEventTypes = [ - "mention", - "unfollow", - "follow", - "followed", - "note", - "reply", - "renote", - "reaction", -] as const; - -@Entity() -export class Webhook { - @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; - - @Index() - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public on: typeof webhookEventTypes[number][]; - - @Column('varchar', { - length: 1024, - }) - public url: string; - - @Column('varchar', { - length: 1024, - }) - public secret: string; - - @Index() - @Column('boolean', { - default: true, - }) - public active: boolean; - - /** - * 直近のリクエスト送信日時 - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public latestSentAt: Date | null; - - /** - * 直近のリクエスト送信時のHTTPステータスコード - */ - @Column('integer', { - nullable: true, - }) - public latestStatus: number | null; -} diff --git a/packages/backend/src/models/id.ts b/packages/backend/src/models/id.ts deleted file mode 100644 index 7e5a787984..0000000000 --- a/packages/backend/src/models/id.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const id = () => ({ - type: "varchar" as const, - length: 32, -}); diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts deleted file mode 100644 index f68166c17f..0000000000 --- a/packages/backend/src/models/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -import {} from "typeorm"; -import { db } from "@/db/postgre.js"; - -import { Announcement } from "./entities/announcement.js"; -import { AnnouncementRead } from "./entities/announcement-read.js"; -import { Instance } from "./entities/instance.js"; -import { Poll } from "./entities/poll.js"; -import { PollVote } from "./entities/poll-vote.js"; -import { Meta } from "./entities/meta.js"; -import { SwSubscription } from "./entities/sw-subscription.js"; -import { NoteWatching } from "./entities/note-watching.js"; -import { NoteThreadMuting } from "./entities/note-thread-muting.js"; -import { NoteUnread } from "./entities/note-unread.js"; -import { RegistrationTicket } from "./entities/registration-tickets.js"; -import { UserRepository } from "./repositories/user.js"; -import { NoteRepository } from "./repositories/note.js"; -import { DriveFileRepository } from "./repositories/drive-file.js"; -import { DriveFolderRepository } from "./repositories/drive-folder.js"; -import { AccessToken } from "./entities/access-token.js"; -import { UserNotePining } from "./entities/user-note-pining.js"; -import { SigninRepository } from "./repositories/signin.js"; -import { MessagingMessageRepository } from "./repositories/messaging-message.js"; -import { UserListRepository } from "./repositories/user-list.js"; -import { UserListJoining } from "./entities/user-list-joining.js"; -import { UserGroupRepository } from "./repositories/user-group.js"; -import { UserGroupJoining } from "./entities/user-group-joining.js"; -import { UserGroupInvitationRepository } from "./repositories/user-group-invitation.js"; -import { FollowRequestRepository } from "./repositories/follow-request.js"; -import { MutingRepository } from "./repositories/muting.js"; -import { RenoteMutingRepository } from "./repositories/renote-muting.js"; -import { BlockingRepository } from "./repositories/blocking.js"; -import { NoteReactionRepository } from "./repositories/note-reaction.js"; -import { NotificationRepository } from "./repositories/notification.js"; -import { NoteFavoriteRepository } from "./repositories/note-favorite.js"; -import { UserPublickey } from "./entities/user-publickey.js"; -import { UserKeypair } from "./entities/user-keypair.js"; -import { AppRepository } from "./repositories/app.js"; -import { FollowingRepository } from "./repositories/following.js"; -import { AbuseUserReportRepository } from "./repositories/abuse-user-report.js"; -import { AuthSessionRepository } from "./repositories/auth-session.js"; -import { UserProfile } from "./entities/user-profile.js"; -import { AttestationChallenge } from "./entities/attestation-challenge.js"; -import { UserSecurityKey } from "./entities/user-security-key.js"; -import { HashtagRepository } from "./repositories/hashtag.js"; -import { PageRepository } from "./repositories/page.js"; -import { PageLikeRepository } from "./repositories/page-like.js"; -import { GalleryPostRepository } from "./repositories/gallery-post.js"; -import { GalleryLikeRepository } from "./repositories/gallery-like.js"; -import { ModerationLogRepository } from "./repositories/moderation-logs.js"; -import { UsedUsername } from "./entities/used-username.js"; -import { ClipRepository } from "./repositories/clip.js"; -import { ClipNote } from "./entities/clip-note.js"; -import { AntennaRepository } from "./repositories/antenna.js"; -import { AntennaNote } from "./entities/antenna-note.js"; -import { PromoNote } from "./entities/promo-note.js"; -import { PromoRead } from "./entities/promo-read.js"; -import { EmojiRepository } from "./repositories/emoji.js"; -import { RelayRepository } from "./repositories/relay.js"; -import { ChannelRepository } from "./repositories/channel.js"; -import { MutedNote } from "./entities/muted-note.js"; -import { ChannelFollowing } from "./entities/channel-following.js"; -import { ChannelNotePining } from "./entities/channel-note-pining.js"; -import { RegistryItem } from "./entities/registry-item.js"; -import { Ad } from "./entities/ad.js"; -import { PasswordResetRequest } from "./entities/password-reset-request.js"; -import { UserPending } from "./entities/user-pending.js"; -import { InstanceRepository } from "./repositories/instance.js"; -import { Webhook } from "./entities/webhook.js"; -import { UserIp } from "./entities/user-ip.js"; - -export const Announcements = db.getRepository(Announcement); -export const AnnouncementReads = db.getRepository(AnnouncementRead); -export const Apps = AppRepository; -export const Notes = NoteRepository; -export const NoteFavorites = NoteFavoriteRepository; -export const NoteWatchings = db.getRepository(NoteWatching); -export const NoteThreadMutings = db.getRepository(NoteThreadMuting); -export const NoteReactions = NoteReactionRepository; -export const NoteUnreads = db.getRepository(NoteUnread); -export const Polls = db.getRepository(Poll); -export const PollVotes = db.getRepository(PollVote); -export const Users = UserRepository; -export const UserProfiles = db.getRepository(UserProfile); -export const UserKeypairs = db.getRepository(UserKeypair); -export const UserPendings = db.getRepository(UserPending); -export const AttestationChallenges = db.getRepository(AttestationChallenge); -export const UserSecurityKeys = db.getRepository(UserSecurityKey); -export const UserPublickeys = db.getRepository(UserPublickey); -export const UserLists = UserListRepository; -export const UserListJoinings = db.getRepository(UserListJoining); -export const UserGroups = UserGroupRepository; -export const UserGroupJoinings = db.getRepository(UserGroupJoining); -export const UserGroupInvitations = UserGroupInvitationRepository; -export const UserNotePinings = db.getRepository(UserNotePining); -export const UserIps = db.getRepository(UserIp); -export const UsedUsernames = db.getRepository(UsedUsername); -export const Followings = FollowingRepository; -export const FollowRequests = FollowRequestRepository; -export const Instances = InstanceRepository; -export const Emojis = EmojiRepository; -export const DriveFiles = DriveFileRepository; -export const DriveFolders = DriveFolderRepository; -export const Notifications = NotificationRepository; -export const Metas = db.getRepository(Meta); -export const Mutings = MutingRepository; -export const RenoteMutings = RenoteMutingRepository; -export const Blockings = BlockingRepository; -export const SwSubscriptions = db.getRepository(SwSubscription); -export const Hashtags = HashtagRepository; -export const AbuseUserReports = AbuseUserReportRepository; -export const RegistrationTickets = db.getRepository(RegistrationTicket); -export const AuthSessions = AuthSessionRepository; -export const AccessTokens = db.getRepository(AccessToken); -export const Signins = SigninRepository; -export const MessagingMessages = MessagingMessageRepository; -export const Pages = PageRepository; -export const PageLikes = PageLikeRepository; -export const GalleryPosts = GalleryPostRepository; -export const GalleryLikes = GalleryLikeRepository; -export const ModerationLogs = ModerationLogRepository; -export const Clips = ClipRepository; -export const ClipNotes = db.getRepository(ClipNote); -export const Antennas = AntennaRepository; -export const AntennaNotes = db.getRepository(AntennaNote); -export const PromoNotes = db.getRepository(PromoNote); -export const PromoReads = db.getRepository(PromoRead); -export const Relays = RelayRepository; -export const MutedNotes = db.getRepository(MutedNote); -export const Channels = ChannelRepository; -export const ChannelFollowings = db.getRepository(ChannelFollowing); -export const ChannelNotePinings = db.getRepository(ChannelNotePining); -export const RegistryItems = db.getRepository(RegistryItem); -export const Webhooks = db.getRepository(Webhook); -export const Ads = db.getRepository(Ad); -export const PasswordResetRequests = db.getRepository(PasswordResetRequest); diff --git a/packages/backend/src/models/repositories/abuse-user-report.ts b/packages/backend/src/models/repositories/abuse-user-report.ts deleted file mode 100644 index 07afef48c4..0000000000 --- a/packages/backend/src/models/repositories/abuse-user-report.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Users } from "../index.js"; -import { AbuseUserReport } from "@/models/entities/abuse-user-report.js"; -import { awaitAll } from "@/prelude/await-all.js"; - -export const AbuseUserReportRepository = db - .getRepository(AbuseUserReport) - .extend({ - async pack(src: AbuseUserReport["id"] | AbuseUserReport) { - const report = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: report.id, - createdAt: report.createdAt.toISOString(), - 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, - forwarded: report.forwarded, - }); - }, - - packMany(reports: any[]) { - return Promise.all(reports.map((x) => this.pack(x))); - }, - }); diff --git a/packages/backend/src/models/repositories/antenna.ts b/packages/backend/src/models/repositories/antenna.ts deleted file mode 100644 index c325e25895..0000000000 --- a/packages/backend/src/models/repositories/antenna.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Antenna } from "@/models/entities/antenna.js"; -import type { Packed } from "@/misc/schema.js"; -import { AntennaNotes, UserGroupJoinings } from "../index.js"; - -export const AntennaRepository = db.getRepository(Antenna).extend({ - async pack(src: Antenna["id"] | Antenna): Promise> { - const antenna = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - const hasUnreadNote = - (await AntennaNotes.findOneBy({ antennaId: antenna.id, read: false })) != - null; - const userGroupJoining = antenna.userGroupJoiningId - ? await UserGroupJoinings.findOneBy({ id: 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, - instances: antenna.instances, - caseSensitive: antenna.caseSensitive, - notify: antenna.notify, - withReplies: antenna.withReplies, - withFile: antenna.withFile, - hasUnreadNote, - }; - }, -}); diff --git a/packages/backend/src/models/repositories/app.ts b/packages/backend/src/models/repositories/app.ts deleted file mode 100644 index af3dfb81a1..0000000000 --- a/packages/backend/src/models/repositories/app.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { App } from "@/models/entities/app.js"; -import { AccessTokens } from "../index.js"; -import type { Packed } from "@/misc/schema.js"; -import type { User } from "../entities/user.js"; - -export const AppRepository = db.getRepository(App).extend({ - 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.findOneByOrFail({ id: src }); - - return { - id: app.id, - name: app.name, - callbackUrl: app.callbackUrl, - permission: app.permission, - ...(opts.includeSecret ? { secret: app.secret } : {}), - ...(me - ? { - isAuthorized: await AccessTokens.countBy({ - appId: app.id, - userId: me.id, - }).then((count) => count > 0), - } - : {}), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/auth-session.ts b/packages/backend/src/models/repositories/auth-session.ts deleted file mode 100644 index d3e1d45d6d..0000000000 --- a/packages/backend/src/models/repositories/auth-session.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Apps } from "../index.js"; -import { AuthSession } from "@/models/entities/auth-session.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import type { User } from "@/models/entities/user.js"; - -export const AuthSessionRepository = db.getRepository(AuthSession).extend({ - async pack( - src: AuthSession["id"] | AuthSession, - me?: { id: User["id"] } | null | undefined, - ) { - const session = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: session.id, - app: Apps.pack(session.appId, me), - token: session.token, - }); - }, -}); diff --git a/packages/backend/src/models/repositories/blocking.ts b/packages/backend/src/models/repositories/blocking.ts deleted file mode 100644 index 3dfa74e763..0000000000 --- a/packages/backend/src/models/repositories/blocking.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Users } from "../index.js"; -import { Blocking } from "@/models/entities/blocking.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import type { Packed } from "@/misc/schema.js"; -import type { User } from "@/models/entities/user.js"; - -export const BlockingRepository = db.getRepository(Blocking).extend({ - async pack( - src: Blocking["id"] | Blocking, - me?: { id: User["id"] } | null | undefined, - ): Promise> { - const blocking = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: blocking.id, - createdAt: blocking.createdAt.toISOString(), - blockeeId: blocking.blockeeId, - blockee: Users.pack(blocking.blockeeId, me, { - detail: true, - }), - }); - }, - - packMany(blockings: any[], me: { id: User["id"] }) { - return Promise.all(blockings.map((x) => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/channel.ts b/packages/backend/src/models/repositories/channel.ts deleted file mode 100644 index 7800a65940..0000000000 --- a/packages/backend/src/models/repositories/channel.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Channel } from "@/models/entities/channel.js"; -import type { Packed } from "@/misc/schema.js"; -import { DriveFiles, ChannelFollowings, NoteUnreads } from "../index.js"; -import type { User } from "@/models/entities/user.js"; - -export const ChannelRepository = db.getRepository(Channel).extend({ - async pack( - src: Channel["id"] | Channel, - me?: { id: User["id"] } | null | undefined, - ): Promise> { - const channel = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - const meId = me ? me.id : null; - - const banner = channel.bannerId - ? await DriveFiles.findOneBy({ id: channel.bannerId }) - : null; - - const hasUnreadNote = meId - ? (await NoteUnreads.findOneBy({ - noteChannelId: channel.id, - userId: meId, - })) != null - : undefined; - - const following = meId - ? await ChannelFollowings.findOneBy({ - 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, - } - : {}), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/clip.ts b/packages/backend/src/models/repositories/clip.ts deleted file mode 100644 index 0c21691bff..0000000000 --- a/packages/backend/src/models/repositories/clip.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Clip } from "@/models/entities/clip.js"; -import type { Packed } from "@/misc/schema.js"; -import { Users } from "../index.js"; -import { awaitAll } from "@/prelude/await-all.js"; - -export const ClipRepository = db.getRepository(Clip).extend({ - async pack(src: Clip["id"] | Clip): Promise> { - const clip = - typeof src === "object" ? src : await this.findOneByOrFail({ id: 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, - }); - }, - - packMany(clips: Clip[]) { - return Promise.all(clips.map((x) => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts deleted file mode 100644 index 3918f7947b..0000000000 --- a/packages/backend/src/models/repositories/drive-file.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { DriveFile } from "@/models/entities/drive-file.js"; -import type { User } from "@/models/entities/user.js"; -import { toPuny } from "@/misc/convert-host.js"; -import { awaitAll, Promiseable } from "@/prelude/await-all.js"; -import type { Packed } from "@/misc/schema.js"; -import config from "@/config/index.js"; -import { query, appendQuery } from "@/prelude/url.js"; -import { Meta } from "@/models/entities/meta.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Users, DriveFolders } from "../index.js"; -import { deepClone } from "@/misc/clone.js"; - -type PackOptions = { - detail?: boolean; - self?: boolean; - withUser?: boolean; -}; - -export const DriveFileRepository = db.getRepository(DriveFile).extend({ - validateFileName(name: string): boolean { - return ( - name.trim().length > 0 && - name.length <= 200 && - name.indexOf("\\") === -1 && - name.indexOf("/") === -1 && - name.indexOf("..") === -1 - ); - }, - - getPublicProperties(file: DriveFile): DriveFile["properties"] { - if (file.properties.orientation != null) { - const properties = deepClone(file.properties); - if (file.properties.orientation >= 5) { - [properties.width, properties.height] = [ - properties.height, - properties.width, - ]; - } - properties.orientation = undefined; - return properties; - } - - return file.properties; - }, - - getPublicUrl(file: DriveFile, thumbnail = false): 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 && config.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", - "image/avif", - ].includes(file.type); - - return thumbnail - ? file.thumbnailUrl || (isImage ? file.webpublicUrl || file.url : null) - : file.webpublicUrl || file.url; - }, - - 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; - }, - - 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; - }, - - 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; - }, - - 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; - }, - - async pack( - src: DriveFile["id"] | DriveFile, - options?: PackOptions, - ): Promise> { - const opts = Object.assign( - { - detail: false, - self: false, - }, - options, - ); - - const file = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - 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: opts.self ? file.properties : this.getPublicProperties(file), - url: opts.self ? file.url : this.getPublicUrl(file, false), - thumbnailUrl: this.getPublicUrl(file, true), - 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, - }); - }, - - async packNullable( - 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.findOneBy({ id: src }); - if (file == null) return null; - - 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: opts.self ? file.properties : this.getPublicProperties(file), - url: opts.self ? file.url : this.getPublicUrl(file, false), - thumbnailUrl: this.getPublicUrl(file, true), - 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, - }); - }, - - async packMany( - files: (DriveFile["id"] | DriveFile)[], - options?: PackOptions, - ): Promise[]> { - const items = await Promise.all( - files.map((f) => this.packNullable(f, options)), - ); - return items.filter((x): x is Packed<"DriveFile"> => x != null); - }, -}); diff --git a/packages/backend/src/models/repositories/drive-folder.ts b/packages/backend/src/models/repositories/drive-folder.ts deleted file mode 100644 index 9823561d0b..0000000000 --- a/packages/backend/src/models/repositories/drive-folder.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { DriveFolders, DriveFiles } from "../index.js"; -import { DriveFolder } from "@/models/entities/drive-folder.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import type { Packed } from "@/misc/schema.js"; - -export const DriveFolderRepository = db.getRepository(DriveFolder).extend({ - 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.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: folder.id, - createdAt: folder.createdAt.toISOString(), - name: folder.name, - parentId: folder.parentId, - - ...(opts.detail - ? { - foldersCount: DriveFolders.countBy({ - parentId: folder.id, - }), - filesCount: DriveFiles.countBy({ - folderId: folder.id, - }), - - ...(folder.parentId - ? { - parent: this.pack(folder.parentId, { - detail: true, - }), - } - : {}), - } - : {}), - }); - }, -}); diff --git a/packages/backend/src/models/repositories/emoji.ts b/packages/backend/src/models/repositories/emoji.ts deleted file mode 100644 index 6eabfe9558..0000000000 --- a/packages/backend/src/models/repositories/emoji.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Emoji } from "@/models/entities/emoji.js"; -import type { Packed } from "@/misc/schema.js"; - -export const EmojiRepository = db.getRepository(Emoji).extend({ - async pack(src: Emoji["id"] | Emoji): Promise> { - const emoji = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return { - id: emoji.id, - aliases: emoji.aliases, - name: emoji.name, - category: emoji.category, - host: emoji.host, - // || emoji.originalUrl してるのは後方互換性のため - url: emoji.publicUrl || emoji.originalUrl, - license: emoji.license, - }; - }, - - packMany(emojis: any[]) { - return Promise.all(emojis.map((x) => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/follow-request.ts b/packages/backend/src/models/repositories/follow-request.ts deleted file mode 100644 index cef6ea7228..0000000000 --- a/packages/backend/src/models/repositories/follow-request.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { FollowRequest } from "@/models/entities/follow-request.js"; -import { Users } from "../index.js"; -import type { User } from "@/models/entities/user.js"; - -export const FollowRequestRepository = db.getRepository(FollowRequest).extend({ - async pack( - src: FollowRequest["id"] | FollowRequest, - me?: { id: User["id"] } | null | undefined, - ) { - const request = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return { - id: request.id, - follower: await Users.pack(request.followerId, me), - followee: await Users.pack(request.followeeId, me), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/following.ts b/packages/backend/src/models/repositories/following.ts deleted file mode 100644 index b102365e09..0000000000 --- a/packages/backend/src/models/repositories/following.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Users } from "../index.js"; -import { Following } from "@/models/entities/following.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import type { Packed } from "@/misc/schema.js"; -import type { User } from "@/models/entities/user.js"; - -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; -}; - -export const FollowingRepository = db.getRepository(Following).extend({ - isLocalFollower(following: Following): following is LocalFollowerFollowing { - return following.followerHost == null; - }, - - isRemoteFollower(following: Following): following is RemoteFollowerFollowing { - return following.followerHost != null; - }, - - isLocalFollowee(following: Following): following is LocalFolloweeFollowing { - return following.followeeHost == null; - }, - - isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing { - return following.followeeHost != null; - }, - - 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.findOneByOrFail({ id: 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, - }); - }, - - 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))); - }, -}); diff --git a/packages/backend/src/models/repositories/gallery-like.ts b/packages/backend/src/models/repositories/gallery-like.ts deleted file mode 100644 index c8920d1ee6..0000000000 --- a/packages/backend/src/models/repositories/gallery-like.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { GalleryLike } from "@/models/entities/gallery-like.js"; -import { GalleryPosts } from "../index.js"; - -export const GalleryLikeRepository = db.getRepository(GalleryLike).extend({ - async pack(src: GalleryLike["id"] | GalleryLike, me?: any) { - const like = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return { - id: like.id, - post: await GalleryPosts.pack(like.post || like.postId, me), - }; - }, - - packMany(likes: any[], me: any) { - return Promise.all(likes.map((x) => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/gallery-post.ts b/packages/backend/src/models/repositories/gallery-post.ts deleted file mode 100644 index b4206b0bf4..0000000000 --- a/packages/backend/src/models/repositories/gallery-post.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { GalleryPost } from "@/models/entities/gallery-post.js"; -import type { Packed } from "@/misc/schema.js"; -import { Users, DriveFiles, GalleryLikes } from "../index.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import type { User } from "@/models/entities/user.js"; - -export const GalleryPostRepository = db.getRepository(GalleryPost).extend({ - 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.findOneByOrFail({ id: 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.findOneBy({ postId: post.id, userId: meId }).then( - (x) => x != null, - ) - : undefined, - }); - }, - - packMany(posts: GalleryPost[], me?: { id: User["id"] } | null | undefined) { - return Promise.all(posts.map((x) => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/hashtag.ts b/packages/backend/src/models/repositories/hashtag.ts deleted file mode 100644 index 7bd76c1c70..0000000000 --- a/packages/backend/src/models/repositories/hashtag.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Hashtag } from "@/models/entities/hashtag.js"; -import type { Packed } from "@/misc/schema.js"; - -export const HashtagRepository = db.getRepository(Hashtag).extend({ - 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, - }; - }, - - packMany(hashtags: Hashtag[]) { - return Promise.all(hashtags.map((x) => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/instance.ts b/packages/backend/src/models/repositories/instance.ts deleted file mode 100644 index fb4498911a..0000000000 --- a/packages/backend/src/models/repositories/instance.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Instance } from "@/models/entities/instance.js"; -import type { Packed } from "@/misc/schema.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { shouldBlockInstance } from "@/misc/should-block-instance.js"; - -export const InstanceRepository = db.getRepository(Instance).extend({ - async pack(instance: Instance): Promise> { - const meta = await fetchMeta(); - return { - id: instance.id, - caughtAt: instance.caughtAt.toISOString(), - host: instance.host, - usersCount: instance.usersCount, - notesCount: instance.notesCount, - followingCount: instance.followingCount, - followersCount: instance.followersCount, - latestRequestSentAt: instance.latestRequestSentAt - ? instance.latestRequestSentAt.toISOString() - : null, - lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(), - isNotResponding: instance.isNotResponding, - isSuspended: instance.isSuspended, - isBlocked: await shouldBlockInstance(instance.host), - softwareName: instance.softwareName, - softwareVersion: instance.softwareVersion, - openRegistrations: instance.openRegistrations, - name: instance.name, - description: instance.description, - maintainerName: instance.maintainerName, - maintainerEmail: instance.maintainerEmail, - iconUrl: instance.iconUrl, - faviconUrl: instance.faviconUrl, - themeColor: instance.themeColor, - infoUpdatedAt: instance.infoUpdatedAt - ? instance.infoUpdatedAt.toISOString() - : null, - }; - }, - - packMany(instances: Instance[]) { - return Promise.all(instances.map((x) => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/messaging-message.ts b/packages/backend/src/models/repositories/messaging-message.ts deleted file mode 100644 index 6c0987bf08..0000000000 --- a/packages/backend/src/models/repositories/messaging-message.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { MessagingMessage } from "@/models/entities/messaging-message.js"; -import { Users, DriveFiles, UserGroups } from "../index.js"; -import type { Packed } from "@/misc/schema.js"; -import type { User } from "@/models/entities/user.js"; - -export const MessagingMessageRepository = db - .getRepository(MessagingMessage) - .extend({ - 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.findOneByOrFail({ id: 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, - }; - }, - }); diff --git a/packages/backend/src/models/repositories/moderation-logs.ts b/packages/backend/src/models/repositories/moderation-logs.ts deleted file mode 100644 index 3858b9509b..0000000000 --- a/packages/backend/src/models/repositories/moderation-logs.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Users } from "../index.js"; -import { ModerationLog } from "@/models/entities/moderation-log.js"; -import { awaitAll } from "@/prelude/await-all.js"; - -export const ModerationLogRepository = db.getRepository(ModerationLog).extend({ - async pack(src: ModerationLog["id"] | ModerationLog) { - const log = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: log.id, - createdAt: log.createdAt.toISOString(), - type: log.type, - info: log.info, - userId: log.userId, - user: Users.pack(log.user || log.userId, null, { - detail: true, - }), - }); - }, - - packMany(reports: any[]) { - return Promise.all(reports.map((x) => this.pack(x))); - }, -}); diff --git a/packages/backend/src/models/repositories/muting.ts b/packages/backend/src/models/repositories/muting.ts deleted file mode 100644 index 4d0201d5a0..0000000000 --- a/packages/backend/src/models/repositories/muting.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Users } from "../index.js"; -import { Muting } from "@/models/entities/muting.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import type { Packed } from "@/misc/schema.js"; -import type { User } from "@/models/entities/user.js"; - -export const MutingRepository = db.getRepository(Muting).extend({ - async pack( - src: Muting["id"] | Muting, - me?: { id: User["id"] } | null | undefined, - ): Promise> { - const muting = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: muting.id, - createdAt: muting.createdAt.toISOString(), - expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null, - muteeId: muting.muteeId, - mutee: Users.pack(muting.muteeId, me, { - detail: true, - }), - }); - }, - - packMany(mutings: any[], me: { id: User["id"] }) { - return Promise.all(mutings.map((x) => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/note-favorite.ts b/packages/backend/src/models/repositories/note-favorite.ts deleted file mode 100644 index ba43e3c3b4..0000000000 --- a/packages/backend/src/models/repositories/note-favorite.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { NoteFavorite } from "@/models/entities/note-favorite.js"; -import { Notes } from "../index.js"; -import type { User } from "@/models/entities/user.js"; - -export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({ - async pack( - src: NoteFavorite["id"] | NoteFavorite, - me?: { id: User["id"] } | null | undefined, - ) { - const favorite = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return { - id: favorite.id, - createdAt: favorite.createdAt.toISOString(), - noteId: favorite.noteId, - // may throw error - note: await Notes.pack(favorite.note || favorite.noteId, me), - }; - }, - - packMany(favorites: any[], me: { id: User["id"] }) { - return Promise.allSettled(favorites.map((x) => this.pack(x, me))).then( - (promises) => - promises.flatMap((result) => - result.status === "fulfilled" ? [result.value] : [], - ), - ); - }, -}); diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts deleted file mode 100644 index 6d1dfbd6fd..0000000000 --- a/packages/backend/src/models/repositories/note-reaction.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { NoteReaction } from "@/models/entities/note-reaction.js"; -import { Notes, Users } from "../index.js"; -import type { Packed } from "@/misc/schema.js"; -import { convertLegacyReaction } from "@/misc/reaction-lib.js"; -import type { User } from "@/models/entities/user.js"; - -export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ - 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.findOneByOrFail({ id: src }); - - return { - id: reaction.id, - createdAt: reaction.createdAt.toISOString(), - user: await Users.pack(reaction.user ?? reaction.userId, me), - type: convertLegacyReaction(reaction.reaction), - ...(opts.withNote - ? { - // may throw error - note: await Notes.pack(reaction.note ?? reaction.noteId, me), - } - : {}), - }; - }, - - async packMany( - src: NoteReaction[], - me?: { id: User["id"] } | null | undefined, - options?: { - withNote: booleam; - }, - ): Promise[]> { - const reactions = await Promise.allSettled( - src.map((reaction) => this.pack(reaction, me, options)), - ); - - // filter out rejected promises, only keep fulfilled values - return reactions.flatMap((result) => - result.status === "fulfilled" ? [result.value] : [], - ); - }, -}); diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts deleted file mode 100644 index 5e56a817bc..0000000000 --- a/packages/backend/src/models/repositories/note.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { In } from "typeorm"; -import * as mfm from "mfm-js"; -import { Note } from "@/models/entities/note.js"; -import type { User } from "@/models/entities/user.js"; -import { - Users, - PollVotes, - DriveFiles, - NoteReactions, - Followings, - Polls, - Channels, -} from "../index.js"; -import type { Packed } from "@/misc/schema.js"; -import { nyaize } from "@/misc/nyaize.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import { - convertLegacyReaction, - convertLegacyReactions, - decodeReaction, -} from "@/misc/reaction-lib.js"; -import type { NoteReaction } from "@/models/entities/note-reaction.js"; -import { - aggregateNoteEmojis, - populateEmojis, - prefetchEmojis, -} from "@/misc/populate-emojis.js"; -import { db } from "@/db/postgre.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; - -async function populatePoll(note: Note, meId: User["id"] | null) { - const poll = await Polls.findOneByOrFail({ noteId: note.id }); - const choices = poll.choices.map((c) => ({ - text: c, - votes: poll.votes[poll.choices.indexOf(c)], - isVoted: false, - })); - - if (meId) { - if (poll.multiple) { - const votes = await PollVotes.findBy({ - 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.findOneBy({ - userId: meId, - noteId: note.id, - }); - - if (vote) { - choices[vote.choice].isVoted = true; - } - } - } - - return { - multiple: poll.multiple, - expiresAt: poll.expiresAt, - choices, - }; -} - -async function populateMyReaction( - note: Note, - meId: User["id"], - _hint_?: { - myReactions: Map; - }, -) { - if (_hint_?.myReactions) { - const reaction = _hint_.myReactions.get(note.id); - if (reaction) { - return convertLegacyReaction(reaction.reaction); - } else if (reaction === null) { - return undefined; - } - // 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない - } - - const reaction = await NoteReactions.findOneBy({ - userId: meId, - noteId: note.id, - }); - - if (reaction) { - return convertLegacyReaction(reaction.reaction); - } - - return undefined; -} - -export const NoteRepository = db.getRepository(Note).extend({ - async isVisibleForMe(note: Note, meId: User["id"] | null): Promise { - // This code must always be synchronized with the checks in generateVisibilityQuery. - // visibility が specified かつ自分が指定されていなかったら非表示 - if (note.visibility === "specified") { - if (meId == null) { - return false; - } else if (meId === note.userId) { - return true; - } else { - // 指定されているかどうか - return note.visibleUserIds.some((id: any) => meId === id); - } - } - - // 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?.some((id) => meId === id)) { - // 自分へのメンション - return true; - } else { - // フォロワーかどうか - const [following, user] = await Promise.all([ - Followings.count({ - where: { - followeeId: note.userId, - followerId: meId, - }, - take: 1, - }), - Users.findOneByOrFail({ id: meId }), - ]); - - /* If we know the following, everyhting is fine. - - But if we do not know the following, it might be that both the - author of the note and the author of the like are remote users, - in which case we can never know the following. Instead we have - to assume that the users are following each other. - */ - return following > 0 || (note.userHost != null && user.host != null); - } - } - - return true; - }, - - async pack( - src: Note["id"] | Note, - me?: { id: User["id"] } | null | undefined, - options?: { - detail?: boolean; - _hint_?: { - myReactions: Map; - }; - }, - ): Promise> { - const opts = Object.assign( - { - detail: true, - }, - options, - ); - - const meId = me ? me.id : null; - const note = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - const host = note.userHost; - - if (!(await this.isVisibleForMe(note, meId))) { - throw new IdentifiableError( - "9725d0ce-ba28-4dde-95a7-2cbb2c15de24", - "No such note.", - ); - } - - 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.findOneBy({ id: note.channelId }) - : null; - - const reactionEmojiNames = Object.keys(note.reactions) - .filter((x) => x?.startsWith(":")) - .map((x) => decodeReaction(x).reaction) - .map((x) => x.replace(/:/g, "")); - - const noteEmoji = await populateEmojis( - note.emojis.concat(reactionEmojiNames), - host, - ); - const reactionEmoji = await populateEmojis(reactionEmojiNames, host); - const packed: Packed<"Note"> = 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, - renoteCount: note.renoteCount, - repliesCount: note.repliesCount, - reactions: convertLegacyReactions(note.reactions), - reactionEmojis: reactionEmoji, - emojis: noteEmoji, - tags: note.tags.length > 0 ? note.tags : undefined, - 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(note, meId) : undefined, - - ...(meId - ? { - myReaction: populateMyReaction(note, meId, options?._hint_), - } - : {}), - } - : {}), - }); - - if (packed.user.isCat && packed.user.speakAsCat && packed.text) { - const tokens = packed.text ? mfm.parse(packed.text) : []; - function nyaizeNode(node: mfm.MfmNode) { - if (node.type === "quote") return; - if (node.type === "text") node.props.text = nyaize(node.props.text); - - if (node.children) { - for (const child of node.children) { - nyaizeNode(child); - } - } - } - - for (const node of tokens) nyaizeNode(node); - - packed.text = mfm.toString(tokens); - } - - return packed; - }, - - async packMany( - notes: Note[], - me?: { id: User["id"] } | null | undefined, - options?: { - detail?: 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.findBy({ - userId: meId, - noteId: In(targets), - }); - - for (const target of targets) { - myReactionsMap.set( - target, - myReactions.find((reaction) => reaction.noteId === target) || null, - ); - } - } - - await prefetchEmojis(aggregateNoteEmojis(notes)); - - const promises = await Promise.allSettled( - notes.map((n) => - this.pack(n, me, { - ...options, - _hint_: { - myReactions: myReactionsMap, - }, - }), - ), - ); - - // filter out rejected promises, only keep fulfilled values - return promises.flatMap((result) => - result.status === "fulfilled" ? [result.value] : [], - ); - }, -}); diff --git a/packages/backend/src/models/repositories/notification.ts b/packages/backend/src/models/repositories/notification.ts deleted file mode 100644 index 1538e67d86..0000000000 --- a/packages/backend/src/models/repositories/notification.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { In, Repository } from "typeorm"; -import { Notification } from "@/models/entities/notification.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import type { Packed } from "@/misc/schema.js"; -import type { Note } from "@/models/entities/note.js"; -import type { NoteReaction } from "@/models/entities/note-reaction.js"; -import type { User } from "@/models/entities/user.js"; -import { aggregateNoteEmojis, prefetchEmojis } from "@/misc/populate-emojis.js"; -import { notificationTypes } from "@/types.js"; -import { db } from "@/db/postgre.js"; -import { - Users, - Notes, - UserGroupInvitations, - AccessTokens, - NoteReactions, -} from "../index.js"; - -export const NotificationRepository = db.getRepository(Notification).extend({ - async pack( - src: Notification["id"] | Notification, - options: { - _hintForEachNotes_?: { - myReactions: Map; - }; - }, - ): Promise> { - const notification = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - const token = notification.appAccessTokenId - ? await AccessTokens.findOneByOrFail({ - id: 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 === "pollEnded" - ? { - note: Notes.pack( - notification.note || notification.noteId!, - { id: notification.notifieeId }, - { - detail: true, - _hint_: options._hintForEachNotes_, - }, - ), - } - : {}), - ...(notification.type === "groupInvited" - ? { - invitation: UserGroupInvitations.pack( - notification.userGroupInvitationId!, - ), - } - : {}), - ...(notification.type === "app" - ? { - body: notification.customBody, - header: notification.customHeader || token?.name, - icon: notification.customIcon || token?.iconUrl, - } - : {}), - }); - }, - - 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.findBy({ - userId: meId, - noteId: In(targets), - }); - - for (const target of targets) { - myReactionsMap.set( - target, - myReactions.find((reaction) => reaction.noteId === target) || null, - ); - } - - await prefetchEmojis(aggregateNoteEmojis(notes)); - - const results = await Promise.all( - notifications.map((x) => - this.pack(x, { - _hintForEachNotes_: { - myReactions: myReactionsMap, - }, - }).catch((e) => null), - ), - ); - return results.filter((x) => x != null); - }, -}); diff --git a/packages/backend/src/models/repositories/page-like.ts b/packages/backend/src/models/repositories/page-like.ts deleted file mode 100644 index f78ef81b02..0000000000 --- a/packages/backend/src/models/repositories/page-like.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { PageLike } from "@/models/entities/page-like.js"; -import type { User } from "@/models/entities/user.js"; -import { Pages } from "../index.js"; - -export const PageLikeRepository = db.getRepository(PageLike).extend({ - async pack( - src: PageLike["id"] | PageLike, - me?: { id: User["id"] } | null | undefined, - ) { - const like = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return { - id: like.id, - page: await Pages.pack(like.page || like.pageId, me), - }; - }, - - packMany(likes: PageLike[], me: { id: User["id"] }) { - return Promise.all(likes.map((x) => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/page.ts b/packages/backend/src/models/repositories/page.ts deleted file mode 100644 index d9241c3629..0000000000 --- a/packages/backend/src/models/repositories/page.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Page } from "@/models/entities/page.js"; -import type { Packed } from "@/misc/schema.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import type { User } from "@/models/entities/user.js"; -import { Users, DriveFiles, PageLikes } from "../index.js"; - -export const PageRepository = db.getRepository(Page).extend({ - 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.findOneByOrFail({ id: src }); - - const attachedFiles: Promise[] = []; - const collectFile = (xs: any[]) => { - for (const x of xs) { - if (x.type === "image") { - attachedFiles.push( - DriveFiles.findOneBy({ - 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, - isPublic: page.isPublic, - 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) - ).filter((x): x is DriveFile => x != null), - ), - likedCount: page.likedCount, - isLiked: meId - ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then( - (x) => x != null, - ) - : undefined, - }); - }, - - packMany(pages: Page[], me?: { id: User["id"] } | null | undefined) { - return Promise.all(pages.map((x) => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/relay.ts b/packages/backend/src/models/repositories/relay.ts deleted file mode 100644 index 633861496a..0000000000 --- a/packages/backend/src/models/repositories/relay.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Relay } from "@/models/entities/relay.js"; - -export const RelayRepository = db.getRepository(Relay).extend({}); diff --git a/packages/backend/src/models/repositories/renote-muting.ts b/packages/backend/src/models/repositories/renote-muting.ts deleted file mode 100644 index 18fd343a05..0000000000 --- a/packages/backend/src/models/repositories/renote-muting.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Packed } from "@/misc/schema.js"; -import { RenoteMuting } from "@/models/entities/renote-muting.js"; -import { User } from "@/models/entities/user.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import { Users } from "../index.js"; - -export const RenoteMutingRepository = db.getRepository(RenoteMuting).extend({ - async pack( - src: RenoteMuting["id"] | RenoteMuting, - me?: { id: User["id"] } | null | undefined, - ): Promise> { - const muting = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: muting.id, - createdAt: muting.createdAt.toISOString(), - muteeId: muting.muteeId, - mutee: Users.pack(muting.muteeId, me, { - detail: true, - }), - }); - }, - - packMany(mutings: any[], me: { id: User["id"] }) { - return Promise.all(mutings.map((x) => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/signin.ts b/packages/backend/src/models/repositories/signin.ts deleted file mode 100644 index 06cf2c2108..0000000000 --- a/packages/backend/src/models/repositories/signin.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { Signin } from "@/models/entities/signin.js"; - -export const SigninRepository = db.getRepository(Signin).extend({ - async pack(src: Signin) { - return src; - }, -}); diff --git a/packages/backend/src/models/repositories/user-group-invitation.ts b/packages/backend/src/models/repositories/user-group-invitation.ts deleted file mode 100644 index 920fb9ba29..0000000000 --- a/packages/backend/src/models/repositories/user-group-invitation.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { UserGroupInvitation } from "@/models/entities/user-group-invitation.js"; -import { UserGroups } from "../index.js"; - -export const UserGroupInvitationRepository = db - .getRepository(UserGroupInvitation) - .extend({ - async pack(src: UserGroupInvitation["id"] | UserGroupInvitation) { - const invitation = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - return { - id: invitation.id, - group: await UserGroups.pack( - invitation.userGroup || invitation.userGroupId, - ), - }; - }, - - packMany(invitations: any[]) { - return Promise.all(invitations.map((x) => this.pack(x))); - }, - }); diff --git a/packages/backend/src/models/repositories/user-group.ts b/packages/backend/src/models/repositories/user-group.ts deleted file mode 100644 index daec94490e..0000000000 --- a/packages/backend/src/models/repositories/user-group.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { UserGroup } from "@/models/entities/user-group.js"; -import { UserGroupJoinings } from "../index.js"; -import type { Packed } from "@/misc/schema.js"; - -export const UserGroupRepository = db.getRepository(UserGroup).extend({ - async pack(src: UserGroup["id"] | UserGroup): Promise> { - const userGroup = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - const users = await UserGroupJoinings.findBy({ - userGroupId: userGroup.id, - }); - - return { - id: userGroup.id, - createdAt: userGroup.createdAt.toISOString(), - name: userGroup.name, - ownerId: userGroup.userId, - userIds: users.map((x) => x.userId), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/user-list.ts b/packages/backend/src/models/repositories/user-list.ts deleted file mode 100644 index e3abeac3f6..0000000000 --- a/packages/backend/src/models/repositories/user-list.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { db } from "@/db/postgre.js"; -import { UserList } from "@/models/entities/user-list.js"; -import { UserListJoinings } from "../index.js"; -import type { Packed } from "@/misc/schema.js"; - -export const UserListRepository = db.getRepository(UserList).extend({ - async pack(src: UserList["id"] | UserList): Promise> { - const userList = - typeof src === "object" ? src : await this.findOneByOrFail({ id: src }); - - const users = await UserListJoinings.findBy({ - userListId: userList.id, - }); - - return { - id: userList.id, - createdAt: userList.createdAt.toISOString(), - name: userList.name, - userIds: users.map((x) => x.userId), - }; - }, -}); diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts deleted file mode 100644 index f6fcdae296..0000000000 --- a/packages/backend/src/models/repositories/user.ts +++ /dev/null @@ -1,619 +0,0 @@ -import { URL } from "url"; -import { In, Not } from "typeorm"; -import Ajv from "ajv"; -import type { ILocalUser, IRemoteUser } from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; -import config from "@/config/index.js"; -import type { Packed } from "@/misc/schema.js"; -import type { Promiseable } from "@/prelude/await-all.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import { populateEmojis } from "@/misc/populate-emojis.js"; -import { getAntennas } from "@/misc/antenna-cache.js"; -import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from "@/const.js"; -import { Cache } from "@/misc/cache.js"; -import { db } from "@/db/postgre.js"; -import { isActor, getApId } from "@/remote/activitypub/type.js"; -import DbResolver from "@/remote/activitypub/db-resolver.js"; -import Resolver from "@/remote/activitypub/resolver.js"; -import { createPerson } from "@/remote/activitypub/models/person.js"; -import { - AnnouncementReads, - Announcements, - AntennaNotes, - Blockings, - ChannelFollowings, - DriveFiles, - Followings, - FollowRequests, - Instances, - MessagingMessages, - Mutings, - RenoteMutings, - Notes, - NoteUnreads, - Notifications, - Pages, - UserGroupJoinings, - UserNotePinings, - UserProfiles, - UserSecurityKeys, -} from "../index.js"; -import type { Instance } from "../entities/instance.js"; - -const userInstanceCache = new Cache(1000 * 60 * 60 * 3); - -type IsUserDetailed = Detailed extends true - ? Packed<"UserDetailed"> - : Packed<"UserLite">; -type IsMeAndIsUserDetailed< - ExpectsMe extends boolean | null, - Detailed extends boolean, -> = Detailed extends true - ? ExpectsMe extends true - ? Packed<"MeDetailed"> - : ExpectsMe extends false - ? Packed<"UserDetailedNotMe"> - : Packed<"UserDetailed"> - : Packed<"UserLite">; - -const ajv = new Ajv(); - -const localUsernameSchema = { - type: "string", - pattern: /^\w{1,20}$/.toString().slice(1, -1), -} as const; -const passwordSchema = { type: "string", minLength: 1 } as const; -const nameSchema = { type: "string", minLength: 1, maxLength: 50 } as const; -const descriptionSchema = { - type: "string", - minLength: 1, - maxLength: 2048, -} as const; -const locationSchema = { type: "string", minLength: 1, maxLength: 50 } as const; -const birthdaySchema = { - type: "string", - pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1), -} as const; - -function isLocalUser(user: User): user is ILocalUser; -function isLocalUser( - user: T, -): user is T & { host: null }; -/** - * Returns true if the user is local. - * - * @param user The user to check. - * @returns True if the user is local. - */ -function isLocalUser(user: User | { host: User["host"] }): boolean { - return user.host == null; -} - -function isRemoteUser(user: User): user is IRemoteUser; -function isRemoteUser( - user: T, -): user is T & { host: string }; -/** - * Returns true if the user is remote. - * - * @param user The user to check. - * @returns True if the user is remote. - */ -function isRemoteUser(user: User | { host: User["host"] }): boolean { - return !isLocalUser(user); -} - -export const UserRepository = db.getRepository(User).extend({ - localUsernameSchema, - passwordSchema, - nameSchema, - descriptionSchema, - locationSchema, - birthdaySchema, - - //#region Validators - validateLocalUsername: ajv.compile(localUsernameSchema), - validatePassword: ajv.compile(passwordSchema), - validateName: ajv.compile(nameSchema), - validateDescription: ajv.compile(descriptionSchema), - validateLocation: ajv.compile(locationSchema), - validateBirthday: ajv.compile(birthdaySchema), - //#endregion - - async getRelation(me: User["id"], target: User["id"]) { - return awaitAll({ - id: target, - isFollowing: Followings.count({ - where: { - followerId: me, - followeeId: target, - }, - take: 1, - }).then((n) => n > 0), - isFollowed: Followings.count({ - where: { - followerId: target, - followeeId: me, - }, - take: 1, - }).then((n) => n > 0), - hasPendingFollowRequestFromYou: FollowRequests.count({ - where: { - followerId: me, - followeeId: target, - }, - take: 1, - }).then((n) => n > 0), - hasPendingFollowRequestToYou: FollowRequests.count({ - where: { - followerId: target, - followeeId: me, - }, - take: 1, - }).then((n) => n > 0), - isBlocking: Blockings.count({ - where: { - blockerId: me, - blockeeId: target, - }, - take: 1, - }).then((n) => n > 0), - isBlocked: Blockings.count({ - where: { - blockerId: target, - blockeeId: me, - }, - take: 1, - }).then((n) => n > 0), - isMuted: Mutings.count({ - where: { - muterId: me, - muteeId: target, - }, - take: 1, - }).then((n) => n > 0), - isRenoteMuted: RenoteMutings.count({ - where: { - muterId: me, - muteeId: target, - }, - take: 1, - }).then((n) => n > 0), - }); - }, - - async getHasUnreadMessagingMessage(userId: User["id"]): Promise { - const mute = await Mutings.findBy({ - muterId: userId, - }); - - const joinings = await UserGroupJoinings.findBy({ 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); - }, - - async getHasUnreadAnnouncement(userId: User["id"]): Promise { - const reads = await AnnouncementReads.findBy({ - userId: userId, - }); - - const count = await Announcements.countBy( - reads.length > 0 - ? { - id: Not(In(reads.map((read) => read.announcementId))), - } - : {}, - ); - - return count > 0; - }, - - async userFromURI(uri: string): Promise { - const dbResolver = new DbResolver(); - let local = await dbResolver.getUserFromApId(uri); - if (local) { - return local; - } - - // fetching Object once from remote - const resolver = new Resolver(); - const object = (await resolver.resolve(uri)) as any; - - // /@user If a URI other than the id is specified, - // the URI is determined here - if (uri !== object.id) { - local = await dbResolver.getUserFromApId(object.id); - if (local != null) return local; - } - - return isActor(object) ? await createPerson(getApId(object)) : null; - }, - - async getHasUnreadAntenna(userId: User["id"]): Promise { - const myAntennas = (await getAntennas()).filter((a) => a.userId === userId); - - const unread = - myAntennas.length > 0 - ? await AntennaNotes.findOneBy({ - antennaId: In(myAntennas.map((x) => x.id)), - read: false, - }) - : null; - - return unread != null; - }, - - async getHasUnreadChannel(userId: User["id"]): Promise { - const channels = await ChannelFollowings.findBy({ followerId: userId }); - - const unread = - channels.length > 0 - ? await NoteUnreads.findOneBy({ - userId: userId, - noteChannelId: In(channels.map((x) => x.followeeId)), - }) - : null; - - return unread != null; - }, - - async getHasUnreadNotification(userId: User["id"]): Promise { - const mute = await Mutings.findBy({ - 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; - }, - - async getHasPendingReceivedFollowRequest( - userId: User["id"], - ): Promise { - const count = await FollowRequests.countBy({ - followeeId: userId, - }); - - return count > 0; - }, - - getOnlineStatus(user: User): "unknown" | "online" | "active" | "offline" { - 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"; - }, - - async getAvatarUrl(user: User): Promise { - if (user.avatar) { - return ( - DriveFiles.getPublicUrl(user.avatar, true) || - this.getIdenticonUrl(user.id) - ); - } else if (user.avatarId) { - const avatar = await DriveFiles.findOneByOrFail({ id: user.avatarId }); - return ( - DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id) - ); - } else { - return this.getIdenticonUrl(user.id); - } - }, - - getAvatarUrlSync(user: User): string { - if (user.avatar) { - return ( - DriveFiles.getPublicUrl(user.avatar, true) || - this.getIdenticonUrl(user.id) - ); - } else { - return this.getIdenticonUrl(user.id); - } - }, - - getIdenticonUrl(userId: User["id"]): string { - return `${config.url}/identicon/${userId}`; - }, - - async pack< - ExpectsMe extends boolean | null = null, - D extends boolean = false, - >( - src: User["id"] | User, - me?: { id: User["id"] } | null | undefined, - options?: { - detail?: D; - includeSecrets?: boolean; - }, - ): Promise> { - const opts = Object.assign( - { - detail: false, - includeSecrets: false, - }, - options, - ); - - let user: User; - - if (typeof src === "object") { - user = src; - if (src.avatar === undefined && src.avatarId) - src.avatar = (await DriveFiles.findOneBy({ id: src.avatarId })) ?? null; - if (src.banner === undefined && src.bannerId) - src.banner = (await DriveFiles.findOneBy({ id: src.bannerId })) ?? null; - } else { - user = await this.findOneOrFail({ - where: { id: src }, - relations: { - avatar: true, - banner: true, - }, - }); - } - - const meId = me ? me.id : null; - const isMe = meId === user.id; - - const relation = - meId && !isMe && 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.findOneByOrFail({ userId: user.id }) - : null; - - const followingCount = - profile == null - ? null - : profile.ffVisibility === "public" || isMe - ? user.followingCount - : profile.ffVisibility === "followers" && - relation && - relation.isFollowing - ? user.followingCount - : null; - - const followersCount = - profile == null - ? null - : profile.ffVisibility === "public" || isMe - ? user.followersCount - : profile.ffVisibility === "followers" && - relation && - 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.getAvatarUrlSync(user), - avatarBlurhash: user.avatar?.blurhash || null, - avatarColor: null, // 後方互換性のため - isAdmin: user.isAdmin || falsy, - isModerator: user.isModerator || falsy, - isBot: user.isBot || falsy, - isCat: user.isCat || falsy, - speakAsCat: user.speakAsCat || falsy, - instance: user.host - ? userInstanceCache - .fetch( - user.host, - () => Instances.findOneBy({ host: user.host! }), - (v) => v != null, - ) - .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), - driveCapacityOverrideMb: user.driveCapacityOverrideMb, - - ...(opts.detail - ? { - url: profile!.url, - uri: user.uri, - movedToUri: user.movedToUri - ? await this.userFromURI(user.movedToUri) - : null, - alsoKnownAs: user.alsoKnownAs, - createdAt: user.createdAt.toISOString(), - updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, - lastFetchedAt: user.lastFetchedAt - ? user.lastFetchedAt.toISOString() - : null, - bannerUrl: user.banner - ? DriveFiles.getPublicUrl(user.banner, false) - : null, - bannerBlurhash: user.banner?.blurhash || null, - bannerColor: null, // 後方互換性のため - isLocked: user.isLocked, - 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.countBy({ - userId: user.id, - }).then((result) => result >= 1) - : false, - } - : {}), - - ...(opts.detail && isMe - ? { - avatarId: user.avatarId, - bannerId: user.bannerId, - injectFeaturedNote: profile!.injectFeaturedNote, - receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, - alwaysMarkNsfw: profile!.alwaysMarkNsfw, - autoSensitive: profile!.autoSensitive, - 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, - mutedInstances: profile!.mutedInstances, - mutingNotificationTypes: profile!.mutingNotificationTypes, - emailNotificationTypes: profile!.emailNotificationTypes, - showTimelineReplies: user.showTimelineReplies || falsy, - } - : {}), - - ...(opts.includeSecrets - ? { - email: profile!.email, - emailVerified: profile!.emailVerified, - securityKeysList: profile!.twoFactorEnabled - ? UserSecurityKeys.find({ - where: { - userId: user.id, - }, - select: { - id: true, - name: true, - lastUsed: true, - }, - }) - : [], - } - : {}), - - ...(relation - ? { - isFollowing: relation.isFollowing, - isFollowed: relation.isFollowed, - hasPendingFollowRequestFromYou: - relation.hasPendingFollowRequestFromYou, - hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou, - isBlocking: relation.isBlocking, - isBlocked: relation.isBlocked, - isMuted: relation.isMuted, - isRenoteMuted: relation.isRenoteMuted, - } - : {}), - } as Promiseable> as Promiseable< - IsMeAndIsUserDetailed - >; - - return await awaitAll(packed); - }, - - packMany( - users: (User["id"] | User)[], - me?: { id: User["id"] } | null | undefined, - options?: { - detail?: D; - includeSecrets?: boolean; - }, - ): Promise[]> { - return Promise.all(users.map((u) => this.pack(u, me, options))); - }, - - isLocalUser, - isRemoteUser, -}); diff --git a/packages/backend/src/models/schema/antenna.ts b/packages/backend/src/models/schema/antenna.ts deleted file mode 100644 index 990e2daa2c..0000000000 --- a/packages/backend/src/models/schema/antenna.ts +++ /dev/null @@ -1,118 +0,0 @@ -export const packedAntennaSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - keywords: { - type: "array", - optional: false, - nullable: false, - items: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - }, - excludeKeywords: { - type: "array", - optional: false, - nullable: false, - items: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - }, - src: { - type: "string", - optional: false, - nullable: false, - enum: ["home", "all", "users", "list", "group", "instances"], - }, - userListId: { - type: "string", - optional: false, - nullable: true, - format: "id", - }, - userGroupId: { - type: "string", - optional: false, - nullable: true, - format: "id", - }, - users: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - instances: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - caseSensitive: { - type: "boolean", - optional: false, - nullable: false, - default: false, - }, - notify: { - type: "boolean", - optional: false, - nullable: false, - }, - withReplies: { - type: "boolean", - optional: false, - nullable: false, - default: false, - }, - withFile: { - type: "boolean", - optional: false, - nullable: false, - }, - hasUnreadNote: { - type: "boolean", - optional: false, - nullable: false, - default: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/app.ts b/packages/backend/src/models/schema/app.ts deleted file mode 100644 index 8ec71159a3..0000000000 --- a/packages/backend/src/models/schema/app.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const packedAppSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - callbackUrl: { - type: "string", - optional: false, - nullable: true, - }, - permission: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - secret: { - type: "string", - optional: true, - nullable: false, - }, - isAuthorized: { - type: "boolean", - optional: true, - nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/blocking.ts b/packages/backend/src/models/schema/blocking.ts deleted file mode 100644 index 1d491e9395..0000000000 --- a/packages/backend/src/models/schema/blocking.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const packedBlockingSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - blockeeId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - blockee: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailed", - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/channel.ts b/packages/backend/src/models/schema/channel.ts deleted file mode 100644 index 67833cb0dd..0000000000 --- a/packages/backend/src/models/schema/channel.ts +++ /dev/null @@ -1,61 +0,0 @@ -export const packedChannelSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - lastNotedAt: { - type: "string", - optional: false, - nullable: true, - format: "date-time", - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - description: { - type: "string", - nullable: true, - optional: false, - }, - bannerUrl: { - type: "string", - format: "url", - nullable: true, - optional: false, - }, - notesCount: { - type: "number", - nullable: false, - optional: false, - }, - usersCount: { - type: "number", - nullable: false, - optional: false, - }, - isFollowing: { - type: "boolean", - optional: true, - nullable: false, - }, - userId: { - type: "string", - nullable: true, - optional: false, - format: "id", - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/clip.ts b/packages/backend/src/models/schema/clip.ts deleted file mode 100644 index 651303ad9f..0000000000 --- a/packages/backend/src/models/schema/clip.ts +++ /dev/null @@ -1,45 +0,0 @@ -export const packedClipSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - userId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - user: { - type: "object", - ref: "UserLite", - optional: false, - nullable: false, - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - description: { - type: "string", - optional: false, - nullable: true, - }, - isPublic: { - type: "boolean", - optional: false, - nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/drive-file.ts b/packages/backend/src/models/schema/drive-file.ts deleted file mode 100644 index 30db9e7d48..0000000000 --- a/packages/backend/src/models/schema/drive-file.ts +++ /dev/null @@ -1,127 +0,0 @@ -export const packedDriveFileSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - name: { - type: "string", - optional: false, - nullable: false, - example: "lenna.jpg", - }, - type: { - type: "string", - optional: false, - nullable: false, - example: "image/jpeg", - }, - md5: { - type: "string", - optional: false, - nullable: false, - format: "md5", - example: "15eca7fba0480996e2245f5185bf39f2", - }, - size: { - type: "number", - optional: false, - nullable: false, - example: 51469, - }, - isSensitive: { - type: "boolean", - optional: false, - nullable: false, - }, - blurhash: { - type: "string", - optional: false, - nullable: true, - }, - properties: { - type: "object", - optional: false, - nullable: false, - properties: { - width: { - type: "number", - optional: true, - nullable: false, - example: 1280, - }, - height: { - type: "number", - optional: true, - nullable: false, - example: 720, - }, - orientation: { - type: "number", - optional: true, - nullable: false, - example: 8, - }, - avgColor: { - type: "string", - optional: true, - nullable: false, - example: "rgb(40,65,87)", - }, - }, - }, - url: { - type: "string", - optional: false, - nullable: true, - format: "url", - }, - thumbnailUrl: { - type: "string", - optional: false, - nullable: true, - format: "url", - }, - comment: { - type: "string", - optional: false, - nullable: true, - }, - folderId: { - type: "string", - optional: false, - nullable: true, - format: "id", - example: "xxxxxxxxxx", - }, - folder: { - type: "object", - optional: true, - nullable: true, - ref: "DriveFolder", - }, - userId: { - type: "string", - optional: false, - nullable: true, - format: "id", - example: "xxxxxxxxxx", - }, - user: { - type: "object", - optional: true, - nullable: true, - ref: "UserLite", - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/drive-folder.ts b/packages/backend/src/models/schema/drive-folder.ts deleted file mode 100644 index 2298b5420c..0000000000 --- a/packages/backend/src/models/schema/drive-folder.ts +++ /dev/null @@ -1,46 +0,0 @@ -export const packedDriveFolderSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - foldersCount: { - type: "number", - optional: true, - nullable: false, - }, - filesCount: { - type: "number", - optional: true, - nullable: false, - }, - parentId: { - type: "string", - optional: false, - nullable: true, - format: "id", - example: "xxxxxxxxxx", - }, - parent: { - type: "object", - optional: true, - nullable: true, - ref: "DriveFolder", - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/schema/emoji.ts deleted file mode 100644 index 8994381b31..0000000000 --- a/packages/backend/src/models/schema/emoji.ts +++ /dev/null @@ -1,49 +0,0 @@ -export const packedEmojiSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - aliases: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - category: { - type: "string", - optional: false, - nullable: true, - }, - host: { - type: "string", - optional: false, - nullable: true, - description: "The local host is represented with `null`.", - }, - url: { - type: "string", - optional: false, - nullable: false, - }, - license: { - type: "string", - optional: false, - nullable: true, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/backend/src/models/schema/federation-instance.ts deleted file mode 100644 index ed3369bf11..0000000000 --- a/packages/backend/src/models/schema/federation-instance.ts +++ /dev/null @@ -1,133 +0,0 @@ -import config from "@/config/index.js"; - -export const packedFederationInstanceSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - caughtAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - host: { - type: "string", - optional: false, - nullable: false, - example: "calckey.example.com", - }, - usersCount: { - type: "number", - optional: false, - nullable: false, - }, - notesCount: { - type: "number", - optional: false, - nullable: false, - }, - followingCount: { - type: "number", - optional: false, - nullable: false, - }, - followersCount: { - type: "number", - optional: false, - nullable: false, - }, - latestRequestSentAt: { - type: "string", - optional: false, - nullable: true, - format: "date-time", - }, - lastCommunicatedAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - isNotResponding: { - type: "boolean", - optional: false, - nullable: false, - }, - isSuspended: { - type: "boolean", - optional: false, - nullable: false, - }, - isBlocked: { - type: "boolean", - optional: false, - nullable: false, - }, - softwareName: { - type: "string", - optional: false, - nullable: true, - example: "calckey", - }, - softwareVersion: { - type: "string", - optional: false, - nullable: true, - example: config.version, - }, - openRegistrations: { - type: "boolean", - optional: false, - nullable: true, - example: true, - }, - name: { - type: "string", - optional: false, - nullable: true, - }, - description: { - type: "string", - optional: false, - nullable: true, - }, - maintainerName: { - type: "string", - optional: false, - nullable: true, - }, - maintainerEmail: { - type: "string", - optional: false, - nullable: true, - }, - iconUrl: { - type: "string", - optional: false, - nullable: true, - format: "url", - }, - faviconUrl: { - type: "string", - optional: false, - nullable: true, - format: "url", - }, - themeColor: { - type: "string", - optional: false, - nullable: true, - }, - infoUpdatedAt: { - type: "string", - optional: false, - nullable: true, - format: "date-time", - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/following.ts b/packages/backend/src/models/schema/following.ts deleted file mode 100644 index f53cafaba5..0000000000 --- a/packages/backend/src/models/schema/following.ts +++ /dev/null @@ -1,42 +0,0 @@ -export const packedFollowingSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - followeeId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - followee: { - type: "object", - optional: true, - nullable: false, - ref: "UserDetailed", - }, - followerId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - follower: { - type: "object", - optional: true, - nullable: false, - ref: "UserDetailed", - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/gallery-post.ts b/packages/backend/src/models/schema/gallery-post.ts deleted file mode 100644 index 9ac348e1fb..0000000000 --- a/packages/backend/src/models/schema/gallery-post.ts +++ /dev/null @@ -1,83 +0,0 @@ -export const packedGalleryPostSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - updatedAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - title: { - type: "string", - optional: false, - nullable: false, - }, - description: { - type: "string", - optional: false, - nullable: true, - }, - userId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - user: { - type: "object", - ref: "UserLite", - optional: false, - nullable: false, - }, - fileIds: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - }, - files: { - type: "array", - optional: true, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFile", - }, - }, - tags: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - isSensitive: { - type: "boolean", - optional: false, - nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/hashtag.ts b/packages/backend/src/models/schema/hashtag.ts deleted file mode 100644 index dacc515070..0000000000 --- a/packages/backend/src/models/schema/hashtag.ts +++ /dev/null @@ -1,41 +0,0 @@ -export const packedHashtagSchema = { - type: "object", - properties: { - tag: { - type: "string", - optional: false, - nullable: false, - example: "calckey", - }, - mentionedUsersCount: { - type: "number", - optional: false, - nullable: false, - }, - mentionedLocalUsersCount: { - type: "number", - optional: false, - nullable: false, - }, - mentionedRemoteUsersCount: { - type: "number", - optional: false, - nullable: false, - }, - attachedUsersCount: { - type: "number", - optional: false, - nullable: false, - }, - attachedLocalUsersCount: { - type: "number", - optional: false, - nullable: false, - }, - attachedRemoteUsersCount: { - type: "number", - optional: false, - nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/messaging-message.ts b/packages/backend/src/models/schema/messaging-message.ts deleted file mode 100644 index d598e6dbc6..0000000000 --- a/packages/backend/src/models/schema/messaging-message.ts +++ /dev/null @@ -1,87 +0,0 @@ -export const packedMessagingMessageSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - userId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - user: { - type: "object", - ref: "UserLite", - optional: true, - nullable: false, - }, - text: { - type: "string", - optional: false, - nullable: true, - }, - fileId: { - type: "string", - optional: true, - nullable: true, - format: "id", - }, - file: { - type: "object", - optional: true, - nullable: true, - ref: "DriveFile", - }, - recipientId: { - type: "string", - optional: false, - nullable: true, - format: "id", - }, - recipient: { - type: "object", - optional: true, - nullable: true, - ref: "UserLite", - }, - groupId: { - type: "string", - optional: false, - nullable: true, - format: "id", - }, - group: { - type: "object", - optional: true, - nullable: true, - ref: "UserGroup", - }, - isRead: { - type: "boolean", - optional: true, - nullable: false, - }, - reads: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/muting.ts b/packages/backend/src/models/schema/muting.ts deleted file mode 100644 index d5815f86d1..0000000000 --- a/packages/backend/src/models/schema/muting.ts +++ /dev/null @@ -1,36 +0,0 @@ -export const packedMutingSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - expiresAt: { - type: "string", - optional: false, - nullable: true, - format: "date-time", - }, - muteeId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - mutee: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailed", - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/note-favorite.ts b/packages/backend/src/models/schema/note-favorite.ts deleted file mode 100644 index 17a42baf0e..0000000000 --- a/packages/backend/src/models/schema/note-favorite.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const packedNoteFavoriteSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - note: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - noteId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/note-reaction.ts b/packages/backend/src/models/schema/note-reaction.ts deleted file mode 100644 index 1080bdcf51..0000000000 --- a/packages/backend/src/models/schema/note-reaction.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const packedNoteReactionSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - user: { - type: "object", - optional: false, - nullable: false, - ref: "UserLite", - }, - type: { - type: "string", - optional: false, - nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts deleted file mode 100644 index e17f054e8e..0000000000 --- a/packages/backend/src/models/schema/note.ts +++ /dev/null @@ -1,200 +0,0 @@ -export const packedNoteSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - text: { - type: "string", - optional: false, - nullable: true, - }, - cw: { - type: "string", - optional: true, - nullable: true, - }, - userId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - user: { - type: "object", - ref: "UserLite", - optional: false, - nullable: false, - }, - replyId: { - type: "string", - optional: true, - nullable: true, - format: "id", - example: "xxxxxxxxxx", - }, - renoteId: { - type: "string", - optional: true, - nullable: true, - format: "id", - example: "xxxxxxxxxx", - }, - reply: { - type: "object", - optional: true, - nullable: true, - ref: "Note", - }, - renote: { - type: "object", - optional: true, - nullable: true, - ref: "Note", - }, - visibility: { - type: "string", - optional: false, - nullable: false, - }, - mentions: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - }, - visibleUserIds: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - }, - fileIds: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - }, - files: { - type: "array", - optional: true, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFile", - }, - }, - tags: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - poll: { - type: "object", - optional: true, - nullable: true, - }, - channelId: { - type: "string", - optional: true, - nullable: true, - format: "id", - example: "xxxxxxxxxx", - }, - channel: { - type: "object", - optional: true, - nullable: true, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - }, - name: { - type: "string", - optional: false, - nullable: true, - }, - }, - }, - }, - localOnly: { - type: "boolean", - optional: true, - nullable: false, - }, - emojis: { - type: "object", - optional: true, - nullable: true, - }, - reactions: { - type: "object", - optional: false, - nullable: false, - }, - renoteCount: { - type: "number", - optional: false, - nullable: false, - }, - repliesCount: { - type: "number", - optional: false, - nullable: false, - }, - uri: { - type: "string", - optional: true, - nullable: false, - }, - url: { - type: "string", - optional: true, - nullable: false, - }, - - myReaction: { - type: "object", - optional: true, - nullable: true, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/notification.ts b/packages/backend/src/models/schema/notification.ts deleted file mode 100644 index 97fd16339c..0000000000 --- a/packages/backend/src/models/schema/notification.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { notificationTypes } from "@/types.js"; - -export const packedNotificationSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - isRead: { - type: "boolean", - optional: false, - nullable: false, - }, - type: { - type: "string", - optional: false, - nullable: false, - enum: [...notificationTypes], - }, - user: { - type: "object", - ref: "UserLite", - optional: true, - nullable: true, - }, - userId: { - type: "string", - optional: true, - nullable: true, - format: "id", - }, - note: { - type: "object", - ref: "Note", - optional: true, - nullable: true, - }, - reaction: { - type: "string", - optional: true, - nullable: true, - }, - choice: { - type: "number", - optional: true, - nullable: true, - }, - invitation: { - type: "object", - optional: true, - nullable: true, - }, - body: { - type: "string", - optional: true, - nullable: true, - }, - header: { - type: "string", - optional: true, - nullable: true, - }, - icon: { - type: "string", - optional: true, - nullable: true, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/page.ts b/packages/backend/src/models/schema/page.ts deleted file mode 100644 index a1b9144b59..0000000000 --- a/packages/backend/src/models/schema/page.ts +++ /dev/null @@ -1,66 +0,0 @@ -export const packedPageSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - updatedAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - title: { - type: "string", - optional: false, - nullable: false, - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - summary: { - type: "string", - optional: false, - nullable: true, - }, - content: { - type: "array", - optional: false, - nullable: false, - }, - variables: { - type: "array", - optional: false, - nullable: false, - }, - userId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - user: { - type: "object", - ref: "UserLite", - optional: false, - nullable: false, - }, - isPublic: { - type: "boolean", - optional: false, - nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/queue.ts b/packages/backend/src/models/schema/queue.ts deleted file mode 100644 index 954ac688be..0000000000 --- a/packages/backend/src/models/schema/queue.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const packedQueueCountSchema = { - type: "object", - properties: { - waiting: { - type: "number", - optional: false, - nullable: false, - }, - active: { - type: "number", - optional: false, - nullable: false, - }, - completed: { - type: "number", - optional: false, - nullable: false, - }, - failed: { - type: "number", - optional: false, - nullable: false, - }, - delayed: { - type: "number", - optional: false, - nullable: false, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/renote-muting.ts b/packages/backend/src/models/schema/renote-muting.ts deleted file mode 100644 index 2a5824e32b..0000000000 --- a/packages/backend/src/models/schema/renote-muting.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const packedRenoteMutingSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - muteeId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - mutee: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailed", - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/user-group.ts b/packages/backend/src/models/schema/user-group.ts deleted file mode 100644 index a4a85f9699..0000000000 --- a/packages/backend/src/models/schema/user-group.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const packedUserGroupSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - ownerId: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - userIds: { - type: "array", - nullable: false, - optional: true, - items: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/user-list.ts b/packages/backend/src/models/schema/user-list.ts deleted file mode 100644 index 1e203b63ae..0000000000 --- a/packages/backend/src/models/schema/user-list.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const packedUserListSchema = { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - userIds: { - type: "array", - nullable: false, - optional: true, - items: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - }, - }, -} as const; diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts deleted file mode 100644 index 80e94fe508..0000000000 --- a/packages/backend/src/models/schema/user.ts +++ /dev/null @@ -1,583 +0,0 @@ -export const packedUserLiteSchema = { - type: "object", - properties: { - id: { - type: "string", - nullable: false, - optional: false, - format: "id", - example: "xxxxxxxxxx", - }, - name: { - type: "string", - nullable: true, - optional: false, - example: "藍", - }, - username: { - type: "string", - nullable: false, - optional: false, - example: "calc", - }, - host: { - type: "string", - nullable: true, - optional: false, - example: "misskey.example.com", - description: "The local host is represented with `null`.", - }, - avatarUrl: { - type: "string", - format: "url", - nullable: true, - optional: false, - }, - avatarBlurhash: { - type: "any", - nullable: true, - optional: false, - }, - avatarColor: { - type: "any", - nullable: true, - optional: false, - default: null, - }, - isAdmin: { - type: "boolean", - nullable: false, - optional: true, - default: false, - }, - isModerator: { - type: "boolean", - nullable: false, - optional: true, - default: false, - }, - isBot: { - type: "boolean", - nullable: false, - optional: true, - }, - isCat: { - type: "boolean", - nullable: false, - optional: true, - }, - speakAsCat: { - type: "boolean", - nullable: false, - optional: true, - }, - emojis: { - type: "array", - nullable: false, - optional: false, - items: { - type: "object", - nullable: false, - optional: false, - properties: { - name: { - type: "string", - nullable: false, - optional: false, - }, - url: { - type: "string", - nullable: false, - optional: false, - format: "url", - }, - }, - }, - }, - onlineStatus: { - type: "string", - format: "url", - nullable: true, - optional: false, - enum: ["unknown", "online", "active", "offline"], - }, - }, -} as const; - -export const packedUserDetailedNotMeOnlySchema = { - type: "object", - properties: { - url: { - type: "string", - format: "url", - nullable: true, - optional: false, - }, - uri: { - type: "string", - format: "uri", - nullable: true, - optional: false, - }, - movedToUri: { - type: "string", - format: "uri", - nullable: true, - optional: false, - }, - alsoKnownAs: { - type: "array", - format: "uri", - nullable: true, - optional: false, - }, - createdAt: { - type: "string", - nullable: false, - optional: false, - format: "date-time", - }, - updatedAt: { - type: "string", - nullable: true, - optional: false, - format: "date-time", - }, - lastFetchedAt: { - type: "string", - nullable: true, - optional: false, - format: "date-time", - }, - bannerUrl: { - type: "string", - format: "url", - nullable: true, - optional: false, - }, - bannerBlurhash: { - type: "any", - nullable: true, - optional: false, - }, - bannerColor: { - type: "any", - nullable: true, - optional: false, - default: null, - }, - isLocked: { - type: "boolean", - nullable: false, - optional: false, - }, - isSilenced: { - type: "boolean", - nullable: false, - optional: false, - }, - isSuspended: { - type: "boolean", - nullable: false, - optional: false, - example: false, - }, - description: { - type: "string", - nullable: true, - optional: false, - example: "Hi masters, I am Ai!", - }, - location: { - type: "string", - nullable: true, - optional: false, - }, - birthday: { - type: "string", - nullable: true, - optional: false, - example: "2018-03-12", - }, - lang: { - type: "string", - nullable: true, - optional: false, - example: "ja-JP", - }, - fields: { - type: "array", - nullable: false, - optional: false, - items: { - type: "object", - nullable: false, - optional: false, - properties: { - name: { - type: "string", - nullable: false, - optional: false, - }, - value: { - type: "string", - nullable: false, - optional: false, - }, - }, - maxLength: 4, - }, - }, - followersCount: { - type: "number", - nullable: false, - optional: false, - }, - followingCount: { - type: "number", - nullable: false, - optional: false, - }, - notesCount: { - type: "number", - nullable: false, - optional: false, - }, - pinnedNoteIds: { - type: "array", - nullable: false, - optional: false, - items: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - }, - pinnedNotes: { - type: "array", - nullable: false, - optional: false, - items: { - type: "object", - nullable: false, - optional: false, - ref: "Note", - }, - }, - pinnedPageId: { - type: "string", - nullable: true, - optional: false, - }, - pinnedPage: { - type: "object", - nullable: true, - optional: false, - ref: "Page", - }, - publicReactions: { - type: "boolean", - nullable: false, - optional: false, - }, - twoFactorEnabled: { - type: "boolean", - nullable: false, - optional: false, - default: false, - }, - usePasswordLessLogin: { - type: "boolean", - nullable: false, - optional: false, - default: false, - }, - securityKeys: { - type: "boolean", - nullable: false, - optional: false, - default: false, - }, - //#region relations - isFollowing: { - type: "boolean", - nullable: false, - optional: true, - }, - isFollowed: { - type: "boolean", - nullable: false, - optional: true, - }, - hasPendingFollowRequestFromYou: { - type: "boolean", - nullable: false, - optional: true, - }, - hasPendingFollowRequestToYou: { - type: "boolean", - nullable: false, - optional: true, - }, - isBlocking: { - type: "boolean", - nullable: false, - optional: true, - }, - isBlocked: { - type: "boolean", - nullable: false, - optional: true, - }, - isMuted: { - type: "boolean", - nullable: false, - optional: true, - }, - isRenoteMuted: { - type: "boolean", - nullable: false, - optional: true, - }, - //#endregion - }, -} as const; - -export const packedMeDetailedOnlySchema = { - type: "object", - properties: { - avatarId: { - type: "string", - nullable: true, - optional: false, - format: "id", - }, - bannerId: { - type: "string", - nullable: true, - optional: false, - format: "id", - }, - injectFeaturedNote: { - type: "boolean", - nullable: true, - optional: false, - }, - receiveAnnouncementEmail: { - type: "boolean", - nullable: true, - optional: false, - }, - alwaysMarkNsfw: { - type: "boolean", - nullable: true, - optional: false, - }, - autoSensitive: { - type: "boolean", - nullable: true, - optional: false, - }, - carefulBot: { - type: "boolean", - nullable: true, - optional: false, - }, - autoAcceptFollowed: { - type: "boolean", - nullable: true, - optional: false, - }, - noCrawle: { - type: "boolean", - nullable: true, - optional: false, - }, - isExplorable: { - type: "boolean", - nullable: false, - optional: false, - }, - isDeleted: { - type: "boolean", - nullable: false, - optional: false, - }, - hideOnlineStatus: { - type: "boolean", - nullable: false, - optional: false, - }, - hasUnreadSpecifiedNotes: { - type: "boolean", - nullable: false, - optional: false, - }, - hasUnreadMentions: { - type: "boolean", - nullable: false, - optional: false, - }, - hasUnreadAnnouncement: { - type: "boolean", - nullable: false, - optional: false, - }, - hasUnreadAntenna: { - type: "boolean", - nullable: false, - optional: false, - }, - hasUnreadChannel: { - type: "boolean", - nullable: false, - optional: false, - }, - hasUnreadMessagingMessage: { - type: "boolean", - nullable: false, - optional: false, - }, - hasUnreadNotification: { - type: "boolean", - nullable: false, - optional: false, - }, - hasPendingReceivedFollowRequest: { - type: "boolean", - nullable: false, - optional: false, - }, - integrations: { - type: "object", - nullable: true, - optional: false, - }, - mutedWords: { - type: "array", - nullable: false, - optional: false, - items: { - type: "array", - nullable: false, - optional: false, - items: { - type: "string", - nullable: false, - optional: false, - }, - }, - }, - mutedInstances: { - type: "array", - nullable: true, - optional: false, - items: { - type: "string", - nullable: false, - optional: false, - }, - }, - mutingNotificationTypes: { - type: "array", - nullable: true, - optional: false, - items: { - type: "string", - nullable: false, - optional: false, - }, - }, - emailNotificationTypes: { - type: "array", - nullable: true, - optional: false, - items: { - type: "string", - nullable: false, - optional: false, - }, - }, - //#region secrets - email: { - type: "string", - nullable: true, - optional: true, - }, - emailVerified: { - type: "boolean", - nullable: true, - optional: true, - }, - securityKeysList: { - type: "array", - nullable: false, - optional: true, - items: { - type: "object", - nullable: false, - optional: false, - }, - }, - //#endregion - }, -} as const; - -export const packedUserDetailedNotMeSchema = { - type: "object", - allOf: [ - { - type: "object", - ref: "UserLite", - }, - { - type: "object", - ref: "UserDetailedNotMeOnly", - }, - ], -} as const; - -export const packedMeDetailedSchema = { - type: "object", - allOf: [ - { - type: "object", - ref: "UserLite", - }, - { - type: "object", - ref: "UserDetailedNotMeOnly", - }, - { - type: "object", - ref: "MeDetailedOnly", - }, - ], -} as const; - -export const packedUserDetailedSchema = { - oneOf: [ - { - type: "object", - ref: "UserDetailedNotMe", - }, - { - type: "object", - ref: "MeDetailed", - }, - ], -} as const; - -export const packedUserSchema = { - oneOf: [ - { - type: "object", - ref: "UserLite", - }, - { - type: "object", - ref: "UserDetailed", - }, - ], -} as const; diff --git a/packages/backend/src/prelude/README.md b/packages/backend/src/prelude/README.md deleted file mode 100644 index bb728cfb1b..0000000000 --- a/packages/backend/src/prelude/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Prelude -このディレクトリのコードはJavaScriptの表現能力を補うためのコードです。 -Misskey固有の処理とは独立したコードの集まりですが、Misskeyのコードを読みやすくすることを目的としています。 diff --git a/packages/backend/src/prelude/array.ts b/packages/backend/src/prelude/array.ts deleted file mode 100644 index 71a24c89b7..0000000000 --- a/packages/backend/src/prelude/array.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { EndoRelation, Predicate } from "./relation.js"; - -/** - * 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 (!Object.prototype.hasOwnProperty.call(obj, 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/packages/backend/src/prelude/await-all.ts b/packages/backend/src/prelude/await-all.ts deleted file mode 100644 index ce11eb87b4..0000000000 --- a/packages/backend/src/prelude/await-all.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type Promiseable = { - [K in keyof T]: Promise | T[K]; -}; - -export async function awaitAll(obj: Promiseable): Promise { - const target = {} as T; - const keys = Object.keys(obj) as unknown as (keyof T)[]; - const values = Object.values(obj) as any[]; - - const resolvedValues = await Promise.all( - values.map((value) => - !value?.constructor || value.constructor.name !== "Object" - ? value - : awaitAll(value), - ), - ); - - for (let i = 0; i < keys.length; i++) { - target[keys[i]] = resolvedValues[i]; - } - - return target; -} diff --git a/packages/backend/src/prelude/math.ts b/packages/backend/src/prelude/math.ts deleted file mode 100644 index 07b94bec30..0000000000 --- a/packages/backend/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/packages/backend/src/prelude/maybe.ts b/packages/backend/src/prelude/maybe.ts deleted file mode 100644 index df7c4ed52a..0000000000 --- a/packages/backend/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/packages/backend/src/prelude/relation.ts b/packages/backend/src/prelude/relation.ts deleted file mode 100644 index 1f4703f52f..0000000000 --- a/packages/backend/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/packages/backend/src/prelude/string.ts b/packages/backend/src/prelude/string.ts deleted file mode 100644 index 9588825cb7..0000000000 --- a/packages/backend/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/packages/backend/src/prelude/symbol.ts b/packages/backend/src/prelude/symbol.ts deleted file mode 100644 index 5b88467d46..0000000000 --- a/packages/backend/src/prelude/symbol.ts +++ /dev/null @@ -1 +0,0 @@ -export const fallback = Symbol("fallback"); diff --git a/packages/backend/src/prelude/time.ts b/packages/backend/src/prelude/time.ts deleted file mode 100644 index 5901b9c484..0000000000 --- a/packages/backend/src/prelude/time.ts +++ /dev/null @@ -1,54 +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 new Error("wrong number of arguments"); - - return new Date(d); -} - -export function isTimeSame(a: Date, b: Date): boolean { - return a.getTime() === b.getTime(); -} - -export function isTimeBefore(a: Date, b: Date): boolean { - return a.getTime() - b.getTime() < 0; -} - -export function isTimeAfter(a: Date, b: Date): boolean { - return a.getTime() - b.getTime() > 0; -} - -export function addTime( - x: Date, - value: number, - span: keyof typeof dateTimeIntervals = "ms", -): Date { - return new Date(x.getTime() + value * dateTimeIntervals[span]); -} - -export function subtractTime( - x: Date, - value: number, - span: keyof typeof dateTimeIntervals = "ms", -): Date { - return new Date(x.getTime() - value * dateTimeIntervals[span]); -} diff --git a/packages/backend/src/prelude/url.ts b/packages/backend/src/prelude/url.ts deleted file mode 100644 index 9e3f3f7c12..0000000000 --- a/packages/backend/src/prelude/url.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function query(obj: Record): 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/packages/backend/src/prelude/xml.ts b/packages/backend/src/prelude/xml.ts deleted file mode 100644 index 9dcc4c96fd..0000000000 --- a/packages/backend/src/prelude/xml.ts +++ /dev/null @@ -1,38 +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/packages/backend/src/queue/get-job-info.ts b/packages/backend/src/queue/get-job-info.ts deleted file mode 100644 index ae3532cdae..0000000000 --- a/packages/backend/src/queue/get-job-info.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type Bull from "bull"; - -export function getJobInfo(job: Bull.Job, increment = false) { - const age = Date.now() - job.timestamp; - - const formated = - age > 60000 - ? `${Math.floor(age / 1000 / 60)}m` - : age > 10000 - ? `${Math.floor(age / 1000)}s` - : `${age}ms`; - - // onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする - const currentAttempts = job.attemptsMade + (increment ? 1 : 0); - const maxAttempts = job.opts ? job.opts.attempts : 0; - - return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`; -} diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts deleted file mode 100644 index 035556e487..0000000000 --- a/packages/backend/src/queue/index.ts +++ /dev/null @@ -1,545 +0,0 @@ -import type httpSignature from "@peertube/http-signature"; -import { v4 as uuid } from "uuid"; - -import config from "@/config/index.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import type { IActivity } from "@/remote/activitypub/type.js"; -import type { Webhook, webhookEventTypes } from "@/models/entities/webhook.js"; -import { envOption } from "../env.js"; - -import processDeliver from "./processors/deliver.js"; -import processInbox from "./processors/inbox.js"; -import processDb from "./processors/db/index.js"; -import processObjectStorage from "./processors/object-storage/index.js"; -import processSystemQueue from "./processors/system/index.js"; -import processWebhookDeliver from "./processors/webhook-deliver.js"; -import processBackground from "./processors/background/index.js"; -import { endedPollNotification } from "./processors/ended-poll-notification.js"; -import { queueLogger } from "./logger.js"; -import { getJobInfo } from "./get-job-info.js"; -import { - systemQueue, - dbQueue, - deliverQueue, - inboxQueue, - objectStorageQueue, - endedPollNotificationQueue, - webhookDeliverQueue, - backgroundQueue, -} from "./queues.js"; -import type { ThinUser } from "./types.js"; - -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 webhookLogger = queueLogger.createSubLogger("webhook"); -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}`)); - -webhookDeliverQueue - .on("waiting", (jobId) => webhookLogger.debug(`waiting id=${jobId}`)) - .on("active", (job) => - webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`), - ) - .on("completed", (job, result) => - webhookLogger.debug( - `completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`, - ), - ) - .on("failed", (job, err) => - webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`), - ) - .on("error", (job: any, err: Error) => - webhookLogger.error(`error ${err}`, { job, e: renderError(err) }), - ) - .on("stalled", (job) => - webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`), - ); - -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 createExportCustomEmojisJob(user: ThinUser) { - return dbQueue.add( - "exportCustomEmojis", - { - 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, - excludeMuting = false, - excludeInactive = false, -) { - return dbQueue.add( - "exportFollowing", - { - user: user, - excludeMuting, - excludeInactive, - }, - { - 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 createImportPostsJob( - user: ThinUser, - fileId: DriveFile["id"], - signatureCheck: boolean, -) { - return dbQueue.add( - "importPosts", - { - user: user, - fileId: fileId, - signatureCheck: signatureCheck, - }, - { - 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 createImportCustomEmojisJob( - user: ThinUser, - fileId: DriveFile["id"], -) { - return dbQueue.add( - "importCustomEmojis", - { - 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 function createIndexAllNotesJob(data = {}) { - return backgroundQueue.add("indexAllNotes", data, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function webhookDeliver( - webhook: Webhook, - type: typeof webhookEventTypes[number], - content: unknown, -) { - const data = { - type, - content, - webhookId: webhook.id, - userId: webhook.userId, - to: webhook.url, - secret: webhook.secret, - createdAt: Date.now(), - eventId: uuid(), - }; - - return webhookDeliverQueue.add(data, { - attempts: 4, - timeout: 1 * 60 * 1000, // 1min - backoff: { - type: "apBackoff", - }, - removeOnComplete: true, - removeOnFail: true, - }); -} - -export default function () { - if (envOption.onlyServer) return; - - deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver); - inboxQueue.process(config.inboxJobConcurrency || 16, processInbox); - endedPollNotificationQueue.process(endedPollNotification); - webhookDeliverQueue.process(64, processWebhookDeliver); - processDb(dbQueue); - processObjectStorage(objectStorageQueue); - processBackground(backgroundQueue); - - systemQueue.add( - "tickCharts", - {}, - { - repeat: { cron: "55 * * * *" }, - removeOnComplete: true, - }, - ); - - systemQueue.add( - "resyncCharts", - {}, - { - repeat: { cron: "0 0 * * *" }, - removeOnComplete: true, - }, - ); - - systemQueue.add( - "cleanCharts", - {}, - { - repeat: { cron: "0 0 * * *" }, - removeOnComplete: true, - }, - ); - - systemQueue.add( - "clean", - {}, - { - repeat: { cron: "0 0 * * *" }, - removeOnComplete: true, - }, - ); - - systemQueue.add( - "checkExpiredMutings", - {}, - { - repeat: { cron: "*/5 * * * *" }, - removeOnComplete: true, - }, - ); - - processSystemQueue(systemQueue); -} - -export function destroy() { - deliverQueue.once("cleaned", (jobs, status) => { - deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); - }); - deliverQueue.clean(0, "delayed"); - - inboxQueue.once("cleaned", (jobs, status) => { - inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); - }); - inboxQueue.clean(0, "delayed"); -} diff --git a/packages/backend/src/queue/initialize.ts b/packages/backend/src/queue/initialize.ts deleted file mode 100644 index d7945d5dad..0000000000 --- a/packages/backend/src/queue/initialize.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Bull from "bull"; -import config from "@/config/index.js"; - -export function initialize(name: string, limitPerSec = -1) { - return new Bull(name, { - redis: { - port: config.redis.port, - host: config.redis.host, - family: config.redis.family == null ? 0 : config.redis.family, - password: config.redis.pass, - db: config.redis.db || 0, - }, - prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : "queue", - limiter: - limitPerSec > 0 - ? { - max: limitPerSec, - duration: 1000, - } - : undefined, - settings: { - backoffStrategies: { - apBackoff, - }, - }, - }); -} - -// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 -function apBackoff(attemptsMade: number, err: Error) { - const baseDelay = 60 * 1000; // 1min - const maxBackoff = 8 * 60 * 60 * 1000; // 8hours - let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay; - backoff = Math.min(backoff, maxBackoff); - backoff += Math.round(backoff * Math.random() * 0.2); - return backoff; -} diff --git a/packages/backend/src/queue/logger.ts b/packages/backend/src/queue/logger.ts deleted file mode 100644 index 929c207e3c..0000000000 --- a/packages/backend/src/queue/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from "@/services/logger.js"; - -export const queueLogger = new Logger("queue", "orange"); diff --git a/packages/backend/src/queue/processors/background/index-all-notes.ts b/packages/backend/src/queue/processors/background/index-all-notes.ts deleted file mode 100644 index 03219199d9..0000000000 --- a/packages/backend/src/queue/processors/background/index-all-notes.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type Bull from "bull"; - -import { queueLogger } from "../../logger.js"; -import { Notes } from "@/models/index.js"; -import { MoreThan } from "typeorm"; -import { index } from "@/services/note/create.js"; -import { Note } from "@/models/entities/note.js"; - -const logger = queueLogger.createSubLogger("index-all-notes"); - -export default async function indexAllNotes( - job: Bull.Job>, - done: () => void, -): Promise { - logger.info("Indexing all notes..."); - - let cursor: string | null = (job.data.cursor as string) ?? null; - let indexedCount: number = (job.data.indexedCount as number) ?? 0; - let total: number = (job.data.total as number) ?? 0; - - let running = true; - const take = 50000; - const batch = 100; - while (running) { - logger.info( - `Querying for ${take} notes ${indexedCount}/${ - total ? total : "?" - } at ${cursor}`, - ); - - let notes: Note[] = []; - try { - notes = await Notes.find({ - where: { - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: take, - order: { - id: 1, - }, - }); - } catch (e) { - logger.error(`Failed to query notes ${e}`); - continue; - } - - if (notes.length === 0) { - job.progress(100); - running = false; - break; - } - - try { - const count = await Notes.count(); - total = count; - job.update({ indexedCount, cursor, total }); - } catch (e) {} - - for (let i = 0; i < notes.length; i += batch) { - const chunk = notes.slice(i, i + batch); - await Promise.all(chunk.map((note) => index(note))); - - indexedCount += chunk.length; - const pct = (indexedCount / total) * 100; - job.update({ indexedCount, cursor, total }); - job.progress(+pct.toFixed(1)); - logger.info(`Indexed notes ${indexedCount}/${total ? total : "?"}`); - } - cursor = notes[notes.length - 1].id; - job.update({ indexedCount, cursor, total }); - - if (notes.length < take) { - running = false; - } - } - - done(); - logger.info("All notes have been indexed."); -} diff --git a/packages/backend/src/queue/processors/background/index.ts b/packages/backend/src/queue/processors/background/index.ts deleted file mode 100644 index 6674f954b0..0000000000 --- a/packages/backend/src/queue/processors/background/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type Bull from "bull"; -import indexAllNotes from "./index-all-notes.js"; - -const jobs = { - indexAllNotes, -} as Record>>; - -export default function (q: Bull.Queue) { - for (const [k, v] of Object.entries(jobs)) { - q.process(k, 16, v); - } -} diff --git a/packages/backend/src/queue/processors/db/delete-account.ts b/packages/backend/src/queue/processors/db/delete-account.ts deleted file mode 100644 index 764b83db2d..0000000000 --- a/packages/backend/src/queue/processors/db/delete-account.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type Bull from "bull"; -import { queueLogger } from "../../logger.js"; -import { DriveFiles, Notes, UserProfiles, Users } from "@/models/index.js"; -import type { DbUserDeleteJobData } from "@/queue/types.js"; -import type { Note } from "@/models/entities/note.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { MoreThan } from "typeorm"; -import { deleteFileSync } from "@/services/drive/delete-file.js"; -import { sendEmail } from "@/services/send-email.js"; - -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.findOneBy({ id: 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, - }, - })) as Note[]; - - 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, - }, - })) as DriveFile[]; - - 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.findOneByOrFail({ userId: user.id }); - if (profile.email && profile.emailVerified) { - sendEmail( - profile.email, - "Account deleted", - "Your account has been deleted.", - "Your account has been deleted.", - ); - } - } - - // soft指定されている場合は物理削除しない - if (job.data.soft) { - // nop - } else { - await Users.delete(job.data.user.id); - } - - return "Account deleted"; -} diff --git a/packages/backend/src/queue/processors/db/delete-drive-files.ts b/packages/backend/src/queue/processors/db/delete-drive-files.ts deleted file mode 100644 index 28e4771329..0000000000 --- a/packages/backend/src/queue/processors/db/delete-drive-files.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type Bull from "bull"; - -import { queueLogger } from "../../logger.js"; -import { deleteFileSync } from "@/services/drive/delete-file.js"; -import { Users, DriveFiles } from "@/models/index.js"; -import { MoreThan } from "typeorm"; -import type { DbUserJobData } from "@/queue/types.js"; - -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.findOneBy({ id: 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.countBy({ - userId: user.id, - }); - - job.progress(deletedCount / total); - } - - logger.succ( - `All drive files (${deletedCount}) of ${user.id} has been deleted.`, - ); - done(); -} diff --git a/packages/backend/src/queue/processors/db/export-blocking.ts b/packages/backend/src/queue/processors/db/export-blocking.ts deleted file mode 100644 index 90da76b872..0000000000 --- a/packages/backend/src/queue/processors/db/export-blocking.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type Bull from "bull"; -import * as fs from "node:fs"; - -import { queueLogger } from "../../logger.js"; -import { addFile } from "@/services/drive/add-file.js"; -import { format as dateFormat } from "date-fns"; -import { getFullApAccount } from "@/misc/convert-host.js"; -import { createTemp } from "@/misc/create-temp.js"; -import { Users, Blockings } from "@/models/index.js"; -import { MoreThan } from "typeorm"; -import type { DbUserJobData } from "@/queue/types.js"; - -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.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - 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.findOneBy({ 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.countBy({ - 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, - name: fileName, - force: true, - }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/db/export-custom-emojis.ts b/packages/backend/src/queue/processors/db/export-custom-emojis.ts deleted file mode 100644 index 7a19d0b60b..0000000000 --- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type Bull from "bull"; -import * as fs from "node:fs"; - -import { ulid } from "ulid"; -import mime from "mime-types"; -import archiver from "archiver"; -import { queueLogger } from "../../logger.js"; -import { addFile } from "@/services/drive/add-file.js"; -import { format as dateFormat } from "date-fns"; -import { Users, Emojis } from "@/models/index.js"; -import {} from "@/queue/types.js"; -import { createTemp, createTempDir } from "@/misc/create-temp.js"; -import { downloadUrl } from "@/misc/download-url.js"; -import config from "@/config/index.js"; -import { IsNull } from "typeorm"; - -const logger = queueLogger.createSubLogger("export-custom-emojis"); - -export async function exportCustomEmojis( - job: Bull.Job, - done: () => void, -): Promise { - logger.info("Exporting custom emojis ..."); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const [path, cleanup] = await createTempDir(); - - logger.info(`Temp dir is ${path}`); - - const metaPath = `${path}/meta.json`; - - fs.writeFileSync(metaPath, "", "utf-8"); - - const metaStream = fs.createWriteStream(metaPath, { flags: "a" }); - - const writeMeta = (text: string): Promise => { - return new Promise((res, rej) => { - metaStream.write(text, (err) => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } - }); - }); - }; - - await writeMeta( - `{"metaVersion":2,"host":"${ - config.host - }","exportedAt":"${new Date().toString()}","emojis":[`, - ); - - const customEmojis = await Emojis.find({ - where: { - host: IsNull(), - }, - order: { - id: "ASC", - }, - }); - - for (const emoji of customEmojis) { - const ext = mime.extension(emoji.type); - const fileName = emoji.name + (ext ? `.${ext}` : ""); - const emojiPath = `${path}/${fileName}`; - fs.writeFileSync(emojiPath, "", "binary"); - let downloaded = false; - - try { - await downloadUrl(emoji.originalUrl, emojiPath); - downloaded = true; - } catch (e) { - // TODO: 何度か再試行 - logger.error(e instanceof Error ? e : new Error(e as string)); - } - - if (!downloaded) { - fs.unlinkSync(emojiPath); - } - - const content = JSON.stringify({ - fileName: fileName, - downloaded: downloaded, - emoji: emoji, - }); - const isFirst = customEmojis.indexOf(emoji) === 0; - - await writeMeta(isFirst ? content : ",\n" + content); - } - - await writeMeta("]}"); - - metaStream.end(); - - // Create archive - const [archivePath, archiveCleanup] = await createTemp(); - const archiveStream = fs.createWriteStream(archivePath); - const archive = archiver("zip", { - zlib: { level: 0 }, - }); - archiveStream.on("close", async () => { - logger.succ(`Exported to: ${archivePath}`); - - const fileName = `custom-emojis-${dateFormat( - new Date(), - "yyyy-MM-dd-HH-mm-ss", - )}.zip`; - const driveFile = await addFile({ - user, - path: archivePath, - name: fileName, - force: true, - }); - - logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); - archiveCleanup(); - done(); - }); - archive.pipe(archiveStream); - archive.directory(path, false); - archive.finalize(); -} diff --git a/packages/backend/src/queue/processors/db/export-following.ts b/packages/backend/src/queue/processors/db/export-following.ts deleted file mode 100644 index 80e8e6b925..0000000000 --- a/packages/backend/src/queue/processors/db/export-following.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type Bull from "bull"; -import * as fs from "node:fs"; - -import { queueLogger } from "../../logger.js"; -import { addFile } from "@/services/drive/add-file.js"; -import { format as dateFormat } from "date-fns"; -import { getFullApAccount } from "@/misc/convert-host.js"; -import { createTemp } from "@/misc/create-temp.js"; -import { Users, Followings, Mutings } from "@/models/index.js"; -import { In, MoreThan, Not } from "typeorm"; -import type { DbUserJobData } from "@/queue/types.js"; -import type { Following } from "@/models/entities/following.js"; - -const logger = queueLogger.createSubLogger("export-following"); - -export async function exportFollowing( - job: Bull.Job, - done: () => void, -): Promise { - logger.info(`Exporting following of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - const stream = fs.createWriteStream(path, { flags: "a" }); - - let cursor: Following["id"] | null = null; - - const mutings = job.data.excludeMuting - ? await Mutings.findBy({ - muterId: user.id, - }) - : []; - - while (true) { - const followings = (await Followings.find({ - where: { - followerId: user.id, - ...(mutings.length > 0 - ? { followeeId: Not(In(mutings.map((x) => x.muteeId))) } - : {}), - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - })) as Following[]; - - if (followings.length === 0) { - break; - } - - cursor = followings[followings.length - 1].id; - - for (const following of followings) { - const u = await Users.findOneBy({ id: following.followeeId }); - if (u == null) { - continue; - } - - if ( - job.data.excludeInactive && - u.updatedAt && - Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90 - ) { - 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(); - } - }); - }); - } - } - - 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, - name: fileName, - force: true, - }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/db/export-mute.ts b/packages/backend/src/queue/processors/db/export-mute.ts deleted file mode 100644 index 87b140b762..0000000000 --- a/packages/backend/src/queue/processors/db/export-mute.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type Bull from "bull"; -import * as fs from "node:fs"; - -import { queueLogger } from "../../logger.js"; -import { addFile } from "@/services/drive/add-file.js"; -import { format as dateFormat } from "date-fns"; -import { getFullApAccount } from "@/misc/convert-host.js"; -import { createTemp } from "@/misc/create-temp.js"; -import { Users, Mutings } from "@/models/index.js"; -import { IsNull, MoreThan } from "typeorm"; -import type { DbUserJobData } from "@/queue/types.js"; - -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.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - 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, - expiresAt: IsNull(), - ...(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.findOneBy({ 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.countBy({ - 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, - name: fileName, - force: true, - }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/db/export-notes.ts b/packages/backend/src/queue/processors/db/export-notes.ts deleted file mode 100644 index de8fac05b4..0000000000 --- a/packages/backend/src/queue/processors/db/export-notes.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type Bull from "bull"; -import * as fs from "node:fs"; - -import { queueLogger } from "../../logger.js"; -import { addFile } from "@/services/drive/add-file.js"; -import { format as dateFormat } from "date-fns"; -import { Users, Notes, Polls } from "@/models/index.js"; -import { MoreThan } from "typeorm"; -import type { Note } from "@/models/entities/note.js"; -import type { Poll } from "@/models/entities/poll.js"; -import type { DbUserJobData } from "@/queue/types.js"; -import { createTemp } from "@/misc/create-temp.js"; - -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.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - const stream = fs.createWriteStream(path, { flags: "a" }); - - const write = (text: string): Promise => { - return new Promise((res, rej) => { - stream.write(text, (err) => { - if (err) { - logger.error(err); - rej(err); - } else { - res(); - } - }); - }); - }; - - await write("["); - - let exportedNotesCount = 0; - 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, - }, - })) as Note[]; - - 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.findOneByOrFail({ noteId: note.id }); - } - const content = JSON.stringify(serialize(note, poll)); - const isFirst = exportedNotesCount === 0; - await write(isFirst ? content : ",\n" + content); - exportedNotesCount++; - } - - const total = await Notes.countBy({ - userId: user.id, - }); - - job.progress(exportedNotesCount / total); - } - - await write("]"); - - 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, - name: fileName, - force: true, - }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} - -function serialize( - note: Note, - poll: Poll | null = null, -): Record { - return { - id: note.id, - text: note.text, - createdAt: note.createdAt, - fileIds: note.fileIds, - replyId: note.replyId, - renoteId: note.renoteId, - poll: poll, - cw: note.cw, - visibility: note.visibility, - visibleUserIds: note.visibleUserIds, - localOnly: note.localOnly, - }; -} diff --git a/packages/backend/src/queue/processors/db/export-user-lists.ts b/packages/backend/src/queue/processors/db/export-user-lists.ts deleted file mode 100644 index e0c9cd8f3f..0000000000 --- a/packages/backend/src/queue/processors/db/export-user-lists.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type Bull from "bull"; -import * as fs from "node:fs"; - -import { queueLogger } from "../../logger.js"; -import { addFile } from "@/services/drive/add-file.js"; -import { format as dateFormat } from "date-fns"; -import { getFullApAccount } from "@/misc/convert-host.js"; -import { createTemp } from "@/misc/create-temp.js"; -import { Users, UserLists, UserListJoinings } from "@/models/index.js"; -import { In } from "typeorm"; -import type { DbUserJobData } from "@/queue/types.js"; - -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.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const lists = await UserLists.findBy({ - userId: user.id, - }); - - // Create temp file - const [path, cleanup] = await createTemp(); - - logger.info(`Temp file is ${path}`); - - try { - const stream = fs.createWriteStream(path, { flags: "a" }); - - for (const list of lists) { - const joinings = await UserListJoinings.findBy({ userListId: list.id }); - const users = await Users.findBy({ - 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, - name: fileName, - force: true, - }); - - logger.succ(`Exported to: ${driveFile.id}`); - } finally { - cleanup(); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/db/import-blocking.ts b/packages/backend/src/queue/processors/db/import-blocking.ts deleted file mode 100644 index 2fdf80a6eb..0000000000 --- a/packages/backend/src/queue/processors/db/import-blocking.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type Bull from "bull"; - -import { queueLogger } from "../../logger.js"; -import * as Acct from "@/misc/acct.js"; -import { resolveUser } from "@/remote/resolve-user.js"; -import { downloadTextFile } from "@/misc/download-text-file.js"; -import { isSelfHost, toPuny } from "@/misc/convert-host.js"; -import { Users, DriveFiles, Blockings } from "@/models/index.js"; -import type { DbUserImportJobData } from "@/queue/types.js"; -import block from "@/services/blocking/create.js"; -import { IsNull } from "typeorm"; - -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.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const csv = await downloadTextFile(file.url); - - let linenum = 0; - - for (const line of csv.trim().split("\n")) { - linenum++; - - try { - const acct = line.split(",")[0].trim(); - const { username, host } = Acct.parse(acct); - - let target = isSelfHost(host!) - ? await Users.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) - : await Users.findOneBy({ - host: toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (host == null && target == null) continue; - - if (target == null) { - target = await resolveUser(username, host); - } - - if (target == null) { - throw new Error(`cannot resolve user: @${username}@${host}`); - } - - // skip myself - if (target.id === job.data.user.id) continue; - - logger.info(`Block[${linenum}] ${target.id} ...`); - - await block(user, target); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - - logger.succ("Imported"); - done(); -} diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts deleted file mode 100644 index 373a3a2daf..0000000000 --- a/packages/backend/src/queue/processors/db/import-custom-emojis.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type Bull from "bull"; -import * as fs from "node:fs"; -import unzipper from "unzipper"; - -import { queueLogger } from "../../logger.js"; -import { createTempDir } from "@/misc/create-temp.js"; -import { downloadUrl } from "@/misc/download-url.js"; -import { DriveFiles, Emojis } from "@/models/index.js"; -import type { DbUserImportJobData } from "@/queue/types.js"; -import { addFile } from "@/services/drive/add-file.js"; -import { genId } from "@/misc/gen-id.js"; -import { db } from "@/db/postgre.js"; - -const logger = queueLogger.createSubLogger("import-custom-emojis"); - -// TODO: 名前衝突時の動作を選べるようにする -export async function importCustomEmojis( - job: Bull.Job, - done: any, -): Promise { - logger.info("Importing custom emojis ..."); - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const [path, cleanup] = await createTempDir(); - - logger.info(`Temp dir is ${path}`); - - const destPath = `${path}/emojis.zip`; - - try { - fs.writeFileSync(destPath, "", "binary"); - await downloadUrl(file.url, destPath); - } catch (e) { - // TODO: 何度か再試行 - if (e instanceof Error || typeof e === "string") { - logger.error(e); - } - throw e; - } - - const outputPath = `${path}/emojis`; - const unzipStream = fs.createReadStream(destPath); - const extractor = unzipper.Extract({ path: outputPath }); - extractor.on("close", async () => { - const metaRaw = fs.readFileSync(`${outputPath}/meta.json`, "utf-8"); - const meta = JSON.parse(metaRaw); - - for (const record of meta.emojis) { - if (!record.downloaded) continue; - const emojiInfo = record.emoji; - const emojiPath = `${outputPath}/${record.fileName}`; - await Emojis.delete({ - name: emojiInfo.name, - }); - const driveFile = await addFile({ - user: null, - path: emojiPath, - name: record.fileName, - force: true, - }); - const emoji = await Emojis.insert({ - id: genId(), - updatedAt: new Date(), - name: emojiInfo.name, - category: emojiInfo.category, - host: null, - aliases: emojiInfo.aliases, - originalUrl: driveFile.url, - publicUrl: driveFile.webpublicUrl ?? driveFile.url, - type: driveFile.webpublicType ?? driveFile.type, - license: emojiInfo.license, - }).then((x) => Emojis.findOneByOrFail(x.identifiers[0])); - } - - await db.queryResultCache!.remove(["meta_emojis"]); - - cleanup(); - - logger.succ("Imported"); - done(); - }); - unzipStream.pipe(extractor); - logger.succ(`Unzipping to ${outputPath}`); -} diff --git a/packages/backend/src/queue/processors/db/import-following.ts b/packages/backend/src/queue/processors/db/import-following.ts deleted file mode 100644 index b1a7cd2c9b..0000000000 --- a/packages/backend/src/queue/processors/db/import-following.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { IsNull } from "typeorm"; -import follow from "@/services/following/create.js"; - -import * as Acct from "@/misc/acct.js"; -import { resolveUser } from "@/remote/resolve-user.js"; -import { downloadTextFile } from "@/misc/download-text-file.js"; -import { isSelfHost, toPuny } from "@/misc/convert-host.js"; -import { Users, DriveFiles } from "@/models/index.js"; -import type { DbUserImportJobData } from "@/queue/types.js"; -import { queueLogger } from "../../logger.js"; -import type Bull from "bull"; - -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.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const csv = await downloadTextFile(file.url); - - let linenum = 0; - - if (file.type.endsWith("json")) { - for (const acct of JSON.parse(csv)) { - try { - const { username, host } = Acct.parse(acct); - - let target = isSelfHost(host!) - ? await Users.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) - : await Users.findOneBy({ - host: toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (host == null && target == null) continue; - - if (target == null) { - target = await resolveUser(username, host); - } - - if (target == null) { - throw new Error(`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}`); - } - } - } else { - for (const line of csv.trim().split("\n")) { - linenum++; - - try { - const acct = line.split(",")[0].trim(); - const { username, host } = Acct.parse(acct); - - let target = isSelfHost(host!) - ? await Users.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) - : await Users.findOneBy({ - host: toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (host == null && target == null) continue; - - if (target == null) { - target = await resolveUser(username, host); - } - - if (target == null) { - throw new Error(`cannot resolve user: @${username}@${host}`); - } - - // skip myself - if (target.id === job.data.user.id) continue; - - logger.info(`Follow[${linenum}] ${target.id} ...`); - - follow(user, target); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - } - - logger.succ("Imported"); - done(); -} diff --git a/packages/backend/src/queue/processors/db/import-muting.ts b/packages/backend/src/queue/processors/db/import-muting.ts deleted file mode 100644 index 80e0567397..0000000000 --- a/packages/backend/src/queue/processors/db/import-muting.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type Bull from "bull"; - -import { queueLogger } from "../../logger.js"; -import * as Acct from "@/misc/acct.js"; -import { resolveUser } from "@/remote/resolve-user.js"; -import { downloadTextFile } from "@/misc/download-text-file.js"; -import { isSelfHost, toPuny } from "@/misc/convert-host.js"; -import { Users, DriveFiles, Mutings } from "@/models/index.js"; -import type { DbUserImportJobData } from "@/queue/types.js"; -import type { User } from "@/models/entities/user.js"; -import { genId } from "@/misc/gen-id.js"; -import { IsNull } from "typeorm"; - -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.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const csv = await downloadTextFile(file.url); - - let linenum = 0; - - for (const line of csv.trim().split("\n")) { - linenum++; - - try { - const acct = line.split(",")[0].trim(); - const { username, host } = Acct.parse(acct); - - let target = isSelfHost(host!) - ? await Users.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) - : await Users.findOneBy({ - host: toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (host == null && target == null) continue; - - if (target == null) { - target = await resolveUser(username, host); - } - - if (target == null) { - throw new Error(`cannot resolve user: @${username}@${host}`); - } - - // skip myself - if (target.id === job.data.user.id) continue; - - logger.info(`Mute[${linenum}] ${target.id} ...`); - - await mute(user, target); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - - logger.succ("Imported"); - done(); -} - -async function mute(user: User, target: User) { - await Mutings.insert({ - id: genId(), - createdAt: new Date(), - muterId: user.id, - muteeId: target.id, - }); -} diff --git a/packages/backend/src/queue/processors/db/import-posts.ts b/packages/backend/src/queue/processors/db/import-posts.ts deleted file mode 100644 index 20ef3a518d..0000000000 --- a/packages/backend/src/queue/processors/db/import-posts.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { IsNull } from "typeorm"; -import follow from "@/services/following/create.js"; - -import * as Post from "@/misc/post.js"; -import create from "@/services/note/create.js"; -import { downloadTextFile } from "@/misc/download-text-file.js"; -import { Users, DriveFiles } from "@/models/index.js"; -import type { DbUserImportPostsJobData } from "@/queue/types.js"; -import { queueLogger } from "../../logger.js"; -import type Bull from "bull"; -import { htmlToMfm } from "@/remote/activitypub/misc/html-to-mfm.js"; - -const logger = queueLogger.createSubLogger("import-posts"); - -export async function importPosts( - job: Bull.Job, - done: any, -): Promise { - logger.info(`Importing posts of ${job.data.user.id} ...`); - - const user = await Users.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const json = await downloadTextFile(file.url); - - let linenum = 0; - - try { - const parsed = JSON.parse(json); - if (parsed instanceof Array) { - logger.info("Parsing key style posts"); - for (const post of JSON.parse(json)) { - try { - linenum++; - if (post.replyId != null) { - continue; - } - if (post.renoteId != null) { - continue; - } - if (post.visibility !== "public") { - continue; - } - const { text, cw, localOnly, createdAt } = Post.parse(post); - - logger.info(`Posting[${linenum}] ...`); - - const note = await create(user, { - createdAt: createdAt, - files: undefined, - poll: undefined, - text: text || undefined, - reply: null, - renote: null, - cw: cw, - localOnly, - visibility: "public", - visibleUsers: [], - channel: null, - apMentions: new Array(0), - apHashtags: undefined, - apEmojis: undefined, - }); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - } else if (parsed instanceof Object) { - logger.info("Parsing animal style posts"); - for (const post of parsed.orderedItems) { - try { - linenum++; - if (post.object.inReplyTo != null) { - continue; - } - if (post.directMessage) { - continue; - } - if (job.data.signatureCheck) { - if (!post.signature) { - continue; - } - } - let text; - try { - text = htmlToMfm(post.object.content, post.object.tag); - } catch (e) { - continue; - } - logger.info(`Posting[${linenum}] ...`); - - const note = await create(user, { - createdAt: new Date(post.object.published), - files: undefined, - poll: undefined, - text: text || undefined, - reply: null, - renote: null, - cw: post.sensitive, - localOnly: false, - visibility: "public", - visibleUsers: [], - channel: null, - apMentions: new Array(0), - apHashtags: undefined, - apEmojis: undefined, - }); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - } - } catch (e) { - // handle error - logger.warn(`Error reading: ${e}`); - } - - logger.succ("Imported"); - done(); -} diff --git a/packages/backend/src/queue/processors/db/import-user-lists.ts b/packages/backend/src/queue/processors/db/import-user-lists.ts deleted file mode 100644 index 0c23f06991..0000000000 --- a/packages/backend/src/queue/processors/db/import-user-lists.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type Bull from "bull"; - -import { queueLogger } from "../../logger.js"; -import * as Acct from "@/misc/acct.js"; -import { resolveUser } from "@/remote/resolve-user.js"; -import { pushUserToUserList } from "@/services/user-list/push.js"; -import { downloadTextFile } from "@/misc/download-text-file.js"; -import { isSelfHost, toPuny } from "@/misc/convert-host.js"; -import { - DriveFiles, - Users, - UserLists, - UserListJoinings, -} from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { DbUserImportJobData } from "@/queue/types.js"; -import { IsNull } from "typeorm"; - -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.findOneBy({ id: job.data.user.id }); - if (user == null) { - done(); - return; - } - - const file = await DriveFiles.findOneBy({ - id: job.data.fileId, - }); - if (file == null) { - done(); - return; - } - - const csv = await downloadTextFile(file.url); - - let linenum = 0; - - for (const line of csv.trim().split("\n")) { - linenum++; - - try { - const listName = line.split(",")[0].trim(); - const { username, host } = Acct.parse(line.split(",")[1].trim()); - - let list = await UserLists.findOneBy({ - userId: user.id, - name: listName, - }); - - if (list == null) { - list = await UserLists.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: listName, - }).then((x) => UserLists.findOneByOrFail(x.identifiers[0])); - } - - let target = isSelfHost(host!) - ? await Users.findOneBy({ - host: IsNull(), - usernameLower: username.toLowerCase(), - }) - : await Users.findOneBy({ - host: toPuny(host!), - usernameLower: username.toLowerCase(), - }); - - if (target == null) { - target = await resolveUser(username, host); - } - - if ( - (await UserListJoinings.findOneBy({ - userListId: list!.id, - userId: target.id, - })) != null - ) - continue; - - pushUserToUserList(target, list!); - } catch (e) { - logger.warn(`Error in line:${linenum} ${e}`); - } - } - - logger.succ("Imported"); - done(); -} diff --git a/packages/backend/src/queue/processors/db/index.ts b/packages/backend/src/queue/processors/db/index.ts deleted file mode 100644 index 22b55a3683..0000000000 --- a/packages/backend/src/queue/processors/db/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type Bull from "bull"; -import type { DbJobData } from "@/queue/types.js"; -import { deleteDriveFiles } from "./delete-drive-files.js"; -import { exportCustomEmojis } from "./export-custom-emojis.js"; -import { exportNotes } from "./export-notes.js"; -import { exportFollowing } from "./export-following.js"; -import { exportMute } from "./export-mute.js"; -import { exportBlocking } from "./export-blocking.js"; -import { exportUserLists } from "./export-user-lists.js"; -import { importFollowing } from "./import-following.js"; -import { importUserLists } from "./import-user-lists.js"; -import { deleteAccount } from "./delete-account.js"; -import { importMuting } from "./import-muting.js"; -import { importPosts } from "./import-posts.js"; -import { importBlocking } from "./import-blocking.js"; -import { importCustomEmojis } from "./import-custom-emojis.js"; - -const jobs = { - deleteDriveFiles, - exportCustomEmojis, - exportNotes, - exportFollowing, - exportMute, - exportBlocking, - exportUserLists, - importFollowing, - importMuting, - importBlocking, - importUserLists, - importPosts, - importCustomEmojis, - deleteAccount, -} as Record< - string, - | Bull.ProcessCallbackFunction - | Bull.ProcessPromiseFunction ->; - -export default function (dbQueue: Bull.Queue) { - for (const [k, v] of Object.entries(jobs)) { - dbQueue.process(k, v); - } -} diff --git a/packages/backend/src/queue/processors/deliver.ts b/packages/backend/src/queue/processors/deliver.ts deleted file mode 100644 index 65471a559f..0000000000 --- a/packages/backend/src/queue/processors/deliver.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { URL } from "node:url"; -import request from "@/remote/activitypub/request.js"; -import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instance-doc.js"; -import Logger from "@/services/logger.js"; -import { Instances } from "@/models/index.js"; -import { - apRequestChart, - federationChart, - instanceChart, -} from "@/services/chart/index.js"; -import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js"; -import { toPuny } from "@/misc/convert-host.js"; -import { StatusError } from "@/misc/fetch.js"; -import { shouldSkipInstance } from "@/misc/skipped-instances.js"; -import type { DeliverJobData } from "@/queue/types.js"; -import type Bull from "bull"; - -const logger = new Logger("deliver"); - -let latest: string | null = null; - -export default async (job: Bull.Job) => { - const { host } = new URL(job.data.to); - const puny = toPuny(host); - - if (await shouldSkipInstance(puny)) return "skip"; - - 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); - apRequestChart.deliverSucc(); - federationChart.deliverd(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); - apRequestChart.deliverFail(); - federationChart.deliverd(i.host, false); - }); - - if (res instanceof StatusError) { - // 4xx - if (res.isClientError) { - // HTTPステータスコード4xxはクライアントエラーであり、それはつまり - // 何回再送しても成功することはないということなのでエラーにはしないでおく - return `${res.statusCode} ${res.statusMessage}`; - } - - // 5xx etc. - throw new Error(`${res.statusCode} ${res.statusMessage}`); - } else { - // DNS error, socket error, timeout ... - throw res; - } - } -}; diff --git a/packages/backend/src/queue/processors/ended-poll-notification.ts b/packages/backend/src/queue/processors/ended-poll-notification.ts deleted file mode 100644 index 9fe57d8da3..0000000000 --- a/packages/backend/src/queue/processors/ended-poll-notification.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type Bull from "bull"; -import { In } from "typeorm"; -import { Notes, Polls, PollVotes } from "@/models/index.js"; -import { queueLogger } from "../logger.js"; -import type { EndedPollNotificationJobData } from "@/queue/types.js"; -import { createNotification } from "@/services/create-notification.js"; - -const logger = queueLogger.createSubLogger("ended-poll-notification"); - -export async function endedPollNotification( - job: Bull.Job, - done: any, -): Promise { - const note = await Notes.findOneBy({ id: job.data.noteId }); - if (note == null || !note.hasPoll) { - done(); - return; - } - - const votes = await PollVotes.createQueryBuilder("vote") - .select("vote.userId") - .where("vote.noteId = :noteId", { noteId: note.id }) - .innerJoinAndSelect("vote.user", "user") - .andWhere("user.host IS NULL") - .getMany(); - - const userIds = [...new Set([note.userId, ...votes.map((v) => v.userId)])]; - - for (const userId of userIds) { - createNotification(userId, "pollEnded", { - noteId: note.id, - }); - } - - done(); -} diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts deleted file mode 100644 index ca063a6f3f..0000000000 --- a/packages/backend/src/queue/processors/inbox.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { URL } from "node:url"; -import type Bull from "bull"; -import httpSignature from "@peertube/http-signature"; -import perform from "@/remote/activitypub/perform.js"; -import Logger from "@/services/logger.js"; -import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instance-doc.js"; -import { Instances } from "@/models/index.js"; -import { - apRequestChart, - federationChart, - instanceChart, -} from "@/services/chart/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { toPuny, extractDbHost } from "@/misc/convert-host.js"; -import { getApId } from "@/remote/activitypub/type.js"; -import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js"; -import type { InboxJobData } from "../types.js"; -import DbResolver from "@/remote/activitypub/db-resolver.js"; -import { resolvePerson } from "@/remote/activitypub/models/person.js"; -import { LdSignature } from "@/remote/activitypub/misc/ld-signature.js"; -import { StatusError } from "@/misc/fetch.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { UserPublickey } from "@/models/entities/user-publickey.js"; -import { shouldBlockInstance } from "@/misc/should-block-instance.js"; - -const logger = new Logger("inbox"); - -// Processing when an activity arrives in the user's 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; - info["@context"] = undefined; - logger.debug(JSON.stringify(info, null, 2)); - //#endregion - const host = toPuny(new URL(signature.keyId).hostname); - - // interrupt if blocked - const meta = await fetchMeta(); - if (await shouldBlockInstance(host, meta)) { - return `Blocked request: ${host}`; - } - - // only whitelisted instances in private mode - if (meta.privateMode && !meta.allowedHosts.includes(host)) { - return `Blocked request: ${host}`; - } - - const keyIdLower = signature.keyId.toLowerCase(); - if (keyIdLower.startsWith("acct:")) { - return `Old keyId is no longer supported. ${keyIdLower}`; - } - - const dbResolver = new DbResolver(); - - // HTTP-Signature keyId from DB - let authUser: { - user: CacheableRemoteUser; - key: UserPublickey | null; - } | null = await dbResolver.getAuthUserFromKeyId(signature.keyId); - - // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 - if (authUser == null) { - try { - authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor)); - } catch (e) { - // Skip if target is 4xx - if (e instanceof StatusError) { - if (e.isClientError) { - return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`; - } - throw new Error( - `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 (await shouldBlockInstance(ldHost, meta)) { - 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); - apRequestChart.inbox(); - federationChart.inbox(i.host); - }); - - // アクティビティを処理 - await perform(authUser.user, activity); - return "ok"; -}; diff --git a/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts b/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts deleted file mode 100644 index fdfe05d1a6..0000000000 --- a/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type Bull from "bull"; - -import { queueLogger } from "../../logger.js"; -import { deleteFileSync } from "@/services/drive/delete-file.js"; -import { DriveFiles } from "@/models/index.js"; -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.countBy({ - userHost: Not(IsNull()), - isLink: false, - }); - - job.progress(deletedCount / total); - } - - logger.succ("All cahced remote files has been deleted."); - done(); -} diff --git a/packages/backend/src/queue/processors/object-storage/delete-file.ts b/packages/backend/src/queue/processors/object-storage/delete-file.ts deleted file mode 100644 index 174aa1906c..0000000000 --- a/packages/backend/src/queue/processors/object-storage/delete-file.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ObjectStorageFileJobData } from "@/queue/types.js"; -import type Bull from "bull"; -import { deleteObjectStorageFile } from "@/services/drive/delete-file.js"; - -export default async (job: Bull.Job) => { - const key: string = job.data.key; - - await deleteObjectStorageFile(key); - - return "Success"; -}; diff --git a/packages/backend/src/queue/processors/object-storage/index.ts b/packages/backend/src/queue/processors/object-storage/index.ts deleted file mode 100644 index 5f90d4cd09..0000000000 --- a/packages/backend/src/queue/processors/object-storage/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type Bull from "bull"; -import type { ObjectStorageJobData } from "@/queue/types.js"; -import deleteFile from "./delete-file.js"; -import cleanRemoteFiles from "./clean-remote-files.js"; - -const jobs = { - deleteFile, - cleanRemoteFiles, -} as Record< - string, - | Bull.ProcessCallbackFunction - | Bull.ProcessPromiseFunction ->; - -export default function (q: Bull.Queue) { - for (const [k, v] of Object.entries(jobs)) { - q.process(k, 16, v); - } -} diff --git a/packages/backend/src/queue/processors/system/check-expired-mutings.ts b/packages/backend/src/queue/processors/system/check-expired-mutings.ts deleted file mode 100644 index a482d0218a..0000000000 --- a/packages/backend/src/queue/processors/system/check-expired-mutings.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type Bull from "bull"; -import { In } from "typeorm"; -import { Mutings } from "@/models/index.js"; -import { queueLogger } from "../../logger.js"; -import { publishUserEvent } from "@/services/stream.js"; - -const logger = queueLogger.createSubLogger("check-expired-mutings"); - -export async function checkExpiredMutings( - job: Bull.Job>, - done: any, -): Promise { - logger.info("Checking expired mutings..."); - - const expired = await Mutings.createQueryBuilder("muting") - .where("muting.expiresAt IS NOT NULL") - .andWhere("muting.expiresAt < :now", { now: new Date() }) - .innerJoinAndSelect("muting.mutee", "mutee") - .getMany(); - - if (expired.length > 0) { - await Mutings.delete({ - id: In(expired.map((m) => m.id)), - }); - - for (const m of expired) { - publishUserEvent(m.muterId, "unmute", m.mutee!); - } - } - - logger.succ("All expired mutings checked."); - done(); -} diff --git a/packages/backend/src/queue/processors/system/clean-charts.ts b/packages/backend/src/queue/processors/system/clean-charts.ts deleted file mode 100644 index dde5d95fe3..0000000000 --- a/packages/backend/src/queue/processors/system/clean-charts.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type Bull from "bull"; - -import { queueLogger } from "../../logger.js"; -import { - activeUsersChart, - driveChart, - federationChart, - hashtagChart, - instanceChart, - notesChart, - perUserDriveChart, - perUserFollowingChart, - perUserNotesChart, - perUserReactionsChart, - usersChart, - apRequestChart, -} from "@/services/chart/index.js"; - -const logger = queueLogger.createSubLogger("clean-charts"); - -export async function cleanCharts( - job: Bull.Job>, - done: any, -): Promise { - logger.info("Clean charts..."); - - await Promise.all([ - federationChart.clean(), - notesChart.clean(), - usersChart.clean(), - activeUsersChart.clean(), - instanceChart.clean(), - perUserNotesChart.clean(), - driveChart.clean(), - perUserReactionsChart.clean(), - hashtagChart.clean(), - perUserFollowingChart.clean(), - perUserDriveChart.clean(), - apRequestChart.clean(), - ]); - - logger.succ("All charts successfully cleaned."); - done(); -} diff --git a/packages/backend/src/queue/processors/system/clean.ts b/packages/backend/src/queue/processors/system/clean.ts deleted file mode 100644 index fbd45b0bb9..0000000000 --- a/packages/backend/src/queue/processors/system/clean.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type Bull from "bull"; -import { LessThan } from "typeorm"; -import { UserIps } from "@/models/index.js"; - -import { queueLogger } from "../../logger.js"; - -const logger = queueLogger.createSubLogger("clean"); - -export async function clean( - job: Bull.Job>, - done: any, -): Promise { - logger.info("Cleaning..."); - - UserIps.delete({ - createdAt: LessThan(new Date(Date.now() - 1000 * 60 * 60 * 24 * 90)), - }); - - logger.succ("Cleaned."); - done(); -} diff --git a/packages/backend/src/queue/processors/system/index.ts b/packages/backend/src/queue/processors/system/index.ts deleted file mode 100644 index 68833d76f4..0000000000 --- a/packages/backend/src/queue/processors/system/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type Bull from "bull"; -import { tickCharts } from "./tick-charts.js"; -import { resyncCharts } from "./resync-charts.js"; -import { cleanCharts } from "./clean-charts.js"; -import { checkExpiredMutings } from "./check-expired-mutings.js"; -import { clean } from "./clean.js"; - -const jobs = { - tickCharts, - resyncCharts, - cleanCharts, - checkExpiredMutings, - clean, -} as Record< - string, - | Bull.ProcessCallbackFunction> - | Bull.ProcessPromiseFunction> ->; - -export default function (dbQueue: Bull.Queue>) { - for (const [k, v] of Object.entries(jobs)) { - dbQueue.process(k, v); - } -} diff --git a/packages/backend/src/queue/processors/system/resync-charts.ts b/packages/backend/src/queue/processors/system/resync-charts.ts deleted file mode 100644 index dbea0df733..0000000000 --- a/packages/backend/src/queue/processors/system/resync-charts.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type Bull from "bull"; - -import { queueLogger } from "../../logger.js"; -import { driveChart, notesChart, usersChart } from "@/services/chart/index.js"; - -const logger = queueLogger.createSubLogger("resync-charts"); - -export 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/packages/backend/src/queue/processors/system/tick-charts.ts b/packages/backend/src/queue/processors/system/tick-charts.ts deleted file mode 100644 index 33eed8a596..0000000000 --- a/packages/backend/src/queue/processors/system/tick-charts.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type Bull from "bull"; - -import { queueLogger } from "../../logger.js"; -import { - activeUsersChart, - driveChart, - federationChart, - hashtagChart, - instanceChart, - notesChart, - perUserDriveChart, - perUserFollowingChart, - perUserNotesChart, - perUserReactionsChart, - usersChart, - apRequestChart, -} from "@/services/chart/index.js"; - -const logger = queueLogger.createSubLogger("tick-charts"); - -export async function tickCharts( - job: Bull.Job>, - done: any, -): Promise { - logger.info("Tick charts..."); - - await Promise.all([ - federationChart.tick(false), - notesChart.tick(false), - usersChart.tick(false), - activeUsersChart.tick(false), - instanceChart.tick(false), - perUserNotesChart.tick(false), - driveChart.tick(false), - perUserReactionsChart.tick(false), - hashtagChart.tick(false), - perUserFollowingChart.tick(false), - perUserDriveChart.tick(false), - apRequestChart.tick(false), - ]); - - logger.succ("All charts successfully ticked."); - done(); -} diff --git a/packages/backend/src/queue/processors/webhook-deliver.ts b/packages/backend/src/queue/processors/webhook-deliver.ts deleted file mode 100644 index 0a54ae7d89..0000000000 --- a/packages/backend/src/queue/processors/webhook-deliver.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { URL } from "node:url"; -import type Bull from "bull"; -import Logger from "@/services/logger.js"; -import type { WebhookDeliverJobData } from "../types.js"; -import { getResponse, StatusError } from "@/misc/fetch.js"; -import { Webhooks } from "@/models/index.js"; -import config from "@/config/index.js"; - -const logger = new Logger("webhook"); - -export default async (job: Bull.Job) => { - try { - logger.debug(`delivering ${job.data.webhookId}`); - - const res = await getResponse({ - url: job.data.to, - method: "POST", - headers: { - "User-Agent": "Calckey-Hooks", - "X-Calckey-Host": config.host, - "X-Calckey-Hook-Id": job.data.webhookId, - "X-Calckey-Hook-Secret": job.data.secret, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - hookId: job.data.webhookId, - userId: job.data.userId, - eventId: job.data.eventId, - createdAt: job.data.createdAt, - type: job.data.type, - body: job.data.content, - }), - }); - - Webhooks.update( - { id: job.data.webhookId }, - { - latestSentAt: new Date(), - latestStatus: res.status, - }, - ); - - return "Success"; - } catch (res) { - Webhooks.update( - { id: job.data.webhookId }, - { - latestSentAt: new Date(), - latestStatus: res instanceof StatusError ? res.statusCode : 1, - }, - ); - - if (res instanceof StatusError) { - // 4xx - if (res.isClientError) { - return `${res.statusCode} ${res.statusMessage}`; - } - - // 5xx etc. - throw new Error(`${res.statusCode} ${res.statusMessage}`); - } else { - // DNS error, socket error, timeout ... - throw res; - } - } -}; diff --git a/packages/backend/src/queue/queues.ts b/packages/backend/src/queue/queues.ts deleted file mode 100644 index 6d7fffcb30..0000000000 --- a/packages/backend/src/queue/queues.ts +++ /dev/null @@ -1,41 +0,0 @@ -import config from "@/config/index.js"; -import { initialize as initializeQueue } from "./initialize.js"; -import type { - DeliverJobData, - InboxJobData, - DbJobData, - ObjectStorageJobData, - EndedPollNotificationJobData, - WebhookDeliverJobData, -} from "./types.js"; - -export const systemQueue = initializeQueue>("system"); -export const endedPollNotificationQueue = - initializeQueue("endedPollNotification"); -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"); -export const webhookDeliverQueue = initializeQueue( - "webhookDeliver", - 64, -); -export const backgroundQueue = initializeQueue>("bg"); - -export const queues = [ - systemQueue, - endedPollNotificationQueue, - deliverQueue, - inboxQueue, - dbQueue, - objectStorageQueue, - webhookDeliverQueue, - backgroundQueue, -]; diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts deleted file mode 100644 index e31619ff27..0000000000 --- a/packages/backend/src/queue/types.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { DriveFile } from "@/models/entities/drive-file.js"; -import type { Note } from "@/models/entities/note"; -import type { User } from "@/models/entities/user.js"; -import type { Webhook } from "@/models/entities/webhook"; -import type { IActivity } from "@/remote/activitypub/type.js"; -import type httpSignature from "@peertube/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 - | DbUserImportPostsJobData - | DbUserImportJobData - | DbUserDeleteJobData; - -export type DbUserJobData = { - user: ThinUser; - excludeMuting: boolean; - excludeInactive: boolean; -}; - -export type DbUserDeleteJobData = { - user: ThinUser; - soft?: boolean; -}; - -export type DbUserImportJobData = { - user: ThinUser; - fileId: DriveFile["id"]; -}; - -export type DbUserImportPostsJobData = { - user: ThinUser; - fileId: DriveFile["id"]; - signatureCheck: boolean; -}; - -export type ObjectStorageJobData = - | ObjectStorageFileJobData - | Record; - -export type ObjectStorageFileJobData = { - key: string; -}; - -export type EndedPollNotificationJobData = { - noteId: Note["id"]; -}; - -export type WebhookDeliverJobData = { - type: string; - content: unknown; - webhookId: Webhook["id"]; - userId: User["id"]; - to: string; - secret: string; - createdAt: number; - eventId: string; -}; - -export type ThinUser = { - id: User["id"]; -}; diff --git a/packages/backend/src/remote/activitypub/ap-request.ts b/packages/backend/src/remote/activitypub/ap-request.ts deleted file mode 100644 index d5a9ec0539..0000000000 --- a/packages/backend/src/remote/activitypub/ap-request.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as crypto from "node:crypto"; -import { URL } from "node: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/packages/backend/src/remote/activitypub/audience.ts b/packages/backend/src/remote/activitypub/audience.ts deleted file mode 100644 index 210d47573c..0000000000 --- a/packages/backend/src/remote/activitypub/audience.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { ApObject } from "./type.js"; -import { getApIds } from "./type.js"; -import type Resolver from "./resolver.js"; -import { resolvePerson } from "./models/person.js"; -import { unique, concat } from "@/prelude/array.js"; -import promiseLimit from "promise-limit"; -import type { - CacheableRemoteUser, - CacheableUser, -} from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; - -type Visibility = "public" | "home" | "followers" | "specified"; - -type AudienceInfo = { - visibility: Visibility; - mentionedUsers: CacheableUser[]; - visibleUsers: CacheableUser[]; -}; - -export async function parseAudience( - actor: CacheableRemoteUser, - 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 CacheableUser => 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: CacheableRemoteUser) { - 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: CacheableRemoteUser) { - return id === (actor.followersUri || `${actor.uri}/followers`); -} diff --git a/packages/backend/src/remote/activitypub/check-fetch.ts b/packages/backend/src/remote/activitypub/check-fetch.ts deleted file mode 100644 index a8bbe61b84..0000000000 --- a/packages/backend/src/remote/activitypub/check-fetch.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { URL } from "url"; -import httpSignature from "@peertube/http-signature"; -import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { toPuny } from "@/misc/convert-host.js"; -import DbResolver from "@/remote/activitypub/db-resolver.js"; -import { getApId } from "@/remote/activitypub/type.js"; -import { shouldBlockInstance } from "@/misc/should-block-instance.js"; -import type { IncomingMessage } from "http"; - -export async function hasSignature(req: IncomingMessage): Promise { - const meta = await fetchMeta(); - const required = meta.secureMode || meta.privateMode; - - try { - httpSignature.parseRequest(req, { headers: [] }); - } catch (e) { - if (e instanceof Error && e.name === "MissingHeaderError") { - return required ? "missing" : "optional"; - } - return "invalid"; - } - return required ? "supplied" : "unneeded"; -} - -export async function checkFetch(req: IncomingMessage): Promise { - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - let signature; - - try { - signature = httpSignature.parseRequest(req, { headers: [] }); - } catch (e) { - return 401; - } - - const keyId = new URL(signature.keyId); - const host = toPuny(keyId.hostname); - - if (await shouldBlockInstance(host, meta)) { - return 403; - } - - if ( - meta.privateMode && - host !== config.host && - !meta.allowedHosts.includes(host) - ) { - return 403; - } - - const keyIdLower = signature.keyId.toLowerCase(); - if (keyIdLower.startsWith("acct:")) { - // Old keyId is no longer supported. - return 401; - } - - const dbResolver = new DbResolver(); - - // HTTP-Signature keyIdを元にDBから取得 - let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId); - - // keyIdでわからなければ、resolveしてみる - if (authUser == null) { - try { - keyId.hash = ""; - authUser = await dbResolver.getAuthUserFromApId( - getApId(keyId.toString()), - ); - } catch (e) { - // できなければ駄目 - return 403; - } - } - - // publicKey がなくても終了 - if (authUser?.key == null) { - return 403; - } - - // もう一回チェック - if (authUser.user.host !== host) { - return 403; - } - - // HTTP-Signatureの検証 - const httpSignatureValidated = httpSignature.verifySignature( - signature, - authUser.key.keyPem, - ); - - if (!httpSignatureValidated) { - return 403; - } - } - return 200; -} diff --git a/packages/backend/src/remote/activitypub/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts deleted file mode 100644 index 0a2aec9e85..0000000000 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ /dev/null @@ -1,189 +0,0 @@ -import escapeRegexp from "escape-regexp"; -import config from "@/config/index.js"; -import type { Note } from "@/models/entities/note.js"; -import type { - CacheableRemoteUser, - CacheableUser, -} from "@/models/entities/user.js"; -import { User, IRemoteUser } from "@/models/entities/user.js"; -import type { UserPublickey } from "@/models/entities/user-publickey.js"; -import type { MessagingMessage } from "@/models/entities/messaging-message.js"; -import { - Notes, - Users, - UserPublickeys, - MessagingMessages, -} from "@/models/index.js"; -import { Cache } from "@/misc/cache.js"; -import { uriPersonCache, userByIdCache } from "@/services/user-cache.js"; -import type { IObject } from "./type.js"; -import { getApId } from "./type.js"; -import { resolvePerson } from "./models/person.js"; - -const publicKeyCache = new Cache(Infinity); -const publicKeyByUserIdCache = new Cache(Infinity); - -export type UriParseResult = - | { - /** wether the URI was generated by us */ - local: true; - /** id in DB */ - id: string; - /** hint of type, e.g. "notes", "users" */ - type: string; - /** any remaining text after type and id, not including the slash after id. undefined if empty */ - rest?: string; - } - | { - /** wether the URI was generated by us */ - local: false; - /** uri in DB */ - uri: string; - }; - -export function parseUri(value: string | IObject): UriParseResult { - const uri = getApId(value); - - // the host part of a URL is case insensitive, so use the 'i' flag. - const localRegex = new RegExp( - `^${escapeRegexp(config.url)}/(\\w+)/(\\w+)(?:/(.+))?`, - "i", - ); - const matchLocal = uri.match(localRegex); - - if (matchLocal) { - return { - local: true, - type: matchLocal[1], - id: matchLocal[2], - rest: matchLocal[3], - }; - } else { - return { - local: false, - uri, - }; - } -} - -export default class DbResolver { - constructor() {} - - /** - * AP Note => Misskey Note in DB - */ - public async getNoteFromApId(value: string | IObject): Promise { - const parsed = parseUri(value); - - if (parsed.local) { - if (parsed.type !== "notes") return null; - - return await Notes.findOneBy({ - id: parsed.id, - }); - } else { - return await Notes.findOneBy({ - uri: parsed.uri, - }); - } - } - - public async getMessageFromApId( - value: string | IObject, - ): Promise { - const parsed = parseUri(value); - - if (parsed.local) { - if (parsed.type !== "notes") return null; - - return await MessagingMessages.findOneBy({ - id: parsed.id, - }); - } else { - return await MessagingMessages.findOneBy({ - uri: parsed.uri, - }); - } - } - - /** - * AP Person => Misskey User in DB - */ - public async getUserFromApId( - value: string | IObject, - ): Promise { - const parsed = parseUri(value); - - if (parsed.local) { - if (parsed.type !== "users") return null; - - return ( - (await userByIdCache.fetchMaybe(parsed.id, () => - Users.findOneBy({ - id: parsed.id, - }).then((x) => x ?? undefined), - )) ?? null - ); - } else { - return await uriPersonCache.fetch(parsed.uri, () => - Users.findOneBy({ - uri: parsed.uri, - }), - ); - } - } - - /** - * AP KeyId => Misskey User and Key - */ - public async getAuthUserFromKeyId(keyId: string): Promise<{ - user: CacheableRemoteUser; - key: UserPublickey; - } | null> { - const key = await publicKeyCache.fetch( - keyId, - async () => { - const key = await UserPublickeys.findOneBy({ - keyId, - }); - - if (key == null) return null; - - return key; - }, - (key) => key != null, - ); - - if (key == null) return null; - - return { - user: (await userByIdCache.fetch(key.userId, () => - Users.findOneByOrFail({ id: key.userId }), - )) as CacheableRemoteUser, - key, - }; - } - - /** - * AP Actor id => Misskey User and Key - */ - public async getAuthUserFromApId(uri: string): Promise<{ - user: CacheableRemoteUser; - key: UserPublickey | null; - } | null> { - const user = (await resolvePerson(uri)) as CacheableRemoteUser; - - if (user == null) return null; - - const key = await publicKeyByUserIdCache.fetch( - user.id, - () => UserPublickeys.findOneBy({ userId: user.id }), - (v) => v != null, - ); - - return { - user, - key, - }; - } -} diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts deleted file mode 100644 index 400e047774..0000000000 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { IsNull, Not } from "typeorm"; -import { Users, Followings } from "@/models/index.js"; -import type { ILocalUser, IRemoteUser, User } from "@/models/entities/user.js"; -import { deliver } from "@/queue/index.js"; -import { skippedInstances } from "@/misc/skipped-instances.js"; - -//#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 - - Process follower recipes first to avoid duplication when processing - direct recipes later. - */ - if (this.recipes.some((r) => isFollowers(r))) { - // followers deliver - // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう - // ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう? - const followers = (await Followings.find({ - where: { - followeeId: this.actor.id, - followerHost: Not(IsNull()), - }, - select: { - followerSharedInbox: true, - followerInbox: true, - }, - })) as { - followerSharedInbox: string | null; - followerInbox: string; - }[]; - - for (const following of followers) { - const inbox = following.followerSharedInbox || following.followerInbox; - inboxes.add(inbox); - } - } - - this.recipes - .filter( - (recipe): recipe is IDirectRecipe => - // followers recipes have already been processed - isDirect(recipe) && - // check that shared inbox has not been added yet - !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) && - // check that they actually have an inbox - recipe.to.inbox != null, - ) - .forEach((recipe) => inboxes.add(recipe.to.inbox!)); - - const instancesToSkip = await skippedInstances( - // get (unique) list of hosts - Array.from( - new Set(Array.from(inboxes).map((inbox) => new URL(inbox).host)), - ), - ); - - // deliver - for (const inbox of inboxes) { - // skip instances as indicated - if (instancesToSkip.includes(new URL(inbox).host)) continue; - - deliver(this.actor, this.activity, inbox); - } - } -} - -//#region Utilities -/** - * Deliver activity to followers - * @param activity Activity - * @param from Followee - */ -export async function deliverToFollowers( - actor: { id: ILocalUser["id"]; host: null }, - 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: { id: ILocalUser["id"]; host: null }, - activity: any, - to: IRemoteUser, -) { - const manager = new DeliverManager(actor, activity); - manager.addDirectRecipe(to); - await manager.execute(); -} -//#endregion diff --git a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts b/packages/backend/src/remote/activitypub/kernel/accept/follow.ts deleted file mode 100644 index e430bbf576..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import accept from "@/services/following/requests/accept.js"; -import type { IFollow } from "../../type.js"; -import DbResolver from "../../db-resolver.js"; -import { relayAccepted } from "@/services/relay.js"; - -export default async ( - actor: CacheableRemoteUser, - 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/packages/backend/src/remote/activitypub/kernel/accept/index.ts b/packages/backend/src/remote/activitypub/kernel/accept/index.ts deleted file mode 100644 index 5c73760ff3..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/accept/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Resolver from "../../resolver.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import acceptFollow from "./follow.js"; -import type { IAccept } from "../../type.js"; -import { isFollow, getApType } from "../../type.js"; -import { apLogger } from "../../logger.js"; - -const logger = apLogger; - -export default async ( - actor: CacheableRemoteUser, - 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/packages/backend/src/remote/activitypub/kernel/add/index.ts b/packages/backend/src/remote/activitypub/kernel/add/index.ts deleted file mode 100644 index b3606e5d93..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/add/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { IAdd } from "../../type.js"; -import { resolveNote } from "../../models/note.js"; -import { addPinned } from "@/services/i/pin.js"; - -export default async ( - actor: CacheableRemoteUser, - 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/packages/backend/src/remote/activitypub/kernel/announce/index.ts b/packages/backend/src/remote/activitypub/kernel/announce/index.ts deleted file mode 100644 index 975e070f92..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/announce/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Resolver from "../../resolver.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import announceNote from "./note.js"; -import type { IAnnounce } from "../../type.js"; -import { getApId } from "../../type.js"; -import { apLogger } from "../../logger.js"; - -const logger = apLogger; - -export default async ( - actor: CacheableRemoteUser, - 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/packages/backend/src/remote/activitypub/kernel/announce/note.ts b/packages/backend/src/remote/activitypub/kernel/announce/note.ts deleted file mode 100644 index 6cdaa61662..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type Resolver from "../../resolver.js"; -import post from "@/services/note/create.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { IAnnounce } from "../../type.js"; -import { getApId } from "../../type.js"; -import { fetchNote, resolveNote } from "../../models/note.js"; -import { apLogger } from "../../logger.js"; -import { extractDbHost } from "@/misc/convert-host.js"; -import { getApLock } from "@/misc/app-lock.js"; -import { parseAudience } from "../../audience.js"; -import { StatusError } from "@/misc/fetch.js"; -import { Notes } from "@/models/index.js"; -import { shouldBlockInstance } from "@/misc/should-block-instance.js"; - -const logger = apLogger; - -/** - * Handle announcement activities - */ -export default async function ( - resolver: Resolver, - actor: CacheableRemoteUser, - activity: IAnnounce, - targetUri: string, -): Promise { - const uri = getApId(activity); - - if (actor.isSuspended) { - return; - } - - // Interrupt if you block the announcement destination - if (await shouldBlockInstance(extractDbHost(uri))) return; - - const unlock = await getApLock(uri); - - try { - // Check if something with the same URI is already registered - const exist = await fetchNote(uri); - if (exist) { - return; - } - - // Resolve Announce target - let renote; - try { - renote = await resolveNote(targetUri); - } catch (e) { - // Skip if target is 4xx - if (e instanceof StatusError) { - if (e.isClientError) { - logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`); - return; - } - - logger.warn( - `Error in announce target ${targetUri} - ${e.statusCode || e}`, - ); - } - throw e; - } - - if (!(await Notes.isVisibleForMe(renote, actor.id))) - return "skip: invalid actor for this activity"; - - logger.info(`Creating the (Re)Note: ${uri}`); - - const activityAudience = await parseAudience( - actor, - activity.to, - activity.cc, - ); - - await post(actor, { - createdAt: activity.published ? new Date(activity.published) : null, - renote, - visibility: activityAudience.visibility, - visibleUsers: activityAudience.visibleUsers, - uri, - }); - } finally { - unlock(); - } -} diff --git a/packages/backend/src/remote/activitypub/kernel/block/index.ts b/packages/backend/src/remote/activitypub/kernel/block/index.ts deleted file mode 100644 index 4dc868ba10..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/block/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { IBlock } from "../../type.js"; -import block from "@/services/blocking/create.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import DbResolver from "../../db-resolver.js"; -import { Users } from "@/models/index.js"; - -export default async ( - actor: CacheableRemoteUser, - activity: IBlock, -): Promise => { - // ※ There is a block target in activity.object, which should be a local user that exists. - - 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: The user you are trying to block is not a local user"; - } - - await block( - await Users.findOneByOrFail({ id: actor.id }), - await Users.findOneByOrFail({ id: blockee.id }), - ); - return "ok"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/create/index.ts b/packages/backend/src/remote/activitypub/kernel/create/index.ts deleted file mode 100644 index 3dcf648247..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/create/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Resolver from "../../resolver.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import createNote from "./note.js"; -import type { ICreate } from "../../type.js"; -import { getApId, isPost, getApType } from "../../type.js"; -import { apLogger } from "../../logger.js"; -import { toArray, concat, unique } from "@/prelude/array.js"; - -const logger = apLogger; - -export default async ( - actor: CacheableRemoteUser, - 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/packages/backend/src/remote/activitypub/kernel/create/note.ts b/packages/backend/src/remote/activitypub/kernel/create/note.ts deleted file mode 100644 index 09c492730c..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/create/note.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type Resolver from "../../resolver.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { createNote, fetchNote } from "../../models/note.js"; -import type { IObject, ICreate } from "../../type.js"; -import { getApId } from "../../type.js"; -import { getApLock } from "@/misc/app-lock.js"; -import { extractDbHost } from "@/misc/convert-host.js"; -import { StatusError } from "@/misc/fetch.js"; - -/** - * Handle post creation activity - */ -export default async function ( - resolver: Resolver, - actor: CacheableRemoteUser, - 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/packages/backend/src/remote/activitypub/kernel/delete/actor.ts b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts deleted file mode 100644 index 3571135aa5..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { apLogger } from "../../logger.js"; -import { createDeleteAccountJob } from "@/queue/index.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; - -const logger = apLogger; - -export async function deleteActor( - actor: CacheableRemoteUser, - uri: string, -): Promise { - logger.info(`Deleting the Actor: ${uri}`); - - if (actor.uri !== uri) { - return `skip: delete actor ${actor.uri} !== ${uri}`; - } - - const user = await Users.findOneByOrFail({ id: actor.id }); - if (user.isDeleted) { - logger.info("skip: already deleted"); - } - - const job = await createDeleteAccountJob(actor); - - await Users.update(actor.id, { - isDeleted: true, - }); - - return `ok: queued ${job.name} ${job.id}`; -} diff --git a/packages/backend/src/remote/activitypub/kernel/delete/index.ts b/packages/backend/src/remote/activitypub/kernel/delete/index.ts deleted file mode 100644 index f9ad52de54..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/delete/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { toSingle } from "@/prelude/array.js"; -import { getApId, isTombstone, validPost, validActor } from "../../type.js"; -import deleteNote from "./note.js"; -import { deleteActor } from "./actor.js"; -import type { IDelete, IObject } from "../../type.js"; - -/** - * Handle delete activity - */ -export default async ( - actor: CacheableRemoteUser, - activity: IDelete, -): Promise => { - if ("actor" in activity && actor.uri !== activity.actor) { - throw new Error("invalid actor"); - } - - // Type of object to be deleted - let formerType: string | undefined; - - if (typeof activity.object === "string") { - // The type is unknown, but it has disappeared - // anyway, so it does not remote resolve - formerType = undefined; - } else { - const object = activity.object as IObject; - if (isTombstone(object)) { - formerType = toSingle(object.formerType); - } else { - formerType = toSingle(object.type); - } - } - - const uri = getApId(activity.object); - - // Even if type is unknown, if actor and object are the same, - // it must be `Person`. - if (!formerType && actor.uri === uri) { - formerType = "Person"; - } - - // If not, fallback to `Note`. - if (!formerType) { - formerType = "Note"; - } - - if (validPost.includes(formerType)) { - return await deleteNote(actor, uri); - } else if (validActor.includes(formerType)) { - return await deleteActor(actor, uri); - } else { - return `Unknown type ${formerType}`; - } -}; diff --git a/packages/backend/src/remote/activitypub/kernel/delete/note.ts b/packages/backend/src/remote/activitypub/kernel/delete/note.ts deleted file mode 100644 index 69298e9175..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import deleteNode from "@/services/note/delete.js"; -import { apLogger } from "../../logger.js"; -import DbResolver from "../../db-resolver.js"; -import { getApLock } from "@/misc/app-lock.js"; -import { deleteMessage } from "@/services/messages/delete.js"; - -const logger = apLogger; - -export default async function ( - actor: CacheableRemoteUser, - 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 "The user trying to delete the post is not the post author"; - } - - await deleteMessage(message); - - return "ok: message deleted"; - } - - if (note.userId !== actor.id) { - return "The user trying to delete the post is not the post author"; - } - - await deleteNode(actor, note); - return "ok: note deleted"; - } finally { - unlock(); - } -} diff --git a/packages/backend/src/remote/activitypub/kernel/flag/index.ts b/packages/backend/src/remote/activitypub/kernel/flag/index.ts deleted file mode 100644 index 39ba8b3f4f..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/flag/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import config from "@/config/index.js"; -import type { IFlag } from "../../type.js"; -import { getApIds } from "../../type.js"; -import { AbuseUserReports, Users } from "@/models/index.js"; -import { In } from "typeorm"; -import { genId } from "@/misc/gen-id.js"; - -export default async ( - actor: CacheableRemoteUser, - activity: IFlag, -): Promise => { - // The object is `(User | Note) | (User | Note) []`, but it cannot be - // matched with all patterns of the DB schema, so the target user is the first - // user and it is stored as a comment. - const uris = getApIds(activity.object); - - const userIds = uris - .filter((uri) => uri.startsWith(`${config.url}/users/`)) - .map((uri) => uri.split("/").pop()!); - const users = await Users.findBy({ - id: In(userIds), - }); - if (users.length < 1) return "skip"; - - await AbuseUserReports.insert({ - id: genId(), - createdAt: new Date(), - targetUserId: users[0].id, - targetUserHost: users[0].host, - reporterId: actor.id, - reporterHost: actor.host, - comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`, - }); - - return "ok"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/follow.ts b/packages/backend/src/remote/activitypub/kernel/follow.ts deleted file mode 100644 index 1c1ef36cfa..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/follow.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import follow from "@/services/following/create.js"; -import type { IFollow } from "../type.js"; -import DbResolver from "../db-resolver.js"; - -export default async ( - actor: CacheableRemoteUser, - 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: user you are trying to follow is not a local user"; - } - - await follow(actor, followee, activity.id); - return "ok"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/index.ts b/packages/backend/src/remote/activitypub/kernel/index.ts deleted file mode 100644 index 58e354a512..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/index.ts +++ /dev/null @@ -1,111 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { toArray } from "@/prelude/array.js"; -import { - isCreate, - isDelete, - isUpdate, - isRead, - isFollow, - isAccept, - isReject, - isAdd, - isRemove, - isAnnounce, - isLike, - isUndo, - isBlock, - isCollectionOrOrderedCollection, - isCollection, - isFlag, - isMove, - getApId, -} from "../type.js"; -import { apLogger } from "../logger.js"; -import Resolver from "../resolver.js"; -import create from "./create/index.js"; -import performDeleteActivity from "./delete/index.js"; -import performUpdateActivity from "./update/index.js"; -import { performReadActivity } from "./read.js"; -import follow from "./follow.js"; -import undo from "./undo/index.js"; -import like from "./like.js"; -import announce from "./announce/index.js"; -import accept from "./accept/index.js"; -import reject from "./reject/index.js"; -import add from "./add/index.js"; -import remove from "./remove/index.js"; -import block from "./block/index.js"; -import flag from "./flag/index.js"; -import move from "./move/index.js"; -import type { IObject } from "../type.js"; -import { extractDbHost } from "@/misc/convert-host.js"; -import { shouldBlockInstance } from "@/misc/should-block-instance.js"; - -export async function performActivity( - actor: CacheableRemoteUser, - 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 (err) { - if (err instanceof Error || typeof err === "string") { - apLogger.error(err); - } - } - } - } else { - await performOneActivity(actor, activity); - } -} - -async function performOneActivity( - actor: CacheableRemoteUser, - activity: IObject, -): Promise { - if (actor.isSuspended) return; - - if (typeof activity.id !== "undefined") { - const host = extractDbHost(getApId(activity)); - if (await shouldBlockInstance(host)) 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 if (isMove(activity)) { - await move(actor, activity); - } else { - apLogger.warn(`unrecognized activity type: ${(activity as any).type}`); - } -} diff --git a/packages/backend/src/remote/activitypub/kernel/like.ts b/packages/backend/src/remote/activitypub/kernel/like.ts deleted file mode 100644 index 7b30d1cd54..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/like.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { ILike } from "../type.js"; -import { getApId } from "../type.js"; -import create from "@/services/note/reaction/create.js"; -import { fetchNote, extractEmojis } from "../models/note.js"; - -export default async (actor: CacheableRemoteUser, activity: ILike) => { - const targetUri = getApId(activity.object); - - const note = await fetchNote(targetUri); - if (!note) return `skip: target note not found ${targetUri}`; - - await extractEmojis(activity.tag || [], actor.host).catch(() => null); - - return await create( - actor, - note, - activity._misskey_reaction || activity.content || activity.name, - ) - .catch((e) => { - if (e.id === "51c42bb4-931a-456b-bff7-e5a8a70dd298") { - return "skip: already reacted"; - } else { - throw e; - } - }) - .then(() => "ok"); -}; diff --git a/packages/backend/src/remote/activitypub/kernel/move/index.ts b/packages/backend/src/remote/activitypub/kernel/move/index.ts deleted file mode 100644 index 800fd7bfea..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/move/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { Followings, Users } from "@/models/index.js"; -import { - resolvePerson, - updatePerson, -} from "@/remote/activitypub/models/person.js"; -import create from "@/services/following/create.js"; -import deleteFollowing from "@/services/following/delete.js"; - -import type { IMove } from "../../type.js"; -import { getApHrefNullable } from "../../type.js"; - -export default async ( - actor: CacheableRemoteUser, - activity: IMove, -): Promise => { - // ※ There is a block target in activity.object, which should be a local user that exists. - - // fetch the new and old accounts - const targetUri = getApHrefNullable(activity.target); - if (!targetUri) return "move: target uri is null"; - let new_acc = await resolvePerson(targetUri); - if (!actor.uri) return "move: actor uri is null"; - let old_acc = await resolvePerson(actor.uri); - - // update them if they're remote - if (new_acc.uri) await updatePerson(new_acc.uri); - if (old_acc.uri) await updatePerson(old_acc.uri); - - // retrieve updated users - new_acc = await resolvePerson(targetUri); - old_acc = await resolvePerson(actor.uri); - - // check if alsoKnownAs of the new account is valid - let isValidMove = true; - if (old_acc.uri) { - if (!new_acc.alsoKnownAs?.includes(old_acc.uri)) { - isValidMove = false; - } - } else if (!new_acc.alsoKnownAs?.includes(old_acc.id)) { - isValidMove = false; - } - if (!isValidMove) { - return "skip: accounts invalid"; - } - - // add target uri to movedToUri in order to indicate that the user has moved - await Users.update(old_acc.id, { movedToUri: targetUri }); - - // follow the new account and unfollow the old one - const followings = await Followings.findBy({ - followeeId: old_acc.id, - }); - followings.forEach(async (following) => { - // If follower is local - if (!following.followerHost) { - try { - const follower = await Users.findOneBy({ id: following.followerId }); - if (!follower) return; - await create(follower, new_acc); - await deleteFollowing(follower, old_acc); - } catch { - /* empty */ - } - } - }); - - return "ok"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts deleted file mode 100644 index 7cc70976c3..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/read.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { IRead } from "../type.js"; -import { getApId } from "../type.js"; -import { isSelfHost, extractDbHost } from "@/misc/convert-host.js"; -import { MessagingMessages } from "@/models/index.js"; -import { readUserMessagingMessage } from "../../../server/api/common/read-messaging-message.js"; - -export const performReadActivity = async ( - actor: CacheableRemoteUser, - 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.findOneBy({ id: messageId }); - if (message == null) { - return "skip: message not found"; - } - - if (actor.id !== message.recipientId) { - return "skip: actor is not a message recipient"; - } - - await readUserMessagingMessage(message.recipientId!, message.userId, [ - message.id, - ]); - return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts b/packages/backend/src/remote/activitypub/kernel/reject/follow.ts deleted file mode 100644 index 670c1556fd..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { remoteReject } from "@/services/following/reject.js"; -import type { IFollow } from "../../type.js"; -import DbResolver from "../../db-resolver.js"; -import { relayRejected } from "@/services/relay.js"; -import { Users } from "@/models/index.js"; - -export default async ( - actor: CacheableRemoteUser, - activity: IFollow, -): Promise => { - // ※ `activity.actor` must be an existing local user, since `activity` is a follow request thrown from us. - - const dbResolver = new DbResolver(); - const follower = await dbResolver.getUserFromApId(activity.actor); - - if (follower == null) { - return "skip: follower not found"; - } - - if (!Users.isLocalUser(follower)) { - 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 remoteReject(actor, follower); - return "ok"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/reject/index.ts b/packages/backend/src/remote/activitypub/kernel/reject/index.ts deleted file mode 100644 index 10edb0f7a2..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/reject/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Resolver from "../../resolver.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import rejectFollow from "./follow.js"; -import type { IReject } from "../../type.js"; -import { isFollow, getApType } from "../../type.js"; -import { apLogger } from "../../logger.js"; - -const logger = apLogger; - -export default async ( - actor: CacheableRemoteUser, - 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/packages/backend/src/remote/activitypub/kernel/remove/index.ts b/packages/backend/src/remote/activitypub/kernel/remove/index.ts deleted file mode 100644 index 0b4be6b5f2..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/remove/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { IRemove } from "../../type.js"; -import { resolveNote } from "../../models/note.js"; -import { removePinned } from "@/services/i/pin.js"; - -export default async ( - actor: CacheableRemoteUser, - 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/packages/backend/src/remote/activitypub/kernel/undo/accept.ts b/packages/backend/src/remote/activitypub/kernel/undo/accept.ts deleted file mode 100644 index 2cd05a77d2..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts +++ /dev/null @@ -1,30 +0,0 @@ -import unfollow from "@/services/following/delete.js"; -import cancelRequest from "@/services/following/requests/cancel.js"; -import type { IAccept } from "../../type.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { Followings } from "@/models/index.js"; -import DbResolver from "../../db-resolver.js"; - -export default async ( - actor: CacheableRemoteUser, - activity: IAccept, -): Promise => { - const dbResolver = new DbResolver(); - - const follower = await dbResolver.getUserFromApId(activity.object); - if (follower == null) { - return "skip: follower not found"; - } - - const following = await Followings.findOneBy({ - followerId: follower.id, - followeeId: actor.id, - }); - - if (following) { - await unfollow(follower, actor); - return "ok: unfollowed"; - } - - return "skip: skip: not followed"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts b/packages/backend/src/remote/activitypub/kernel/undo/announce.ts deleted file mode 100644 index a6e9c88c61..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Notes } from "@/models/index.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { IAnnounce } from "../../type.js"; -import { getApId } from "../../type.js"; -import deleteNote from "@/services/note/delete.js"; - -export const undoAnnounce = async ( - actor: CacheableRemoteUser, - activity: IAnnounce, -): Promise => { - const uri = getApId(activity); - - const note = await Notes.findOneBy({ - uri, - userId: actor.id, - }); - - if (!note) return "skip: no such Announce"; - - await deleteNote(actor, note); - return "ok: deleted"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/block.ts b/packages/backend/src/remote/activitypub/kernel/undo/block.ts deleted file mode 100644 index b4e1d8ee43..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/block.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { IBlock } from "../../type.js"; -import unblock from "@/services/blocking/delete.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import DbResolver from "../../db-resolver.js"; -import { Users } from "@/models/index.js"; - -export default async ( - actor: CacheableRemoteUser, - 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: The user you are trying to unblock is not a local user"; - } - - await unblock(await Users.findOneByOrFail({ id: actor.id }), blockee); - return "ok"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts b/packages/backend/src/remote/activitypub/kernel/undo/follow.ts deleted file mode 100644 index 1c4648cf90..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts +++ /dev/null @@ -1,44 +0,0 @@ -import unfollow from "@/services/following/delete.js"; -import cancelRequest from "@/services/following/requests/cancel.js"; -import type { IFollow } from "../../type.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { FollowRequests, Followings } from "@/models/index.js"; -import DbResolver from "../../db-resolver.js"; - -export default async ( - actor: CacheableRemoteUser, - 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: The user you are trying to unfollow is not a local user"; - } - - const req = await FollowRequests.findOneBy({ - followerId: actor.id, - followeeId: followee.id, - }); - - const following = await Followings.findOneBy({ - 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: Not requested or followed"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/index.ts b/packages/backend/src/remote/activitypub/kernel/undo/index.ts deleted file mode 100644 index f0e2316fab..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { IUndo } from "../../type.js"; -import { - isFollow, - isBlock, - isLike, - isAnnounce, - getApType, - isAccept, -} from "../../type.js"; -import unfollow from "./follow.js"; -import unblock from "./block.js"; -import undoLike from "./like.js"; -import undoAccept from "./accept.js"; -import { undoAnnounce } from "./announce.js"; -import Resolver from "../../resolver.js"; -import { apLogger } from "../../logger.js"; - -const logger = apLogger; - -export default async ( - actor: CacheableRemoteUser, - 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); - if (isAccept(object)) return await undoAccept(actor, object); - - return `skip: unknown object type ${getApType(object)}`; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/undo/like.ts b/packages/backend/src/remote/activitypub/kernel/undo/like.ts deleted file mode 100644 index 90220e203d..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/like.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { ILike } from "../../type.js"; -import { getApId } from "../../type.js"; -import deleteReaction from "@/services/note/reaction/delete.js"; -import { fetchNote } from "../../models/note.js"; - -/** - * Process Undo.Like activity - */ -export default async (actor: CacheableRemoteUser, activity: ILike) => { - const targetUri = getApId(activity.object); - - const note = await fetchNote(targetUri); - if (!note) return `skip: target note not found ${targetUri}`; - - await deleteReaction(actor, note).catch((e) => { - if (e.id === "60527ec9-b4cb-4a88-a6bd-32d3ad26817d") return; - throw e; - }); - - return "ok"; -}; diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts deleted file mode 100644 index 4f1514ddd1..0000000000 --- a/packages/backend/src/remote/activitypub/kernel/update/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import type { IUpdate } from "../../type.js"; -import { getApType, isActor } from "../../type.js"; -import { apLogger } from "../../logger.js"; -import { updateQuestion } from "../../models/question.js"; -import Resolver from "../../resolver.js"; -import { updatePerson } from "../../models/person.js"; - -/** - * Handler for the Update activity - */ -export default async ( - actor: CacheableRemoteUser, - 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, resolver).catch((e) => console.log(e)); - return "ok: Question updated"; - } else { - return `skip: Unknown type: ${getApType(object)}`; - } -}; diff --git a/packages/backend/src/remote/activitypub/logger.ts b/packages/backend/src/remote/activitypub/logger.ts deleted file mode 100644 index 47383cf7fa..0000000000 --- a/packages/backend/src/remote/activitypub/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { remoteLogger } from "../logger.js"; - -export const apLogger = remoteLogger.createSubLogger("ap", "magenta"); diff --git a/packages/backend/src/remote/activitypub/misc/contexts.ts b/packages/backend/src/remote/activitypub/misc/contexts.ts deleted file mode 100644 index 8c97b59729..0000000000 --- a/packages/backend/src/remote/activitypub/misc/contexts.ts +++ /dev/null @@ -1,525 +0,0 @@ -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/packages/backend/src/remote/activitypub/misc/get-note-html.ts b/packages/backend/src/remote/activitypub/misc/get-note-html.ts deleted file mode 100644 index cb5294f731..0000000000 --- a/packages/backend/src/remote/activitypub/misc/get-note-html.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as mfm from "mfm-js"; -import type { Note } from "@/models/entities/note.js"; -import { toHtml } from "../../../mfm/to-html.js"; - -export default function (note: Note) { - if (!note.text) return ""; - return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); -} diff --git a/packages/backend/src/remote/activitypub/misc/html-to-mfm.ts b/packages/backend/src/remote/activitypub/misc/html-to-mfm.ts deleted file mode 100644 index 9d71a46a36..0000000000 --- a/packages/backend/src/remote/activitypub/misc/html-to-mfm.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { IObject } from "../type.js"; -import { extractApHashtagObjects } from "../models/tag.js"; -import { fromHtml } from "../../../mfm/from-html.js"; - -export function htmlToMfm(html: string, tag?: IObject | IObject[]) { - const hashtagNames = extractApHashtagObjects(tag) - .map((x) => x.name) - .filter((x): x is string => x != null); - - return fromHtml(html, hashtagNames); -} diff --git a/packages/backend/src/remote/activitypub/misc/ld-signature.ts b/packages/backend/src/remote/activitypub/misc/ld-signature.ts deleted file mode 100644 index 1657597ab6..0000000000 --- a/packages/backend/src/remote/activitypub/misc/ld-signature.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as crypto from "node:crypto"; -import jsonld from "jsonld"; -import { CONTEXTS } from "./contexts.js"; -import fetch from "node-fetch"; -import { httpAgent, httpsAgent } from "@/misc/fetch.js"; - -// 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) { - options.domain = undefined; - } - - 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 new Error(`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", - }, - // TODO - //timeout: this.loderTimeout, - agent: (u) => (u.protocol === "http:" ? httpAgent : httpsAgent), - }).then((res) => { - if (!res.ok) { - throw new Error(`${res.status} ${res.statusText}`); - } else { - return res.json(); - } - }); - - return json; - } - - public sha256(data: string): string { - const hash = crypto.createHash("sha256"); - hash.update(data); - return hash.digest("hex"); - } -} diff --git a/packages/backend/src/remote/activitypub/models/icon.ts b/packages/backend/src/remote/activitypub/models/icon.ts deleted file mode 100644 index 50794a937d..0000000000 --- a/packages/backend/src/remote/activitypub/models/icon.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type IIcon = { - type: string; - mediaType?: string; - url?: string; -}; diff --git a/packages/backend/src/remote/activitypub/models/identifier.ts b/packages/backend/src/remote/activitypub/models/identifier.ts deleted file mode 100644 index f6c3bb8c88..0000000000 --- a/packages/backend/src/remote/activitypub/models/identifier.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type IIdentifier = { - type: string; - name: string; - value: string; -}; diff --git a/packages/backend/src/remote/activitypub/models/image.ts b/packages/backend/src/remote/activitypub/models/image.ts deleted file mode 100644 index 211aa3931e..0000000000 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { uploadFromUrl } from "@/services/drive/upload-from-url.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { IRemoteUser } from "@/models/entities/user.js"; -import Resolver from "../resolver.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { apLogger } from "../logger.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { DriveFiles, Users } from "@/models/index.js"; -import { truncate } from "@/misc/truncate.js"; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; - -const logger = apLogger; - -/** - * create an Image. - */ -export async function createImage( - actor: CacheableRemoteUser, - value: any, -): Promise { - // Skip if author is frozen. - 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"); - } - - if (!image.url.startsWith("https://") && !image.url.startsWith("http://")) { - throw new Error("invalid image: unexpected shcema of url: " + image.url); - } - - logger.info(`Creating the Image: ${image.url}`); - - const instance = await fetchMeta(); - - let file = await uploadFromUrl({ - url: image.url, - user: actor, - uri: image.url, - sensitive: image.sensitive, - isLink: !instance.cacheRemoteFiles, - comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH), - }); - - if (file.isLink) { - // If the URL is different, it means that the same image was previously - // registered with a different URL, so update the URL - if (file.url !== image.url) { - await DriveFiles.update( - { id: file.id }, - { - url: image.url, - uri: image.url, - }, - ); - - file = await DriveFiles.findOneByOrFail({ id: file.id }); - } - } - - return file; -} - -/** - * Resolve Image. - * - * If the target Image is registered in Calckey, return it, otherwise - * Fetch from remote server, register with Calckey and return it. - */ -export async function resolveImage( - actor: CacheableRemoteUser, - value: any, -): Promise { - // TODO - - // Fetch from remote server and register - return await createImage(actor, value); -} diff --git a/packages/backend/src/remote/activitypub/models/mention.ts b/packages/backend/src/remote/activitypub/models/mention.ts deleted file mode 100644 index b888fa21a4..0000000000 --- a/packages/backend/src/remote/activitypub/models/mention.ts +++ /dev/null @@ -1,36 +0,0 @@ -import promiseLimit from "promise-limit"; -import { toArray, unique } from "@/prelude/array.js"; -import type { CacheableUser } from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; -import type { IObject, IApMention } from "../type.js"; -import { isMention } from "../type.js"; -import Resolver from "../resolver.js"; -import { resolvePerson } from "./person.js"; - -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 CacheableUser => x != null); - - return mentionedUsers; -} - -export function extractApMentionObjects( - tags: IObject | IObject[] | null | undefined, -): IApMention[] { - if (tags == null) return []; - return toArray(tags).filter(isMention); -} diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts deleted file mode 100644 index 033157b081..0000000000 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ /dev/null @@ -1,499 +0,0 @@ -import promiseLimit from "promise-limit"; - -import config from "@/config/index.js"; -import Resolver from "../resolver.js"; -import post from "@/services/note/create.js"; -import { resolvePerson } from "./person.js"; -import { resolveImage } from "./image.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { htmlToMfm } from "../misc/html-to-mfm.js"; -import { extractApHashtags } from "./tag.js"; -import { unique, toArray, toSingle } from "@/prelude/array.js"; -import { extractPollFromQuestion } from "./question.js"; -import vote from "@/services/note/polls/vote.js"; -import { apLogger } from "../logger.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { deliverQuestionUpdate } from "@/services/note/polls/update.js"; -import { extractDbHost, toPuny } from "@/misc/convert-host.js"; -import { Emojis, Polls, MessagingMessages } from "@/models/index.js"; -import type { Note } from "@/models/entities/note.js"; -import type { IObject, IPost } from "../type.js"; -import { - getOneApId, - getApId, - getOneApHrefNullable, - validPost, - isEmoji, - getApType, -} from "../type.js"; -import type { Emoji } from "@/models/entities/emoji.js"; -import { genId } from "@/misc/gen-id.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { getApLock } from "@/misc/app-lock.js"; -import { createMessage } from "@/services/messages/create.js"; -import { parseAudience } from "../audience.js"; -import { extractApMentions } from "./mention.js"; -import DbResolver from "../db-resolver.js"; -import { StatusError } from "@/misc/fetch.js"; -import { shouldBlockInstance } from "@/misc/should-block-instance.js"; - -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; -} - -/** - * Fetch Notes. - * - * If the target Note is registered in Calckey, it will be returned. - */ -export async function fetchNote( - object: string | IObject, -): Promise { - const dbResolver = new DbResolver(); - return await dbResolver.getNoteFromApId(object); -} - -/** - * Create a 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; - - if (note.id && !note.id.startsWith("https://")) { - throw new Error(`unexpected schema of note.id: ${note.id}`); - } - - const url = getOneApHrefNullable(note.url); - - if (url && !url.startsWith("https://")) { - throw new Error(`unexpected schema of note url: ${url}`); - } - - logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); - logger.info(`Creating the Note: ${note.id}`); - - // Skip if note is made before 2007 (1yr before Fedi was created) - // OR skip if note is made 3 days in advance - if (note.published) { - const DateChecker = new Date(note.published); - const FutureCheck = new Date(); - FutureCheck.setDate(FutureCheck.getDate() + 3); // Allow some wiggle room for misconfigured hosts - if (DateChecker.getFullYear() < 2007) { - logger.warn( - "Note somehow made before Activitypub was created; discarding", - ); - return null; - } - if (DateChecker > FutureCheck) { - logger.warn("Note somehow made after today; discarding"); - return null; - } - } - - // Fetch author - const actor = (await resolvePerson( - getOneApId(note.attributedTo), - resolver, - )) as CacheableRemoteUser; - - // Skip if author is suspended. - if (actor.isSuspended) { - logger.debug( - `User ${actor.usernameLower}@${actor.host} suspended; discarding.`, - ); - return null; - } - - const noteAudience = await parseAudience(actor, note.to, note.cc); - let visibility = noteAudience.visibility; - const visibleUsers = noteAudience.visibleUsers; - - // If Audience (to, cc) was not specified - if (visibility === "specified" && visibleUsers.length === 0) { - if (typeof value === "string") { - // If the input is a string, GET occurs in resolver - // Public if you can GET anonymously from here - visibility = "public"; - } - } - - let isTalk = note._misskey_talk && visibility === "specified"; - - const apMentions = await extractApMentions(note.tag); - const apHashtags = await extractApHashtags(note.tag); - - // Attachments - // 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) - : []; - - // Reply - 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.findOneBy({ id }); - if (talk) { - isTalk = true; - return null; - } - } - - logger.warn( - `Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`, - ); - throw e; - }) - : null; - - // Quote - let quote: Note | undefined | null; - - if (note._misskey_quote || note.quoteUrl || note.quoteUri) { - 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, note.quoteUri].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 new Error("quote resolve failed"); - } - } - } - - const cw = note.summary === "" ? null : note.summary; - - // Text parsing - let text: string | null = null; - if ( - note.source?.mediaType === "text/x.misskeymarkdown" && - typeof note.source?.content === "string" - ) { - text = note.source.content; - } else if (typeof note._misskey_content !== "undefined") { - text = note._misskey_content; - } else if (typeof note.content === "string") { - text = htmlToMfm(note.content, note.tag); - } - - // vote - if (reply?.hasPoll) { - const poll = await Polls.findOneByOrFail({ noteId: 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 (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, - localOnly: false, - visibility, - visibleUsers, - apMentions, - apHashtags, - apEmojis, - poll, - uri: note.id, - url: url, - }, - silent, - ); -} - -/** - * Resolve Note. - * - * If the target Note is registered in Calckey, return it, otherwise - * Fetch from remote server, register with Calckey and return it. - */ -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"); - - // Abort if origin host is blocked - if (await shouldBlockInstance(extractDbHost(uri))) - throw new StatusError( - "host blocked", - 451, - `host ${extractDbHost(uri)} is blocked`, - ); - - const unlock = await getApLock(uri); - - try { - //#region Returns if already registered with this server - 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", - ); - } - - // Fetch from remote server and register - // If the attached `Note` Object is specified here instead of the uri, the note will be generated without going through the server fetch. - // Since the attached Note Object may be disguised, always specify the uri and fetch it from the server. - 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.findOneBy({ - 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.originalUrl - ) { - await Emojis.update( - { - host, - name, - }, - { - uri: tag.id, - originalUrl: tag.icon!.url, - publicUrl: tag.icon!.url, - updatedAt: new Date(), - }, - ); - - return (await Emojis.findOneBy({ - host, - name, - })) as Emoji; - } - - return exists; - } - - logger.info(`register emoji host=${host}, name=${name}`); - - return await Emojis.insert({ - id: genId(), - host, - name, - uri: tag.id, - originalUrl: tag.icon!.url, - publicUrl: tag.icon!.url, - updatedAt: new Date(), - aliases: [], - } as Partial).then((x) => - Emojis.findOneByOrFail(x.identifiers[0]), - ); - }), - ); -} diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts deleted file mode 100644 index 877f5f3323..0000000000 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ /dev/null @@ -1,698 +0,0 @@ -import { URL } from "node:url"; -import promiseLimit from "promise-limit"; - -import config from "@/config/index.js"; -import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instance-doc.js"; -import type { Note } from "@/models/entities/note.js"; -import { updateUsertags } from "@/services/update-hashtag.js"; -import { - Users, - Instances, - DriveFiles, - Followings, - UserProfiles, - UserPublickeys, -} from "@/models/index.js"; -import type { IRemoteUser, CacheableUser } from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; -import type { Emoji } from "@/models/entities/emoji.js"; -import { UserNotePining } from "@/models/entities/user-note-pining.js"; -import { genId } from "@/misc/gen-id.js"; -import { instanceChart, usersChart } from "@/services/chart/index.js"; -import { UserPublickey } from "@/models/entities/user-publickey.js"; -import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; -import { toPuny } from "@/misc/convert-host.js"; -import { UserProfile } from "@/models/entities/user-profile.js"; -import { toArray } from "@/prelude/array.js"; -import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js"; -import { normalizeForSearch } from "@/misc/normalize-for-search.js"; -import { truncate } from "@/misc/truncate.js"; -import { StatusError } from "@/misc/fetch.js"; -import { uriPersonCache } from "@/services/user-cache.js"; -import { publishInternalEvent } from "@/services/stream.js"; -import { db } from "@/db/postgre.js"; -import { apLogger } from "../logger.js"; -import { htmlToMfm } from "../misc/html-to-mfm.js"; -import { fromHtml } from "../../../mfm/from-html.js"; -import type { IActor, IObject, IApPropertyValue } from "../type.js"; -import { - isCollectionOrOrderedCollection, - isCollection, - getApId, - getOneApHrefNullable, - isPropertyValue, - getApType, - isActor, -} from "../type.js"; -import Resolver from "../resolver.js"; -import { extractApHashtags } from "./tag.js"; -import { resolveNote, extractEmojis } from "./note.js"; -import { resolveImage } from "./image.js"; - -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}'`); - } - - if (!(typeof x.id === "string" && x.id.length > 0)) { - throw new Error("invalid Actor: wrong id"); - } - - if (!(typeof x.inbox === "string" && x.inbox.length > 0)) { - throw new Error("invalid Actor: wrong inbox"); - } - - if ( - !( - typeof x.preferredUsername === "string" && - x.preferredUsername.length > 0 && - x.preferredUsername.length <= 128 && - /^\w([\w-.]*\w)?$/.test(x.preferredUsername) - ) - ) { - throw new Error("invalid Actor: wrong username"); - } - - // 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. - if (x.name) { - if (!(typeof x.name === "string" && x.name.length > 0)) { - throw new Error("invalid Actor: wrong name"); - } - x.name = truncate(x.name, nameLength); - } - if (x.summary) { - if (!(typeof x.summary === "string" && x.summary.length > 0)) { - throw new Error("invalid Actor: wrong summary"); - } - x.summary = truncate(x.summary, summaryLength); - } - - 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; -} - -/** - * Fetch a Person. - * - * If the target Person is registered in Calckey, it will be returned. - */ -export async function fetchPerson( - uri: string, - resolver?: Resolver, -): Promise { - if (typeof uri !== "string") throw new Error("uri is not string"); - - const cached = uriPersonCache.get(uri); - if (cached) return cached; - - // Fetch from the database if the URI points to this server - if (uri.startsWith(`${config.url}/`)) { - const id = uri.split("/").pop(); - const u = await Users.findOneBy({ id }); - if (u) uriPersonCache.set(uri, u); - return u; - } - - //#region Returns if already registered with this server - const exist = await Users.findOneBy({ uri }); - - if (exist) { - uriPersonCache.set(uri, exist); - return exist; - } - //#endregion - - return null; -} - -/** - * Create 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}/); - - const url = getOneApHrefNullable(person.url); - - if (url && !url.startsWith("https://")) { - throw new Error(`unexpected schema of person url: ${url}`); - } - - let followersCount: number | undefined; - - if (typeof person.followers === "string") { - try { - let data = await fetch(person.followers, { - headers: { Accept: "application/json" }, - }); - let json_data = JSON.parse(await data.text()); - - followersCount = json_data.totalItems; - } catch { - followersCount = undefined; - } - } - - let followingCount: number | undefined; - - if (typeof person.following === "string") { - try { - let data = await fetch(person.following, { - headers: { Accept: "application/json" }, - }); - let json_data = JSON.parse(await data.text()); - - followingCount = json_data.totalItems; - } catch (e) { - followingCount = undefined; - } - } - - // Create user - let user: IRemoteUser; - try { - // Start transaction - await db.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, - movedToUri: person.movedTo, - alsoKnownAs: person.alsoKnownAs, - 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, - followersCount: - followersCount !== undefined - ? followersCount - : person.followers && - typeof person.followers !== "string" && - isCollectionOrOrderedCollection(person.followers) - ? person.followers.totalItems - : undefined, - followingCount: - followingCount !== undefined - ? followingCount - : person.following && - typeof person.following !== "string" && - isCollectionOrOrderedCollection(person.following) - ? person.following.totalItems - : undefined, - featured: person.featured ? getApId(person.featured) : undefined, - uri: person.id, - tags, - isBot, - isCat: (person as any).isCat === true, - showTimelineReplies: false, - }), - )) as IRemoteUser; - - await transactionalEntityManager.save( - new UserProfile({ - userId: user.id, - description: person.summary - ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) - : null, - url: 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 Corresponds to an error that may occur when the input is an alias like - const u = await Users.findOneBy({ - uri: person.id, - }); - - if (u) { - user = u as IRemoteUser; - } else { - throw new Error("already registered"); - } - } else { - logger.error(e instanceof Error ? e : new Error(e as string)); - 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); - - // Hashtag update - updateUsertags(user!, tags); - - //#region Fetch avatar and header image - 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; - - await Users.update(user!.id, { - avatarId, - bannerId, - }); - - user!.avatarId = avatarId; - user!.bannerId = bannerId; - //#endregion - - //#region Get custom emoji - 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, resolver).catch((err) => logger.error(err)); - - return user!; -} - -/** - * Update Person data from remote. - * If the target Person is not registered in Calckey, it is ignored. - * @param uri URI of Person - * @param resolver Resolver - * @param hint Hint of Person object (If this value is a valid Person, it is used for updating without Remote resolve) - */ -export async function updatePerson( - uri: string, - resolver?: Resolver | null, - hint?: IObject, -): Promise { - if (typeof uri !== "string") throw new Error("uri is not string"); - - // Skip if the URI points to this server - if (uri.startsWith(`${config.url}/`)) { - return; - } - - //#region Already registered on this server? - const exist = (await Users.findOneBy({ uri })) as IRemoteUser; - - if (exist == null) { - return; - } - //#endregion - - if (resolver == null) resolver = new Resolver(); - - const object = hint || (await resolver.resolve(uri)); - - const person = validateActor(object, uri); - - logger.info(`Updating the Person: ${person.id}`); - - // Fetch avatar and header image - const [avatar, banner] = await Promise.all( - [person.icon, person.image].map((img) => - img == null - ? Promise.resolve(null) - : resolveImage(exist, img).catch(() => null), - ), - ); - - // Custom pictogram acquisition - 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 url = getOneApHrefNullable(person.url); - - if (url && !url.startsWith("https://")) { - throw new Error(`unexpected schema of person url: ${url}`); - } - - let followersCount: number | undefined; - - if (typeof person.followers === "string") { - try { - let data = await fetch(person.followers, { - headers: { Accept: "application/json" }, - }); - let json_data = JSON.parse(await data.text()); - - followersCount = json_data.totalItems; - } catch { - followersCount = undefined; - } - } - - let followingCount: number | undefined; - - if (typeof person.following === "string") { - try { - let data = await fetch(person.following, { - headers: { Accept: "application/json" }, - }); - let json_data = JSON.parse(await data.text()); - - followingCount = json_data.totalItems; - } catch { - followingCount = undefined; - } - } - - const updates = { - lastFetchedAt: new Date(), - inbox: person.inbox, - sharedInbox: - person.sharedInbox || - (person.endpoints ? person.endpoints.sharedInbox : undefined), - followersUri: person.followers ? getApId(person.followers) : undefined, - followersCount: - followersCount !== undefined - ? followersCount - : person.followers && - typeof person.followers !== "string" && - isCollectionOrOrderedCollection(person.followers) - ? person.followers.totalItems - : undefined, - followingCount: - followingCount !== undefined - ? followingCount - : person.following && - typeof person.following !== "string" && - isCollectionOrOrderedCollection(person.following) - ? person.following.totalItems - : 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, - movedToUri: person.movedTo || null, - alsoKnownAs: person.alsoKnownAs || null, - isExplorable: !!person.discoverable, - } as Partial; - - if (avatar) { - updates.avatarId = avatar.id; - } - - if (banner) { - updates.bannerId = banner.id; - } - - // 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: url, - fields, - description: person.summary - ? htmlToMfm(truncate(person.summary, summaryLength), person.tag) - : null, - birthday: bday ? bday[0] : null, - location: person["vcard:Address"] || null, - }, - ); - - publishInternalEvent("remoteUserUpdated", { id: exist.id }); - - // Hashtag Update - updateUsertags(exist, tags); - - // If the user in question is a follower, followers will also be updated. - await Followings.update( - { - followerId: exist.id, - }, - { - followerSharedInbox: - person.sharedInbox || - (person.endpoints ? person.endpoints.sharedInbox : undefined), - }, - ); - - await updateFeatured(exist.id, resolver).catch((err) => logger.error(err)); -} - -/** - * Resolve Person. - * - * If the target person is registered in Calckey, it returns it; - * otherwise, it fetches it from the remote server, registers it in Calckey, and returns it. - */ -export async function resolvePerson( - uri: string, - resolver?: Resolver, -): Promise { - if (typeof uri !== "string") throw new Error("uri is not string"); - - //#region If already registered on this server, return it. - const exist = await fetchPerson(uri); - - if (exist) { - return exist; - } - //#endregion - - // Fetched from remote server and registered - 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"], resolver?: Resolver) { - const user = await Users.findOneByOrFail({ id: userId }); - if (!Users.isRemoteUser(user)) return; - if (!user.featured) return; - - logger.info(`Updating the featured: ${user.uri}`); - - if (resolver == null) 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: Maybe it doesn't have to be a Note. - .slice(0, 5) - .map((item) => limit(() => resolveNote(item, resolver))), - ); - - await db.transaction(async (transactionalEntityManager) => { - await transactionalEntityManager.delete(UserNotePining, { - userId: user.id, - }); - - // For now, generate the id at a different time and maintain the order. - let td = 0; - for (const note of featuredNotes.filter((note) => note != null)) { - td -= 1000; - transactionalEntityManager.insert(UserNotePining, { - id: genId(new Date(Date.now() + td)), - createdAt: new Date(), - userId: user.id, - noteId: note!.id, - }); - } - }); -} diff --git a/packages/backend/src/remote/activitypub/models/question.ts b/packages/backend/src/remote/activitypub/models/question.ts deleted file mode 100644 index 520b9fee94..0000000000 --- a/packages/backend/src/remote/activitypub/models/question.ts +++ /dev/null @@ -1,97 +0,0 @@ -import config from "@/config/index.js"; -import Resolver from "../resolver.js"; -import type { IObject, IQuestion } from "../type.js"; -import { isQuestion } from "../type.js"; -import { apLogger } from "../logger.js"; -import { Notes, Polls } from "@/models/index.js"; -import type { IPoll } from "@/models/entities/poll.js"; - -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?.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, resolver?: Resolver) { - const uri = typeof value === "string" ? value : value.id; - - // Skip if URI points to this server - if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local"); - - //#region Already registered with this server? - const note = await Notes.findOneBy({ uri }); - if (note == null) throw new Error("Question is not registed"); - - const poll = await Polls.findOneBy({ noteId: note.id }); - if (poll == null) throw new Error("Question is not registed"); - //#endregion - - // resolve new Question object - if (resolver == null) resolver = new Resolver(); - const question = (await resolver.resolve(value)) as IQuestion; - apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); - - if (question.type !== "Question") throw new Error("object is not a Question"); - - const apChoices = question.oneOf || question.anyOf; - - let changed = false; - - for (const choice of poll.choices) { - const oldCount = poll.votes[poll.choices.indexOf(choice)]; - const newCount = apChoices!.filter((ap) => ap.name === choice)[0].replies! - .totalItems; - - if (oldCount !== newCount) { - changed = true; - poll.votes[poll.choices.indexOf(choice)] = newCount; - } - } - - await Polls.update( - { noteId: note.id }, - { - votes: poll.votes, - }, - ); - - return changed; -} diff --git a/packages/backend/src/remote/activitypub/models/tag.ts b/packages/backend/src/remote/activitypub/models/tag.ts deleted file mode 100644 index 537cdecbd5..0000000000 --- a/packages/backend/src/remote/activitypub/models/tag.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { toArray } from "@/prelude/array.js"; -import type { IObject, IApHashtag } from "../type.js"; -import { isHashtag } from "../type.js"; - -export function extractApHashtags( - tags: IObject | IObject[] | null | undefined, -) { - if (tags == null) return []; - - const hashtags = extractApHashtagObjects(tags); - - return hashtags - .map((tag) => { - const m = tag.name.match(/^#(.+)/); - return m ? m[1] : null; - }) - .filter((x): x is string => x != null); -} - -export function extractApHashtagObjects( - tags: IObject | IObject[] | null | undefined, -): IApHashtag[] { - if (tags == null) return []; - return toArray(tags).filter(isHashtag); -} diff --git a/packages/backend/src/remote/activitypub/perform.ts b/packages/backend/src/remote/activitypub/perform.ts deleted file mode 100644 index 0d2cdb4a5e..0000000000 --- a/packages/backend/src/remote/activitypub/perform.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { IObject } from "./type.js"; -import type { CacheableRemoteUser } from "@/models/entities/user.js"; -import { performActivity } from "./kernel/index.js"; -import { updatePerson } from "./models/person.js"; - -export default async ( - actor: CacheableRemoteUser, - activity: IObject, -): Promise => { - await performActivity(actor, activity); - - // Update the remote user information if it is out of date - if (actor.uri) { - if ( - actor.lastFetchedAt == null || - Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24 - ) { - setImmediate(() => { - updatePerson(actor.uri!); - }); - } - } -}; diff --git a/packages/backend/src/remote/activitypub/renderer/accept.ts b/packages/backend/src/remote/activitypub/renderer/accept.ts deleted file mode 100644 index fd145dcf97..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/accept.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; - -export default (object: any, user: { id: User["id"]; host: null }) => ({ - type: "Accept", - actor: `${config.url}/users/${user.id}`, - object, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/add.ts b/packages/backend/src/remote/activitypub/renderer/add.ts deleted file mode 100644 index d8203ac1ea..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/add.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from "@/config/index.js"; -import type { ILocalUser } from "@/models/entities/user.js"; - -export default (user: ILocalUser, target: any, object: any) => ({ - type: "Add", - actor: `${config.url}/users/${user.id}`, - target, - object, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/announce.ts b/packages/backend/src/remote/activitypub/renderer/announce.ts deleted file mode 100644 index 1fd1842acf..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/announce.ts +++ /dev/null @@ -1,37 +0,0 @@ -import config from "@/config/index.js"; -import type { Note } from "@/models/entities/note.js"; - -export default (object: any, note: Note) => { - const attributedTo = `${config.url}/users/${note.userId}`; - - 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`]; - } else if (note.visibility === "home") { - to = [`${attributedTo}/followers`]; - cc = ["https://www.w3.org/ns/activitystreams#Public"]; - } else if (note.visibility === "followers") { - to = [`${attributedTo}/followers`]; - } else if (note.visibility === "specified") { - to = mentions; - } else { - return null; - } - - return { - id: `${config.url}/notes/${note.id}/activity`, - actor: `${config.url}/users/${note.userId}`, - type: "Announce", - published: note.createdAt.toISOString(), - to, - cc, - object, - }; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/block.ts b/packages/backend/src/remote/activitypub/renderer/block.ts deleted file mode 100644 index c2ea267f38..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/block.ts +++ /dev/null @@ -1,20 +0,0 @@ -import config from "@/config/index.js"; -import type { Blocking } from "@/models/entities/blocking.js"; - -/** - * Renders a block into its ActivityPub representation. - * - * @param block The block to be rendered. The blockee relation must be loaded. - */ -export function renderBlock(block: Blocking) { - if (block.blockee?.uri == null) { - throw new Error("renderBlock: missing blockee uri"); - } - - return { - type: "Block", - id: `${config.url}/blocks/${block.id}`, - actor: `${config.url}/users/${block.blockerId}`, - object: block.blockee.uri, - }; -} diff --git a/packages/backend/src/remote/activitypub/renderer/create.ts b/packages/backend/src/remote/activitypub/renderer/create.ts deleted file mode 100644 index 857f5722cc..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/create.ts +++ /dev/null @@ -1,17 +0,0 @@ -import config from "@/config/index.js"; -import type { Note } from "@/models/entities/note.js"; - -export default (object: any, note: Note) => { - const activity = { - id: `${config.url}/notes/${note.id}/activity`, - actor: `${config.url}/users/${note.userId}`, - type: "Create", - published: note.createdAt.toISOString(), - object, - } as any; - - if (object.to) activity.to = object.to; - if (object.cc) activity.cc = object.cc; - - return activity; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/delete.ts b/packages/backend/src/remote/activitypub/renderer/delete.ts deleted file mode 100644 index 70bdc34922..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/delete.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; - -export default (object: any, user: { id: User["id"]; host: null }) => ({ - type: "Delete", - actor: `${config.url}/users/${user.id}`, - object, - published: new Date().toISOString(), -}); diff --git a/packages/backend/src/remote/activitypub/renderer/document.ts b/packages/backend/src/remote/activitypub/renderer/document.ts deleted file mode 100644 index 1c2ca89d94..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/document.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { DriveFiles } from "@/models/index.js"; - -export default (file: DriveFile) => ({ - type: "Document", - mediaType: file.type, - url: DriveFiles.getPublicUrl(file), - name: file.comment, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/emoji.ts b/packages/backend/src/remote/activitypub/renderer/emoji.ts deleted file mode 100644 index 3d9b8cd55b..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/emoji.ts +++ /dev/null @@ -1,17 +0,0 @@ -import config from "@/config/index.js"; -import type { Emoji } from "@/models/entities/emoji.js"; - -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.publicUrl || emoji.originalUrl, // || emoji.originalUrl してるのは後方互換性のため - }, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/flag.ts b/packages/backend/src/remote/activitypub/renderer/flag.ts deleted file mode 100644 index f94d508e1d..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/flag.ts +++ /dev/null @@ -1,20 +0,0 @@ -import config from "@/config/index.js"; -import { IObject, IActivity } from "@/remote/activitypub/type.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import { IRemoteUser } from "@/models/entities/user.js"; -import { getInstanceActor } from "@/services/instance-actor.js"; - -// to anonymise reporters, the reporting actor must be a system user -// object has to be a uri or array of uris -export const renderFlag = ( - user: ILocalUser, - object: [string], - content: string, -) => { - return { - type: "Flag", - actor: `${config.url}/users/${user.id}`, - content, - object, - }; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/follow-relay.ts b/packages/backend/src/remote/activitypub/renderer/follow-relay.ts deleted file mode 100644 index ad7f05bf84..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/follow-relay.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from "@/config/index.js"; -import type { Relay } from "@/models/entities/relay.js"; -import type { ILocalUser } from "@/models/entities/user.js"; - -export function renderFollowRelay(relay: Relay, relayActor: ILocalUser) { - const follow = { - id: `${config.url}/activities/follow-relay/${relay.id}`, - type: "Follow", - actor: `${config.url}/users/${relayActor.id}`, - object: "https://www.w3.org/ns/activitystreams#Public", - }; - - return follow; -} diff --git a/packages/backend/src/remote/activitypub/renderer/follow-user.ts b/packages/backend/src/remote/activitypub/renderer/follow-user.ts deleted file mode 100644 index 22ee429ff6..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/follow-user.ts +++ /dev/null @@ -1,12 +0,0 @@ -import config from "@/config/index.js"; -import { Users } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; - -/** - * 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.findOneByOrFail({ id: id }); - return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri; -} diff --git a/packages/backend/src/remote/activitypub/renderer/follow.ts b/packages/backend/src/remote/activitypub/renderer/follow.ts deleted file mode 100644 index 3ff89c12aa..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/follow.ts +++ /dev/null @@ -1,22 +0,0 @@ -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; - -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 = { - id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`, - 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; - - return follow; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/hashtag.ts b/packages/backend/src/remote/activitypub/renderer/hashtag.ts deleted file mode 100644 index a00cd1ff5e..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/hashtag.ts +++ /dev/null @@ -1,7 +0,0 @@ -import config from "@/config/index.js"; - -export default (tag: string) => ({ - type: "Hashtag", - href: `${config.url}/tags/${encodeURIComponent(tag)}`, - name: `#${tag}`, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/image.ts b/packages/backend/src/remote/activitypub/renderer/image.ts deleted file mode 100644 index 96183c7ad0..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/image.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { DriveFiles } from "@/models/index.js"; - -export default (file: DriveFile) => ({ - type: "Image", - url: DriveFiles.getPublicUrl(file), - sensitive: file.isSensitive, - name: file.comment, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts deleted file mode 100644 index 7b98cf2d77..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { v4 as uuid } from "uuid"; -import config from "@/config/index.js"; -import { getUserKeypair } from "@/misc/keypair-store.js"; -import type { User } from "@/models/entities/user.js"; -import { LdSignature } from "../misc/ld-signature.js"; -import type { IActivity } from "../type.js"; - -export const renderActivity = (x: any): IActivity | null => { - if (x == null) return null; - - if (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", - movedToUri: "as:movedTo", - sensitive: "as:sensitive", - Hashtag: "as:Hashtag", - quoteUri: "fedibird:quoteUri", - 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: "https://misskey-hub.net/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", - // Fedibird - fedibird: "http://fedibird.com/ns#", - // 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/packages/backend/src/remote/activitypub/renderer/key.ts b/packages/backend/src/remote/activitypub/renderer/key.ts deleted file mode 100644 index 084bb5361a..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/key.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from "@/config/index.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import type { UserKeypair } from "@/models/entities/user-keypair.js"; -import { createPublicKey } from "node:crypto"; - -export default (user: ILocalUser, key: UserKeypair, postfix?: string) => ({ - id: `${config.url}/users/${user.id}${postfix || "/publickey"}`, - type: "Key", - owner: `${config.url}/users/${user.id}`, - publicKeyPem: createPublicKey(key.publicKey).export({ - type: "spki", - format: "pem", - }), -}); diff --git a/packages/backend/src/remote/activitypub/renderer/like.ts b/packages/backend/src/remote/activitypub/renderer/like.ts deleted file mode 100644 index 53c66c5c92..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/like.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { IsNull } from "typeorm"; -import config from "@/config/index.js"; -import type { NoteReaction } from "@/models/entities/note-reaction.js"; -import type { Note } from "@/models/entities/note.js"; -import { Emojis } from "@/models/index.js"; -import renderEmoji from "./emoji.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; - -export const renderLike = async (noteReaction: NoteReaction, note: Note) => { - const reaction = noteReaction.reaction; - const meta = await fetchMeta(); - - 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}`, - ...(!meta.defaultReaction.includes(reaction) - ? { - content: reaction, - _misskey_reaction: reaction, - } - : {}), - } as any; - - if (reaction.startsWith(":")) { - const name = reaction.replace(/:/g, ""); - const emoji = await Emojis.findOneBy({ - name, - host: IsNull(), - }); - - if (emoji) object.tag = [renderEmoji(emoji)]; - } - - return object; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/mention.ts b/packages/backend/src/remote/activitypub/renderer/mention.ts deleted file mode 100644 index e7f0435c16..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/mention.ts +++ /dev/null @@ -1,13 +0,0 @@ -import config from "@/config/index.js"; -import type { User, ILocalUser } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; - -export default (mention: User) => ({ - type: "Mention", - href: Users.isRemoteUser(mention) - ? mention.uri - : `${config.url}/users/${(mention as ILocalUser).id}`, - name: Users.isRemoteUser(mention) - ? `@${mention.username}@${mention.host}` - : `@${(mention as ILocalUser).username}`, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts deleted file mode 100644 index 2ad2fec9fb..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { In, IsNull } from "typeorm"; -import config from "@/config/index.js"; -import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { DriveFiles, Notes, Users, Emojis, Polls } from "@/models/index.js"; -import type { Emoji } from "@/models/entities/emoji.js"; -import type { Poll } from "@/models/entities/poll.js"; -import toHtml from "../misc/get-note-html.js"; -import renderEmoji from "./emoji.js"; -import renderMention from "./mention.js"; -import renderHashtag from "./hashtag.js"; -import renderDocument from "./document.js"; - -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.findBy({ id: In(ids) }); - return ids - .map((id) => items.find((item) => item.id === id)) - .filter((item) => item != null) as DriveFile[]; - }; - - let inReplyTo; - let inReplyToNote: Note | null; - - if (note.replyId) { - inReplyToNote = await Notes.findOneBy({ id: note.replyId }); - - if (inReplyToNote != null) { - const inReplyToUser = await Users.findOneBy({ id: 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.findOneBy({ id: note.renoteId }); - - if (renote) { - quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`; - } - } - - const attributedTo = `${config.url}/users/${note.userId}`; - - 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.findBy({ - 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 | null = null; - - if (note.hasPoll) { - poll = await Polls.findOneBy({ noteId: note.id }); - } - - let apText = text; - - 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, - source: { - content: text, - mediaType: "text/x.misskeymarkdown", - }, - _misskey_quote: quote, - quoteUri: 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.findOneBy({ - name, - host: IsNull(), - }), - ), - ); - - return emojis.filter((emoji) => emoji != null) as Emoji[]; -} diff --git a/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts b/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts deleted file mode 100644 index 2275c9c94e..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts +++ /dev/null @@ -1,30 +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/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts b/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts deleted file mode 100644 index b975399b68..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts +++ /dev/null @@ -1,34 +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?: Record[], -): { - id: string | null; - type: "OrderedCollection"; - totalItems: any; - first?: string; - last?: string; - orderedItems?: Record[]; -} { - const page: any = { - id, - type: "OrderedCollection", - totalItems, - }; - - if (first) page.first = first; - if (last) page.last = last; - if (orderedItems) page.orderedItems = orderedItems; - - return page; -} diff --git a/packages/backend/src/remote/activitypub/renderer/person.ts b/packages/backend/src/remote/activitypub/renderer/person.ts deleted file mode 100644 index 1122a3a279..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/person.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { URL } from "node:url"; -import * as mfm from "mfm-js"; -import config from "@/config/index.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import { DriveFiles, UserProfiles } from "@/models/index.js"; -import { getUserKeypair } from "@/misc/keypair-store.js"; -import { toHtml } from "../../../mfm/to-html.js"; -import renderImage from "./image.js"; -import renderKey from "./key.js"; -import { getEmojis } from "./note.js"; -import renderEmoji from "./emoji.js"; -import renderHashtag from "./hashtag.js"; -import type { IIdentifier } from "../models/identifier.js"; - -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.findOneBy({ id: user.avatarId }) - : Promise.resolve(undefined), - user.bannerId - ? DriveFiles.findOneBy({ id: user.bannerId }) - : Promise.resolve(undefined), - UserProfiles.findOneByOrFail({ userId: 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?.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)) - : 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 (user.movedToUri) { - person.movedTo = user.movedToUri; - } - - if (user.alsoKnownAs) { - person.alsoKnownAs = user.alsoKnownAs; - } - - if (profile.birthday) { - person["vcard:bday"] = profile.birthday; - } - - if (profile.location) { - person["vcard:Address"] = profile.location; - } - - return person; -} diff --git a/packages/backend/src/remote/activitypub/renderer/question.ts b/packages/backend/src/remote/activitypub/renderer/question.ts deleted file mode 100644 index cb89aa7583..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/question.ts +++ /dev/null @@ -1,27 +0,0 @@ -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -import type { Poll } from "@/models/entities/poll.js"; - -export default async function renderQuestion( - user: { id: User["id"] }, - note: Note, - poll: Poll, -) { - const question = { - type: "Question", - id: `${config.url}/questions/${note.id}`, - actor: `${config.url}/users/${user.id}`, - content: note.text || "", - [poll.multiple ? "anyOf" : "oneOf"]: poll.choices.map((text, i) => ({ - name: text, - _misskey_votes: poll.votes[i], - replies: { - type: "Collection", - totalItems: poll.votes[i], - }, - })), - }; - - return question; -} diff --git a/packages/backend/src/remote/activitypub/renderer/read.ts b/packages/backend/src/remote/activitypub/renderer/read.ts deleted file mode 100644 index 212e7e8ddf..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/read.ts +++ /dev/null @@ -1,12 +0,0 @@ -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; -import type { MessagingMessage } from "@/models/entities/messaging-message.js"; - -export const renderReadActivity = ( - user: { id: User["id"] }, - message: MessagingMessage, -) => ({ - type: "Read", - actor: `${config.url}/users/${user.id}`, - object: message.uri, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/reject.ts b/packages/backend/src/remote/activitypub/renderer/reject.ts deleted file mode 100644 index 7ac4452411..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/reject.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; - -export default (object: any, user: { id: User["id"] }) => ({ - type: "Reject", - actor: `${config.url}/users/${user.id}`, - object, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/remove.ts b/packages/backend/src/remote/activitypub/renderer/remove.ts deleted file mode 100644 index e3b3fef856..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/remove.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; - -export default (user: { id: User["id"] }, target: any, object: any) => ({ - type: "Remove", - actor: `${config.url}/users/${user.id}`, - target, - object, -}); diff --git a/packages/backend/src/remote/activitypub/renderer/tombstone.ts b/packages/backend/src/remote/activitypub/renderer/tombstone.ts deleted file mode 100644 index 5c4003c75a..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/tombstone.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default (id: string) => ({ - id, - type: "Tombstone", -}); diff --git a/packages/backend/src/remote/activitypub/renderer/undo.ts b/packages/backend/src/remote/activitypub/renderer/undo.ts deleted file mode 100644 index 249d643b25..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/undo.ts +++ /dev/null @@ -1,19 +0,0 @@ -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; -import { ILocalUser } from "@/models/entities/user.js"; - -export default (object: any, user: { id: User["id"] }) => { - if (object == null) return null; - const id = - typeof object.id === "string" && object.id.startsWith(config.url) - ? `${object.id}/undo` - : undefined; - - return { - type: "Undo", - ...(id ? { id } : {}), - actor: `${config.url}/users/${user.id}`, - object, - published: new Date().toISOString(), - }; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/update.ts b/packages/backend/src/remote/activitypub/renderer/update.ts deleted file mode 100644 index 765a52f06d..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/update.ts +++ /dev/null @@ -1,15 +0,0 @@ -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; - -export default (object: any, user: { id: User["id"] }) => { - const activity = { - id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`, - actor: `${config.url}/users/${user.id}`, - type: "Update", - to: ["https://www.w3.org/ns/activitystreams#Public"], - object, - published: new Date().toISOString(), - } as any; - - return activity; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/vote.ts b/packages/backend/src/remote/activitypub/renderer/vote.ts deleted file mode 100644 index 21234a112d..0000000000 --- a/packages/backend/src/remote/activitypub/renderer/vote.ts +++ /dev/null @@ -1,29 +0,0 @@ -import config from "@/config/index.js"; -import type { Note } from "@/models/entities/note.js"; -import type { IRemoteUser, User } from "@/models/entities/user.js"; -import type { PollVote } from "@/models/entities/poll-vote.js"; -import type { Poll } from "@/models/entities/poll.js"; - -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/packages/backend/src/remote/activitypub/request.ts b/packages/backend/src/remote/activitypub/request.ts deleted file mode 100644 index ffb3e25a33..0000000000 --- a/packages/backend/src/remote/activitypub/request.ts +++ /dev/null @@ -1,58 +0,0 @@ -import config from "@/config/index.js"; -import { getUserKeypair } from "@/misc/keypair-store.js"; -import type { User } from "@/models/entities/user.js"; -import { getResponse } from "../../misc/fetch.js"; -import { createSignedPost, createSignedGet } from "./ap-request.js"; - -export default async (user: { id: User["id"] }, url: string, object: any) => { - const body = JSON.stringify(object); - - const keypair = await getUserKeypair(user.id); - - const req = createSignedPost({ - key: { - privateKeyPem: keypair.privateKey, - keyId: `${config.url}/users/${user.id}#main-key`, - }, - url, - body, - additionalHeaders: { - "User-Agent": config.userAgent, - }, - }); - - await getResponse({ - url, - method: req.request.method, - headers: req.request.headers, - body, - }); -}; - -/** - * Get AP object with http-signature - * @param user http-signature user - * @param url URL to fetch - */ -export async function signedGet(url: string, user: { id: User["id"] }) { - const keypair = await getUserKeypair(user.id); - - const req = createSignedGet({ - key: { - privateKeyPem: keypair.privateKey, - keyId: `${config.url}/users/${user.id}#main-key`, - }, - url, - additionalHeaders: { - "User-Agent": config.userAgent, - }, - }); - - const res = await getResponse({ - url, - method: req.request.method, - headers: req.request.headers, - }); - - return await res.json(); -} diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts deleted file mode 100644 index 0547927609..0000000000 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ /dev/null @@ -1,169 +0,0 @@ -import config from "@/config/index.js"; -import { getJson } from "@/misc/fetch.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import { getInstanceActor } from "@/services/instance-actor.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { extractDbHost, isSelfHost } from "@/misc/convert-host.js"; -import { signedGet } from "./request.js"; -import type { IObject, ICollection, IOrderedCollection } from "./type.js"; -import { isCollectionOrOrderedCollection, getApId } from "./type.js"; -import { - FollowRequests, - Notes, - NoteReactions, - Polls, - Users, -} from "@/models/index.js"; -import { parseUri } from "./db-resolver.js"; -import renderNote from "@/remote/activitypub/renderer/note.js"; -import { renderLike } from "@/remote/activitypub/renderer/like.js"; -import { renderPerson } from "@/remote/activitypub/renderer/person.js"; -import renderQuestion from "@/remote/activitypub/renderer/question.js"; -import renderCreate from "@/remote/activitypub/renderer/create.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderFollow from "@/remote/activitypub/renderer/follow.js"; -import { shouldBlockInstance } from "@/misc/should-block-instance.js"; - -export default class Resolver { - private history: Set; - private user?: ILocalUser; - private recursionLimit?: number; - - constructor(recursionLimit = 100) { - this.history = new Set(); - this.recursionLimit = recursionLimit; - } - - public getHistory(): string[] { - return Array.from(this.history); - } - - public async resolveCollection( - value: string | IObject, - ): Promise { - const collection = await this.resolve(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") { - if (typeof value.id !== "undefined") { - const host = extractDbHost(getApId(value)); - if (await shouldBlockInstance(host)) { - throw new Error("instance is blocked"); - } - } - return value; - } - - if (value.includes("#")) { - // URLs with fragment parts cannot be resolved correctly because - // the fragment part does not get transmitted over HTTP(S). - // Avoid strange behaviour by not trying to resolve these at all. - throw new Error(`cannot resolve URL with fragment: ${value}`); - } - - if (this.history.has(value)) { - throw new Error("cannot resolve already resolved one"); - } - if (this.recursionLimit && this.history.size > this.recursionLimit) { - throw new Error("hit recursion limit"); - } - this.history.add(value); - - const host = extractDbHost(value); - if (isSelfHost(host)) { - return await this.resolveLocal(value); - } - - const meta = await fetchMeta(); - if (await shouldBlockInstance(host, meta)) { - throw new Error("Instance is blocked"); - } - - if ( - meta.privateMode && - config.host !== host && - !meta.allowedHosts.includes(host) - ) { - throw new Error("Instance is not allowed"); - } - - if (!this.user) { - this.user = await getInstanceActor(); - } - - const object = ( - this.user - ? await signedGet(value, this.user) - : await getJson(value, "application/activity+json, application/ld+json") - ) as IObject; - - if ( - object == null || - (Array.isArray(object["@context"]) - ? !(object["@context"] as unknown[]).includes( - "https://www.w3.org/ns/activitystreams", - ) - : object["@context"] !== "https://www.w3.org/ns/activitystreams") - ) { - throw new Error("invalid response"); - } - - return object; - } - - private resolveLocal(url: string): Promise { - const parsed = parseUri(url); - if (!parsed.local) throw new Error("resolveLocal: not local"); - - switch (parsed.type) { - case "notes": - return Notes.findOneByOrFail({ id: parsed.id }).then((note) => { - if (parsed.rest === "activity") { - // this refers to the create activity and not the note itself - return renderActivity(renderCreate(renderNote(note))); - } else { - return renderNote(note); - } - }); - case "users": - return Users.findOneByOrFail({ id: parsed.id }).then((user) => - renderPerson(user as ILocalUser), - ); - case "questions": - // Polls are indexed by the note they are attached to. - return Promise.all([ - Notes.findOneByOrFail({ id: parsed.id }), - Polls.findOneByOrFail({ noteId: parsed.id }), - ]).then(([note, poll]) => - renderQuestion({ id: note.userId }, note, poll), - ); - case "likes": - return NoteReactions.findOneByOrFail({ id: parsed.id }).then( - (reaction) => renderActivity(renderLike(reaction, { uri: null })), - ); - case "follows": - // rest should be - if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) - throw new Error("resolveLocal: invalid follow URI"); - - return Promise.all( - [parsed.id, parsed.rest].map((id) => Users.findOneByOrFail({ id })), - ).then(([follower, followee]) => - renderActivity(renderFollow(follower, followee, url)), - ); - default: - throw new Error(`resolveLocal: type ${type} unhandled`); - } - } -} diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts deleted file mode 100644 index b0bdb0a8b4..0000000000 --- a/packages/backend/src/remote/activitypub/type.ts +++ /dev/null @@ -1,354 +0,0 @@ -export type obj = { [x: string]: any }; -export type ApObject = IObject | string | (IObject | string)[]; - -export interface IObject { - "@context": string | 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"; - source?: { - content: string; - mediaType: string; - }; - _misskey_quote?: string; - quoteUrl?: string; - quoteUri?: string; - _misskey_talk: boolean; -} - -export interface IQuestion extends IObject { - type: "Note" | "Question"; - source?: { - content: string; - mediaType: 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; - movedTo?: string; - alsoKnownAs?: string[]; - discoverable?: boolean; - inbox: string; - sharedInbox?: string; // backward compatibility.. ig - 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 interface IMove extends IActivity { - type: "Move"; - target: IObject | string; -} - -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"; -export const isMove = (object: IObject): object is IMove => - getApType(object) === "Move"; diff --git a/packages/backend/src/remote/logger.ts b/packages/backend/src/remote/logger.ts deleted file mode 100644 index b6bc5bf6dd..0000000000 --- a/packages/backend/src/remote/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from "@/services/logger.js"; - -export const remoteLogger = new Logger("remote", "cyan"); diff --git a/packages/backend/src/remote/resolve-user.ts b/packages/backend/src/remote/resolve-user.ts deleted file mode 100644 index a6c1e399a5..0000000000 --- a/packages/backend/src/remote/resolve-user.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { URL } from "node:url"; -import chalk from "chalk"; -import { IsNull } from "typeorm"; -import config from "@/config/index.js"; -import type { User, IRemoteUser } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; -import { toPuny } from "@/misc/convert-host.js"; -import webFinger from "./webfinger.js"; -import { createPerson, updatePerson } from "./activitypub/models/person.js"; -import { remoteLogger } from "./logger.js"; - -const logger = remoteLogger.createSubLogger("resolve-user"); - -export async function resolveUser( - username: string, - host: string | null, -): Promise { - const usernameLower = username.toLowerCase(); - - if (host == null) { - logger.info(`return local user: ${usernameLower}`); - return await Users.findOneBy({ usernameLower, host: IsNull() }).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.findOneBy({ usernameLower, host: IsNull() }).then( - (u) => { - if (u == null) { - throw new Error("user not found"); - } else { - return u; - } - }, - ); - } - - const user = (await Users.findOneBy({ - usernameLower, - host, - })) as IRemoteUser | null; - - 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); - } - - // If user information is out of date, return it by starting over from WebFilger - if ( - user.lastFetchedAt == null || - Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24 - ) { - // Prevent multiple attempts to connect to unconnected instances, update before each attempt to prevent subsequent similar attempts - 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.findOneBy({ uri: self.href }).then((u) => { - if (u == null) { - throw new Error("user not found"); - } else { - return u; - } - }); - } - - logger.info(`return existing remote user: ${acctLower}`); - return user; -} - -async function resolveSelf(acctLower: string) { - logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); - const finger = await webFinger(acctLower).catch((e) => { - logger.error( - `Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ - e.statusCode || e.message - }`, - ); - throw new Error( - `Failed to WebFinger for ${acctLower}: ${e.statusCode || e.message}`, - ); - }); - const self = finger.links.find( - (link) => link.rel != null && link.rel.toLowerCase() === "self", - ); - if (!self) { - logger.error( - `Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`, - ); - throw new Error("self link not found"); - } - return self; -} diff --git a/packages/backend/src/remote/webfinger.ts b/packages/backend/src/remote/webfinger.ts deleted file mode 100644 index 1e929c8ff1..0000000000 --- a/packages/backend/src/remote/webfinger.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { URL } from "node:url"; -import { getJson } from "@/misc/fetch.js"; -import { query as urlQuery } from "@/prelude/url.js"; - -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", - )) as IWebFinger; -} - -function genUrl(query: string) { - if (query.match(/^https?:\/\//)) { - const u = new URL(query); - return `${u.protocol}//${u.hostname}/.well-known/webfinger?${urlQuery({ - resource: query, - })}`; - } - - const m = query.match(/^([^@]+)@(.*)/); - if (m) { - const hostname = m[2]; - return `https://${hostname}/.well-known/webfinger?${urlQuery({ - resource: `acct:${query}`, - })}`; - } - - throw new Error(`Invalid query (${query})`); -} diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts deleted file mode 100644 index 29ac726efc..0000000000 --- a/packages/backend/src/server/activitypub.ts +++ /dev/null @@ -1,368 +0,0 @@ -import Router from "@koa/router"; -import json from "koa-json-body"; -import httpSignature from "@peertube/http-signature"; - -import { In, IsNull, Not } from "typeorm"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderNote from "@/remote/activitypub/renderer/note.js"; -import renderKey from "@/remote/activitypub/renderer/key.js"; -import { renderPerson } from "@/remote/activitypub/renderer/person.js"; -import renderEmoji from "@/remote/activitypub/renderer/emoji.js"; -import { inbox as processInbox } from "@/queue/index.js"; -import { isSelfHost, toPuny } from "@/misc/convert-host.js"; -import { Notes, Users, Emojis, NoteReactions } from "@/models/index.js"; -import type { ILocalUser, User } from "@/models/entities/user.js"; -import { renderLike } from "@/remote/activitypub/renderer/like.js"; -import { getUserKeypair } from "@/misc/keypair-store.js"; -import { checkFetch, hasSignature } from "@/remote/activitypub/check-fetch.js"; -import { getInstanceActor } from "@/services/instance-actor.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import renderFollow from "@/remote/activitypub/renderer/follow.js"; -import Featured from "./activitypub/featured.js"; -import Following from "./activitypub/following.js"; -import Followers from "./activitypub/followers.js"; -import Outbox, { packActivity } from "./activitypub/outbox.js"; - -// 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 verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const note = await Notes.findOneBy({ - id: ctx.params.note, - visibility: In(["public" as const, "home" as const]), - localOnly: false, - }); - - if (note == null) { - ctx.status = 404; - return; - } - - // redirect if remote - 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)); - - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } - setResponseType(ctx); -}); - -// note activity -router.get("/notes/:note/activity", async (ctx) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const note = await Notes.findOneBy({ - id: ctx.params.note, - userHost: IsNull(), - visibility: In(["public" as const, "home" as const]), - localOnly: false, - }); - - if (note == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await packActivity(note)); - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - 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 instanceActor = await getInstanceActor(); - if (ctx.params.user === instanceActor.id) { - ctx.body = renderActivity( - renderKey(instanceActor, await getUserKeypair(instanceActor.id)), - ); - ctx.set("Cache-Control", "public, max-age=180"); - setResponseType(ctx); - return; - } - - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const userId = ctx.params.user; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - if (user == null) { - ctx.status = 404; - return; - } - - const keypair = await getUserKeypair(user.id); - - if (Users.isLocalUser(user)) { - ctx.body = renderActivity(renderKey(user, keypair)); - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } - setResponseType(ctx); - } else { - ctx.status = 400; - } -}); - -// user -async function userInfo(ctx: Router.RouterContext, user: User | null) { - if (user == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await renderPerson(user as ILocalUser)); - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } - setResponseType(ctx); -} - -router.get("/users/:user", async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - - const instanceActor = await getInstanceActor(); - if (ctx.params.user === instanceActor.id) { - await userInfo(ctx, instanceActor); - return; - } - - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const userId = ctx.params.user; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - isSuspended: false, - }); - - await userInfo(ctx, user); -}); - -router.get("/@:user", async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - - if (ctx.params.user === "instance.actor") { - const instanceActor = await getInstanceActor(); - await userInfo(ctx, instanceActor); - return; - } - - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const user = await Users.findOneBy({ - usernameLower: ctx.params.user.toLowerCase(), - host: IsNull(), - isSuspended: false, - }); - - await userInfo(ctx, user); -}); - -router.get("/actor", async (ctx, next) => { - const instanceActor = await getInstanceActor(); - await userInfo(ctx, instanceActor); -}); -//#endregion - -// emoji -router.get("/emojis/:emoji", async (ctx) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const emoji = await Emojis.findOneBy({ - host: IsNull(), - name: ctx.params.emoji, - }); - - if (emoji == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await renderEmoji(emoji)); - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } - setResponseType(ctx); -}); - -// like -router.get("/likes/:like", async (ctx) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const reaction = await NoteReactions.findOneBy({ id: ctx.params.like }); - - if (reaction == null) { - ctx.status = 404; - return; - } - - const note = await Notes.findOneBy({ id: reaction.noteId }); - - if (note == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await renderLike(reaction, note)); - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } - setResponseType(ctx); -}); - -// follow -router.get("/follows/:follower/:followee", async (ctx) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - // This may be used before the follow is completed, so we do not - // check if the following exists. - - const [follower, followee] = await Promise.all([ - Users.findOneBy({ - id: ctx.params.follower, - host: IsNull(), - }), - Users.findOneBy({ - id: ctx.params.followee, - host: Not(IsNull()), - }), - ]); - - if (follower == null || followee == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(renderFollow(follower, followee)); - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } - setResponseType(ctx); -}); - -export default router; diff --git a/packages/backend/src/server/activitypub/featured.ts b/packages/backend/src/server/activitypub/featured.ts deleted file mode 100644 index 82bb19fa12..0000000000 --- a/packages/backend/src/server/activitypub/featured.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { IsNull } from "typeorm"; -import config from "@/config/index.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderOrderedCollection from "@/remote/activitypub/renderer/ordered-collection.js"; -import renderNote from "@/remote/activitypub/renderer/note.js"; -import { Users, Notes, UserNotePinings } from "@/models/index.js"; -import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { setResponseType } from "../activitypub.js"; -import type Router from "@koa/router"; - -export default async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const userId = ctx.params.user; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - 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.findOneByOrFail({ id: 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); - - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } - setResponseType(ctx); -}; diff --git a/packages/backend/src/server/activitypub/followers.ts b/packages/backend/src/server/activitypub/followers.ts deleted file mode 100644 index 146ca51928..0000000000 --- a/packages/backend/src/server/activitypub/followers.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { IsNull, LessThan } from "typeorm"; -import config from "@/config/index.js"; -import * as url from "@/prelude/url.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderOrderedCollection from "@/remote/activitypub/renderer/ordered-collection.js"; -import renderOrderedCollectionPage from "@/remote/activitypub/renderer/ordered-collection-page.js"; -import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js"; -import { Users, Followings, UserProfiles } from "@/models/index.js"; -import type { Following } from "@/models/entities/following.js"; -import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { setResponseType } from "../activitypub.js"; -import type { FindOptionsWhere } from "typeorm"; -import type Router from "@koa/router"; - -export default async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const userId = ctx.params.user; - - const cursor = ctx.request.query.cursor; - if (cursor != null && typeof cursor !== "string") { - ctx.status = 400; - return; - } - - const page = ctx.request.query.page === "true"; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - if (user == null) { - ctx.status = 404; - return; - } - - //#region Check ff visibility - const profile = await UserProfiles.findOneByOrFail({ userId: 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 FindOptionsWhere; - - // カーソルが指定されている場合 - 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); - setResponseType(ctx); - } - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } -}; diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts deleted file mode 100644 index eab513ce64..0000000000 --- a/packages/backend/src/server/activitypub/following.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { LessThan, IsNull } from "typeorm"; -import config from "@/config/index.js"; -import * as url from "@/prelude/url.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderOrderedCollection from "@/remote/activitypub/renderer/ordered-collection.js"; -import renderOrderedCollectionPage from "@/remote/activitypub/renderer/ordered-collection-page.js"; -import renderFollowUser from "@/remote/activitypub/renderer/follow-user.js"; -import { Users, Followings, UserProfiles } from "@/models/index.js"; -import type { Following } from "@/models/entities/following.js"; -import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { setResponseType } from "../activitypub.js"; -import type { FindOptionsWhere } from "typeorm"; -import type Router from "@koa/router"; - -export default async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const userId = ctx.params.user; - - const cursor = ctx.request.query.cursor; - if (cursor != null && typeof cursor !== "string") { - ctx.status = 400; - return; - } - - const page = ctx.request.query.page === "true"; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - if (user == null) { - ctx.status = 404; - return; - } - - //#region Check ff visibility - const profile = await UserProfiles.findOneByOrFail({ userId: 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 FindOptionsWhere; - - // If a cursor is specified - if (cursor) { - query.id = LessThan(cursor); - } - - // Get followings - const followings = await Followings.find({ - where: query, - take: limit + 1, - order: { id: -1 }, - }); - - // Whether there is a "next page" or not - 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); - setResponseType(ctx); - } - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } -}; diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts deleted file mode 100644 index e0a380ffb6..0000000000 --- a/packages/backend/src/server/activitypub/outbox.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Brackets, IsNull } from "typeorm"; -import config from "@/config/index.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderOrderedCollection from "@/remote/activitypub/renderer/ordered-collection.js"; -import renderOrderedCollectionPage from "@/remote/activitypub/renderer/ordered-collection-page.js"; -import renderNote from "@/remote/activitypub/renderer/note.js"; -import renderCreate from "@/remote/activitypub/renderer/create.js"; -import renderAnnounce from "@/remote/activitypub/renderer/announce.js"; -import { countIf } from "@/prelude/array.js"; -import * as url from "@/prelude/url.js"; -import { Users, Notes } from "@/models/index.js"; -import type { Note } from "@/models/entities/note.js"; -import { checkFetch } from "@/remote/activitypub/check-fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { makePaginationQuery } from "../api/common/make-pagination-query.js"; -import { setResponseType } from "../activitypub.js"; -import type Router from "@koa/router"; - -export default async (ctx: Router.RouterContext) => { - const verify = await checkFetch(ctx.req); - if (verify !== 200) { - ctx.status = verify; - return; - } - - const userId = ctx.params.user; - - const sinceId = ctx.request.query.since_id; - if (sinceId != null && typeof sinceId !== "string") { - ctx.status = 400; - return; - } - - const untilId = ctx.request.query.until_id; - if (untilId != null && typeof untilId !== "string") { - ctx.status = 400; - return; - } - - const page = ctx.request.query.page === "true"; - - if (countIf((x) => x != null, [sinceId, untilId]) > 1) { - ctx.status = 400; - return; - } - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - }); - - 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); - - setResponseType(ctx); - } - const meta = await fetchMeta(); - if (meta.secureMode || meta.privateMode) { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } else { - ctx.set("Cache-Control", "public, max-age=180"); - } -}; - -/** - * 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.findOneByOrFail({ id: note.renoteId }); - return renderAnnounce( - renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, - note, - ); - } - - return renderCreate(await renderNote(note, false), note); -} diff --git a/packages/backend/src/server/api/2fa.ts b/packages/backend/src/server/api/2fa.ts deleted file mode 100644 index 7318f0f433..0000000000 --- a/packages/backend/src/server/api/2fa.ts +++ /dev/null @@ -1,417 +0,0 @@ -import * as crypto from "node:crypto"; -import * as jsrsasign from "jsrsasign"; -import config from "@/config/index.js"; - -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/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts deleted file mode 100644 index 99a12fd110..0000000000 --- a/packages/backend/src/server/api/api-handler.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type Koa from "koa"; - -import type { User } from "@/models/entities/user.js"; -import { UserIps } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import type { IEndpoint } from "./endpoints.js"; -import authenticate, { AuthenticationError } from "./authenticate.js"; -import call from "./call.js"; -import { ApiError } from "./error.js"; - -const userIpHistories = new Map>(); - -setInterval(() => { - userIpHistories.clear(); -}, 1000 * 60 * 60); - -export default (endpoint: IEndpoint, ctx: Koa.Context) => - new Promise((res) => { - const body = ctx.is("multipart/form-data") - ? (ctx.request as any).body - : ctx.method === "GET" - ? ctx.query - : 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 - // for GET requests, do not even pass on the body parameter as it is considered unsafe - authenticate( - ctx.headers.authorization, - ctx.method === "GET" ? null : body["i"], - ) - .then(([user, app]) => { - // API invoking - call(endpoint.name, user, app, body, ctx) - .then((res: any) => { - if ( - ctx.method === "GET" && - endpoint.meta.cacheSec && - !body["i"] && - !user - ) { - ctx.set( - "Cache-Control", - `public, max-age=${endpoint.meta.cacheSec}`, - ); - } - reply(res); - }) - .catch((e: ApiError) => { - reply( - e.httpStatusCode - ? e.httpStatusCode - : e.kind === "client" - ? 400 - : 500, - e, - ); - }); - - // Log IP - if (user) { - fetchMeta().then((meta) => { - if (!meta.enableIpLogging) return; - const ip = ctx.ip; - const ips = userIpHistories.get(user.id); - if (ips == null || !ips.has(ip)) { - if (ips == null) { - userIpHistories.set(user.id, new Set([ip])); - } else { - ips.add(ip); - } - - try { - UserIps.createQueryBuilder() - .insert() - .values({ - createdAt: new Date(), - userId: user.id, - ip: ip, - }) - .orIgnore(true) - .execute(); - } catch {} - } - }); - } - }) - .catch((e) => { - if (e instanceof AuthenticationError) { - ctx.response.status = 403; - ctx.response.set("WWW-Authenticate", "Bearer"); - ctx.response.body = { - message: `Authentication failed: ${e.message}`, - code: "AUTHENTICATION_FAILED", - id: "b0a7f5f8-dc2f-4171-b91f-de88ad238e14", - kind: "client", - }; - res(); - } else { - reply(500, new ApiError()); - } - }); - }); diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts deleted file mode 100644 index 42274ad2a4..0000000000 --- a/packages/backend/src/server/api/authenticate.ts +++ /dev/null @@ -1,103 +0,0 @@ -import isNativeToken from "./common/is-native-token.js"; -import type { CacheableLocalUser, ILocalUser } from "@/models/entities/user.js"; -import { Users, AccessTokens, Apps } from "@/models/index.js"; -import type { AccessToken } from "@/models/entities/access-token.js"; -import { Cache } from "@/misc/cache.js"; -import type { App } from "@/models/entities/app.js"; -import { - localUserByIdCache, - localUserByNativeTokenCache, -} from "@/services/user-cache.js"; - -const appCache = new Cache(Infinity); - -export class AuthenticationError extends Error { - constructor(message: string) { - super(message); - this.name = "AuthenticationError"; - } -} - -export default async ( - authorization: string | null | undefined, - bodyToken: string | null, -): Promise< - [CacheableLocalUser | null | undefined, AccessToken | null | undefined] -> => { - let token: string | null = null; - - // check if there is an authorization header set - if (authorization != null) { - if (bodyToken != null) { - throw new AuthenticationError("using multiple authorization schemes"); - } - - // check if OAuth 2.0 Bearer tokens are being used - // Authorization schemes are case insensitive - if (authorization.substring(0, 7).toLowerCase() === "bearer ") { - token = authorization.substring(7); - } else { - throw new AuthenticationError("unsupported authentication scheme"); - } - } else if (bodyToken != null) { - token = bodyToken; - } else { - return [null, null]; - } - - if (isNativeToken(token)) { - const user = await localUserByNativeTokenCache.fetch( - token, - () => Users.findOneBy({ token }) as Promise, - ); - - if (user == null) { - throw new AuthenticationError("unknown token"); - } - - return [user, null]; - } else { - const accessToken = await AccessTokens.findOne({ - where: [ - { - hash: token.toLowerCase(), // app - }, - { - token: token, // miauth - }, - ], - }); - - if (accessToken == null) { - throw new AuthenticationError("unknown token"); - } - - AccessTokens.update(accessToken.id, { - lastUsedAt: new Date(), - }); - - const user = await localUserByIdCache.fetch( - accessToken.userId, - () => - Users.findOneBy({ - id: accessToken.userId, - }) as Promise, - ); - - if (accessToken.appId) { - const app = await appCache.fetch(accessToken.appId, () => - Apps.findOneByOrFail({ id: accessToken.appId! }), - ); - - return [ - user, - { - id: accessToken.id, - permission: app.permission, - } as AccessToken, - ]; - } else { - return [user, accessToken]; - } - } -}; diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts deleted file mode 100644 index 45471ed564..0000000000 --- a/packages/backend/src/server/api/call.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { performance } from "perf_hooks"; -import type Koa from "koa"; -import type { CacheableLocalUser } from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; -import type { AccessToken } from "@/models/entities/access-token.js"; -import { getIpHash } from "@/misc/get-ip-hash.js"; -import { limiter } from "./limiter.js"; -import type { IEndpointMeta } from "./endpoints.js"; -import endpoints from "./endpoints.js"; -import compatibility from "./compatibility.js"; -import { ApiError } from "./error.js"; -import { apiLogger } from "./logger.js"; -import type { AccessToken } from "@/models/entities/access-token.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; - -const accessDenied = { - message: "Access denied.", - code: "ACCESS_DENIED", - id: "56f35758-7dd5-468b-8439-5d6fb8ec9b8e", -}; - -export default async ( - endpoint: string, - user: CacheableLocalUser | null | undefined, - token: AccessToken | null | undefined, - data: any, - ctx?: Koa.Context, -) => { - const isSecure = user != null && token == null; - const isModerator = user != null && (user.isModerator || user.isAdmin); - - const ep = - endpoints.find((e) => e.name === endpoint) || - compatibility.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.limit) { - // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app. - let limitActor: string; - if (user) { - limitActor = user.id; - } else { - limitActor = getIpHash(ctx!.ip); - } - - const limit = Object.assign({}, ep.meta.limit); - - if (!limit.key) { - limit.key = ep.name; - } - - // Rate limit - await limiter( - limit as IEndpointMeta["limit"] & { key: NonNullable }, - limitActor, - ).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, - }); - }); - } - - 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 && !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", - }); - } - - // private mode - const meta = await fetchMeta(); - if ( - meta.privateMode && - ep.meta.requireCredentialPrivateMode && - user == null - ) { - throw new ApiError({ - message: "Credential required.", - code: "CREDENTIAL_REQUIRED", - id: "1384574d-a912-4b81-8601-c7b1c4085df1", - httpStatusCode: 401, - }); - } - - // Cast non JSON input - if ((ep.meta.requireFile || ctx?.method === "GET") && ep.params.properties) { - for (const k of Object.keys(ep.params.properties)) { - const param = ep.params.properties![k]; - if ( - ["boolean", "number", "integer"].includes(param.type ?? "") && - typeof data[k] === "string" - ) { - try { - data[k] = JSON.parse(data[k]); - } catch (e) { - throw new ApiError( - { - message: "Invalid param.", - code: "INVALID_PARAM", - id: "0b5f1631-7c1a-41a6-b399-cce335f34d85", - }, - { - param: k, - reason: `cannot cast to ${param.type}`, - }, - ); - } - } - } - } - - // API invoking - const before = performance.now(); - return await ep - .exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers) - .catch((e: Error) => { - if (e instanceof ApiError) { - throw e; - } else { - apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, { - ep: ep.name, - ps: data, - e: { - message: e.message, - code: e.name, - stack: e.stack, - }, - }); - throw new ApiError(null, { - e: { - message: e.message, - code: e.name, - stack: e.stack, - }, - }); - } - }) - .finally(() => { - const after = performance.now(); - const time = after - before; - if (time > 1000) { - apiLogger.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`); - } - }); -}; diff --git a/packages/backend/src/server/api/common/generate-block-query.ts b/packages/backend/src/server/api/common/generate-block-query.ts deleted file mode 100644 index a37b607eb9..0000000000 --- a/packages/backend/src/server/api/common/generate-block-query.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import { Blockings } from "@/models/index.js"; -import type { SelectQueryBuilder } from "typeorm"; -import { Brackets } 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/packages/backend/src/server/api/common/generate-channel-query.ts b/packages/backend/src/server/api/common/generate-channel-query.ts deleted file mode 100644 index 318062266e..0000000000 --- a/packages/backend/src/server/api/common/generate-channel-query.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import { ChannelFollowings } from "@/models/index.js"; -import type { SelectQueryBuilder } from "typeorm"; -import { Brackets } 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/packages/backend/src/server/api/common/generate-muted-note-query.ts b/packages/backend/src/server/api/common/generate-muted-note-query.ts deleted file mode 100644 index 86da2ea883..0000000000 --- a/packages/backend/src/server/api/common/generate-muted-note-query.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import { MutedNotes } from "@/models/index.js"; -import type { 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/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts b/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts deleted file mode 100644 index 61f44f4a76..0000000000 --- a/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import { NoteThreadMutings } from "@/models/index.js"; -import type { SelectQueryBuilder } from "typeorm"; -import { Brackets } 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/packages/backend/src/server/api/common/generate-muted-user-query.ts b/packages/backend/src/server/api/common/generate-muted-user-query.ts deleted file mode 100644 index 3538fbf0af..0000000000 --- a/packages/backend/src/server/api/common/generate-muted-user-query.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { SelectQueryBuilder } from "typeorm"; -import { Brackets } from "typeorm"; -import type { User } from "@/models/entities/user.js"; -import { Mutings, UserProfiles } from "@/models/index.js"; - -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, - }); - } - - const mutingInstanceQuery = UserProfiles.createQueryBuilder("user_profile") - .select("user_profile.mutedInstances") - .where("user_profile.userId = :muterId", { muterId: me.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()})`, - ); - }), - ) - // mute instances - .andWhere( - new Brackets((qb) => { - qb.andWhere("note.userHost IS NULL").orWhere( - `NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.userHost)`, - ); - }), - ) - .andWhere( - new Brackets((qb) => { - qb.where("note.replyUserHost IS NULL").orWhere( - `NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.replyUserHost)`, - ); - }), - ) - .andWhere( - new Brackets((qb) => { - qb.where("note.renoteUserHost IS NULL").orWhere( - `NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.renoteUserHost)`, - ); - }), - ); - - q.setParameters(mutingQuery.getParameters()); - q.setParameters(mutingInstanceQuery.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/packages/backend/src/server/api/common/generate-native-user-token.ts b/packages/backend/src/server/api/common/generate-native-user-token.ts deleted file mode 100644 index 5a8b41b70e..0000000000 --- a/packages/backend/src/server/api/common/generate-native-user-token.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { secureRndstr } from "@/misc/secure-rndstr.js"; - -export default () => secureRndstr(16, true); diff --git a/packages/backend/src/server/api/common/generate-replies-query.ts b/packages/backend/src/server/api/common/generate-replies-query.ts deleted file mode 100644 index 140c1d74a0..0000000000 --- a/packages/backend/src/server/api/common/generate-replies-query.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import type { SelectQueryBuilder } from "typeorm"; -import { Brackets } from "typeorm"; - -export function generateRepliesQuery( - q: SelectQueryBuilder, - me?: Pick | 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 if (!me.showTimelineReplies) { - q.andWhere( - new Brackets((qb) => { - qb.where("note.replyId IS NULL") // 返信ではない - .orWhere("note.replyUserId = :meId", { meId: me.id }) // 返信だけど自分のノートへの返信 - .orWhere( - new Brackets((qb) => { - qb.where( - // 返信だけど自分の行った返信 - "note.replyId IS NOT NULL", - ).andWhere("note.userId = :meId", { meId: me.id }); - }), - ) - .orWhere( - new Brackets((qb) => { - qb.where( - // 返信だけど投稿者自身への返信 - "note.replyId IS NOT NULL", - ).andWhere("note.replyUserId = note.userId"); - }), - ); - }), - ); - } -} diff --git a/packages/backend/src/server/api/common/generate-visibility-query.ts b/packages/backend/src/server/api/common/generate-visibility-query.ts deleted file mode 100644 index c434df0820..0000000000 --- a/packages/backend/src/server/api/common/generate-visibility-query.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import { Followings } from "@/models/index.js"; -import type { SelectQueryBuilder } from "typeorm"; -import { Brackets } from "typeorm"; - -export function generateVisibilityQuery( - q: SelectQueryBuilder, - me?: { id: User["id"] } | null, -) { - // This code must always be synchronized with the checks in Notes.isVisibleForMe. - 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 = :meId"); - - q.andWhere( - new Brackets((qb) => { - qb - // 公開投稿である - .where( - new Brackets((qb) => { - qb.where(`note.visibility = 'public'`).orWhere( - `note.visibility = 'home'`, - ); - }), - ) - // または 自分自身 - .orWhere("note.userId = :meId") - // または 自分宛て - .orWhere(":meId = ANY(note.visibleUserIds)") - .orWhere(":meId = ANY(note.mentions)") - .orWhere( - new Brackets((qb) => { - qb - // または フォロワー宛ての投稿であり、 - .where(`note.visibility = 'followers'`) - .andWhere( - new Brackets((qb) => { - qb - // 自分がフォロワーである - .where(`note.userId IN (${followingQuery.getQuery()})`) - // または 自分の投稿へのリプライ - .orWhere("note.replyUserId = :meId"); - }), - ); - }), - ); - }), - ); - - q.setParameters({ meId: me.id }); - } -} diff --git a/packages/backend/src/server/api/common/generated-muted-renote-query.ts b/packages/backend/src/server/api/common/generated-muted-renote-query.ts deleted file mode 100644 index 3fcd9b28e8..0000000000 --- a/packages/backend/src/server/api/common/generated-muted-renote-query.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Brackets, SelectQueryBuilder } from "typeorm"; -import { User } from "@/models/entities/user.js"; -import { RenoteMutings } from "@/models/index.js"; - -export function generateMutedUserRenotesQueryForNotes( - q: SelectQueryBuilder, - me: { id: User["id"] }, -): void { - const mutingQuery = RenoteMutings.createQueryBuilder("renote_muting") - .select("renote_muting.muteeId") - .where("renote_muting.muterId = :muterId", { muterId: me.id }); - - q.andWhere( - new Brackets((qb) => { - qb.where( - new Brackets((qb) => { - qb.where("note.renoteId IS NOT NULL"); - qb.andWhere("note.text IS NULL"); - qb.andWhere(`note.userId NOT IN (${mutingQuery.getQuery()})`); - }), - ) - .orWhere("note.renoteId IS NULL") - .orWhere("note.text IS NOT NULL"); - }), - ); - - q.setParameters(mutingQuery.getParameters()); -} diff --git a/packages/backend/src/server/api/common/getters.ts b/packages/backend/src/server/api/common/getters.ts deleted file mode 100644 index fd7580775a..0000000000 --- a/packages/backend/src/server/api/common/getters.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import type { User } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -import { Notes, Users } from "@/models/index.js"; -import { generateVisibilityQuery } from "./generate-visibility-query.js"; - -/** - * Get note for API processing, taking into account visibility. - */ -export async function getNote( - noteId: Note["id"], - me: { id: User["id"] } | null, -) { - const query = Notes.createQueryBuilder("note").where("note.id = :id", { - id: noteId, - }); - - generateVisibilityQuery(query, me); - - const note = await query.getOne(); - - 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.findOneBy({ id: userId }); - - if (user == null) { - throw new IdentifiableError( - "15348ddd-432d-49c2-8a5a-8069753becff", - "No such user.", - ); - } - - return user; -} - -/** - * Get remote user for API processing - */ -export async function getRemoteUser(userId: User["id"]) { - const user = await getUser(userId); - - if (!Users.isRemoteUser(user)) { - throw new Error("user is not a remote user"); - } - - return user; -} - -/** - * Get local user for API processing - */ -export async function getLocalUser(userId: User["id"]) { - const user = await getUser(userId); - - if (!Users.isLocalUser(user)) { - throw new Error("user is not a local user"); - } - - return user; -} diff --git a/packages/backend/src/server/api/common/inject-featured.ts b/packages/backend/src/server/api/common/inject-featured.ts deleted file mode 100644 index 30ba3eca93..0000000000 --- a/packages/backend/src/server/api/common/inject-featured.ts +++ /dev/null @@ -1,53 +0,0 @@ -import rndstr from "rndstr"; -import type { Note } from "@/models/entities/note.js"; -import type { User } from "@/models/entities/user.js"; -import { Notes, UserProfiles, NoteReactions } from "@/models/index.js"; -import { generateMutedUserQuery } from "./generate-muted-user-query.js"; -import { generateBlockedUserQuery } from "./generate-block-query.js"; - -// TODO: リアクション、Renote、返信などをしたノートは除外する - -export async function injectFeatured(timeline: Note[], user?: User | null) { - if (timeline.length < 5) return; - - if (user) { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - if (!profile.injectFeaturedNote) return; - } - - const max = 30; - const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで - - const query = Notes.createQueryBuilder("note") - .addSelect("note.score") - .where("note.userHost IS NULL") - .andWhere("note.score > 0") - .andWhere("note.createdAt > :date", { date: new Date(Date.now() - day) }) - .andWhere(`note.visibility = 'public'`) - .innerJoinAndSelect("note.user", "user"); - - if (user) { - query.andWhere("note.userId != :userId", { userId: user.id }); - - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - - const reactionQuery = NoteReactions.createQueryBuilder("reaction") - .select("reaction.noteId") - .where("reaction.userId = :userId", { userId: user.id }); - - query.andWhere(`note.id NOT IN (${reactionQuery.getQuery()})`); - } - - const notes = await query.orderBy("note.score", "DESC").take(max).getMany(); - - if (notes.length === 0) return; - - // Pick random one - const featured = notes[Math.floor(Math.random() * notes.length)]; - - (featured as any)._featuredId_ = rndstr("a-z0-9", 8); - - // Inject featured - timeline.splice(3, 0, featured); -} diff --git a/packages/backend/src/server/api/common/inject-promo.ts b/packages/backend/src/server/api/common/inject-promo.ts deleted file mode 100644 index dcc4e5f3fa..0000000000 --- a/packages/backend/src/server/api/common/inject-promo.ts +++ /dev/null @@ -1,36 +0,0 @@ -import rndstr from "rndstr"; -import type { Note } from "@/models/entities/note.js"; -import type { User } from "@/models/entities/user.js"; -import { PromoReads, PromoNotes, Notes, Users } from "@/models/index.js"; - -export async function injectPromo(timeline: Note[], user?: User | null) { - if (timeline.length < 5) return; - - // TODO: readやexpireフィルタはクエリ側でやる - - const reads = user - ? await PromoReads.findBy({ - 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.findOneByOrFail({ id: promo.noteId }); - - // Join - note.user = await Users.findOneByOrFail({ id: note.userId }); - - (note as any)._prId_ = rndstr("a-z0-9", 8); - - // Inject promo - timeline.splice(3, 0, note); -} diff --git a/packages/backend/src/server/api/common/is-native-token.ts b/packages/backend/src/server/api/common/is-native-token.ts deleted file mode 100644 index 2833c570c8..0000000000 --- a/packages/backend/src/server/api/common/is-native-token.ts +++ /dev/null @@ -1 +0,0 @@ -export default (token: string) => token.length === 16; diff --git a/packages/backend/src/server/api/common/make-pagination-query.ts b/packages/backend/src/server/api/common/make-pagination-query.ts deleted file mode 100644 index a2c3275693..0000000000 --- a/packages/backend/src/server/api/common/make-pagination-query.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { 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/packages/backend/src/server/api/common/read-messaging-message.ts b/packages/backend/src/server/api/common/read-messaging-message.ts deleted file mode 100644 index fc22c843af..0000000000 --- a/packages/backend/src/server/api/common/read-messaging-message.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { - publishMainStream, - publishGroupMessagingStream, -} from "@/services/stream.js"; -import { publishMessagingStream } from "@/services/stream.js"; -import { publishMessagingIndexStream } from "@/services/stream.js"; -import { pushNotification } from "@/services/push-notification.js"; -import type { User, IRemoteUser } from "@/models/entities/user.js"; -import type { MessagingMessage } from "@/models/entities/messaging-message.js"; -import { MessagingMessages, UserGroupJoinings, Users } from "@/models/index.js"; -import { In } from "typeorm"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import type { UserGroup } from "@/models/entities/user-group.js"; -import { toArray } from "@/prelude/array.js"; -import { renderReadActivity } from "@/remote/activitypub/renderer/read.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { deliver } from "@/queue/index.js"; -import orderedCollection from "@/remote/activitypub/renderer/ordered-collection.js"; - -/** - * 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.findBy({ - 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"); - pushNotification(userId, "readAllMessagingMessages", undefined); - } else { - // そのユーザーとのメッセージで未読がなければイベント発行 - const count = await MessagingMessages.count({ - where: { - userId: otherpartyId, - recipientId: userId, - isRead: false, - }, - take: 1, - }); - - if (!count) { - pushNotification(userId, "readAllMessagingMessagesOfARoom", { - userId: otherpartyId, - }); - } - } -} - -/** - * 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.findOneBy({ - userId: userId, - userGroupId: groupId, - }); - - if (joining == null) { - throw new IdentifiableError( - "930a270c-714a-46b2-b776-ad27276dc569", - "Access denied (group).", - ); - } - - const messages = await MessagingMessages.findBy({ - 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"); - pushNotification(userId, "readAllMessagingMessages", undefined); - } else { - // そのグループにおいて未読がなければイベント発行 - const unreadExist = await MessagingMessages.createQueryBuilder("message") - .where("message.groupId = :groupId", { groupId: groupId }) - .andWhere("message.userId != :userId", { userId: userId }) - .andWhere("NOT (:userId = ANY(message.reads))", { userId: userId }) - .andWhere("message.createdAt > :joinedAt", { - joinedAt: joining.createdAt, - }) // 自分が加入する前の会話については、未読扱いしない - .getOne() - .then((x) => x != null); - - if (!unreadExist) { - pushNotification(userId, "readAllMessagingMessagesOfARoom", { groupId }); - } - } -} - -export async function deliverReadActivity( - user: { id: User["id"]; host: null }, - recipient: IRemoteUser, - messages: MessagingMessage | MessagingMessage[], -) { - messages = toArray(messages).filter((x) => x.uri); - const contents = messages.map((x) => renderReadActivity(user, x)); - - if (contents.length > 1) { - const collection = orderedCollection( - null, - contents.length, - undefined, - undefined, - contents, - ); - deliver(user, renderActivity(collection), recipient.inbox); - } else { - for (const content of contents) { - deliver(user, renderActivity(content), recipient.inbox); - } - } -} diff --git a/packages/backend/src/server/api/common/read-notification.ts b/packages/backend/src/server/api/common/read-notification.ts deleted file mode 100644 index 1fb1d642fe..0000000000 --- a/packages/backend/src/server/api/common/read-notification.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { In } from "typeorm"; -import { publishMainStream } from "@/services/stream.js"; -import { pushNotification } from "@/services/push-notification.js"; -import type { User } from "@/models/entities/user.js"; -import type { Notification } from "@/models/entities/notification.js"; -import { Notifications, Users } from "@/models/index.js"; - -export async function readNotification( - userId: User["id"], - notificationIds: Notification["id"][], -) { - if (notificationIds.length === 0) return; - - // Update documents - const result = await Notifications.update( - { - notifieeId: userId, - id: In(notificationIds), - isRead: false, - }, - { - isRead: true, - }, - ); - - if (result.affected === 0) return; - - if (!(await Users.getHasUnreadNotification(userId))) - return postReadAllNotifications(userId); - else return postReadNotifications(userId, notificationIds); -} - -export async function readNotificationByQuery( - userId: User["id"], - query: Record, -) { - const notificationIds = await Notifications.findBy({ - ...query, - notifieeId: userId, - isRead: false, - }).then((notifications) => - notifications.map((notification) => notification.id), - ); - - return readNotification(userId, notificationIds); -} - -function postReadAllNotifications(userId: User["id"]) { - publishMainStream(userId, "readAllNotifications"); - return pushNotification(userId, "readAllNotifications", undefined); -} - -function postReadNotifications( - userId: User["id"], - notificationIds: Notification["id"][], -) { - publishMainStream(userId, "readNotifications", notificationIds); - return pushNotification(userId, "readNotifications", { notificationIds }); -} diff --git a/packages/backend/src/server/api/common/signin.ts b/packages/backend/src/server/api/common/signin.ts deleted file mode 100644 index a8a435843f..0000000000 --- a/packages/backend/src/server/api/common/signin.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type Koa from "koa"; - -import config from "@/config/index.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import { Signins } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { publishMainStream } from "@/services/stream.js"; - -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.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - ip: ctx.ip, - headers: ctx.headers, - success: true, - }).then((x) => Signins.findOneByOrFail(x.identifiers[0])); - - // Publish signin event - publishMainStream(user.id, "signin", await Signins.pack(record)); - })(); -} diff --git a/packages/backend/src/server/api/common/signup.ts b/packages/backend/src/server/api/common/signup.ts deleted file mode 100644 index 6beae2c782..0000000000 --- a/packages/backend/src/server/api/common/signup.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { generateKeyPair } from "node:crypto"; -import generateUserToken from "./generate-native-user-token.js"; -import { User } from "@/models/entities/user.js"; -import { Users, UsedUsernames } from "@/models/index.js"; -import { UserProfile } from "@/models/entities/user-profile.js"; -import { IsNull } from "typeorm"; -import { genId } from "@/misc/gen-id.js"; -import { toPunyNullable } from "@/misc/convert-host.js"; -import { UserKeypair } from "@/models/entities/user-keypair.js"; -import { usersChart } from "@/services/chart/index.js"; -import { UsedUsername } from "@/models/entities/used-username.js"; -import { db } from "@/db/postgre.js"; -import config from "@/config/index.js"; -import { hashPassword } from "@/misc/password.js"; - -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; - - const userCount = await Users.countBy({ - host: IsNull(), - }); - - if (config.maxUserSignups != null && userCount > config.maxUserSignups) { - throw new Error("MAX_USERS_REACHED"); - } - - // Validate username - if (!Users.validateLocalUsername(username)) { - throw new Error("INVALID_USERNAME"); - } - - if (password != null && passwordHash == null) { - // Validate password - if (!Users.validatePassword(password)) { - throw new Error("INVALID_PASSWORD"); - } - - // Generate hash of password - hash = await hashPassword(password); - } - - // Generate secret - const secret = generateUserToken(); - - // Check username duplication - if ( - await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: IsNull(), - }) - ) { - throw new Error("DUPLICATED_USERNAME"); - } - - // Check deleted username duplication - if (await UsedUsernames.findOneBy({ 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 db.transaction(async (transactionalEntityManager) => { - const exist = await transactionalEntityManager.findOneBy(User, { - usernameLower: username.toLowerCase(), - host: IsNull(), - }); - - 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.countBy({ - host: IsNull(), - isAdmin: true, - })) === 0, - }), - ); - - await transactionalEntityManager.save( - new UserKeypair({ - publicKey: keyPair[0], - privateKey: keyPair[1], - userId: account.id, - }), - ); - - await transactionalEntityManager.save( - new UserProfile({ - userId: account.id, - autoAcceptFollowed: true, - password: hash, - }), - ); - - await transactionalEntityManager.save( - new UsedUsername({ - createdAt: new Date(), - username: username.toLowerCase(), - }), - ); - }); - - usersChart.update(account, true); - - return { account, secret }; -} diff --git a/packages/backend/src/server/api/compatibility.ts b/packages/backend/src/server/api/compatibility.ts deleted file mode 100644 index 7e44fa8b2e..0000000000 --- a/packages/backend/src/server/api/compatibility.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { IEndpoint } from "./endpoints"; - -import * as cp___instanceInfo from "./endpoints/compatibility/instance-info.js"; -import * as cp___customEmojis from "./endpoints/compatibility/custom-emojis.js"; - -const cps = [ - ["v1/instance", cp___instanceInfo], - ["v1/custom_emojis", cp___customEmojis], -]; - -const compatibility: IEndpoint[] = cps.map(([name, cp]) => { - return { - name: name, - exec: cp.default, - meta: cp.meta || {}, - params: cp.paramDef, - } as IEndpoint; -}); - -export default compatibility; diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts deleted file mode 100644 index ee0844185f..0000000000 --- a/packages/backend/src/server/api/define.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as fs from "node:fs"; -import Ajv from "ajv"; -import type { CacheableLocalUser } from "@/models/entities/user.js"; -import { ILocalUser } from "@/models/entities/user.js"; -import type { Schema, SchemaType } from "@/misc/schema.js"; -import type { AccessToken } from "@/models/entities/access-token.js"; -import type { IEndpointMeta } from "./endpoints.js"; -import { ApiError } from "./error.js"; - -export type Response = Record | void; - -// TODO: paramsの型をT['params']のスキーマ定義から推論する -type executor = ( - params: SchemaType, - user: T["requireCredential"] extends true - ? CacheableLocalUser - : CacheableLocalUser | null, - token: AccessToken | null, - file?: any, - cleanup?: () => any, - ip?: string | null, - headers?: Record | null, -) => Promise< - T["res"] extends undefined ? Response : SchemaType> ->; - -const ajv = new Ajv({ - useDefaults: true, -}); - -ajv.addFormat("misskey:id", /^[a-zA-Z0-9]+$/); - -export default function ( - meta: T, - paramDef: Ps, - cb: executor, -): ( - params: any, - user: T["requireCredential"] extends true - ? CacheableLocalUser - : CacheableLocalUser | null, - token: AccessToken | null, - file?: any, - ip?: string | null, - headers?: Record | null, -) => Promise { - const validate = ajv.compile(paramDef); - - return ( - params: any, - user: T["requireCredential"] extends true - ? CacheableLocalUser - : CacheableLocalUser | null, - token: AccessToken | null, - file?: any, - ip?: string | null, - headers?: Record | null, - ) => { - let cleanup: undefined | (() => void) = undefined; - - if (meta.requireFile) { - cleanup = () => { - fs.unlink(file.path, () => {}); - }; - - if (file == null) - return Promise.reject( - new ApiError({ - message: "File required.", - code: "FILE_REQUIRED", - id: "4267801e-70d1-416a-b011-4ee502885d8b", - }), - ); - } - - const valid = validate(params); - if (!valid) { - if (file) cleanup!(); - - const errors = validate.errors!; - const err = new ApiError( - { - message: "Invalid param.", - code: "INVALID_PARAM", - id: "3d81ceae-475f-4600-b2a8-2bc116157532", - }, - { - param: errors[0].schemaPath, - reason: errors[0].message, - }, - ); - return Promise.reject(err); - } - - return cb( - params as SchemaType, - user, - token, - file, - cleanup, - ip, - headers, - ); - }; -} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts deleted file mode 100644 index 3f82eb7a73..0000000000 --- a/packages/backend/src/server/api/endpoints.ts +++ /dev/null @@ -1,805 +0,0 @@ -import type { Schema } from "@/misc/schema.js"; - -import * as ep___admin_meta from "./endpoints/admin/meta.js"; -import * as ep___admin_abuseUserReports from "./endpoints/admin/abuse-user-reports.js"; -import * as ep___admin_accounts_create from "./endpoints/admin/accounts/create.js"; -import * as ep___admin_accounts_delete from "./endpoints/admin/accounts/delete.js"; -import * as ep___admin_accounts_hosted from "./endpoints/admin/accounts/hosted.js"; -import * as ep___admin_ad_create from "./endpoints/admin/ad/create.js"; -import * as ep___admin_ad_delete from "./endpoints/admin/ad/delete.js"; -import * as ep___admin_ad_list from "./endpoints/admin/ad/list.js"; -import * as ep___admin_ad_update from "./endpoints/admin/ad/update.js"; -import * as ep___admin_announcements_create from "./endpoints/admin/announcements/create.js"; -import * as ep___admin_announcements_delete from "./endpoints/admin/announcements/delete.js"; -import * as ep___admin_announcements_list from "./endpoints/admin/announcements/list.js"; -import * as ep___admin_announcements_update from "./endpoints/admin/announcements/update.js"; -import * as ep___admin_deleteAllFilesOfAUser from "./endpoints/admin/delete-all-files-of-a-user.js"; -import * as ep___admin_drive_cleanRemoteFiles from "./endpoints/admin/drive/clean-remote-files.js"; -import * as ep___admin_drive_cleanup from "./endpoints/admin/drive/cleanup.js"; -import * as ep___admin_drive_files from "./endpoints/admin/drive/files.js"; -import * as ep___admin_drive_showFile from "./endpoints/admin/drive/show-file.js"; -import * as ep___admin_emoji_addAliasesBulk from "./endpoints/admin/emoji/add-aliases-bulk.js"; -import * as ep___admin_emoji_add from "./endpoints/admin/emoji/add.js"; -import * as ep___admin_emoji_copy from "./endpoints/admin/emoji/copy.js"; -import * as ep___admin_emoji_deleteBulk from "./endpoints/admin/emoji/delete-bulk.js"; -import * as ep___admin_emoji_delete from "./endpoints/admin/emoji/delete.js"; -import * as ep___admin_emoji_importZip from "./endpoints/admin/emoji/import-zip.js"; -import * as ep___admin_emoji_listRemote from "./endpoints/admin/emoji/list-remote.js"; -import * as ep___admin_emoji_list from "./endpoints/admin/emoji/list.js"; -import * as ep___admin_emoji_removeAliasesBulk from "./endpoints/admin/emoji/remove-aliases-bulk.js"; -import * as ep___admin_emoji_setAliasesBulk from "./endpoints/admin/emoji/set-aliases-bulk.js"; -import * as ep___admin_emoji_setCategoryBulk from "./endpoints/admin/emoji/set-category-bulk.js"; -import * as ep___admin_emoji_setLicenseBulk from "./endpoints/admin/emoji/set-license-bulk.js"; -import * as ep___admin_emoji_update from "./endpoints/admin/emoji/update.js"; -import * as ep___admin_federation_deleteAllFiles from "./endpoints/admin/federation/delete-all-files.js"; -import * as ep___admin_federation_refreshRemoteInstanceMetadata from "./endpoints/admin/federation/refresh-remote-instance-metadata.js"; -import * as ep___admin_federation_removeAllFollowing from "./endpoints/admin/federation/remove-all-following.js"; -import * as ep___admin_federation_updateInstance from "./endpoints/admin/federation/update-instance.js"; -import * as ep___admin_getIndexStats from "./endpoints/admin/get-index-stats.js"; -import * as ep___admin_getTableStats from "./endpoints/admin/get-table-stats.js"; -import * as ep___admin_getUserIps from "./endpoints/admin/get-user-ips.js"; -import * as ep___admin_invite from "./endpoints/admin/invite.js"; -import * as ep___admin_moderators_add from "./endpoints/admin/moderators/add.js"; -import * as ep___admin_moderators_remove from "./endpoints/admin/moderators/remove.js"; -import * as ep___admin_promo_create from "./endpoints/admin/promo/create.js"; -import * as ep___admin_queue_clear from "./endpoints/admin/queue/clear.js"; -import * as ep___admin_queue_deliverDelayed from "./endpoints/admin/queue/deliver-delayed.js"; -import * as ep___admin_queue_inboxDelayed from "./endpoints/admin/queue/inbox-delayed.js"; -import * as ep___admin_queue_stats from "./endpoints/admin/queue/stats.js"; -import * as ep___admin_relays_add from "./endpoints/admin/relays/add.js"; -import * as ep___admin_relays_list from "./endpoints/admin/relays/list.js"; -import * as ep___admin_relays_remove from "./endpoints/admin/relays/remove.js"; -import * as ep___admin_resetPassword from "./endpoints/admin/reset-password.js"; -import * as ep___admin_resolveAbuseUserReport from "./endpoints/admin/resolve-abuse-user-report.js"; -import * as ep___admin_search_indexAll from "./endpoints/admin/search/index-all.js"; -import * as ep___admin_sendEmail from "./endpoints/admin/send-email.js"; -import * as ep___admin_serverInfo from "./endpoints/admin/server-info.js"; -import * as ep___admin_showModerationLogs from "./endpoints/admin/show-moderation-logs.js"; -import * as ep___admin_showUser from "./endpoints/admin/show-user.js"; -import * as ep___admin_showUsers from "./endpoints/admin/show-users.js"; -import * as ep___admin_silenceUser from "./endpoints/admin/silence-user.js"; -import * as ep___admin_suspendUser from "./endpoints/admin/suspend-user.js"; -import * as ep___admin_unsilenceUser from "./endpoints/admin/unsilence-user.js"; -import * as ep___admin_unsuspendUser from "./endpoints/admin/unsuspend-user.js"; -import * as ep___admin_updateMeta from "./endpoints/admin/update-meta.js"; -import * as ep___admin_vacuum from "./endpoints/admin/vacuum.js"; -import * as ep___admin_deleteAccount from "./endpoints/admin/delete-account.js"; -import * as ep___admin_updateUserNote from "./endpoints/admin/update-user-note.js"; -import * as ep___announcements from "./endpoints/announcements.js"; -import * as ep___antennas_create from "./endpoints/antennas/create.js"; -import * as ep___antennas_delete from "./endpoints/antennas/delete.js"; -import * as ep___antennas_list from "./endpoints/antennas/list.js"; -import * as ep___antennas_markRead from "./endpoints/antennas/markread.js"; -import * as ep___antennas_notes from "./endpoints/antennas/notes.js"; -import * as ep___antennas_show from "./endpoints/antennas/show.js"; -import * as ep___antennas_update from "./endpoints/antennas/update.js"; -import * as ep___ap_get from "./endpoints/ap/get.js"; -import * as ep___ap_show from "./endpoints/ap/show.js"; -import * as ep___app_create from "./endpoints/app/create.js"; -import * as ep___app_show from "./endpoints/app/show.js"; -import * as ep___auth_accept from "./endpoints/auth/accept.js"; -import * as ep___auth_session_generate from "./endpoints/auth/session/generate.js"; -import * as ep___auth_session_show from "./endpoints/auth/session/show.js"; -import * as ep___auth_session_userkey from "./endpoints/auth/session/userkey.js"; -import * as ep___blocking_create from "./endpoints/blocking/create.js"; -import * as ep___blocking_delete from "./endpoints/blocking/delete.js"; -import * as ep___blocking_list from "./endpoints/blocking/list.js"; -import * as ep___channels_create from "./endpoints/channels/create.js"; -import * as ep___channels_featured from "./endpoints/channels/featured.js"; -import * as ep___channels_follow from "./endpoints/channels/follow.js"; -import * as ep___channels_followed from "./endpoints/channels/followed.js"; -import * as ep___channels_owned from "./endpoints/channels/owned.js"; -import * as ep___channels_show from "./endpoints/channels/show.js"; -import * as ep___channels_timeline from "./endpoints/channels/timeline.js"; -import * as ep___channels_unfollow from "./endpoints/channels/unfollow.js"; -import * as ep___channels_update from "./endpoints/channels/update.js"; -import * as ep___charts_activeUsers from "./endpoints/charts/active-users.js"; -import * as ep___charts_apRequest from "./endpoints/charts/ap-request.js"; -import * as ep___charts_drive from "./endpoints/charts/drive.js"; -import * as ep___charts_federation from "./endpoints/charts/federation.js"; -import * as ep___charts_hashtag from "./endpoints/charts/hashtag.js"; -import * as ep___charts_instance from "./endpoints/charts/instance.js"; -import * as ep___charts_notes from "./endpoints/charts/notes.js"; -import * as ep___charts_user_drive from "./endpoints/charts/user/drive.js"; -import * as ep___charts_user_following from "./endpoints/charts/user/following.js"; -import * as ep___charts_user_notes from "./endpoints/charts/user/notes.js"; -import * as ep___charts_user_reactions from "./endpoints/charts/user/reactions.js"; -import * as ep___charts_users from "./endpoints/charts/users.js"; -import * as ep___clips_addNote from "./endpoints/clips/add-note.js"; -import * as ep___clips_removeNote from "./endpoints/clips/remove-note.js"; -import * as ep___clips_create from "./endpoints/clips/create.js"; -import * as ep___clips_delete from "./endpoints/clips/delete.js"; -import * as ep___clips_list from "./endpoints/clips/list.js"; -import * as ep___clips_notes from "./endpoints/clips/notes.js"; -import * as ep___clips_show from "./endpoints/clips/show.js"; -import * as ep___clips_update from "./endpoints/clips/update.js"; -import * as ep___drive from "./endpoints/drive.js"; -import * as ep___drive_files from "./endpoints/drive/files.js"; -import * as ep___drive_files_attachedNotes from "./endpoints/drive/files/attached-notes.js"; -import * as ep___drive_files_checkExistence from "./endpoints/drive/files/check-existence.js"; -import * as ep___drive_files_captionImage from "./endpoints/drive/files/caption-image.js"; -import * as ep___drive_files_create from "./endpoints/drive/files/create.js"; -import * as ep___drive_files_delete from "./endpoints/drive/files/delete.js"; -import * as ep___drive_files_findByHash from "./endpoints/drive/files/find-by-hash.js"; -import * as ep___drive_files_find from "./endpoints/drive/files/find.js"; -import * as ep___drive_files_show from "./endpoints/drive/files/show.js"; -import * as ep___drive_files_update from "./endpoints/drive/files/update.js"; -import * as ep___drive_files_uploadFromUrl from "./endpoints/drive/files/upload-from-url.js"; -import * as ep___drive_folders from "./endpoints/drive/folders.js"; -import * as ep___drive_folders_create from "./endpoints/drive/folders/create.js"; -import * as ep___drive_folders_delete from "./endpoints/drive/folders/delete.js"; -import * as ep___drive_folders_find from "./endpoints/drive/folders/find.js"; -import * as ep___drive_folders_show from "./endpoints/drive/folders/show.js"; -import * as ep___drive_folders_update from "./endpoints/drive/folders/update.js"; -import * as ep___drive_stream from "./endpoints/drive/stream.js"; -import * as ep___emailAddress_available from "./endpoints/email-address/available.js"; -import * as ep___emoji from "./endpoints/emoji.js"; -import * as ep___endpoint from "./endpoints/endpoint.js"; -import * as ep___endpoints from "./endpoints/endpoints.js"; -import * as ep___exportCustomEmojis from "./endpoints/export-custom-emojis.js"; -import * as ep___federation_followers from "./endpoints/federation/followers.js"; -import * as ep___federation_following from "./endpoints/federation/following.js"; -import * as ep___federation_instances from "./endpoints/federation/instances.js"; -import * as ep___federation_showInstance from "./endpoints/federation/show-instance.js"; -import * as ep___federation_updateRemoteUser from "./endpoints/federation/update-remote-user.js"; -import * as ep___federation_users from "./endpoints/federation/users.js"; -import * as ep___federation_stats from "./endpoints/federation/stats.js"; -import * as ep___following_create from "./endpoints/following/create.js"; -import * as ep___following_delete from "./endpoints/following/delete.js"; -import * as ep___following_invalidate from "./endpoints/following/invalidate.js"; -import * as ep___following_requests_accept from "./endpoints/following/requests/accept.js"; -import * as ep___following_requests_cancel from "./endpoints/following/requests/cancel.js"; -import * as ep___following_requests_list from "./endpoints/following/requests/list.js"; -import * as ep___following_requests_reject from "./endpoints/following/requests/reject.js"; -import * as ep___gallery_featured from "./endpoints/gallery/featured.js"; -import * as ep___gallery_popular from "./endpoints/gallery/popular.js"; -import * as ep___gallery_posts from "./endpoints/gallery/posts.js"; -import * as ep___gallery_posts_create from "./endpoints/gallery/posts/create.js"; -import * as ep___gallery_posts_delete from "./endpoints/gallery/posts/delete.js"; -import * as ep___gallery_posts_like from "./endpoints/gallery/posts/like.js"; -import * as ep___gallery_posts_show from "./endpoints/gallery/posts/show.js"; -import * as ep___gallery_posts_unlike from "./endpoints/gallery/posts/unlike.js"; -import * as ep___gallery_posts_update from "./endpoints/gallery/posts/update.js"; -import * as ep___getOnlineUsersCount from "./endpoints/get-online-users-count.js"; -import * as ep___hashtags_list from "./endpoints/hashtags/list.js"; -import * as ep___hashtags_search from "./endpoints/hashtags/search.js"; -import * as ep___hashtags_show from "./endpoints/hashtags/show.js"; -import * as ep___hashtags_trend from "./endpoints/hashtags/trend.js"; -import * as ep___hashtags_users from "./endpoints/hashtags/users.js"; -import * as ep___i from "./endpoints/i.js"; -import * as ep___i_2fa_done from "./endpoints/i/2fa/done.js"; -import * as ep___i_2fa_keyDone from "./endpoints/i/2fa/key-done.js"; -import * as ep___i_2fa_passwordLess from "./endpoints/i/2fa/password-less.js"; -import * as ep___i_2fa_registerKey from "./endpoints/i/2fa/register-key.js"; -import * as ep___i_2fa_register from "./endpoints/i/2fa/register.js"; -import * as ep___i_2fa_removeKey from "./endpoints/i/2fa/remove-key.js"; -import * as ep___i_2fa_unregister from "./endpoints/i/2fa/unregister.js"; -import * as ep___i_apps from "./endpoints/i/apps.js"; -import * as ep___i_authorizedApps from "./endpoints/i/authorized-apps.js"; -import * as ep___i_changePassword from "./endpoints/i/change-password.js"; -import * as ep___i_deleteAccount from "./endpoints/i/delete-account.js"; -import * as ep___i_exportBlocking from "./endpoints/i/export-blocking.js"; -import * as ep___i_exportFollowing from "./endpoints/i/export-following.js"; -import * as ep___i_exportMute from "./endpoints/i/export-mute.js"; -import * as ep___i_exportNotes from "./endpoints/i/export-notes.js"; -import * as ep___i_importPosts from "./endpoints/i/import-posts.js"; -import * as ep___i_exportUserLists from "./endpoints/i/export-user-lists.js"; -import * as ep___i_favorites from "./endpoints/i/favorites.js"; -import * as ep___i_gallery_likes from "./endpoints/i/gallery/likes.js"; -import * as ep___i_gallery_posts from "./endpoints/i/gallery/posts.js"; -import * as ep___i_getWordMutedNotesCount from "./endpoints/i/get-word-muted-notes-count.js"; -import * as ep___i_importBlocking from "./endpoints/i/import-blocking.js"; -import * as ep___i_importFollowing from "./endpoints/i/import-following.js"; -import * as ep___i_importMuting from "./endpoints/i/import-muting.js"; -import * as ep___i_importUserLists from "./endpoints/i/import-user-lists.js"; -import * as ep___i_notifications from "./endpoints/i/notifications.js"; -import * as ep___i_pageLikes from "./endpoints/i/page-likes.js"; -import * as ep___i_pages from "./endpoints/i/pages.js"; -import * as ep___i_pin from "./endpoints/i/pin.js"; -import * as ep___i_readAllMessagingMessages from "./endpoints/i/read-all-messaging-messages.js"; -import * as ep___i_readAllUnreadNotes from "./endpoints/i/read-all-unread-notes.js"; -import * as ep___i_readAnnouncement from "./endpoints/i/read-announcement.js"; -import * as ep___i_regenerateToken from "./endpoints/i/regenerate-token.js"; -import * as ep___i_registry_getAll from "./endpoints/i/registry/get-all.js"; -import * as ep___i_registry_getDetail from "./endpoints/i/registry/get-detail.js"; -import * as ep___i_registry_getUnsecure from "./endpoints/i/registry/get-unsecure.js"; -import * as ep___i_registry_get from "./endpoints/i/registry/get.js"; -import * as ep___i_registry_keysWithType from "./endpoints/i/registry/keys-with-type.js"; -import * as ep___i_registry_keys from "./endpoints/i/registry/keys.js"; -import * as ep___i_registry_remove from "./endpoints/i/registry/remove.js"; -import * as ep___i_registry_scopes from "./endpoints/i/registry/scopes.js"; -import * as ep___i_registry_set from "./endpoints/i/registry/set.js"; -import * as ep___i_revokeToken from "./endpoints/i/revoke-token.js"; -import * as ep___i_signinHistory from "./endpoints/i/signin-history.js"; -import * as ep___i_unpin from "./endpoints/i/unpin.js"; -import * as ep___i_updateEmail from "./endpoints/i/update-email.js"; -import * as ep___i_update from "./endpoints/i/update.js"; -import * as ep___i_userGroupInvites from "./endpoints/i/user-group-invites.js"; -import * as ep___i_webhooks_create from "./endpoints/i/webhooks/create.js"; -import * as ep___i_webhooks_show from "./endpoints/i/webhooks/show.js"; -import * as ep___i_webhooks_list from "./endpoints/i/webhooks/list.js"; -import * as ep___i_webhooks_update from "./endpoints/i/webhooks/update.js"; -import * as ep___i_webhooks_delete from "./endpoints/i/webhooks/delete.js"; -import * as ep___messaging_history from "./endpoints/messaging/history.js"; -import * as ep___messaging_messages from "./endpoints/messaging/messages.js"; -import * as ep___messaging_messages_create from "./endpoints/messaging/messages/create.js"; -import * as ep___messaging_messages_delete from "./endpoints/messaging/messages/delete.js"; -import * as ep___messaging_messages_read from "./endpoints/messaging/messages/read.js"; -import * as ep___meta from "./endpoints/meta.js"; -import * as ep___sounds from "./endpoints/get-sounds.js"; -import * as ep___miauth_genToken from "./endpoints/miauth/gen-token.js"; -import * as ep___mute_create from "./endpoints/mute/create.js"; -import * as ep___mute_delete from "./endpoints/mute/delete.js"; -import * as ep___mute_list from "./endpoints/mute/list.js"; -import * as ep___renote_mute_create from "./endpoints/renote-mute/create.js"; -import * as ep___renote_mute_delete from "./endpoints/renote-mute/delete.js"; -import * as ep___renote_mute_list from "./endpoints/renote-mute/list.js"; -import * as ep___my_apps from "./endpoints/my/apps.js"; -import * as ep___notes from "./endpoints/notes.js"; -import * as ep___notes_children from "./endpoints/notes/children.js"; -import * as ep___notes_clips from "./endpoints/notes/clips.js"; -import * as ep___notes_conversation from "./endpoints/notes/conversation.js"; -import * as ep___notes_create from "./endpoints/notes/create.js"; -import * as ep___notes_delete from "./endpoints/notes/delete.js"; -import * as ep___notes_favorites_create from "./endpoints/notes/favorites/create.js"; -import * as ep___notes_favorites_delete from "./endpoints/notes/favorites/delete.js"; -import * as ep___notes_featured from "./endpoints/notes/featured.js"; -import * as ep___notes_globalTimeline from "./endpoints/notes/global-timeline.js"; -import * as ep___notes_hybridTimeline from "./endpoints/notes/hybrid-timeline.js"; -import * as ep___notes_localTimeline from "./endpoints/notes/local-timeline.js"; -import * as ep___notes_recommendedTimeline from "./endpoints/notes/recommended-timeline.js"; -import * as ep___notes_mentions from "./endpoints/notes/mentions.js"; -import * as ep___notes_polls_recommendation from "./endpoints/notes/polls/recommendation.js"; -import * as ep___notes_polls_vote from "./endpoints/notes/polls/vote.js"; -import * as ep___notes_reactions from "./endpoints/notes/reactions.js"; -import * as ep___notes_reactions_create from "./endpoints/notes/reactions/create.js"; -import * as ep___notes_reactions_delete from "./endpoints/notes/reactions/delete.js"; -import * as ep___notes_renotes from "./endpoints/notes/renotes.js"; -import * as ep___notes_replies from "./endpoints/notes/replies.js"; -import * as ep___notes_searchByTag from "./endpoints/notes/search-by-tag.js"; -import * as ep___notes_search from "./endpoints/notes/search.js"; -import * as ep___notes_show from "./endpoints/notes/show.js"; -import * as ep___notes_state from "./endpoints/notes/state.js"; -import * as ep___notes_threadMuting_create from "./endpoints/notes/thread-muting/create.js"; -import * as ep___notes_threadMuting_delete from "./endpoints/notes/thread-muting/delete.js"; -import * as ep___notes_timeline from "./endpoints/notes/timeline.js"; -import * as ep___notes_translate from "./endpoints/notes/translate.js"; -import * as ep___notes_unrenote from "./endpoints/notes/unrenote.js"; -import * as ep___notes_userListTimeline from "./endpoints/notes/user-list-timeline.js"; -import * as ep___notes_watching_create from "./endpoints/notes/watching/create.js"; -import * as ep___notes_watching_delete from "./endpoints/notes/watching/delete.js"; -import * as ep___notifications_create from "./endpoints/notifications/create.js"; -import * as ep___notifications_markAllAsRead from "./endpoints/notifications/mark-all-as-read.js"; -import * as ep___notifications_read from "./endpoints/notifications/read.js"; -import * as ep___pagePush from "./endpoints/page-push.js"; -import * as ep___pages_create from "./endpoints/pages/create.js"; -import * as ep___pages_delete from "./endpoints/pages/delete.js"; -import * as ep___pages_featured from "./endpoints/pages/featured.js"; -import * as ep___pages_like from "./endpoints/pages/like.js"; -import * as ep___pages_show from "./endpoints/pages/show.js"; -import * as ep___pages_unlike from "./endpoints/pages/unlike.js"; -import * as ep___pages_update from "./endpoints/pages/update.js"; -import * as ep___ping from "./endpoints/ping.js"; -import * as ep___recommendedInstances from "./endpoints/recommended-instances.js"; -import * as ep___pinnedUsers from "./endpoints/pinned-users.js"; -import * as ep___customMOTD from "./endpoints/custom-motd.js"; -import * as ep___customSplashIcons from "./endpoints/custom-splash-icons.js"; -import * as ep___latestVersion from "./endpoints/latest-version.js"; -import * as ep___patrons from "./endpoints/patrons.js"; -import * as ep___release from "./endpoints/release.js"; -import * as ep___promo_read from "./endpoints/promo/read.js"; -import * as ep___requestResetPassword from "./endpoints/request-reset-password.js"; -import * as ep___resetDb from "./endpoints/reset-db.js"; -import * as ep___resetPassword from "./endpoints/reset-password.js"; -import * as ep___serverInfo from "./endpoints/server-info.js"; -import * as ep___stats from "./endpoints/stats.js"; -import * as ep___sw_show_registration from "./endpoints/sw/show-registration.js"; -import * as ep___sw_update_registration from "./endpoints/sw/update-registration.js"; -import * as ep___sw_register from "./endpoints/sw/register.js"; -import * as ep___sw_unregister from "./endpoints/sw/unregister.js"; -import * as ep___test from "./endpoints/test.js"; -import * as ep___username_available from "./endpoints/username/available.js"; -import * as ep___users from "./endpoints/users.js"; -import * as ep___users_clips from "./endpoints/users/clips.js"; -import * as ep___users_followers from "./endpoints/users/followers.js"; -import * as ep___users_following from "./endpoints/users/following.js"; -import * as ep___users_gallery_posts from "./endpoints/users/gallery/posts.js"; -import * as ep___users_getFrequentlyRepliedUsers from "./endpoints/users/get-frequently-replied-users.js"; -import * as ep___users_groups_create from "./endpoints/users/groups/create.js"; -import * as ep___users_groups_delete from "./endpoints/users/groups/delete.js"; -import * as ep___users_groups_invitations_accept from "./endpoints/users/groups/invitations/accept.js"; -import * as ep___users_groups_invitations_reject from "./endpoints/users/groups/invitations/reject.js"; -import * as ep___users_groups_invite from "./endpoints/users/groups/invite.js"; -import * as ep___users_groups_joined from "./endpoints/users/groups/joined.js"; -import * as ep___users_groups_leave from "./endpoints/users/groups/leave.js"; -import * as ep___users_groups_owned from "./endpoints/users/groups/owned.js"; -import * as ep___users_groups_pull from "./endpoints/users/groups/pull.js"; -import * as ep___users_groups_show from "./endpoints/users/groups/show.js"; -import * as ep___users_groups_transfer from "./endpoints/users/groups/transfer.js"; -import * as ep___users_groups_update from "./endpoints/users/groups/update.js"; -import * as ep___users_lists_create from "./endpoints/users/lists/create.js"; -import * as ep___users_lists_delete from "./endpoints/users/lists/delete.js"; -import * as ep___users_lists_delete_all from "./endpoints/users/lists/delete-all.js"; -import * as ep___users_lists_list from "./endpoints/users/lists/list.js"; -import * as ep___users_lists_pull from "./endpoints/users/lists/pull.js"; -import * as ep___users_lists_push from "./endpoints/users/lists/push.js"; -import * as ep___users_lists_show from "./endpoints/users/lists/show.js"; -import * as ep___users_lists_update from "./endpoints/users/lists/update.js"; -import * as ep___users_notes from "./endpoints/users/notes.js"; -import * as ep___users_pages from "./endpoints/users/pages.js"; -import * as ep___users_reactions from "./endpoints/users/reactions.js"; -import * as ep___users_recommendation from "./endpoints/users/recommendation.js"; -import * as ep___users_relation from "./endpoints/users/relation.js"; -import * as ep___users_reportAbuse from "./endpoints/users/report-abuse.js"; -import * as ep___users_searchByUsernameAndHost from "./endpoints/users/search-by-username-and-host.js"; -import * as ep___users_search from "./endpoints/users/search.js"; -import * as ep___users_show from "./endpoints/users/show.js"; -import * as ep___users_stats from "./endpoints/users/stats.js"; -import * as ep___fetchRss from "./endpoints/fetch-rss.js"; -import * as ep___admin_driveCapOverride from "./endpoints/admin/drive-capacity-override.js"; - -//Calckey Move -import * as ep___i_move from "./endpoints/i/move.js"; -import * as ep___i_known_as from "./endpoints/i/known-as.js"; - -const eps = [ - ["admin/meta", ep___admin_meta], - ["admin/abuse-user-reports", ep___admin_abuseUserReports], - ["admin/accounts/create", ep___admin_accounts_create], - ["admin/accounts/delete", ep___admin_accounts_delete], - ["admin/accounts/hosted", ep___admin_accounts_hosted], - ["admin/ad/create", ep___admin_ad_create], - ["admin/ad/delete", ep___admin_ad_delete], - ["admin/ad/list", ep___admin_ad_list], - ["admin/ad/update", ep___admin_ad_update], - ["admin/announcements/create", ep___admin_announcements_create], - ["admin/announcements/delete", ep___admin_announcements_delete], - ["admin/announcements/list", ep___admin_announcements_list], - ["admin/announcements/update", ep___admin_announcements_update], - ["admin/delete-all-files-of-a-user", ep___admin_deleteAllFilesOfAUser], - ["admin/drive/clean-remote-files", ep___admin_drive_cleanRemoteFiles], - ["admin/drive/cleanup", ep___admin_drive_cleanup], - ["admin/drive/files", ep___admin_drive_files], - ["admin/drive/show-file", ep___admin_drive_showFile], - ["admin/emoji/add-aliases-bulk", ep___admin_emoji_addAliasesBulk], - ["admin/emoji/add", ep___admin_emoji_add], - ["admin/emoji/copy", ep___admin_emoji_copy], - ["admin/emoji/delete-bulk", ep___admin_emoji_deleteBulk], - ["admin/emoji/delete", ep___admin_emoji_delete], - ["admin/emoji/import-zip", ep___admin_emoji_importZip], - ["admin/emoji/list-remote", ep___admin_emoji_listRemote], - ["admin/emoji/list", ep___admin_emoji_list], - ["admin/emoji/remove-aliases-bulk", ep___admin_emoji_removeAliasesBulk], - ["admin/emoji/set-aliases-bulk", ep___admin_emoji_setAliasesBulk], - ["admin/emoji/set-category-bulk", ep___admin_emoji_setCategoryBulk], - ["admin/emoji/set-license-bulk", ep___admin_emoji_setLicenseBulk], - ["admin/emoji/update", ep___admin_emoji_update], - ["admin/federation/delete-all-files", ep___admin_federation_deleteAllFiles], - [ - "admin/federation/refresh-remote-instance-metadata", - ep___admin_federation_refreshRemoteInstanceMetadata, - ], - [ - "admin/federation/remove-all-following", - ep___admin_federation_removeAllFollowing, - ], - ["admin/federation/update-instance", ep___admin_federation_updateInstance], - ["admin/get-index-stats", ep___admin_getIndexStats], - ["admin/get-table-stats", ep___admin_getTableStats], - ["admin/get-user-ips", ep___admin_getUserIps], - ["admin/invite", ep___admin_invite], - ["admin/moderators/add", ep___admin_moderators_add], - ["admin/moderators/remove", ep___admin_moderators_remove], - ["admin/promo/create", ep___admin_promo_create], - ["admin/queue/clear", ep___admin_queue_clear], - ["admin/queue/deliver-delayed", ep___admin_queue_deliverDelayed], - ["admin/queue/inbox-delayed", ep___admin_queue_inboxDelayed], - ["admin/queue/stats", ep___admin_queue_stats], - ["admin/relays/add", ep___admin_relays_add], - ["admin/relays/list", ep___admin_relays_list], - ["admin/relays/remove", ep___admin_relays_remove], - ["admin/reset-password", ep___admin_resetPassword], - ["admin/resolve-abuse-user-report", ep___admin_resolveAbuseUserReport], - ["admin/search/index-all", ep___admin_search_indexAll], - ["admin/send-email", ep___admin_sendEmail], - ["admin/server-info", ep___admin_serverInfo], - ["admin/show-moderation-logs", ep___admin_showModerationLogs], - ["admin/show-user", ep___admin_showUser], - ["admin/show-users", ep___admin_showUsers], - ["admin/silence-user", ep___admin_silenceUser], - ["admin/suspend-user", ep___admin_suspendUser], - ["admin/unsilence-user", ep___admin_unsilenceUser], - ["admin/unsuspend-user", ep___admin_unsuspendUser], - ["admin/update-meta", ep___admin_updateMeta], - ["admin/vacuum", ep___admin_vacuum], - ["admin/delete-account", ep___admin_deleteAccount], - ["admin/update-user-note", ep___admin_updateUserNote], - ["announcements", ep___announcements], - ["antennas/create", ep___antennas_create], - ["antennas/delete", ep___antennas_delete], - ["antennas/list", ep___antennas_list], - ["antennas/mark-read", ep___antennas_markRead], - ["antennas/notes", ep___antennas_notes], - ["antennas/show", ep___antennas_show], - ["antennas/update", ep___antennas_update], - ["ap/get", ep___ap_get], - ["ap/show", ep___ap_show], - ["app/create", ep___app_create], - ["app/show", ep___app_show], - ["auth/accept", ep___auth_accept], - ["auth/session/generate", ep___auth_session_generate], - ["auth/session/show", ep___auth_session_show], - ["auth/session/userkey", ep___auth_session_userkey], - ["blocking/create", ep___blocking_create], - ["blocking/delete", ep___blocking_delete], - ["blocking/list", ep___blocking_list], - ["channels/create", ep___channels_create], - ["channels/featured", ep___channels_featured], - ["channels/follow", ep___channels_follow], - ["channels/followed", ep___channels_followed], - ["channels/owned", ep___channels_owned], - ["channels/show", ep___channels_show], - ["channels/timeline", ep___channels_timeline], - ["channels/unfollow", ep___channels_unfollow], - ["channels/update", ep___channels_update], - ["charts/active-users", ep___charts_activeUsers], - ["charts/ap-request", ep___charts_apRequest], - ["charts/drive", ep___charts_drive], - ["charts/federation", ep___charts_federation], - ["charts/hashtag", ep___charts_hashtag], - ["charts/instance", ep___charts_instance], - ["charts/notes", ep___charts_notes], - ["charts/user/drive", ep___charts_user_drive], - ["charts/user/following", ep___charts_user_following], - ["charts/user/notes", ep___charts_user_notes], - ["charts/user/reactions", ep___charts_user_reactions], - ["charts/users", ep___charts_users], - ["clips/add-note", ep___clips_addNote], - ["clips/remove-note", ep___clips_removeNote], - ["clips/create", ep___clips_create], - ["clips/delete", ep___clips_delete], - ["clips/list", ep___clips_list], - ["clips/notes", ep___clips_notes], - ["clips/show", ep___clips_show], - ["clips/update", ep___clips_update], - ["drive", ep___drive], - ["drive/files", ep___drive_files], - ["drive/files/attached-notes", ep___drive_files_attachedNotes], - ["drive/files/caption-image", ep___drive_files_captionImage], - ["drive/files/check-existence", ep___drive_files_checkExistence], - ["drive/files/create", ep___drive_files_create], - ["drive/files/delete", ep___drive_files_delete], - ["drive/files/find-by-hash", ep___drive_files_findByHash], - ["drive/files/find", ep___drive_files_find], - ["drive/files/show", ep___drive_files_show], - ["drive/files/update", ep___drive_files_update], - ["drive/files/upload-from-url", ep___drive_files_uploadFromUrl], - ["drive/folders", ep___drive_folders], - ["drive/folders/create", ep___drive_folders_create], - ["drive/folders/delete", ep___drive_folders_delete], - ["drive/folders/find", ep___drive_folders_find], - ["drive/folders/show", ep___drive_folders_show], - ["drive/folders/update", ep___drive_folders_update], - ["drive/stream", ep___drive_stream], - ["email-address/available", ep___emailAddress_available], - ["emoji", ep___emoji], - ["endpoint", ep___endpoint], - ["endpoints", ep___endpoints], - ["export-custom-emojis", ep___exportCustomEmojis], - ["federation/followers", ep___federation_followers], - ["federation/following", ep___federation_following], - ["federation/instances", ep___federation_instances], - ["federation/show-instance", ep___federation_showInstance], - ["federation/update-remote-user", ep___federation_updateRemoteUser], - ["federation/users", ep___federation_users], - ["federation/stats", ep___federation_stats], - ["following/create", ep___following_create], - ["following/delete", ep___following_delete], - ["following/invalidate", ep___following_invalidate], - ["following/requests/accept", ep___following_requests_accept], - ["following/requests/cancel", ep___following_requests_cancel], - ["following/requests/list", ep___following_requests_list], - ["following/requests/reject", ep___following_requests_reject], - ["gallery/featured", ep___gallery_featured], - ["gallery/popular", ep___gallery_popular], - ["gallery/posts", ep___gallery_posts], - ["gallery/posts/create", ep___gallery_posts_create], - ["gallery/posts/delete", ep___gallery_posts_delete], - ["gallery/posts/like", ep___gallery_posts_like], - ["gallery/posts/show", ep___gallery_posts_show], - ["gallery/posts/unlike", ep___gallery_posts_unlike], - ["gallery/posts/update", ep___gallery_posts_update], - ["get-online-users-count", ep___getOnlineUsersCount], - ["hashtags/list", ep___hashtags_list], - ["hashtags/search", ep___hashtags_search], - ["hashtags/show", ep___hashtags_show], - ["hashtags/trend", ep___hashtags_trend], - ["hashtags/users", ep___hashtags_users], - ["i", ep___i], - ["i/known-as", ep___i_known_as], - ["i/move", ep___i_move], - ["i/2fa/done", ep___i_2fa_done], - ["i/2fa/key-done", ep___i_2fa_keyDone], - ["i/2fa/password-less", ep___i_2fa_passwordLess], - ["i/2fa/register-key", ep___i_2fa_registerKey], - ["i/2fa/register", ep___i_2fa_register], - ["i/2fa/remove-key", ep___i_2fa_removeKey], - ["i/2fa/unregister", ep___i_2fa_unregister], - ["i/apps", ep___i_apps], - ["i/authorized-apps", ep___i_authorizedApps], - ["i/change-password", ep___i_changePassword], - ["i/delete-account", ep___i_deleteAccount], - ["i/export-blocking", ep___i_exportBlocking], - ["i/export-following", ep___i_exportFollowing], - ["i/export-mute", ep___i_exportMute], - ["i/export-notes", ep___i_exportNotes], - ["i/import-posts", ep___i_importPosts], - ["i/export-user-lists", ep___i_exportUserLists], - ["i/favorites", ep___i_favorites], - ["i/gallery/likes", ep___i_gallery_likes], - ["i/gallery/posts", ep___i_gallery_posts], - ["i/get-word-muted-notes-count", ep___i_getWordMutedNotesCount], - ["i/import-blocking", ep___i_importBlocking], - ["i/import-following", ep___i_importFollowing], - ["i/import-muting", ep___i_importMuting], - ["i/import-user-lists", ep___i_importUserLists], - ["i/notifications", ep___i_notifications], - ["i/page-likes", ep___i_pageLikes], - ["i/pages", ep___i_pages], - ["i/pin", ep___i_pin], - ["i/read-all-messaging-messages", ep___i_readAllMessagingMessages], - ["i/read-all-unread-notes", ep___i_readAllUnreadNotes], - ["i/read-announcement", ep___i_readAnnouncement], - ["i/regenerate-token", ep___i_regenerateToken], - ["i/registry/get-all", ep___i_registry_getAll], - ["i/registry/get-detail", ep___i_registry_getDetail], - ["i/registry/get-unsecure", ep___i_registry_getUnsecure], - ["i/registry/get", ep___i_registry_get], - ["i/registry/keys-with-type", ep___i_registry_keysWithType], - ["i/registry/keys", ep___i_registry_keys], - ["i/registry/remove", ep___i_registry_remove], - ["i/registry/scopes", ep___i_registry_scopes], - ["i/registry/set", ep___i_registry_set], - ["i/revoke-token", ep___i_revokeToken], - ["i/signin-history", ep___i_signinHistory], - ["i/unpin", ep___i_unpin], - ["i/update-email", ep___i_updateEmail], - ["i/update", ep___i_update], - ["i/user-group-invites", ep___i_userGroupInvites], - ["i/webhooks/create", ep___i_webhooks_create], - ["i/webhooks/list", ep___i_webhooks_list], - ["i/webhooks/show", ep___i_webhooks_show], - ["i/webhooks/update", ep___i_webhooks_update], - ["i/webhooks/delete", ep___i_webhooks_delete], - ["messaging/history", ep___messaging_history], - ["messaging/messages", ep___messaging_messages], - ["messaging/messages/create", ep___messaging_messages_create], - ["messaging/messages/delete", ep___messaging_messages_delete], - ["messaging/messages/read", ep___messaging_messages_read], - ["meta", ep___meta], - ["miauth/gen-token", ep___miauth_genToken], - ["mute/create", ep___mute_create], - ["mute/delete", ep___mute_delete], - ["mute/list", ep___mute_list], - ["my/apps", ep___my_apps], - ["notes", ep___notes], - ["notes/children", ep___notes_children], - ["notes/clips", ep___notes_clips], - ["notes/conversation", ep___notes_conversation], - ["notes/create", ep___notes_create], - ["notes/delete", ep___notes_delete], - ["notes/favorites/create", ep___notes_favorites_create], - ["notes/favorites/delete", ep___notes_favorites_delete], - ["notes/featured", ep___notes_featured], - ["notes/global-timeline", ep___notes_globalTimeline], - ["notes/hybrid-timeline", ep___notes_hybridTimeline], - ["notes/local-timeline", ep___notes_localTimeline], - ["notes/recommended-timeline", ep___notes_recommendedTimeline], - ["notes/mentions", ep___notes_mentions], - ["notes/polls/recommendation", ep___notes_polls_recommendation], - ["notes/polls/vote", ep___notes_polls_vote], - ["notes/reactions", ep___notes_reactions], - ["notes/reactions/create", ep___notes_reactions_create], - ["notes/reactions/delete", ep___notes_reactions_delete], - ["notes/renotes", ep___notes_renotes], - ["notes/replies", ep___notes_replies], - ["notes/search-by-tag", ep___notes_searchByTag], - ["notes/search", ep___notes_search], - ["notes/show", ep___notes_show], - ["notes/state", ep___notes_state], - ["notes/thread-muting/create", ep___notes_threadMuting_create], - ["notes/thread-muting/delete", ep___notes_threadMuting_delete], - ["notes/timeline", ep___notes_timeline], - ["notes/translate", ep___notes_translate], - ["notes/unrenote", ep___notes_unrenote], - ["notes/user-list-timeline", ep___notes_userListTimeline], - ["notes/watching/create", ep___notes_watching_create], - ["notes/watching/delete", ep___notes_watching_delete], - ["notifications/create", ep___notifications_create], - ["notifications/mark-all-as-read", ep___notifications_markAllAsRead], - ["notifications/read", ep___notifications_read], - ["page-push", ep___pagePush], - ["pages/create", ep___pages_create], - ["pages/delete", ep___pages_delete], - ["pages/featured", ep___pages_featured], - ["pages/like", ep___pages_like], - ["pages/show", ep___pages_show], - ["pages/unlike", ep___pages_unlike], - ["pages/update", ep___pages_update], - ["ping", ep___ping], - ["pinned-users", ep___pinnedUsers], - ["recommended-instances", ep___recommendedInstances], - ["renote-mute/create", ep___renote_mute_create], - ["renote-mute/delete", ep___renote_mute_delete], - ["renote-mute/list", ep___renote_mute_list], - ["custom-motd", ep___customMOTD], - ["custom-splash-icons", ep___customSplashIcons], - ["latest-version", ep___latestVersion], - ["patrons", ep___patrons], - ["release", ep___release], - ["promo/read", ep___promo_read], - ["request-reset-password", ep___requestResetPassword], - ["reset-db", ep___resetDb], - ["reset-password", ep___resetPassword], - ["server-info", ep___serverInfo], - ["stats", ep___stats], - ["sw/register", ep___sw_register], - ["sw/unregister", ep___sw_unregister], - ["sw/show-registration", ep___sw_show_registration], - ["sw/update-registration", ep___sw_update_registration], - ["test", ep___test], - ["username/available", ep___username_available], - ["users", ep___users], - ["users/clips", ep___users_clips], - ["users/followers", ep___users_followers], - ["users/following", ep___users_following], - ["users/gallery/posts", ep___users_gallery_posts], - ["users/get-frequently-replied-users", ep___users_getFrequentlyRepliedUsers], - ["users/groups/create", ep___users_groups_create], - ["users/groups/delete", ep___users_groups_delete], - ["users/groups/invitations/accept", ep___users_groups_invitations_accept], - ["users/groups/invitations/reject", ep___users_groups_invitations_reject], - ["users/groups/invite", ep___users_groups_invite], - ["users/groups/joined", ep___users_groups_joined], - ["users/groups/leave", ep___users_groups_leave], - ["users/groups/owned", ep___users_groups_owned], - ["users/groups/pull", ep___users_groups_pull], - ["users/groups/show", ep___users_groups_show], - ["users/groups/transfer", ep___users_groups_transfer], - ["users/groups/update", ep___users_groups_update], - ["users/lists/create", ep___users_lists_create], - ["users/lists/delete", ep___users_lists_delete], - ["users/lists/delete-all", ep___users_lists_delete_all], - ["users/lists/list", ep___users_lists_list], - ["users/lists/pull", ep___users_lists_pull], - ["users/lists/push", ep___users_lists_push], - ["users/lists/show", ep___users_lists_show], - ["users/lists/update", ep___users_lists_update], - ["users/notes", ep___users_notes], - ["users/pages", ep___users_pages], - ["users/reactions", ep___users_reactions], - ["users/recommendation", ep___users_recommendation], - ["users/relation", ep___users_relation], - ["users/report-abuse", ep___users_reportAbuse], - ["users/search-by-username-and-host", ep___users_searchByUsernameAndHost], - ["users/search", ep___users_search], - ["users/show", ep___users_show], - ["users/stats", ep___users_stats], - ["admin/drive-capacity-override", ep___admin_driveCapOverride], - ["fetch-rss", ep___fetchRss], - ["get-sounds", ep___sounds], -]; - -export interface IEndpointMeta { - readonly stability?: "deprecated" | "experimental" | "stable"; - - readonly tags?: ReadonlyArray; - - readonly errors?: { - readonly [key: string]: { - readonly message: string; - readonly code: string; - readonly id: string; - }; - }; - - readonly res?: Schema; - - /** - * このエンドポイントにリクエストするのにユーザー情報が必須か否か - * 省略した場合は false として解釈されます。 - */ - readonly requireCredential?: boolean; - - /** - * 管理者のみ使えるエンドポイントか否か - */ - readonly requireAdmin?: boolean; - - /** - * 管理者またはモデレーターのみ使えるエンドポイントか否か - */ - readonly requireModerator?: boolean; - - /** - * エンドポイントのリミテーションに関するやつ - * 省略した場合はリミテーションは無いものとして解釈されます。 - */ - readonly limit?: { - /** - * 複数のエンドポイントでリミットを共有したい場合に指定するキー - */ - readonly key?: string; - - /** - * リミットを適用する期間(ms) - * このプロパティを設定する場合、max プロパティも設定する必要があります。 - */ - readonly duration?: number; - - /** - * durationで指定した期間内にいくつまでリクエストできるのか - * このプロパティを設定する場合、duration プロパティも設定する必要があります。 - */ - readonly max?: number; - - /** - * 最低でもどれくらいの間隔を開けてリクエストしなければならないか(ms) - */ - readonly minInterval?: number; - }; - - /** - * ファイルの添付を必要とするか否か - * 省略した場合は false として解釈されます。 - */ - readonly requireFile?: boolean; - - /** - * サードパーティアプリからはリクエストすることができないか否か - * 省略した場合は false として解釈されます。 - */ - readonly secure?: boolean; - - /** - * プライベートモードでなら、このエンドポイントにリクエストするときにユーザー情報が必要か否か - * 省略した場合は false として解釈されます - */ - readonly requireCredentialPrivateMode?: boolean; - - /** - * エンドポイントの種類 - * パーミッションの実現に利用されます。 - */ - readonly kind?: string; - - readonly description?: string; - - /** - * GETでのリクエストを許容するか否か - */ - readonly allowGet?: boolean; - - /** - * 正常応答をキャッシュ (Cache-Control: public) する秒数 - */ - readonly cacheSec?: number; -} - -export interface IEndpoint { - name: string; - exec: any; // TODO: may be obosolete @ThatOneCalculator - meta: IEndpointMeta; - params: Schema; -} - -const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { - return { - name: name, - exec: ep.default, - meta: ep.meta ?? {}, - params: ep.paramDef, - }; -}); - -export default endpoints; diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts deleted file mode 100644 index 4861431403..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ /dev/null @@ -1,144 +0,0 @@ -import define from "../../define.js"; -import { AbuseUserReports } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - nullable: false, - optional: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - nullable: false, - optional: false, - format: "date-time", - }, - comment: { - type: "string", - nullable: false, - optional: false, - }, - resolved: { - type: "boolean", - nullable: false, - optional: false, - example: false, - }, - reporterId: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - targetUserId: { - type: "string", - nullable: false, - optional: false, - format: "id", - }, - assigneeId: { - type: "string", - nullable: true, - optional: false, - format: "id", - }, - reporter: { - type: "object", - nullable: false, - optional: false, - ref: "User", - }, - targetUser: { - type: "object", - nullable: false, - optional: false, - ref: "User", - }, - assignee: { - type: "object", - nullable: true, - optional: true, - ref: "User", - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - state: { type: "string", nullable: true, default: null }, - reporterOrigin: { - type: "string", - enum: ["combined", "local", "remote"], - default: "combined", - }, - targetUserOrigin: { - type: "string", - enum: ["combined", "local", "remote"], - default: "combined", - }, - forwarded: { type: "boolean", default: false }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery( - AbuseUserReports.createQueryBuilder("report"), - ps.sinceId, - ps.untilId, - ); - - switch (ps.state) { - case "resolved": - query.andWhere("report.resolved = TRUE"); - break; - case "unresolved": - query.andWhere("report.resolved = FALSE"); - break; - } - - switch (ps.reporterOrigin) { - case "local": - query.andWhere("report.reporterHost IS NULL"); - break; - case "remote": - query.andWhere("report.reporterHost IS NOT NULL"); - break; - } - - switch (ps.targetUserOrigin) { - case "local": - query.andWhere("report.targetUserHost IS NULL"); - break; - case "remote": - query.andWhere("report.targetUserHost IS NOT NULL"); - break; - } - - const reports = await query.take(ps.limit).getMany(); - - return await AbuseUserReports.packMany(reports); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts deleted file mode 100644 index 2e035d1695..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ /dev/null @@ -1,55 +0,0 @@ -import define from "../../../define.js"; -import { Users } from "@/models/index.js"; -import { signup } from "../../../common/signup.js"; -import { IsNull } from "typeorm"; - -export const meta = { - tags: ["admin"], - - res: { - type: "object", - optional: false, - nullable: false, - ref: "User", - properties: { - token: { - type: "string", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - username: Users.localUsernameSchema, - password: Users.passwordSchema, - }, - required: ["username", "password"], -} as const; - -export default define(meta, paramDef, async (ps, _me) => { - const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null; - const noUsers = - (await Users.countBy({ - host: IsNull(), - isAdmin: true, - })) === 0; - if (!(noUsers || me?.isAdmin)) throw new Error("access denied"); - - const { account, secret } = await signup({ - username: ps.username, - password: ps.password, - }); - - const res = await Users.pack(account, account, { - detail: true, - includeSecrets: true, - }); - - (res as any).token = secret; - - return res; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts deleted file mode 100644 index 3f7243ab50..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ /dev/null @@ -1,58 +0,0 @@ -import define from "../../../define.js"; -import { Users } from "@/models/index.js"; -import { doPostSuspend } from "@/services/suspend-user.js"; -import { publishUserEvent } from "@/services/stream.js"; -import { createDeleteAccountJob } from "@/queue/index.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error("user not found"); - } - - if (user.isAdmin) { - throw new Error("cannot suspend admin"); - } - - if (user.isModerator) { - throw new Error("cannot suspend moderator"); - } - - if (Users.isLocalUser(user)) { - // 物理削除する前にDelete activityを送信する - await doPostSuspend(user).catch((e) => {}); - - createDeleteAccountJob(user, { - soft: false, - }); - } else { - createDeleteAccountJob(user, { - soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する - }); - } - - await Users.update(user.id, { - isDeleted: true, - }); - - if (Users.isLocalUser(user)) { - // Terminate streaming - publishUserEvent(user.id, "terminate", {}); - } -}); diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts b/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts deleted file mode 100644 index 15ad1f9a17..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/accounts/hosted.ts +++ /dev/null @@ -1,116 +0,0 @@ -import config from "@/config/index.js"; -import { Meta } from "@/models/entities/meta.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { db } from "@/db/postgre.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireAdmin: true, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const hostedConfig = config.isManagedHosting; - const hosted = hostedConfig != null && hostedConfig === true; - if (hosted) { - const set = {} as Partial; - if (config.deepl.managed != null && config.deepl.managed === true) { - if (typeof config.deepl.authKey === "boolean") { - set.deeplAuthKey = config.deepl.authKey; - } - if (typeof config.deepl.isPro === "boolean") { - set.deeplIsPro = config.deepl.isPro; - } - } - if (config.email.managed != null && config.email.managed === true) { - set.enableEmail = true; - if (typeof config.email.address === "string") { - set.email = config.email.address; - } - if (typeof config.email.host === "string") { - set.smtpHost = config.email.host; - } - if (typeof config.email.port === "number") { - set.smtpPort = config.email.port; - } - if (typeof config.email.user === "string") { - set.smtpUser = config.email.user; - } - if (typeof config.email.pass === "string") { - set.smtpPass = config.email.pass; - } - if (typeof config.email.useImplicitSslTls === "boolean") { - set.smtpSecure = config.email.useImplicitSslTls; - } - } - if ( - config.objectStorage.managed != null && - config.objectStorage.managed === true - ) { - set.useObjectStorage = true; - if (typeof config.objectStorage.baseUrl === "string") { - set.objectStorageBaseUrl = config.objectStorage.baseUrl; - } - if (typeof config.objectStorage.bucket === "string") { - set.objectStorageBucket = config.objectStorage.bucket; - } - if (typeof config.objectStorage.prefix === "string") { - set.objectStoragePrefix = config.objectStorage.prefix; - } - if (typeof config.objectStorage.endpoint === "string") { - set.objectStorageEndpoint = config.objectStorage.endpoint; - } - if (typeof config.objectStorage.region === "string") { - set.objectStorageRegion = config.objectStorage.region; - } - if (typeof config.objectStorage.accessKey === "string") { - set.objectStorageAccessKey = config.objectStorage.accessKey; - } - if (typeof config.objectStorage.secretKey === "string") { - set.objectStorageSecretKey = config.objectStorage.secretKey; - } - if (typeof config.objectStorage.useSsl === "boolean") { - set.objectStorageUseSSL = config.objectStorage.useSsl; - } - if (typeof config.objectStorage.connnectOverProxy === "boolean") { - set.objectStorageUseProxy = config.objectStorage.connnectOverProxy; - } - if (typeof config.objectStorage.setPublicReadOnUpload === "boolean") { - set.objectStorageSetPublicRead = - config.objectStorage.setPublicReadOnUpload; - } - if (typeof config.objectStorage.s3ForcePathStyle === "boolean") { - set.objectStorageS3ForcePathStyle = - config.objectStorage.s3ForcePathStyle; - } - } - if (config.summalyProxyUrl !== undefined) { - set.summalyProxy = config.summalyProxyUrl; - } - await db.transaction(async (transactionalEntityManager) => { - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: "DESC", - }, - }); - - const meta = metas[0]; - - if (meta) { - await transactionalEntityManager.update(Meta, meta.id, set); - } else { - await transactionalEntityManager.save(Meta, set); - } - }); - insertModerationLog(me, "updateMeta"); - } - return hosted; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts deleted file mode 100644 index db39f3eb27..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ /dev/null @@ -1,46 +0,0 @@ -import define from "../../../define.js"; -import { Ads } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - url: { type: "string", minLength: 1 }, - memo: { type: "string" }, - place: { type: "string" }, - priority: { type: "string" }, - ratio: { type: "integer" }, - expiresAt: { type: "integer" }, - imageUrl: { type: "string", minLength: 1 }, - }, - required: [ - "url", - "memo", - "place", - "priority", - "ratio", - "expiresAt", - "imageUrl", - ], -} as const; - -export default define(meta, paramDef, async (ps) => { - await Ads.insert({ - id: genId(), - createdAt: new Date(), - expiresAt: new Date(ps.expiresAt), - url: ps.url, - imageUrl: ps.imageUrl, - priority: ps.priority, - ratio: ps.ratio, - place: ps.place, - memo: ps.memo, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts deleted file mode 100644 index ee6d314de7..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import define from "../../../define.js"; -import { Ads } from "@/models/index.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - noSuchAd: { - message: "No such ad.", - code: "NO_SUCH_AD", - id: "ccac9863-3a03-416e-b899-8a64041118b1", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - id: { type: "string", format: "misskey:id" }, - }, - required: ["id"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const ad = await Ads.findOneBy({ id: ps.id }); - - if (ad == null) throw new ApiError(meta.errors.noSuchAd); - - await Ads.delete(ad.id); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts deleted file mode 100644 index 65944d31e9..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ /dev/null @@ -1,32 +0,0 @@ -import define from "../../../define.js"; -import { Ads } from "@/models/index.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery( - Ads.createQueryBuilder("ad"), - ps.sinceId, - ps.untilId, - ).andWhere("ad.expiresAt > :now", { now: new Date() }); - - const ads = await query.take(ps.limit).getMany(); - - return ads; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts deleted file mode 100644 index 2c70387310..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ /dev/null @@ -1,58 +0,0 @@ -import define from "../../../define.js"; -import { Ads } from "@/models/index.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - noSuchAd: { - message: "No such ad.", - code: "NO_SUCH_AD", - id: "b7aa1727-1354-47bc-a182-3a9c3973d300", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - id: { type: "string", format: "misskey:id" }, - memo: { type: "string" }, - url: { type: "string", minLength: 1 }, - imageUrl: { type: "string", minLength: 1 }, - place: { type: "string" }, - priority: { type: "string" }, - ratio: { type: "integer" }, - expiresAt: { type: "integer" }, - }, - required: [ - "id", - "memo", - "url", - "imageUrl", - "place", - "priority", - "ratio", - "expiresAt", - ], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const ad = await Ads.findOneBy({ id: ps.id }); - - if (ad == null) throw new ApiError(meta.errors.noSuchAd); - - await Ads.update(ad.id, { - url: ps.url, - place: ps.place, - priority: ps.priority, - ratio: ps.ratio, - memo: ps.memo, - imageUrl: ps.imageUrl, - expiresAt: new Date(ps.expiresAt), - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts b/packages/backend/src/server/api/endpoints/admin/announcements/create.ts deleted file mode 100644 index a532b6677f..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ /dev/null @@ -1,78 +0,0 @@ -import define from "../../../define.js"; -import { Announcements } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - updatedAt: { - type: "string", - optional: false, - nullable: true, - format: "date-time", - }, - title: { - type: "string", - optional: false, - nullable: false, - }, - text: { - type: "string", - optional: false, - nullable: false, - }, - imageUrl: { - type: "string", - optional: false, - nullable: true, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - title: { type: "string", minLength: 1 }, - text: { type: "string", minLength: 1 }, - imageUrl: { type: "string", nullable: true, minLength: 1 }, - }, - required: ["title", "text", "imageUrl"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const announcement = await Announcements.insert({ - id: genId(), - createdAt: new Date(), - updatedAt: null, - title: ps.title, - text: ps.text, - imageUrl: ps.imageUrl, - }).then((x) => Announcements.findOneByOrFail(x.identifiers[0])); - - return Object.assign({}, announcement, { - createdAt: announcement.createdAt.toISOString(), - updatedAt: null, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts b/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts deleted file mode 100644 index 5665b94a7b..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ /dev/null @@ -1,34 +0,0 @@ -import define from "../../../define.js"; -import { Announcements } from "@/models/index.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - noSuchAnnouncement: { - message: "No such announcement.", - code: "NO_SUCH_ANNOUNCEMENT", - id: "ecad8040-a276-4e85-bda9-015a708d291e", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - id: { type: "string", format: "misskey:id" }, - }, - required: ["id"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const announcement = await Announcements.findOneBy({ id: ps.id }); - - if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); - - await Announcements.delete(announcement.id); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts deleted file mode 100644 index fc5b020706..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Announcements, AnnouncementReads } from "@/models/index.js"; -import type { Announcement } from "@/models/entities/announcement.js"; -import define from "../../../define.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - updatedAt: { - type: "string", - optional: false, - nullable: true, - format: "date-time", - }, - text: { - type: "string", - optional: false, - nullable: false, - }, - title: { - type: "string", - optional: false, - nullable: false, - }, - imageUrl: { - type: "string", - optional: false, - nullable: true, - }, - reads: { - type: "number", - optional: false, - nullable: false, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery( - Announcements.createQueryBuilder("announcement"), - ps.sinceId, - ps.untilId, - ); - - const announcements = await query.take(ps.limit).getMany(); - - const reads = new Map(); - - for (const announcement of announcements) { - reads.set( - announcement, - await AnnouncementReads.countBy({ - announcementId: announcement.id, - }), - ); - } - - return announcements.map((announcement) => ({ - id: announcement.id, - createdAt: announcement.createdAt.toISOString(), - updatedAt: announcement.updatedAt?.toISOString() ?? null, - title: announcement.title, - text: announcement.text, - imageUrl: announcement.imageUrl, - reads: reads.get(announcement)!, - })); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts deleted file mode 100644 index 35e64f2819..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ /dev/null @@ -1,42 +0,0 @@ -import define from "../../../define.js"; -import { Announcements } from "@/models/index.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - noSuchAnnouncement: { - message: "No such announcement.", - code: "NO_SUCH_ANNOUNCEMENT", - id: "d3aae5a7-6372-4cb4-b61c-f511ffc2d7cc", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - id: { type: "string", format: "misskey:id" }, - title: { type: "string", minLength: 1 }, - text: { type: "string", minLength: 1 }, - imageUrl: { type: "string", nullable: true, minLength: 1 }, - }, - required: ["id", "title", "text", "imageUrl"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const announcement = await Announcements.findOneBy({ id: ps.id }); - - if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement); - - await Announcements.update(announcement.id, { - updatedAt: new Date(), - title: ps.title, - text: ps.text, - imageUrl: ps.imageUrl, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/delete-account.ts b/packages/backend/src/server/api/endpoints/admin/delete-account.ts deleted file mode 100644 index 9fd196888d..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/delete-account.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Users } from "@/models/index.js"; -import { deleteAccount } from "@/services/delete-account.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireAdmin: true, - - res: {}, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneByOrFail({ id: ps.userId }); - if (user.isDeleted) { - return; - } - - await deleteAccount(user); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts deleted file mode 100644 index 7969008113..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from "../../define.js"; -import { deleteFile } from "@/services/drive/delete-file.js"; -import { DriveFiles } from "@/models/index.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const files = await DriveFiles.findBy({ - userId: ps.userId, - }); - - for (const file of files) { - deleteFile(file); - } -}); diff --git a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts deleted file mode 100644 index c8be344696..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts +++ /dev/null @@ -1,43 +0,0 @@ -import define from "../../define.js"; -import { Users } from "@/models/index.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { publishInternalEvent } from "@/services/stream.js"; -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - overrideMb: { type: "number", nullable: true }, - }, - required: ["userId", "overrideMb"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error("user not found"); - } - - if (!Users.isLocalUser(user)) { - throw new Error("user is not local user"); - } - - await Users.update(user.id, { - driveCapacityOverrideMb: ps.overrideMb, - }); - - publishInternalEvent("localUserUpdated", { - id: user.id, - }); - - insertModerationLog(me, "change-drive-capacity-override", { - targetId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts deleted file mode 100644 index 1b0c1260bd..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts +++ /dev/null @@ -1,19 +0,0 @@ -import define from "../../../define.js"; -import { createCleanRemoteFilesJob } from "@/queue/index.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - createCleanRemoteFilesJob(); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts deleted file mode 100644 index 04208f6004..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IsNull } from "typeorm"; -import define from "../../../define.js"; -import { deleteFile } from "@/services/drive/delete-file.js"; -import { DriveFiles } from "@/models/index.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const files = await DriveFiles.findBy({ - userId: IsNull(), - }); - - for (const file of files) { - deleteFile(file); - } -}); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/files.ts b/packages/backend/src/server/api/endpoints/admin/drive/files.ts deleted file mode 100644 index 5cb0aecd81..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { DriveFiles } from "@/models/index.js"; -import define from "../../../define.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: false, - requireModerator: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFile", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - userId: { type: "string", format: "misskey:id", nullable: true }, - type: { - type: "string", - nullable: true, - pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1), - }, - origin: { - type: "string", - enum: ["combined", "local", "remote"], - default: "local", - }, - hostname: { - type: "string", - nullable: true, - default: null, - description: "The local host is represented with `null`.", - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - DriveFiles.createQueryBuilder("file"), - ps.sinceId, - ps.untilId, - ); - - if (ps.userId) { - query.andWhere("file.userId = :userId", { userId: ps.userId }); - } else { - if (ps.origin === "local") { - query.andWhere("file.userHost IS NULL"); - } else if (ps.origin === "remote") { - query.andWhere("file.userHost IS NOT NULL"); - } - - if (ps.hostname) { - query.andWhere("file.userHost = :hostname", { hostname: ps.hostname }); - } - } - - if (ps.type) { - if (ps.type.endsWith("/*")) { - query.andWhere("file.type like :type", { - type: `${ps.type.replace("/*", "/")}%`, - }); - } else { - query.andWhere("file.type = :type", { type: ps.type }); - } - } - - const files = await query.take(ps.limit).getMany(); - - return await DriveFiles.packMany(files, { - detail: true, - withUser: true, - self: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts deleted file mode 100644 index d65ec09fc2..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { DriveFiles } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - noSuchFile: { - message: "No such file.", - code: "NO_SUCH_FILE", - id: "caf3ca38-c6e5-472e-a30c-b05377dcc240", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - userId: { - type: "string", - optional: false, - nullable: true, - format: "id", - example: "xxxxxxxxxx", - }, - userHost: { - type: "string", - optional: false, - nullable: true, - description: "The local host is represented with `null`.", - }, - md5: { - type: "string", - optional: false, - nullable: false, - format: "md5", - example: "15eca7fba0480996e2245f5185bf39f2", - }, - name: { - type: "string", - optional: false, - nullable: false, - example: "lenna.jpg", - }, - type: { - type: "string", - optional: false, - nullable: false, - example: "image/jpeg", - }, - size: { - type: "number", - optional: false, - nullable: false, - example: 51469, - }, - comment: { - type: "string", - optional: false, - nullable: true, - }, - blurhash: { - type: "string", - optional: false, - nullable: true, - }, - properties: { - type: "object", - optional: false, - nullable: false, - properties: { - width: { - type: "number", - optional: false, - nullable: false, - example: 1280, - }, - height: { - type: "number", - optional: false, - nullable: false, - example: 720, - }, - avgColor: { - type: "string", - optional: true, - nullable: false, - example: "rgb(40,65,87)", - }, - }, - }, - storedInternal: { - type: "boolean", - optional: false, - nullable: true, - example: true, - }, - url: { - type: "string", - optional: false, - nullable: true, - format: "url", - }, - thumbnailUrl: { - type: "string", - optional: false, - nullable: true, - format: "url", - }, - webpublicUrl: { - type: "string", - optional: false, - nullable: true, - format: "url", - }, - accessKey: { - type: "string", - optional: false, - nullable: false, - }, - thumbnailAccessKey: { - type: "string", - optional: false, - nullable: false, - }, - webpublicAccessKey: { - type: "string", - optional: false, - nullable: false, - }, - uri: { - type: "string", - optional: false, - nullable: true, - }, - src: { - type: "string", - optional: false, - nullable: true, - }, - folderId: { - type: "string", - optional: false, - nullable: true, - format: "id", - example: "xxxxxxxxxx", - }, - isSensitive: { - type: "boolean", - optional: false, - nullable: false, - }, - isLink: { - type: "boolean", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - anyOf: [ - { - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], - }, - { - properties: { - url: { type: "string" }, - }, - required: ["url"], - }, - ], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const file = ps.fileId - ? await DriveFiles.findOneBy({ id: ps.fileId }) - : await DriveFiles.findOne({ - where: [ - { - url: ps.url, - }, - { - thumbnailUrl: ps.url, - }, - { - webpublicUrl: ps.url, - }, - ], - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - - if (!me.isAdmin) { - file.requestIp = undefined; - file.requestHeaders = undefined; - } - - return file; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts deleted file mode 100644 index 1ea457adf2..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ /dev/null @@ -1,47 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { In } from "typeorm"; -import { ApiError } from "../../../error.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - ids: { - type: "array", - items: { - type: "string", - format: "misskey:id", - }, - }, - aliases: { - type: "array", - items: { - type: "string", - }, - }, - }, - required: ["ids", "aliases"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const emojis = await Emojis.findBy({ - id: In(ps.ids), - }); - - for (const emoji of emojis) { - await Emojis.update(emoji.id, { - updatedAt: new Date(), - aliases: [...new Set(emoji.aliases.concat(ps.aliases))], - }); - } - - await db.queryResultCache!.remove(["meta_emojis"]); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts deleted file mode 100644 index bfc025834f..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ /dev/null @@ -1,68 +0,0 @@ -import define from "../../../define.js"; -import { Emojis, DriveFiles } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { ApiError } from "../../../error.js"; -import rndstr from "rndstr"; -import { publishBroadcastStream } from "@/services/stream.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - noSuchFile: { - message: "No such file.", - code: "MO_SUCH_FILE", - id: "fc46b5a4-6b92-4c33-ac66-b806659bb5cf", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const file = await DriveFiles.findOneBy({ id: 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.insert({ - id: genId(), - updatedAt: new Date(), - name: name, - category: null, - host: null, - aliases: [], - originalUrl: file.url, - publicUrl: file.webpublicUrl ?? file.url, - type: file.webpublicType ?? file.type, - license: null, - }).then((x) => Emojis.findOneByOrFail(x.identifiers[0])); - - await db.queryResultCache!.remove(["meta_emojis"]); - - publishBroadcastStream("emojiAdded", { - emoji: await Emojis.pack(emoji.id), - }); - - insertModerationLog(me, "addEmoji", { - emojiId: emoji.id, - }); - - return { - id: emoji.id, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts deleted file mode 100644 index 951158f7d4..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ /dev/null @@ -1,88 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { ApiError } from "../../../error.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { uploadFromUrl } from "@/services/drive/upload-from-url.js"; -import { publishBroadcastStream } from "@/services/stream.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - noSuchEmoji: { - message: "No such emoji.", - code: "NO_SUCH_EMOJI", - id: "e2785b66-dca3-4087-9cac-b93c541cc425", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - emojiId: { type: "string", format: "misskey:id" }, - }, - required: ["emojiId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const emoji = await Emojis.findOneBy({ id: ps.emojiId }); - - if (emoji == null) { - throw new ApiError(meta.errors.noSuchEmoji); - } - - let driveFile: DriveFile; - - try { - // Create file - driveFile = await uploadFromUrl({ - url: emoji.originalUrl, - user: null, - force: true, - }); - } catch (e) { - throw new ApiError(); - } - - const copied = await Emojis.insert({ - id: genId(), - updatedAt: new Date(), - name: emoji.name, - host: null, - aliases: [], - originalUrl: driveFile.url, - publicUrl: driveFile.webpublicUrl ?? driveFile.url, - type: driveFile.webpublicType ?? driveFile.type, - license: emoji.license, - }).then((x) => Emojis.findOneByOrFail(x.identifiers[0])); - - await db.queryResultCache!.remove(["meta_emojis"]); - - publishBroadcastStream("emojiAdded", { - emoji: await Emojis.pack(copied.id), - }); - - return { - id: copied.id, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts deleted file mode 100644 index 585af231f6..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ /dev/null @@ -1,43 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { In } from "typeorm"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { ApiError } from "../../../error.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - ids: { - type: "array", - items: { - type: "string", - format: "misskey:id", - }, - }, - }, - required: ["ids"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const emojis = await Emojis.findBy({ - id: In(ps.ids), - }); - - for (const emoji of emojis) { - await Emojis.delete(emoji.id); - - await db.queryResultCache!.remove(["meta_emojis"]); - - insertModerationLog(me, "deleteEmoji", { - emoji: emoji, - }); - } -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts deleted file mode 100644 index 761c7c3776..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ /dev/null @@ -1,42 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { ApiError } from "../../../error.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - noSuchEmoji: { - message: "No such emoji.", - code: "NO_SUCH_EMOJI", - id: "be83669b-773a-44b7-b1f8-e5e5170ac3c2", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - id: { type: "string", format: "misskey:id" }, - }, - required: ["id"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const emoji = await Emojis.findOneBy({ id: ps.id }); - - if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - - await Emojis.delete(emoji.id); - - await db.queryResultCache!.remove(["meta_emojis"]); - - insertModerationLog(me, "deleteEmoji", { - emoji: emoji, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts deleted file mode 100644 index 6f49d6d18d..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ /dev/null @@ -1,21 +0,0 @@ -import define from "../../../define.js"; -import { createImportCustomEmojisJob } from "@/queue/index.js"; -import ms from "ms"; - -export const meta = { - secure: true, - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - createImportCustomEmojisJob(user, ps.fileId); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts deleted file mode 100644 index fae986dd96..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ /dev/null @@ -1,105 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { toPuny } from "@/misc/convert-host.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - aliases: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - category: { - type: "string", - optional: false, - nullable: true, - }, - host: { - type: "string", - optional: false, - nullable: true, - description: "The local host is represented with `null`.", - }, - url: { - type: "string", - optional: false, - nullable: false, - }, - license: { - type: "string", - optional: false, - nullable: true, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - query: { type: "string", nullable: true, default: null }, - host: { - type: "string", - nullable: true, - default: null, - description: "Use `null` to represent the local host.", - }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const q = makePaginationQuery( - Emojis.createQueryBuilder("emoji"), - ps.sinceId, - ps.untilId, - ); - - if (ps.host == null) { - q.andWhere("emoji.host IS NOT NULL"); - } else { - q.andWhere("emoji.host = :host", { host: toPuny(ps.host) }); - } - - if (ps.query) { - q.andWhere("emoji.name like :query", { query: `%${ps.query}%` }); - } - - const emojis = await q.orderBy("emoji.id", "DESC").take(ps.limit).getMany(); - - return Emojis.packMany(emojis); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts deleted file mode 100644 index aa49f14803..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ /dev/null @@ -1,107 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; -import type { Emoji } from "@/models/entities/emoji.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - aliases: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - category: { - type: "string", - optional: false, - nullable: true, - }, - host: { - type: "null", - optional: false, - description: - "The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.", - }, - url: { - type: "string", - optional: false, - nullable: false, - }, - license: { - type: "string", - optional: false, - nullable: true, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - query: { type: "string", nullable: true, default: null }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const q = makePaginationQuery( - Emojis.createQueryBuilder("emoji"), - ps.sinceId, - ps.untilId, - ).andWhere("emoji.host IS NULL"); - - let emojis: Emoji[]; - - if (ps.query) { - //q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` }); - //const emojis = await q.take(ps.limit).getMany(); - - emojis = await q.getMany(); - - emojis = emojis.filter( - (emoji) => - emoji.name.includes(ps.query!) || - emoji.aliases.some((a) => a.includes(ps.query!)) || - emoji.category?.includes(ps.query!), - ); - - emojis.splice(ps.limit + 1); - } else { - emojis = await q.take(ps.limit).getMany(); - } - - return Emojis.packMany(emojis); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts deleted file mode 100644 index 4e57fa3dda..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ /dev/null @@ -1,47 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { In } from "typeorm"; -import { ApiError } from "../../../error.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - ids: { - type: "array", - items: { - type: "string", - format: "misskey:id", - }, - }, - aliases: { - type: "array", - items: { - type: "string", - }, - }, - }, - required: ["ids", "aliases"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const emojis = await Emojis.findBy({ - id: In(ps.ids), - }); - - for (const emoji of emojis) { - await Emojis.update(emoji.id, { - updatedAt: new Date(), - aliases: emoji.aliases.filter((x) => !ps.aliases.includes(x)), - }); - } - - await db.queryResultCache!.remove(["meta_emojis"]); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts deleted file mode 100644 index 1197f60779..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ /dev/null @@ -1,46 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { In } from "typeorm"; -import { ApiError } from "../../../error.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - ids: { - type: "array", - items: { - type: "string", - format: "misskey:id", - }, - }, - aliases: { - type: "array", - items: { - type: "string", - }, - }, - }, - required: ["ids", "aliases"], -} as const; - -export default define(meta, paramDef, async (ps) => { - await Emojis.update( - { - id: In(ps.ids), - }, - { - updatedAt: new Date(), - aliases: ps.aliases, - }, - ); - - await db.queryResultCache!.remove(["meta_emojis"]); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts deleted file mode 100644 index 17881a4454..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { In } from "typeorm"; -import { ApiError } from "../../../error.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - ids: { - type: "array", - items: { - type: "string", - format: "misskey:id", - }, - }, - category: { - type: "string", - nullable: true, - description: "Use `null` to reset the category.", - }, - }, - required: ["ids"], -} as const; - -export default define(meta, paramDef, async (ps) => { - await Emojis.update( - { - id: In(ps.ids), - }, - { - updatedAt: new Date(), - category: ps.category, - }, - ); - - await db.queryResultCache!.remove(["meta_emojis"]); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts deleted file mode 100644 index c98ca03fae..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { In } from "typeorm"; -import { ApiError } from "../../../error.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - ids: { - type: "array", - items: { - type: "string", - format: "misskey:id", - }, - }, - license: { - type: "string", - nullable: true, - description: "Use `null` to reset the license.", - }, - }, - required: ["ids"], -} as const; - -export default define(meta, paramDef, async (ps) => { - await Emojis.update( - { - id: In(ps.ids), - }, - { - updatedAt: new Date(), - license: ps.license, - }, - ); - - await db.queryResultCache!.remove(["meta_emojis"]); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts deleted file mode 100644 index 9e2e854760..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ /dev/null @@ -1,59 +0,0 @@ -import define from "../../../define.js"; -import { Emojis } from "@/models/index.js"; -import { ApiError } from "../../../error.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - noSuchEmoji: { - message: "No such emoji.", - code: "NO_SUCH_EMOJI", - id: "684dec9d-a8c2-4364-9aa8-456c49cb1dc8", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - id: { type: "string", format: "misskey:id" }, - name: { type: "string" }, - category: { - type: "string", - nullable: true, - description: "Use `null` to reset the category.", - }, - aliases: { - type: "array", - items: { - type: "string", - }, - }, - license: { - type: "string", - nullable: true, - }, - }, - required: ["id", "name", "aliases"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const emoji = await Emojis.findOneBy({ id: 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, - license: ps.license, - }); - - await db.queryResultCache!.remove(["meta_emojis"]); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts b/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts deleted file mode 100644 index 534f226c28..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from "../../../define.js"; -import { deleteFile } from "@/services/drive/delete-file.js"; -import { DriveFiles } from "@/models/index.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - host: { type: "string" }, - }, - required: ["host"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const files = await DriveFiles.findBy({ - userHost: ps.host, - }); - - for (const file of files) { - deleteFile(file); - } -}); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts b/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts deleted file mode 100644 index 9c7165593c..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ /dev/null @@ -1,29 +0,0 @@ -import define from "../../../define.js"; -import { Instances } from "@/models/index.js"; -import { toPuny } from "@/misc/convert-host.js"; -import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - host: { type: "string" }, - }, - required: ["host"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const instance = await Instances.findOneBy({ host: toPuny(ps.host) }); - - if (instance == null) { - throw new Error("instance not found"); - } - - fetchInstanceMetadata(instance, true); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts deleted file mode 100644 index a1ccf11af0..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ /dev/null @@ -1,37 +0,0 @@ -import define from "../../../define.js"; -import deleteFollowing from "@/services/following/delete.js"; -import { Followings, Users } from "@/models/index.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - host: { type: "string" }, - }, - required: ["host"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const followings = await Followings.findBy({ - followerHost: ps.host, - }); - - const pairs = await Promise.all( - followings.map((f) => - Promise.all([ - Users.findOneByOrFail({ id: f.followerId }), - Users.findOneByOrFail({ id: f.followeeId }), - ]), - ), - ); - - for (const pair of pairs) { - deleteFollowing(pair[0], pair[1]); - } -}); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts deleted file mode 100644 index 016989b541..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ /dev/null @@ -1,34 +0,0 @@ -import define from "../../../define.js"; -import { Instances } from "@/models/index.js"; -import { toPuny } from "@/misc/convert-host.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - host: { type: "string" }, - isSuspended: { type: "boolean" }, - }, - required: ["host", "isSuspended"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const instance = await Instances.findOneBy({ host: toPuny(ps.host) }); - - if (instance == null) { - throw new Error("instance not found"); - } - - Instances.update( - { host: toPuny(ps.host) }, - { - isSuspended: ps.isSuspended, - }, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts deleted file mode 100644 index f39a369ecb..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ /dev/null @@ -1,27 +0,0 @@ -import define from "../../define.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - requireCredential: true, - requireModerator: true, - - tags: ["admin"], -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const stats = await db.query("SELECT * FROM pg_indexes;").then((recs) => { - const res = [] as { tablename: string; indexname: string }[]; - for (const rec of recs) { - res.push(rec); - } - return res; - }); - - return stats; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts deleted file mode 100644 index 25d07f327a..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { db } from "@/db/postgre.js"; -import define from "../../define.js"; - -export const meta = { - requireCredential: true, - requireModerator: true, - - tags: ["admin"], - - res: { - type: "object", - optional: false, - nullable: false, - example: { - migrations: { - count: 66, - size: 32768, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const sizes = await db - .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/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts deleted file mode 100644 index da76ae624e..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { UserIps } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireAdmin: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const ips = await UserIps.find({ - where: { userId: ps.userId }, - order: { createdAt: "DESC" }, - take: 30, - }); - - return ips.map((x) => ({ - ip: x.ip, - createdAt: x.createdAt.toISOString(), - })); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/invite.ts b/packages/backend/src/server/api/endpoints/admin/invite.ts deleted file mode 100644 index b8bdb38b46..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/invite.ts +++ /dev/null @@ -1,50 +0,0 @@ -import rndstr from "rndstr"; -import define from "../../define.js"; -import { RegistrationTickets } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - code: { - type: "string", - optional: false, - nullable: false, - example: "2ERUA5VR", - maxLength: 8, - minLength: 8, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const code = rndstr({ - length: 8, - chars: "2-9A-HJ-NP-Z", // [0-9A-Z] w/o [01IO] (32 patterns) - }); - - await RegistrationTickets.insert({ - id: genId(), - createdAt: new Date(), - code, - }); - - return { - code, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts deleted file mode 100644 index c8c639f504..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ /dev/null @@ -1,570 +0,0 @@ -import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["meta"], - - requireCredential: true, - requireAdmin: true, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - driveCapacityPerLocalUserMb: { - type: "number", - optional: false, - nullable: false, - }, - driveCapacityPerRemoteUserMb: { - type: "number", - optional: false, - nullable: false, - }, - cacheRemoteFiles: { - type: "boolean", - optional: false, - nullable: false, - }, - emailRequiredForSignup: { - type: "boolean", - optional: false, - nullable: false, - }, - enableHcaptcha: { - type: "boolean", - optional: false, - nullable: false, - }, - hcaptchaSiteKey: { - type: "string", - optional: false, - nullable: true, - }, - enableRecaptcha: { - type: "boolean", - optional: false, - nullable: false, - }, - recaptchaSiteKey: { - type: "string", - optional: false, - nullable: true, - }, - swPublickey: { - type: "string", - optional: false, - nullable: true, - }, - mascotImageUrl: { - type: "string", - optional: false, - nullable: false, - default: "/assets/ai.png", - }, - bannerUrl: { - type: "string", - optional: false, - nullable: false, - }, - errorImageUrl: { - type: "string", - optional: false, - nullable: false, - default: "https://xn--931a.moe/aiart/yubitun.png", - }, - iconUrl: { - type: "string", - optional: false, - nullable: true, - }, - maxNoteTextLength: { - type: "number", - optional: false, - nullable: false, - }, - maxCaptionTextLength: { - type: "number", - optional: false, - nullable: false, - }, - emojis: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - aliases: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - category: { - type: "string", - optional: false, - nullable: true, - }, - host: { - type: "string", - optional: false, - nullable: true, - }, - url: { - type: "string", - optional: false, - nullable: false, - format: "url", - }, - }, - }, - }, - ads: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - place: { - type: "string", - optional: false, - nullable: false, - }, - url: { - type: "string", - optional: false, - nullable: false, - format: "url", - }, - imageUrl: { - type: "string", - optional: false, - nullable: false, - format: "url", - }, - }, - }, - }, - enableEmail: { - type: "boolean", - optional: false, - nullable: false, - }, - enableTwitterIntegration: { - type: "boolean", - optional: false, - nullable: false, - }, - enableGithubIntegration: { - type: "boolean", - optional: false, - nullable: false, - }, - enableDiscordIntegration: { - type: "boolean", - optional: false, - nullable: false, - }, - enableServiceWorker: { - type: "boolean", - optional: false, - nullable: false, - }, - translatorAvailable: { - type: "boolean", - optional: false, - nullable: false, - }, - proxyAccountName: { - type: "string", - optional: false, - nullable: true, - }, - recommendedInstances: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - pinnedUsers: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - customMOTD: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - customSplashIcons: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - hiddenTags: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - blockedHosts: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - allowedHosts: { - type: "array", - optional: true, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - privateMode: { - type: "boolean", - optional: false, - nullable: false, - }, - secureMode: { - type: "boolean", - optional: false, - nullable: false, - }, - hcaptchaSecretKey: { - type: "string", - optional: true, - nullable: true, - }, - recaptchaSecretKey: { - type: "string", - optional: true, - nullable: true, - }, - sensitiveMediaDetection: { - type: "string", - optional: true, - nullable: false, - }, - sensitiveMediaDetectionSensitivity: { - type: "string", - optional: true, - nullable: false, - }, - setSensitiveFlagAutomatically: { - type: "boolean", - optional: true, - nullable: false, - }, - enableSensitiveMediaDetectionForVideos: { - type: "boolean", - optional: true, - nullable: false, - }, - proxyAccountId: { - type: "string", - optional: true, - nullable: true, - format: "id", - }, - twitterConsumerKey: { - type: "string", - optional: true, - nullable: true, - }, - twitterConsumerSecret: { - type: "string", - optional: true, - nullable: true, - }, - githubClientId: { - type: "string", - optional: true, - nullable: true, - }, - githubClientSecret: { - type: "string", - optional: true, - nullable: true, - }, - discordClientId: { - type: "string", - optional: true, - nullable: true, - }, - discordClientSecret: { - type: "string", - optional: true, - nullable: true, - }, - summaryProxy: { - type: "string", - optional: true, - nullable: true, - }, - email: { - type: "string", - optional: true, - nullable: true, - }, - smtpSecure: { - type: "boolean", - optional: true, - nullable: false, - }, - smtpHost: { - type: "string", - optional: true, - nullable: true, - }, - smtpPort: { - type: "string", - optional: true, - nullable: true, - }, - smtpUser: { - type: "string", - optional: true, - nullable: true, - }, - smtpPass: { - type: "string", - optional: true, - nullable: true, - }, - swPrivateKey: { - type: "string", - optional: true, - nullable: true, - }, - useObjectStorage: { - type: "boolean", - optional: true, - nullable: false, - }, - objectStorageBaseUrl: { - type: "string", - optional: true, - nullable: true, - }, - objectStorageBucket: { - type: "string", - optional: true, - nullable: true, - }, - objectStoragePrefix: { - type: "string", - optional: true, - nullable: true, - }, - objectStorageEndpoint: { - type: "string", - optional: true, - nullable: true, - }, - objectStorageRegion: { - type: "string", - optional: true, - nullable: true, - }, - objectStoragePort: { - type: "number", - optional: true, - nullable: true, - }, - objectStorageAccessKey: { - type: "string", - optional: true, - nullable: true, - }, - objectStorageSecretKey: { - type: "string", - optional: true, - nullable: true, - }, - objectStorageUseSSL: { - type: "boolean", - optional: true, - nullable: false, - }, - objectStorageUseProxy: { - type: "boolean", - optional: true, - nullable: false, - }, - objectStorageSetPublicRead: { - type: "boolean", - optional: true, - nullable: false, - }, - enableIpLogging: { - type: "boolean", - optional: true, - nullable: false, - }, - enableActiveEmailValidation: { - type: "boolean", - optional: true, - nullable: false, - }, - defaultReaction: { - type: "string", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const instance = await fetchMeta(true); - - return { - 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, - disableRegistration: instance.disableRegistration, - disableLocalTimeline: instance.disableLocalTimeline, - disableRecommendedTimeline: instance.disableRecommendedTimeline, - 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, - themeColor: instance.themeColor, - mascotImageUrl: instance.mascotImageUrl, - bannerUrl: instance.bannerUrl, - errorImageUrl: instance.errorImageUrl, - iconUrl: instance.iconUrl, - backgroundImageUrl: instance.backgroundImageUrl, - logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため - maxCaptionTextLength: MAX_CAPTION_TEXT_LENGTH, - defaultLightTheme: instance.defaultLightTheme, - defaultDarkTheme: instance.defaultDarkTheme, - enableEmail: instance.enableEmail, - enableTwitterIntegration: instance.enableTwitterIntegration, - enableGithubIntegration: instance.enableGithubIntegration, - enableDiscordIntegration: instance.enableDiscordIntegration, - enableServiceWorker: instance.enableServiceWorker, - translatorAvailable: instance.deeplAuthKey != null, - pinnedPages: instance.pinnedPages, - pinnedClipId: instance.pinnedClipId, - cacheRemoteFiles: instance.cacheRemoteFiles, - defaultReaction: instance.defaultReaction, - recommendedInstances: instance.recommendedInstances, - pinnedUsers: instance.pinnedUsers, - customMOTD: instance.customMOTD, - customSplashIcons: instance.customSplashIcons, - hiddenTags: instance.hiddenTags, - blockedHosts: instance.blockedHosts, - allowedHosts: instance.allowedHosts, - privateMode: instance.privateMode, - secureMode: instance.secureMode, - hcaptchaSecretKey: instance.hcaptchaSecretKey, - recaptchaSecretKey: instance.recaptchaSecretKey, - sensitiveMediaDetection: instance.sensitiveMediaDetection, - sensitiveMediaDetectionSensitivity: - instance.sensitiveMediaDetectionSensitivity, - setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, - enableSensitiveMediaDetectionForVideos: - instance.enableSensitiveMediaDetectionForVideos, - proxyAccountId: instance.proxyAccountId, - twitterConsumerKey: instance.twitterConsumerKey, - twitterConsumerSecret: instance.twitterConsumerSecret, - githubClientId: instance.githubClientId, - githubClientSecret: instance.githubClientSecret, - discordClientId: instance.discordClientId, - discordClientSecret: instance.discordClientSecret, - summalyProxy: instance.summalyProxy, - email: instance.email, - smtpSecure: instance.smtpSecure, - smtpHost: instance.smtpHost, - smtpPort: instance.smtpPort, - smtpUser: instance.smtpUser, - smtpPass: instance.smtpPass, - swPrivateKey: instance.swPrivateKey, - useObjectStorage: instance.useObjectStorage, - objectStorageBaseUrl: instance.objectStorageBaseUrl, - objectStorageBucket: instance.objectStorageBucket, - objectStoragePrefix: instance.objectStoragePrefix, - objectStorageEndpoint: instance.objectStorageEndpoint, - objectStorageRegion: instance.objectStorageRegion, - objectStoragePort: instance.objectStoragePort, - objectStorageAccessKey: instance.objectStorageAccessKey, - objectStorageSecretKey: instance.objectStorageSecretKey, - objectStorageUseSSL: instance.objectStorageUseSSL, - objectStorageUseProxy: instance.objectStorageUseProxy, - objectStorageSetPublicRead: instance.objectStorageSetPublicRead, - objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, - deeplAuthKey: instance.deeplAuthKey, - deeplIsPro: instance.deeplIsPro, - enableIpLogging: instance.enableIpLogging, - enableActiveEmailValidation: instance.enableActiveEmailValidation, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts deleted file mode 100644 index 478f2661b6..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts +++ /dev/null @@ -1,39 +0,0 @@ -import define from "../../../define.js"; -import { Users } from "@/models/index.js"; -import { publishInternalEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireAdmin: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneBy({ id: ps.userId }); - - 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, - }); - - publishInternalEvent("userChangeModeratorState", { - id: user.id, - isModerator: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts deleted file mode 100644 index a43cc0cbe1..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts +++ /dev/null @@ -1,35 +0,0 @@ -import define from "../../../define.js"; -import { Users } from "@/models/index.js"; -import { publishInternalEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireAdmin: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error("user not found"); - } - - await Users.update(user.id, { - isModerator: false, - }); - - publishInternalEvent("userChangeModeratorState", { - id: user.id, - isModerator: false, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts deleted file mode 100644 index a6d1f35191..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ /dev/null @@ -1,54 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getNote } from "../../../common/getters.js"; -import { PromoNotes } from "@/models/index.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - expiresAt: { type: "integer" }, - }, - required: ["noteId", "expiresAt"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - const exist = await PromoNotes.findOneBy({ noteId: note.id }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyPromoted); - } - - await PromoNotes.insert({ - noteId: note.id, - expiresAt: new Date(ps.expiresAt), - userId: note.userId, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts b/packages/backend/src/server/api/endpoints/admin/queue/clear.ts deleted file mode 100644 index 9b828bb241..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ /dev/null @@ -1,22 +0,0 @@ -import define from "../../../define.js"; -import { destroy } from "@/queue/index.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - destroy(); - - insertModerationLog(me, "clearQueue"); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts deleted file mode 100644 index 15fdfb0250..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { deliverQueue } from "@/queue/queues.js"; -import { URL } from "node:url"; -import define from "../../../define.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "array", - optional: false, - nullable: false, - items: { - anyOf: [ - { - type: "string", - }, - { - type: "number", - }, - ], - }, - }, - example: [["example.com", 12]], - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const jobs = await deliverQueue.getJobs(["delayed"]); - - const res = [] as [string, number][]; - - for (const job of jobs) { - const host = new URL(job.data.to).host; - if (res.find((x) => x[0] === host)) { - res.find((x) => x[0] === host)![1]++; - } else { - res.push([host, 1]); - } - } - - res.sort((a, b) => b[1] - a[1]); - - return res; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts deleted file mode 100644 index 1890bd4345..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { URL } from "node:url"; -import define from "../../../define.js"; -import { inboxQueue } from "@/queue/queues.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "array", - optional: false, - nullable: false, - items: { - anyOf: [ - { - type: "string", - }, - { - type: "number", - }, - ], - }, - }, - example: [["example.com", 12]], - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const jobs = await inboxQueue.getJobs(["delayed"]); - - const res = [] as [string, number][]; - - for (const job of jobs) { - const host = new URL(job.data.signature.keyId).host; - if (res.find((x) => x[0] === host)) { - res.find((x) => x[0] === host)![1]++; - } else { - res.push([host, 1]); - } - } - - res.sort((a, b) => b[1] - a[1]); - - return res; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts deleted file mode 100644 index 4a437c3d12..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - deliverQueue, - inboxQueue, - dbQueue, - objectStorageQueue, - backgroundQueue, -} from "@/queue/queues.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - deliver: { - optional: false, - nullable: false, - ref: "QueueCount", - }, - inbox: { - optional: false, - nullable: false, - ref: "QueueCount", - }, - db: { - optional: false, - nullable: false, - ref: "QueueCount", - }, - objectStorage: { - optional: false, - nullable: false, - ref: "QueueCount", - }, - backgroundQueue: { - optional: false, - nullable: false, - ref: "QueueCount", - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const deliverJobCounts = await deliverQueue.getJobCounts(); - const inboxJobCounts = await inboxQueue.getJobCounts(); - const dbJobCounts = await dbQueue.getJobCounts(); - const objectStorageJobCounts = await objectStorageQueue.getJobCounts(); - const backgroundJobCounts = await backgroundQueue.getJobCounts(); - - return { - deliver: deliverJobCounts, - inbox: inboxJobCounts, - db: dbJobCounts, - objectStorage: objectStorageJobCounts, - backgroundQueue: backgroundJobCounts, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts deleted file mode 100644 index bb56216a74..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { URL } from "node:url"; -import define from "../../../define.js"; -import { addRelay } from "@/services/relay.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - errors: { - invalidUrl: { - message: "Invalid URL", - code: "INVALID_URL", - id: "fb8c92d3-d4e5-44e7-b3d4-800d5cef8b2c", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - inbox: { - description: "URL of the inbox, must be a https scheme URL", - type: "string", - optional: false, - nullable: false, - format: "url", - }, - status: { - type: "string", - optional: false, - nullable: false, - default: "requesting", - enum: ["requesting", "accepted", "rejected"], - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - inbox: { type: "string" }, - }, - required: ["inbox"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - try { - if (new URL(ps.inbox).protocol !== "https:") throw new Error("https only"); - } catch { - throw new ApiError(meta.errors.invalidUrl); - } - - return await addRelay(ps.inbox); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/relays/list.ts b/packages/backend/src/server/api/endpoints/admin/relays/list.ts deleted file mode 100644 index 4c294ba9b2..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ /dev/null @@ -1,51 +0,0 @@ -import define from "../../../define.js"; -import { listRelay } from "@/services/relay.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - inbox: { - type: "string", - optional: false, - nullable: false, - format: "url", - }, - status: { - type: "string", - optional: false, - nullable: false, - default: "requesting", - enum: ["requesting", "accepted", "rejected"], - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - return await listRelay(); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts deleted file mode 100644 index 1b3d90628b..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ /dev/null @@ -1,21 +0,0 @@ -import define from "../../../define.js"; -import { removeRelay } from "@/services/relay.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - inbox: { type: "string" }, - }, - required: ["inbox"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - return await removeRelay(ps.inbox); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts deleted file mode 100644 index cbe6735ce5..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ /dev/null @@ -1,66 +0,0 @@ -import define from "../../define.js"; -// import bcrypt from "bcryptjs"; -import rndstr from "rndstr"; -import { Users, UserProfiles } from "@/models/index.js"; -import { hashPassword } from "@/misc/password.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - password: { - type: "string", - optional: false, - nullable: false, - minLength: 8, - maxLength: 8, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneBy({ id: ps.userId }); - - 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); - const hash = await hashPassword(passwd); - - await UserProfiles.update( - { - userId: user.id, - }, - { - password: hash, - }, - ); - - return { - password: passwd, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts deleted file mode 100644 index c876a21984..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ /dev/null @@ -1,47 +0,0 @@ -import define from "../../define.js"; -import { AbuseUserReports, Users } from "@/models/index.js"; -import { getInstanceActor } from "@/services/instance-actor.js"; -import { deliver } from "@/queue/index.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { renderFlag } from "@/remote/activitypub/renderer/flag.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - reportId: { type: "string", format: "misskey:id" }, - forward: { type: "boolean", default: false }, - }, - required: ["reportId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const report = await AbuseUserReports.findOneByOrFail({ id: ps.reportId }); - - if (report == null) { - throw new Error("report not found"); - } - - if (ps.forward && report.targetUserHost != null) { - const actor = await getInstanceActor(); - const targetUser = await Users.findOneByOrFail({ id: report.targetUserId }); - - deliver( - actor, - renderActivity(renderFlag(actor, [targetUser.uri!], report.comment)), - targetUser.inbox, - ); - } - - await AbuseUserReports.update(report.id, { - resolved: true, - assigneeId: me.id, - forwarded: ps.forward && report.targetUserHost != null, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/search/index-all.ts b/packages/backend/src/server/api/endpoints/admin/search/index-all.ts deleted file mode 100644 index 135b48eccd..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/search/index-all.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from "../../../define.js"; -import { createIndexAllNotesJob } from "@/queue/index.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - cursor: { - type: "string", - format: "misskey:id", - nullable: true, - default: null, - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, _me) => { - createIndexAllNotesJob({ - cursor: ps.cursor ?? undefined, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/send-email.ts b/packages/backend/src/server/api/endpoints/admin/send-email.ts deleted file mode 100644 index 1676f68907..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/send-email.ts +++ /dev/null @@ -1,23 +0,0 @@ -import define from "../../define.js"; -import { sendEmail } from "@/services/send-email.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - to: { type: "string" }, - subject: { type: "string" }, - text: { type: "string" }, - }, - required: ["to", "subject", "text"], -} as const; - -export default define(meta, paramDef, async (ps) => { - await sendEmail(ps.to, ps.subject, ps.text, ps.text); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts deleted file mode 100644 index 8998032cc9..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as os from "node:os"; -import si from "systeminformation"; -import define from "../../define.js"; -import { redisClient } from "../../../../db/redis.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - requireCredential: true, - requireModerator: true, - - tags: ["admin", "meta"], - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - machine: { - type: "string", - optional: false, - nullable: false, - }, - os: { - type: "string", - optional: false, - nullable: false, - example: "linux", - }, - node: { - type: "string", - optional: false, - nullable: false, - }, - psql: { - type: "string", - optional: false, - nullable: false, - }, - cpu: { - type: "object", - optional: false, - nullable: false, - properties: { - model: { - type: "string", - optional: false, - nullable: false, - }, - cores: { - type: "number", - optional: false, - nullable: false, - }, - }, - }, - mem: { - type: "object", - optional: false, - nullable: false, - properties: { - total: { - type: "number", - optional: false, - nullable: false, - format: "bytes", - }, - }, - }, - fs: { - type: "object", - optional: false, - nullable: false, - properties: { - total: { - type: "number", - optional: false, - nullable: false, - format: "bytes", - }, - used: { - type: "number", - optional: false, - nullable: false, - format: "bytes", - }, - }, - }, - net: { - type: "object", - optional: false, - nullable: false, - properties: { - interface: { - type: "string", - optional: false, - nullable: false, - example: "eth0", - }, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const memStats = await si.mem(); - const fsStats = await si.fsSize(); - const netInterface = await si.networkInterfaceDefault(); - - const redisServerInfo = await redisClient.info("Server"); - const m = redisServerInfo.match(new RegExp("^redis_version:(.*)", "m")); - const redis_version = m?.[1]; - - return { - machine: os.hostname(), - os: os.platform(), - node: process.version, - psql: await db - .query("SHOW server_version") - .then((x) => x[0].server_version), - redis: redis_version, - cpu: { - model: os.cpus()[0].model, - cores: os.cpus().length, - }, - mem: { - total: memStats.total, - }, - fs: { - total: fsStats[0].size, - used: fsStats[0].used, - }, - net: { - interface: netInterface, - }, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts deleted file mode 100644 index df7e8979ca..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ /dev/null @@ -1,79 +0,0 @@ -import define from "../../define.js"; -import { ModerationLogs } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - type: { - type: "string", - optional: false, - nullable: false, - }, - info: { - type: "object", - optional: false, - nullable: false, - }, - userId: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - user: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailed", - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery( - ModerationLogs.createQueryBuilder("report"), - ps.sinceId, - ps.untilId, - ); - - const reports = await query.take(ps.limit).getMany(); - - return await ModerationLogs.packMany(reports); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts deleted file mode 100644 index e91f07f838..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Signins, UserProfiles, Users } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "object", - nullable: false, - optional: false, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const [user, profile] = await Promise.all([ - Users.findOneBy({ id: ps.userId }), - UserProfiles.findOneBy({ userId: ps.userId }), - ]); - - if (user == null || profile == null) { - throw new Error("user not found"); - } - - const _me = await Users.findOneByOrFail({ id: me.id }); - if (_me.isModerator && !_me.isAdmin && user.isAdmin) { - throw new Error("cannot show info of admin"); - } - - if (!_me.isAdmin) { - return { - isModerator: user.isModerator, - isSilenced: user.isSilenced, - isSuspended: user.isSuspended, - }; - } - - const maskedKeys = ["accessToken", "accessTokenSecret", "refreshToken"]; - Object.keys(profile.integrations).forEach((integration) => { - maskedKeys.forEach( - (key) => (profile.integrations[integration][key] = ""), - ); - }); - - const signins = await Signins.findBy({ userId: user.id }); - - return { - email: profile.email, - emailVerified: profile.emailVerified, - autoAcceptFollowed: profile.autoAcceptFollowed, - noCrawle: profile.noCrawle, - alwaysMarkNsfw: profile.alwaysMarkNsfw, - autoSensitive: profile.autoSensitive, - carefulBot: profile.carefulBot, - injectFeaturedNote: profile.injectFeaturedNote, - receiveAnnouncementEmail: profile.receiveAnnouncementEmail, - integrations: profile.integrations, - mutedWords: profile.mutedWords, - mutedInstances: profile.mutedInstances, - mutingNotificationTypes: profile.mutingNotificationTypes, - isModerator: user.isModerator, - isSilenced: user.isSilenced, - isSuspended: user.isSuspended, - lastActiveDate: user.lastActiveDate, - moderationNote: profile.moderationNote, - signins, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts deleted file mode 100644 index 868df9dc9b..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Users } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, - - res: { - type: "array", - nullable: false, - optional: false, - items: { - type: "object", - nullable: false, - optional: false, - ref: "UserDetailed", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - sort: { - type: "string", - enum: [ - "+follower", - "-follower", - "+createdAt", - "-createdAt", - "+updatedAt", - "-updatedAt", - ], - }, - state: { - type: "string", - enum: [ - "all", - "alive", - "available", - "admin", - "moderator", - "adminOrModerator", - "silenced", - "suspended", - ], - default: "all", - }, - origin: { - type: "string", - enum: ["combined", "local", "remote"], - default: "combined", - }, - username: { type: "string", nullable: true, default: null }, - hostname: { - type: "string", - nullable: true, - default: null, - description: "The local host is represented with `null`.", - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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 = :hostname", { - hostname: ps.hostname.toLowerCase(), - }); - } - - switch (ps.sort) { - case "+follower": - query.orderBy("user.followersCount", "DESC"); - break; - case "-follower": - query.orderBy("user.followersCount", "ASC"); - break; - case "+createdAt": - query.orderBy("user.createdAt", "DESC"); - break; - case "-createdAt": - query.orderBy("user.createdAt", "ASC"); - break; - case "+updatedAt": - query.orderBy("user.updatedAt", "DESC", "NULLS LAST"); - break; - case "-updatedAt": - query.orderBy("user.updatedAt", "ASC", "NULLS FIRST"); - break; - default: - query.orderBy("user.id", "ASC"); - break; - } - - query.take(ps.limit); - query.skip(ps.offset); - - const users = await query.getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts deleted file mode 100644 index a61823297b..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ /dev/null @@ -1,44 +0,0 @@ -import define from "../../define.js"; -import { Users } from "@/models/index.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { publishInternalEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - 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, - }); - - publishInternalEvent("userChangeSilencedState", { - id: user.id, - isSilenced: true, - }); - - insertModerationLog(me, "silence", { - targetId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts deleted file mode 100644 index 984bc0789e..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ /dev/null @@ -1,87 +0,0 @@ -import define from "../../define.js"; -import deleteFollowing from "@/services/following/delete.js"; -import { Users, Followings, Notifications } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { doPostSuspend } from "@/services/suspend-user.js"; -import { publishUserEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: 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"); - } - - 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.findBy({ - followerId: follower.id, - }); - - for (const following of followings) { - const followee = await Users.findOneBy({ - id: following.followeeId, - }); - - if (followee == null) { - throw new Error(`Cant find followee ${following.followeeId}`); - } - - await deleteFollowing(follower, followee, true); - } -} - -async function readAllNotify(notifier: User) { - await Notifications.update( - { - notifierId: notifier.id, - isRead: false, - }, - { - isRead: true, - }, - ); -} diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts deleted file mode 100644 index 6a01b8e8d3..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ /dev/null @@ -1,40 +0,0 @@ -import define from "../../define.js"; -import { Users } from "@/models/index.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { publishInternalEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error("user not found"); - } - - await Users.update(user.id, { - isSilenced: false, - }); - - publishInternalEvent("userChangeSilencedState", { - id: user.id, - isSilenced: false, - }); - - insertModerationLog(me, "unsilence", { - targetId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts deleted file mode 100644 index e51d5851c2..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ /dev/null @@ -1,37 +0,0 @@ -import define from "../../define.js"; -import { Users } from "@/models/index.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { doPostUnsuspend } from "@/services/unsuspend-user.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error("user not found"); - } - - await Users.update(user.id, { - isSuspended: false, - }); - - insertModerationLog(me, "unsuspend", { - targetId: user.id, - }); - - doPostUnsuspend(user); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts deleted file mode 100644 index f7e79b64b5..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ /dev/null @@ -1,543 +0,0 @@ -import { Meta } from "@/models/entities/meta.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { DB_MAX_NOTE_TEXT_LENGTH } from "@/misc/hard-limits.js"; -import { db } from "@/db/postgre.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireAdmin: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - disableRegistration: { type: "boolean", nullable: true }, - disableLocalTimeline: { type: "boolean", nullable: true }, - disableRecommendedTimeline: { type: "boolean", nullable: true }, - disableGlobalTimeline: { type: "boolean", nullable: true }, - defaultReaction: { type: "string", nullable: true }, - recommendedInstances: { - type: "array", - nullable: true, - items: { - type: "string", - }, - }, - pinnedUsers: { - type: "array", - nullable: true, - items: { - type: "string", - }, - }, - customMOTD: { - type: "array", - nullable: true, - items: { - type: "string", - }, - }, - customSplashIcons: { - type: "array", - nullable: true, - items: { - type: "string", - }, - }, - hiddenTags: { - type: "array", - nullable: true, - items: { - type: "string", - }, - }, - blockedHosts: { - type: "array", - nullable: true, - items: { - type: "string", - }, - }, - allowedHosts: { - type: "array", - nullable: true, - items: { - type: "string", - }, - }, - secureMode: { type: "boolean", nullable: true }, - privateMode: { type: "boolean", nullable: true }, - themeColor: { - type: "string", - nullable: true, - pattern: "^#[0-9a-fA-F]{6}$", - }, - mascotImageUrl: { type: "string", nullable: true }, - bannerUrl: { type: "string", nullable: true }, - logoImageUrl: { type: "string", nullable: true }, - errorImageUrl: { type: "string", nullable: true }, - iconUrl: { type: "string", nullable: true }, - backgroundImageUrl: { type: "string", nullable: true }, - name: { type: "string", nullable: true }, - description: { type: "string", nullable: true }, - defaultLightTheme: { type: "string", nullable: true }, - defaultDarkTheme: { type: "string", nullable: true }, - localDriveCapacityMb: { type: "integer" }, - remoteDriveCapacityMb: { type: "integer" }, - cacheRemoteFiles: { type: "boolean" }, - emailRequiredForSignup: { type: "boolean" }, - enableHcaptcha: { type: "boolean" }, - hcaptchaSiteKey: { type: "string", nullable: true }, - hcaptchaSecretKey: { type: "string", nullable: true }, - enableRecaptcha: { type: "boolean" }, - recaptchaSiteKey: { type: "string", nullable: true }, - recaptchaSecretKey: { type: "string", nullable: true }, - sensitiveMediaDetection: { - type: "string", - enum: ["none", "all", "local", "remote"], - }, - sensitiveMediaDetectionSensitivity: { - type: "string", - enum: ["medium", "low", "high", "veryLow", "veryHigh"], - }, - setSensitiveFlagAutomatically: { type: "boolean" }, - enableSensitiveMediaDetectionForVideos: { type: "boolean" }, - proxyAccountId: { type: "string", format: "misskey:id", nullable: true }, - maintainerName: { type: "string", nullable: true }, - maintainerEmail: { type: "string", nullable: true }, - pinnedPages: { - type: "array", - items: { - type: "string", - }, - }, - pinnedClipId: { type: "string", format: "misskey:id", nullable: true }, - langs: { - type: "array", - items: { - type: "string", - }, - }, - summalyProxy: { type: "string", nullable: true }, - deeplAuthKey: { type: "string", nullable: true }, - deeplIsPro: { type: "boolean" }, - enableTwitterIntegration: { type: "boolean" }, - twitterConsumerKey: { type: "string", nullable: true }, - twitterConsumerSecret: { type: "string", nullable: true }, - enableGithubIntegration: { type: "boolean" }, - githubClientId: { type: "string", nullable: true }, - githubClientSecret: { type: "string", nullable: true }, - enableDiscordIntegration: { type: "boolean" }, - discordClientId: { type: "string", nullable: true }, - discordClientSecret: { type: "string", nullable: true }, - enableEmail: { type: "boolean" }, - email: { type: "string", nullable: true }, - smtpSecure: { type: "boolean" }, - smtpHost: { type: "string", nullable: true }, - smtpPort: { type: "integer", nullable: true }, - smtpUser: { type: "string", nullable: true }, - smtpPass: { type: "string", nullable: true }, - enableServiceWorker: { type: "boolean" }, - swPublicKey: { type: "string", nullable: true }, - swPrivateKey: { type: "string", nullable: true }, - tosUrl: { type: "string", nullable: true }, - repositoryUrl: { type: "string" }, - feedbackUrl: { type: "string" }, - useObjectStorage: { type: "boolean" }, - objectStorageBaseUrl: { type: "string", nullable: true }, - objectStorageBucket: { type: "string", nullable: true }, - objectStoragePrefix: { type: "string", nullable: true }, - objectStorageEndpoint: { type: "string", nullable: true }, - objectStorageRegion: { type: "string", nullable: true }, - objectStoragePort: { type: "integer", nullable: true }, - objectStorageAccessKey: { type: "string", nullable: true }, - objectStorageSecretKey: { type: "string", nullable: true }, - objectStorageUseSSL: { type: "boolean" }, - objectStorageUseProxy: { type: "boolean" }, - objectStorageSetPublicRead: { type: "boolean" }, - objectStorageS3ForcePathStyle: { type: "boolean" }, - enableIpLogging: { type: "boolean" }, - enableActiveEmailValidation: { type: "boolean" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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.disableRecommendedTimeline === "boolean") { - set.disableRecommendedTimeline = ps.disableRecommendedTimeline; - } - - if (typeof ps.disableGlobalTimeline === "boolean") { - set.disableGlobalTimeline = ps.disableGlobalTimeline; - } - - if (typeof ps.defaultReaction === "string") { - set.defaultReaction = ps.defaultReaction; - } - - if (Array.isArray(ps.pinnedUsers)) { - set.pinnedUsers = ps.pinnedUsers.filter(Boolean); - } - - if (Array.isArray(ps.customMOTD)) { - set.customMOTD = ps.customMOTD.filter(Boolean); - } - - if (Array.isArray(ps.customSplashIcons)) { - set.customSplashIcons = ps.customSplashIcons.filter(Boolean); - } - - if (Array.isArray(ps.recommendedInstances)) { - set.recommendedInstances = ps.recommendedInstances.filter(Boolean); - } - - if (Array.isArray(ps.hiddenTags)) { - set.hiddenTags = ps.hiddenTags.filter(Boolean); - } - - if (Array.isArray(ps.blockedHosts)) { - let lastValue = ""; - set.blockedHosts = ps.blockedHosts.sort().filter((h) => { - const lv = lastValue; - lastValue = h; - return h !== "" && h !== lv; - }); - } - - if (ps.themeColor !== undefined) { - set.themeColor = ps.themeColor; - } - - if (Array.isArray(ps.allowedHosts)) { - set.allowedHosts = ps.allowedHosts.filter(Boolean); - } - - if (typeof ps.privateMode === "boolean") { - set.privateMode = ps.privateMode; - } - - if (typeof ps.secureMode === "boolean") { - set.secureMode = ps.secureMode; - } - - if (ps.mascotImageUrl !== undefined) { - set.mascotImageUrl = ps.mascotImageUrl; - } - - if (ps.bannerUrl !== undefined) { - set.bannerUrl = ps.bannerUrl; - } - - if (ps.logoImageUrl !== undefined) { - set.logoImageUrl = ps.logoImageUrl; - } - - 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.defaultLightTheme !== undefined) { - set.defaultLightTheme = ps.defaultLightTheme; - } - - if (ps.defaultDarkTheme !== undefined) { - set.defaultDarkTheme = ps.defaultDarkTheme; - } - - 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.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.sensitiveMediaDetection !== undefined) { - set.sensitiveMediaDetection = ps.sensitiveMediaDetection; - } - - if (ps.sensitiveMediaDetectionSensitivity !== undefined) { - set.sensitiveMediaDetectionSensitivity = - ps.sensitiveMediaDetectionSensitivity; - } - - if (ps.setSensitiveFlagAutomatically !== undefined) { - set.setSensitiveFlagAutomatically = ps.setSensitiveFlagAutomatically; - } - - if (ps.enableSensitiveMediaDetectionForVideos !== undefined) { - set.enableSensitiveMediaDetectionForVideos = - ps.enableSensitiveMediaDetectionForVideos; - } - - 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; - } - - if (ps.enableIpLogging !== undefined) { - set.enableIpLogging = ps.enableIpLogging; - } - - if (ps.enableActiveEmailValidation !== undefined) { - set.enableActiveEmailValidation = ps.enableActiveEmailValidation; - } - - await db.transaction(async (transactionalEntityManager) => { - const metas = await transactionalEntityManager.find(Meta, { - order: { - id: "DESC", - }, - }); - - const meta = metas[0]; - - if (meta) { - await transactionalEntityManager.update(Meta, meta.id, set); - } else { - await transactionalEntityManager.save(Meta, set); - } - }); - - insertModerationLog(me, "updateMeta"); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts deleted file mode 100644 index 04870d1c1c..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { UserProfiles, Users } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - text: { type: "string" }, - }, - required: ["userId", "text"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new Error("user not found"); - } - - await UserProfiles.update( - { userId: user.id }, - { - moderationNote: ps.text, - }, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/vacuum.ts b/packages/backend/src/server/api/endpoints/admin/vacuum.ts deleted file mode 100644 index 559b310e46..0000000000 --- a/packages/backend/src/server/api/endpoints/admin/vacuum.ts +++ /dev/null @@ -1,35 +0,0 @@ -import define from "../../define.js"; -import { insertModerationLog } from "@/services/insert-moderation-log.js"; -import { db } from "@/db/postgre.js"; - -export const meta = { - tags: ["admin"], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - full: { type: "boolean" }, - analyze: { type: "boolean" }, - }, - required: ["full", "analyze"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const params: string[] = []; - - if (ps.full) { - params.push("FULL"); - } - - if (ps.analyze) { - params.push("ANALYZE"); - } - - db.query(`VACUUM ${params.join(" ")}`); - - insertModerationLog(me, "vacuum", ps); -}); diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts deleted file mode 100644 index 00634cc421..0000000000 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Announcements, AnnouncementReads } from "@/models/index.js"; -import define from "../define.js"; -import { makePaginationQuery } from "../common/make-pagination-query.js"; - -export const meta = { - tags: ["meta"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - example: "xxxxxxxxxx", - }, - createdAt: { - type: "string", - optional: false, - nullable: false, - format: "date-time", - }, - updatedAt: { - type: "string", - optional: false, - nullable: true, - format: "date-time", - }, - text: { - type: "string", - optional: false, - nullable: false, - }, - title: { - type: "string", - optional: false, - nullable: false, - }, - imageUrl: { - type: "string", - optional: false, - nullable: true, - }, - isRead: { - type: "boolean", - optional: true, - nullable: false, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - withUnreads: { type: "boolean", default: false }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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.findBy({ - 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 - ).map((a) => ({ - ...a, - createdAt: a.createdAt.toISOString(), - updatedAt: a.updatedAt?.toISOString() ?? null, - })); -}); diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts deleted file mode 100644 index c1ba7bcdfd..0000000000 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ /dev/null @@ -1,141 +0,0 @@ -import define from "../../define.js"; -import { genId } from "@/misc/gen-id.js"; -import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js"; -import { ApiError } from "../../error.js"; -import { publishInternalEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["antennas"], - - requireCredential: true, - - kind: "write:account", - - 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", - optional: false, - nullable: false, - ref: "Antenna", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string", minLength: 1, maxLength: 100 }, - src: { - type: "string", - enum: ["home", "all", "users", "list", "group", "instances"], - }, - userListId: { type: "string", format: "misskey:id", nullable: true }, - userGroupId: { type: "string", format: "misskey:id", nullable: true }, - keywords: { - type: "array", - items: { - type: "array", - items: { - type: "string", - }, - }, - }, - excludeKeywords: { - type: "array", - items: { - type: "array", - items: { - type: "string", - }, - }, - }, - users: { - type: "array", - items: { - type: "string", - }, - }, - instances: { - type: "array", - items: { - type: "string", - }, - }, - caseSensitive: { type: "boolean" }, - withReplies: { type: "boolean" }, - withFile: { type: "boolean" }, - notify: { type: "boolean" }, - }, - required: [ - "name", - "src", - "keywords", - "excludeKeywords", - "users", - "instances", - "caseSensitive", - "withReplies", - "withFile", - "notify", - ], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - if (user.movedToUri != null) throw new ApiError(meta.errors.noSuchUserGroup); - let userList; - let userGroupJoining; - - if (ps.src === "list" && ps.userListId) { - userList = await UserLists.findOneBy({ - 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.findOneBy({ - 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, - instances: ps.instances, - caseSensitive: ps.caseSensitive, - withReplies: ps.withReplies, - withFile: ps.withFile, - notify: ps.notify, - }).then((x) => Antennas.findOneByOrFail(x.identifiers[0])); - - publishInternalEvent("antennaCreated", antenna); - - return await Antennas.pack(antenna); -}); diff --git a/packages/backend/src/server/api/endpoints/antennas/delete.ts b/packages/backend/src/server/api/endpoints/antennas/delete.ts deleted file mode 100644 index a6cf79011a..0000000000 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ /dev/null @@ -1,43 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Antennas } from "@/models/index.js"; -import { publishInternalEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["antennas"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchAntenna: { - message: "No such antenna.", - code: "NO_SUCH_ANTENNA", - id: "b34dcf9d-348f-44bb-99d0-6c9314cfe2df", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - antennaId: { type: "string", format: "misskey:id" }, - }, - required: ["antennaId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const antenna = await Antennas.findOneBy({ - id: ps.antennaId, - userId: user.id, - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } - - await Antennas.delete(antenna.id); - - publishInternalEvent("antennaDeleted", antenna); -}); diff --git a/packages/backend/src/server/api/endpoints/antennas/list.ts b/packages/backend/src/server/api/endpoints/antennas/list.ts deleted file mode 100644 index 929b761d4f..0000000000 --- a/packages/backend/src/server/api/endpoints/antennas/list.ts +++ /dev/null @@ -1,36 +0,0 @@ -import define from "../../define.js"; -import { Antennas } from "@/models/index.js"; - -export const meta = { - tags: ["antennas", "account"], - - requireCredential: true, - - kind: "read:account", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Antenna", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const antennas = await Antennas.findBy({ - userId: me.id, - }); - - return await Promise.all(antennas.map((x) => Antennas.pack(x))); -}); diff --git a/packages/backend/src/server/api/endpoints/antennas/markread.ts b/packages/backend/src/server/api/endpoints/antennas/markread.ts deleted file mode 100644 index e29e13bbbb..0000000000 --- a/packages/backend/src/server/api/endpoints/antennas/markread.ts +++ /dev/null @@ -1,43 +0,0 @@ -import define from "../../define.js"; -import { Antennas, AntennaNotes } from "@/models/index.js"; -import { FindOptionsWhere } from "typeorm"; -import { AntennaNote } from "@/models/entities/antenna-note.js"; - -export const meta = { - tags: ["antennas", "account"], - - requireCredential: true, - - kind: "write:account", -} as const; - -export const paramDef = { - type: "object", - properties: { - antennaId: { type: "string", format: "misskey:id" }, - }, - required: ["antennaId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const antenna = await Antennas.findOneBy({ - userId: me.id, - id: ps.antennaId, - }); - - if (!antenna) { - return null; - } - - await AntennaNotes.update( - { - antennaId: antenna.id, - read: false, - }, - { - read: true, - }, - ); - - return true; -}); diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts deleted file mode 100644 index d011c5fb80..0000000000 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ /dev/null @@ -1,97 +0,0 @@ -import define from "../../define.js"; -import readNote from "@/services/note/read.js"; -import { Antennas, Notes, AntennaNotes } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { ApiError } from "../../error.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; - -export const meta = { - tags: ["antennas", "account", "notes"], - - requireCredential: true, - - kind: "read:account", - - errors: { - noSuchAntenna: { - message: "No such antenna.", - code: "NO_SUCH_ANTENNA", - id: "850926e0-fd3b-49b6-b69a-b28a5dbd82fe", - }, - }, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - antennaId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - }, - required: ["antennaId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const antenna = await Antennas.findOneBy({ - id: ps.antennaId, - userId: user.id, - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } - - const query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ps.sinceDate, - ps.untilDate, - ) - .innerJoin( - AntennaNotes.metadata.targetName, - "antennaNote", - "antennaNote.noteId = note.id", - ) - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") - .andWhere("antennaNote.antennaId = :antennaId", { antennaId: antenna.id }); - - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - - const notes = await query.take(ps.limit).getMany(); - - if (notes.length > 0) { - readNote(user.id, notes); - } - - return await Notes.packMany(notes, user); -}); diff --git a/packages/backend/src/server/api/endpoints/antennas/show.ts b/packages/backend/src/server/api/endpoints/antennas/show.ts deleted file mode 100644 index 350d739216..0000000000 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ /dev/null @@ -1,48 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Antennas } from "@/models/index.js"; - -export const meta = { - tags: ["antennas", "account"], - - requireCredential: true, - - kind: "read:account", - - errors: { - noSuchAntenna: { - message: "No such antenna.", - code: "NO_SUCH_ANTENNA", - id: "c06569fb-b025-4f23-b22d-1fcd20d2816b", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Antenna", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - antennaId: { type: "string", format: "misskey:id" }, - }, - required: ["antennaId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the antenna - const antenna = await Antennas.findOneBy({ - id: ps.antennaId, - userId: me.id, - }); - - if (antenna == null) { - throw new ApiError(meta.errors.noSuchAntenna); - } - - return await Antennas.pack(antenna); -}); diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts deleted file mode 100644 index f491c0b638..0000000000 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ /dev/null @@ -1,157 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js"; -import { publishInternalEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["antennas"], - - requireCredential: true, - - kind: "write:account", - - 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", - optional: false, - nullable: false, - ref: "Antenna", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - antennaId: { type: "string", format: "misskey:id" }, - name: { type: "string", minLength: 1, maxLength: 100 }, - src: { - type: "string", - enum: ["home", "all", "users", "list", "group", "instances"], - }, - userListId: { type: "string", format: "misskey:id", nullable: true }, - userGroupId: { type: "string", format: "misskey:id", nullable: true }, - keywords: { - type: "array", - items: { - type: "array", - items: { - type: "string", - }, - }, - }, - excludeKeywords: { - type: "array", - items: { - type: "array", - items: { - type: "string", - }, - }, - }, - users: { - type: "array", - items: { - type: "string", - }, - }, - instances: { - type: "array", - items: { - type: "string", - }, - }, - caseSensitive: { type: "boolean" }, - withReplies: { type: "boolean" }, - withFile: { type: "boolean" }, - notify: { type: "boolean" }, - }, - required: [ - "antennaId", - "name", - "src", - "keywords", - "excludeKeywords", - "users", - "instances", - "caseSensitive", - "withReplies", - "withFile", - "notify", - ], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch the antenna - const antenna = await Antennas.findOneBy({ - 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.findOneBy({ - 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.findOneBy({ - 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, - instances: ps.instances, - caseSensitive: ps.caseSensitive, - withReplies: ps.withReplies, - withFile: ps.withFile, - notify: ps.notify, - }); - - publishInternalEvent( - "antennaUpdated", - await Antennas.findOneByOrFail({ id: antenna.id }), - ); - - return await Antennas.pack(antenna.id); -}); diff --git a/packages/backend/src/server/api/endpoints/ap/get.ts b/packages/backend/src/server/api/endpoints/ap/get.ts deleted file mode 100644 index f0db67a343..0000000000 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ /dev/null @@ -1,36 +0,0 @@ -import define from "../../define.js"; -import Resolver from "@/remote/activitypub/resolver.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["federation"], - - requireCredential: true, - - limit: { - duration: HOUR, - max: 30, - }, - - errors: {}, - - res: { - type: "object", - optional: false, - nullable: false, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - uri: { type: "string" }, - }, - required: ["uri"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const resolver = new Resolver(); - const object = await resolver.resolve(ps.uri); - return object; -}); diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts deleted file mode 100644 index fa804802eb..0000000000 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ /dev/null @@ -1,164 +0,0 @@ -import define from "../../define.js"; -import config from "@/config/index.js"; -import { createPerson } from "@/remote/activitypub/models/person.js"; -import { createNote } from "@/remote/activitypub/models/note.js"; -import DbResolver from "@/remote/activitypub/db-resolver.js"; -import Resolver from "@/remote/activitypub/resolver.js"; -import { ApiError } from "../../error.js"; -import { extractDbHost } from "@/misc/convert-host.js"; -import { Users, Notes } from "@/models/index.js"; -import type { Note } from "@/models/entities/note.js"; -import type { CacheableLocalUser, User } from "@/models/entities/user.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { isActor, isPost, getApId } from "@/remote/activitypub/type.js"; -import type { SchemaType } from "@/misc/schema.js"; -import { HOUR } from "@/const.js"; -import { shouldBlockInstance } from "@/misc/should-block-instance.js"; - -export const meta = { - tags: ["federation"], - - requireCredential: true, - - limit: { - duration: HOUR, - max: 30, - }, - - errors: { - noSuchObject: { - message: "No such object.", - code: "NO_SUCH_OBJECT", - id: "dc94d745-1262-4e63-a17d-fecaa57efc82", - }, - }, - - res: { - optional: false, - nullable: false, - oneOf: [ - { - type: "object", - properties: { - type: { - type: "string", - optional: false, - nullable: false, - enum: ["User"], - }, - object: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailedNotMe", - }, - }, - }, - { - type: "object", - properties: { - type: { - type: "string", - optional: false, - nullable: false, - enum: ["Note"], - }, - object: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - }, - ], - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - uri: { type: "string" }, - }, - required: ["uri"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const object = await fetchAny(ps.uri, me); - if (object) { - return object; - } else { - throw new ApiError(meta.errors.noSuchObject); - } -}); - -/*** - * Resolve User or Note from URI - */ -async function fetchAny( - uri: string, - me: CacheableLocalUser | null | undefined, -): Promise | null> { - // Wait if blocked. - if (await shouldBlockInstance(extractDbHost(uri))) return null; - - const dbResolver = new DbResolver(); - - let local = await mergePack( - me, - ...(await Promise.all([ - dbResolver.getUserFromApId(uri), - dbResolver.getNoteFromApId(uri), - ])), - ); - if (local != null) return local; - - // fetching Object once from remote - const resolver = new Resolver(); - const object = (await resolver.resolve(uri)) as any; - - // /@user If a URI other than the id is specified, - // the URI is determined here - if (uri !== object.id) { - local = await mergePack( - me, - ...(await Promise.all([ - dbResolver.getUserFromApId(object.id), - dbResolver.getNoteFromApId(object.id), - ])), - ); - if (local != null) return local; - } - - return await mergePack( - me, - isActor(object) ? await createPerson(getApId(object)) : null, - isPost(object) ? await createNote(getApId(object), undefined, true) : null, - ); -} - -async function mergePack( - me: CacheableLocalUser | null | undefined, - user: User | null | undefined, - note: Note | null | undefined, -): Promise | null> { - if (user != null) { - return { - type: "User", - object: await Users.pack(user, me, { detail: true }), - }; - } else if (note != null) { - try { - const object = await Notes.pack(note, me, { detail: true }); - - return { - type: "Note", - object, - }; - } catch (e) { - return null; - } - } - - return null; -} diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts deleted file mode 100644 index 013c5a10b9..0000000000 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ /dev/null @@ -1,67 +0,0 @@ -import define from "../../define.js"; -import { Apps } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { unique } from "@/prelude/array.js"; -import { secureRndstr } from "@/misc/secure-rndstr.js"; - -export const meta = { - tags: ["app"], - - requireCredential: false, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "App", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string" }, - description: { type: "string" }, - permission: { - type: "array", - uniqueItems: true, - items: { - type: "string", - }, - }, - callbackUrl: { type: "string", nullable: true }, - }, - required: ["name", "description", "permission"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - if (user?.movedToUri != null) - return await Apps.pack("", null, { - detail: true, - includeSecret: true, - }); - // 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.insert({ - id: genId(), - createdAt: new Date(), - userId: user ? user.id : null, - name: ps.name, - description: ps.description, - permission, - callbackUrl: ps.callbackUrl, - secret: secret, - }).then((x) => Apps.findOneByOrFail(x.identifiers[0])); - - return await Apps.pack(app, null, { - detail: true, - includeSecret: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/app/show.ts b/packages/backend/src/server/api/endpoints/app/show.ts deleted file mode 100644 index 60949512e0..0000000000 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ /dev/null @@ -1,46 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Apps } from "@/models/index.js"; - -export const meta = { - tags: ["app"], - - errors: { - noSuchApp: { - message: "No such app.", - code: "NO_SUCH_APP", - id: "dce83913-2dc6-4093-8a7b-71dbb11718a3", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "App", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - appId: { type: "string", format: "misskey:id" }, - }, - required: ["appId"], -} as const; - -export default define(meta, paramDef, async (ps, user, token) => { - const isSecure = user != null && token == null; - - // Lookup app - const ap = await Apps.findOneBy({ id: ps.appId }); - - if (ap == null) { - throw new ApiError(meta.errors.noSuchApp); - } - - return await Apps.pack(ap, user, { - detail: true, - includeSecret: isSecure && ap.userId === user!.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts deleted file mode 100644 index 35565e2560..0000000000 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as crypto from "node:crypto"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { AuthSessions, AccessTokens, Apps } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { secureRndstr } from "@/misc/secure-rndstr.js"; - -export const meta = { - tags: ["auth"], - - requireCredential: true, - - secure: true, - - errors: { - noSuchSession: { - message: "No such session.", - code: "NO_SUCH_SESSION", - id: "9c72d8de-391a-43c1-9d06-08d29efde8df", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - token: { type: "string" }, - }, - required: ["token"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch token - const session = await AuthSessions.findOneBy({ 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.findOneBy({ - appId: session.appId, - userId: user.id, - }); - - if (exist == null) { - // Lookup app - const app = await Apps.findOneByOrFail({ id: session.appId }); - - // Generate Hash - const sha256 = crypto.createHash("sha256"); - sha256.update(accessToken + app.secret); - const hash = sha256.digest("hex"); - - const now = new Date(); - - // Insert access token doc - await AccessTokens.insert({ - id: genId(), - createdAt: now, - lastUsedAt: now, - appId: session.appId, - userId: user.id, - token: accessToken, - hash: hash, - }); - } - - // Update session - await AuthSessions.update(session.id, { - userId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts deleted file mode 100644 index 1defb94006..0000000000 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { v4 as uuid } from "uuid"; -import config from "@/config/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { Apps, AuthSessions } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; - -export const meta = { - tags: ["auth"], - - requireCredential: false, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - token: { - type: "string", - optional: false, - nullable: false, - }, - url: { - type: "string", - optional: false, - nullable: false, - format: "url", - }, - }, - }, - - errors: { - noSuchApp: { - message: "No such app.", - code: "NO_SUCH_APP", - id: "92f93e63-428e-4f2f-a5a4-39e1407fe998", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - appSecret: { type: "string" }, - }, - required: ["appSecret"], -} as const; - -export default define(meta, paramDef, async (ps) => { - // Lookup app - const app = await Apps.findOneBy({ - 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.insert({ - id: genId(), - createdAt: new Date(), - appId: app.id, - token: token, - }).then((x) => AuthSessions.findOneByOrFail(x.identifiers[0])); - - return { - token: doc.token, - url: `${config.authUrl}/${doc.token}`, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/auth/session/show.ts b/packages/backend/src/server/api/endpoints/auth/session/show.ts deleted file mode 100644 index 01a5fe5dc7..0000000000 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ /dev/null @@ -1,63 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { AuthSessions } from "@/models/index.js"; - -export const meta = { - tags: ["auth"], - - requireCredential: false, - - errors: { - noSuchSession: { - message: "No such session.", - code: "NO_SUCH_SESSION", - id: "bd72c97d-eba7-4adb-a467-f171b8847250", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - app: { - type: "object", - optional: false, - nullable: false, - ref: "App", - }, - token: { - type: "string", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - token: { type: "string" }, - }, - required: ["token"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Lookup session - const session = await AuthSessions.findOneBy({ - token: ps.token, - }); - - if (session == null) { - throw new ApiError(meta.errors.noSuchSession); - } - - return await AuthSessions.pack(session, user); -}); diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts deleted file mode 100644 index 0e97bf414e..0000000000 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ /dev/null @@ -1,99 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { Apps, AuthSessions, AccessTokens, Users } from "@/models/index.js"; - -export const meta = { - tags: ["auth"], - - requireCredential: false, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - accessToken: { - type: "string", - optional: false, - nullable: false, - }, - - user: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailedNotMe", - }, - }, - }, - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - appSecret: { type: "string" }, - token: { type: "string" }, - }, - required: ["appSecret", "token"], -} as const; - -export default define(meta, paramDef, async (ps) => { - // Lookup app - const app = await Apps.findOneBy({ - secret: ps.appSecret, - }); - - if (app == null) { - throw new ApiError(meta.errors.noSuchApp); - } - - // Fetch token - const session = await AuthSessions.findOneBy({ - 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.findOneByOrFail({ - appId: app.id, - userId: session.userId, - }); - - // Delete session - AuthSessions.delete(session.id); - - return { - accessToken: accessToken.token, - user: await Users.pack(session.userId, null, { - detail: true, - }), - }; -}); diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts deleted file mode 100644 index 4bd58d5ef5..0000000000 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ /dev/null @@ -1,91 +0,0 @@ -import create from "@/services/blocking/create.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; -import { Blockings, NoteWatchings, Users } from "@/models/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["account"], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: "write:blocks", - - 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", - optional: false, - nullable: false, - ref: "UserDetailedNotMe", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const blocker = await Users.findOneByOrFail({ id: 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.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyBlocking); - } - - await create(blocker, blockee); - - NoteWatchings.delete({ - userId: blocker.id, - noteUserId: blockee.id, - }); - - return await Users.pack(blockee.id, blocker, { - detail: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts deleted file mode 100644 index 6c4ca27755..0000000000 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ /dev/null @@ -1,87 +0,0 @@ -import deleteBlocking from "@/services/blocking/delete.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; -import { Blockings, Users } from "@/models/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["account"], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: "write:blocks", - - 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", - optional: false, - nullable: false, - ref: "UserDetailedNotMe", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const blocker = await Users.findOneByOrFail({ id: 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.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notBlocking); - } - - // Delete blocking - await deleteBlocking(blocker, blockee); - - return await Users.pack(blockee.id, blocker, { - detail: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/blocking/list.ts b/packages/backend/src/server/api/endpoints/blocking/list.ts deleted file mode 100644 index 83fca7b42c..0000000000 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../define.js"; -import { Blockings } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "read:blocks", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Blocking", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Blockings.createQueryBuilder("blocking"), - ps.sinceId, - ps.untilId, - ).andWhere("blocking.blockerId = :meId", { meId: me.id }); - - const blockings = await query.take(ps.limit).getMany(); - - return await Blockings.packMany(blockings, me); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts deleted file mode 100644 index 26a3448b2b..0000000000 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ /dev/null @@ -1,68 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Channels, DriveFiles } from "@/models/index.js"; -import type { Channel } from "@/models/entities/channel.js"; -import { genId } from "@/misc/gen-id.js"; - -export const meta = { - tags: ["channels"], - - requireCredential: true, - - kind: "write:channels", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Channel", - }, - - errors: { - noSuchFile: { - message: "No such file.", - code: "NO_SUCH_FILE", - id: "cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string", minLength: 1, maxLength: 128 }, - description: { - type: "string", - nullable: true, - minLength: 1, - maxLength: 2048, - }, - bannerId: { type: "string", format: "misskey:id", nullable: true }, - }, - required: ["name"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - let banner = null; - if (ps.bannerId != null) { - banner = await DriveFiles.findOneBy({ - id: ps.bannerId, - userId: user.id, - }); - - if (banner == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - const channel = await Channels.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - description: ps.description || null, - bannerId: banner ? banner.id : null, - } as Channel).then((x) => Channels.findOneByOrFail(x.identifiers[0])); - - return await Channels.pack(channel, user); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/featured.ts b/packages/backend/src/server/api/endpoints/channels/featured.ts deleted file mode 100644 index 06e0e850fd..0000000000 --- a/packages/backend/src/server/api/endpoints/channels/featured.ts +++ /dev/null @@ -1,37 +0,0 @@ -import define from "../../define.js"; -import { Channels } from "@/models/index.js"; - -export const meta = { - tags: ["channels"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Channel", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = Channels.createQueryBuilder("channel") - .where("channel.lastNotedAt IS NOT NULL") - .orderBy("channel.lastNotedAt", "DESC"); - - const channels = await query.take(10).getMany(); - - return await Promise.all(channels.map((x) => Channels.pack(x, me))); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/follow.ts b/packages/backend/src/server/api/endpoints/channels/follow.ts deleted file mode 100644 index de0554383e..0000000000 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ /dev/null @@ -1,48 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Channels, ChannelFollowings } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { publishUserEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["channels"], - - requireCredential: true, - - kind: "write:channels", - - errors: { - noSuchChannel: { - message: "No such channel.", - code: "NO_SUCH_CHANNEL", - id: "c0031718-d573-4e85-928e-10039f1fbb68", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - channelId: { type: "string", format: "misskey:id" }, - }, - required: ["channelId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - - await ChannelFollowings.insert({ - id: genId(), - createdAt: new Date(), - followerId: user.id, - followeeId: channel.id, - }); - - publishUserEvent(user.id, "followChannel", channel); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/followed.ts b/packages/backend/src/server/api/endpoints/channels/followed.ts deleted file mode 100644 index 993a211f7e..0000000000 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ /dev/null @@ -1,59 +0,0 @@ -import define from "../../define.js"; -import { Channels, ChannelFollowings } from "@/models/index.js"; - -export const meta = { - tags: ["channels", "account"], - - requireCredential: true, - - kind: "read:channels", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Channel", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 5 }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = ChannelFollowings.createQueryBuilder("following").andWhere({ - followerId: me.id, - }); - if (ps.sinceId) { - query.andWhere('following."followeeId" > :sinceId', { - sinceId: ps.sinceId, - }); - } - if (ps.untilId) { - query.andWhere('following."followeeId" < :untilId', { - untilId: ps.untilId, - }); - } - if (ps.sinceId && !ps.untilId) { - query.orderBy('following."followeeId"', "ASC"); - } else { - query.orderBy('following."followeeId"', "DESC"); - } - - const followings = await query.take(ps.limit).getMany(); - - return await Promise.all( - followings.map((x) => Channels.pack(x.followeeId, me)), - ); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts deleted file mode 100644 index 78d9e80ccf..0000000000 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../define.js"; -import { Channels } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["channels", "account"], - - requireCredential: true, - - kind: "read:channels", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Channel", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 5 }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Channels.createQueryBuilder(), - ps.sinceId, - ps.untilId, - ).andWhere({ userId: me.id }); - - const channels = await query.take(ps.limit).getMany(); - - return await Promise.all(channels.map((x) => Channels.pack(x, me))); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/show.ts b/packages/backend/src/server/api/endpoints/channels/show.ts deleted file mode 100644 index e4ca756634..0000000000 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Channels } from "@/models/index.js"; - -export const meta = { - tags: ["channels"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Channel", - }, - - errors: { - noSuchChannel: { - message: "No such channel.", - code: "NO_SUCH_CHANNEL", - id: "6f6c314b-7486-4897-8966-c04a66a02923", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - channelId: { type: "string", format: "misskey:id" }, - }, - required: ["channelId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - - return await Channels.pack(channel, me); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts deleted file mode 100644 index b5d5325234..0000000000 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ /dev/null @@ -1,84 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Notes, Channels } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { activeUsersChart } from "@/services/chart/index.js"; - -export const meta = { - tags: ["notes", "channels"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - noSuchChannel: { - message: "No such channel.", - code: "NO_SUCH_CHANNEL", - id: "4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - channelId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - }, - required: ["channelId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const channel = await Channels.findOneBy({ - 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") - .leftJoinAndSelect("note.channel", "channel"); - //#endregion - - const timeline = await query.take(ps.limit).getMany(); - - if (user) activeUsersChart.read(user); - - return await Notes.packMany(timeline, user); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/unfollow.ts b/packages/backend/src/server/api/endpoints/channels/unfollow.ts deleted file mode 100644 index 654a4fbba5..0000000000 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Channels, ChannelFollowings } from "@/models/index.js"; -import { publishUserEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["channels"], - - requireCredential: true, - - kind: "write:channels", - - errors: { - noSuchChannel: { - message: "No such channel.", - code: "NO_SUCH_CHANNEL", - id: "19959ee9-0153-4c51-bbd9-a98c49dc59d6", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - channelId: { type: "string", format: "misskey:id" }, - }, - required: ["channelId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - - await ChannelFollowings.delete({ - followerId: user.id, - followeeId: channel.id, - }); - - publishUserEvent(user.id, "unfollowChannel", channel); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts deleted file mode 100644 index d9f6f7644c..0000000000 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ /dev/null @@ -1,90 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Channels, DriveFiles } from "@/models/index.js"; - -export const meta = { - tags: ["channels"], - - requireCredential: true, - - kind: "write:channels", - - res: { - type: "object", - optional: false, - nullable: false, - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - channelId: { type: "string", format: "misskey:id" }, - name: { type: "string", minLength: 1, maxLength: 128 }, - description: { - type: "string", - nullable: true, - minLength: 1, - maxLength: 2048, - }, - bannerId: { type: "string", format: "misskey:id", nullable: true }, - }, - required: ["channelId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - - if (channel.userId !== me.id) { - throw new ApiError(meta.errors.accessDenied); - } - - let banner = undefined; - if (ps.bannerId != null) { - banner = await DriveFiles.findOneBy({ - id: ps.bannerId, - userId: me.id, - }); - - if (banner == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } else if (ps.bannerId === null) { - banner = null; - } - - await Channels.update(channel.id, { - ...(ps.name !== undefined ? { name: ps.name } : {}), - ...(ps.description !== undefined ? { description: ps.description } : {}), - ...(banner ? { bannerId: banner.id } : {}), - }); - - return await Channels.pack(channel.id, me); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/active-users.ts b/packages/backend/src/server/api/endpoints/charts/active-users.ts deleted file mode 100644 index 3817a32ca9..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { activeUsersChart } from "@/services/chart/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["charts", "users"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(activeUsersChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - }, - required: ["span"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await activeUsersChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/ap-request.ts b/packages/backend/src/server/api/endpoints/charts/ap-request.ts deleted file mode 100644 index 9e9013ce53..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { apRequestChart } from "@/services/chart/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["charts"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(apRequestChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - }, - required: ["span"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await apRequestChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/drive.ts b/packages/backend/src/server/api/endpoints/charts/drive.ts deleted file mode 100644 index 03ac4c0473..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { driveChart } from "@/services/chart/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["charts", "drive"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(driveChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - }, - required: ["span"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await driveChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/federation.ts b/packages/backend/src/server/api/endpoints/charts/federation.ts deleted file mode 100644 index 5862aad56e..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { federationChart } from "@/services/chart/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["charts"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(federationChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - }, - required: ["span"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await federationChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/hashtag.ts b/packages/backend/src/server/api/endpoints/charts/hashtag.ts deleted file mode 100644 index 0af1e35ea9..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/hashtag.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { hashtagChart } from "@/services/chart/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["charts", "hashtags"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(hashtagChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - tag: { type: "string" }, - }, - required: ["span", "tag"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await hashtagChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ps.tag, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/instance.ts b/packages/backend/src/server/api/endpoints/charts/instance.ts deleted file mode 100644 index 11a1dbce1b..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { instanceChart } from "@/services/chart/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["charts"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(instanceChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - host: { type: "string" }, - }, - required: ["span", "host"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await instanceChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ps.host, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/notes.ts b/packages/backend/src/server/api/endpoints/charts/notes.ts deleted file mode 100644 index 27e69a4c9b..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { notesChart } from "@/services/chart/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["charts", "notes"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(notesChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - }, - required: ["span"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await notesChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/user/drive.ts b/packages/backend/src/server/api/endpoints/charts/user/drive.ts deleted file mode 100644 index 178ba453c6..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { perUserDriveChart } from "@/services/chart/index.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["charts", "drive", "users"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(perUserDriveChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - userId: { type: "string", format: "misskey:id" }, - }, - required: ["span", "userId"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await perUserDriveChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ps.userId, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/user/following.ts b/packages/backend/src/server/api/endpoints/charts/user/following.ts deleted file mode 100644 index 6a0c22df11..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ /dev/null @@ -1,33 +0,0 @@ -import define from "../../../define.js"; -import { getJsonSchema } from "@/services/chart/core.js"; -import { perUserFollowingChart } from "@/services/chart/index.js"; - -export const meta = { - tags: ["charts", "users", "following"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(perUserFollowingChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - userId: { type: "string", format: "misskey:id" }, - }, - required: ["span", "userId"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await perUserFollowingChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ps.userId, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/user/notes.ts b/packages/backend/src/server/api/endpoints/charts/user/notes.ts deleted file mode 100644 index d788076962..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { perUserNotesChart } from "@/services/chart/index.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["charts", "users", "notes"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(perUserNotesChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - userId: { type: "string", format: "misskey:id" }, - }, - required: ["span", "userId"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await perUserNotesChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ps.userId, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts b/packages/backend/src/server/api/endpoints/charts/user/reactions.ts deleted file mode 100644 index 5b0048c50e..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { perUserReactionsChart } from "@/services/chart/index.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["charts", "users", "reactions"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(perUserReactionsChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - userId: { type: "string", format: "misskey:id" }, - }, - required: ["span", "userId"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await perUserReactionsChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ps.userId, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/charts/users.ts b/packages/backend/src/server/api/endpoints/charts/users.ts deleted file mode 100644 index 8973f013b7..0000000000 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getJsonSchema } from "@/services/chart/core.js"; -import { usersChart } from "@/services/chart/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["charts", "users"], - requireCredentialPrivateMode: true, - - res: getJsonSchema(usersChart.schema), - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - span: { type: "string", enum: ["day", "hour"] }, - limit: { type: "integer", minimum: 1, maximum: 500, default: 30 }, - offset: { type: "integer", nullable: true, default: null }, - }, - required: ["span"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await usersChart.getChart( - ps.span, - ps.limit, - ps.offset ? new Date(ps.offset) : null, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts deleted file mode 100644 index b9d6b54c95..0000000000 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ /dev/null @@ -1,74 +0,0 @@ -import define from "../../define.js"; -import { ClipNotes, Clips } from "@/models/index.js"; -import { ApiError } from "../../error.js"; -import { genId } from "@/misc/gen-id.js"; -import { getNote } from "../../common/getters.js"; - -export const meta = { - tags: ["account", "notes", "clips"], - - requireCredential: true, - - kind: "write:account", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - clipId: { type: "string", format: "misskey:id" }, - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["clipId", "noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.findOneBy({ - id: ps.clipId, - userId: user.id, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - const exist = await ClipNotes.findOneBy({ - noteId: note.id, - clipId: clip.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyClipped); - } - - await ClipNotes.insert({ - id: genId(), - noteId: note.id, - clipId: clip.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts deleted file mode 100644 index 918e9462a4..0000000000 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ /dev/null @@ -1,46 +0,0 @@ -import define from "../../define.js"; -import { genId } from "@/misc/gen-id.js"; -import { Clips } from "@/models/index.js"; - -export const meta = { - tags: ["clips"], - - requireCredential: true, - - kind: "write:account", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Clip", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string", minLength: 1, maxLength: 100 }, - isPublic: { type: "boolean", default: false }, - description: { - type: "string", - nullable: true, - minLength: 1, - maxLength: 2048, - }, - }, - required: ["name"], -} as const; - -export default define(meta, paramDef, 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.findOneByOrFail(x.identifiers[0])); - - return await Clips.pack(clip); -}); diff --git a/packages/backend/src/server/api/endpoints/clips/delete.ts b/packages/backend/src/server/api/endpoints/clips/delete.ts deleted file mode 100644 index 8f2489dddd..0000000000 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ /dev/null @@ -1,40 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Clips } from "@/models/index.js"; - -export const meta = { - tags: ["clips"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchClip: { - message: "No such clip.", - code: "NO_SUCH_CLIP", - id: "70ca08ba-6865-4630-b6fb-8494759aa754", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - clipId: { type: "string", format: "misskey:id" }, - }, - required: ["clipId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.findOneBy({ - id: ps.clipId, - userId: user.id, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - await Clips.delete(clip.id); -}); diff --git a/packages/backend/src/server/api/endpoints/clips/list.ts b/packages/backend/src/server/api/endpoints/clips/list.ts deleted file mode 100644 index d1625ee036..0000000000 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ /dev/null @@ -1,36 +0,0 @@ -import define from "../../define.js"; -import { Clips } from "@/models/index.js"; - -export const meta = { - tags: ["clips", "account"], - - requireCredential: true, - - kind: "read:account", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Clip", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const clips = await Clips.findBy({ - userId: me.id, - }); - - return await Promise.all(clips.map((x) => Clips.pack(x))); -}); diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts deleted file mode 100644 index c641d9ba9f..0000000000 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ /dev/null @@ -1,94 +0,0 @@ -import define from "../../define.js"; -import { ClipNotes, Clips, Notes } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { ApiError } from "../../error.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; - -export const meta = { - tags: ["account", "notes", "clips"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - kind: "read:account", - - errors: { - noSuchClip: { - message: "No such clip.", - code: "NO_SUCH_CLIP", - id: "1d7645e6-2b6d-4635-b0fe-fe22b0e72e00", - }, - }, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - clipId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: ["clipId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.findOneBy({ - 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 query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ) - .innerJoin( - ClipNotes.metadata.targetName, - "clipNote", - "clipNote.noteId = note.id", - ) - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") - .andWhere("clipNote.clipId = :clipId", { clipId: clip.id }); - - if (user) { - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - } - - const notes = await query.take(ps.limit).getMany(); - - return await Notes.packMany(notes, user); -}); diff --git a/packages/backend/src/server/api/endpoints/clips/remove-note.ts b/packages/backend/src/server/api/endpoints/clips/remove-note.ts deleted file mode 100644 index 2cc19aca94..0000000000 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ /dev/null @@ -1,57 +0,0 @@ -import define from "../../define.js"; -import { ClipNotes, Clips } from "@/models/index.js"; -import { ApiError } from "../../error.js"; -import { getNote } from "../../common/getters.js"; - -export const meta = { - tags: ["account", "notes", "clips"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchClip: { - message: "No such clip.", - code: "NO_SUCH_CLIP", - id: "b80525c6-97f7-49d7-a42d-ebccd49cfd52", - }, - - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "aff017de-190e-434b-893e-33a9ff5049d8", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - clipId: { type: "string", format: "misskey:id" }, - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["clipId", "noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.findOneBy({ - 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; - }); - - await ClipNotes.delete({ - noteId: note.id, - clipId: clip.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/clips/show.ts b/packages/backend/src/server/api/endpoints/clips/show.ts deleted file mode 100644 index 14709b5040..0000000000 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ /dev/null @@ -1,52 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Clips } from "@/models/index.js"; - -export const meta = { - tags: ["clips", "account"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - kind: "read:account", - - errors: { - noSuchClip: { - message: "No such clip.", - code: "NO_SUCH_CLIP", - id: "c3c5fe33-d62c-44d2-9ea5-d997703f5c20", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Clip", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - clipId: { type: "string", format: "misskey:id" }, - }, - required: ["clipId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the clip - const clip = await Clips.findOneBy({ - id: ps.clipId, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - if (!clip.isPublic && (me == null || clip.userId !== me.id)) { - throw new ApiError(meta.errors.noSuchClip); - } - - return await Clips.pack(clip); -}); diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts deleted file mode 100644 index e78f36e455..0000000000 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ /dev/null @@ -1,62 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Clips } from "@/models/index.js"; - -export const meta = { - tags: ["clips"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchClip: { - message: "No such clip.", - code: "NO_SUCH_CLIP", - id: "b4d92d70-b216-46fa-9a3f-a8c811699257", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Clip", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - clipId: { type: "string", format: "misskey:id" }, - name: { type: "string", minLength: 1, maxLength: 100 }, - isPublic: { type: "boolean" }, - description: { - type: "string", - nullable: true, - minLength: 1, - maxLength: 2048, - }, - }, - required: ["clipId", "name"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch the clip - const clip = await Clips.findOneBy({ - id: ps.clipId, - userId: user.id, - }); - - if (clip == null) { - throw new ApiError(meta.errors.noSuchClip); - } - - await Clips.update(clip.id, { - name: ps.name, - description: ps.description, - isPublic: ps.isPublic, - }); - - return await Clips.pack(clip.id); -}); diff --git a/packages/backend/src/server/api/endpoints/compatibility/custom-emojis.ts b/packages/backend/src/server/api/endpoints/compatibility/custom-emojis.ts deleted file mode 100644 index 62e0836e85..0000000000 --- a/packages/backend/src/server/api/endpoints/compatibility/custom-emojis.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Emojis } from "@/models/index.js"; -import type { Emoji } from "@/models/entities/emoji.js"; -import { IsNull, In } from "typeorm"; -import { FILE_TYPE_BROWSERSAFE } from "@/const.js"; -import define from "../../define.js"; - -export const meta = { - requireCredential: false, - requireCredentialPrivateMode: true, - allowGet: true, - - tags: ["meta"], -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const now = Date.now(); - const emojis: Emoji[] = await Emojis.find({ - where: { host: IsNull(), type: In(FILE_TYPE_BROWSERSAFE) }, - select: ["name", "originalUrl", "publicUrl", "category"], - }); - - const emojiList = emojis.map((emoji) => ({ - shortcode: emoji.name, - url: emoji.originalUrl, - static_url: emoji.publicUrl, - visible_in_picker: true, - category: emoji.category, - })); - - return emojiList; -}); diff --git a/packages/backend/src/server/api/endpoints/compatibility/instance-info.ts b/packages/backend/src/server/api/endpoints/compatibility/instance-info.ts deleted file mode 100644 index 4e692568c5..0000000000 --- a/packages/backend/src/server/api/endpoints/compatibility/instance-info.ts +++ /dev/null @@ -1,232 +0,0 @@ -import * as mfm from "mfm-js"; -import { toHtml } from "@/mfm/to-html.js"; -import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { - Users, - Notes, - Instances, - UserProfiles, - Emojis, - DriveFiles, -} from "@/models/index.js"; -import type { Emoji } from "@/models/entities/emoji.js"; -import type { User } from "@/models/entities/user.js"; -import { IsNull, In } from "typeorm"; -import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js"; -import define from "../../define.js"; - -export const meta = { - requireCredential: false, - requireCredentialPrivateMode: true, - allowGet: true, - - tags: ["meta"], -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const now = Date.now(); - const [meta, total, localPosts, instanceCount, firstAdmin, emojis] = - await Promise.all([ - fetchMeta(true), - Users.count({ where: { host: IsNull() } }), - Notes.count({ where: { userHost: IsNull(), replyId: IsNull() } }), - Instances.count(), - Users.findOne({ - where: { - host: IsNull(), - isAdmin: true, - isDeleted: false, - isBot: false, - }, - order: { id: "ASC" }, - }), - Emojis.find({ - where: { host: IsNull(), type: In(FILE_TYPE_BROWSERSAFE) }, - select: ["id", "name", "originalUrl", "publicUrl"], - }).then((l) => - l.reduce((a, e) => { - a[e.name] = e; - return a; - }, {} as Record), - ), - ]); - - const descSplit = splitN(meta.description, "\n", 2); - const shortDesc = markup(descSplit.length > 0 ? descSplit[0] : ""); - const longDesc = markup(meta.description ?? ""); - - return { - uri: config.hostname, - title: meta.name, - short_description: shortDesc, - description: longDesc, - email: meta.maintainerEmail, - version: config.version, - urls: { - streaming_api: `wss://${config.host}`, - }, - stats: { - user_count: total, - status_count: localPosts, - domain_count: instanceCount, - }, - thumbnail: meta.logoImageUrl, - languages: meta.langs, - registrations: !meta.disableRegistration, - approval_required: false, - invites_enabled: false, - configuration: { - accounts: { - max_featured_tags: 16, - }, - statuses: { - max_characters: MAX_NOTE_TEXT_LENGTH, - max_media_attachments: 16, - characters_reserved_per_url: 0, - }, - media_attachments: { - supported_mime_types: FILE_TYPE_BROWSERSAFE, - image_size_limit: 10485760, - image_matrix_limit: 16777216, - video_size_limit: 41943040, - video_frame_rate_limit: 60, - video_matrix_limit: 2304000, - }, - polls: { - max_options: 10, - max_characters_per_option: 50, - min_expiration: 15, - max_expiration: -1, - }, - }, - contact_account: await getContact(firstAdmin, emojis), - rules: [], - }; -}); - -const splitN = (s: string | null, split: string, n: number): string[] => { - const ret: string[] = []; - if (s == null) return ret; - if (s === "") { - ret.push(s); - return ret; - } - - let start = 0; - let pos = s.indexOf(split); - if (pos === -1) { - ret.push(s); - return ret; - } - - for (let i = 0; i < n - 1; i++) { - ret.push(s.substring(start, pos)); - start = pos + split.length; - pos = s.indexOf(split, start); - if (pos === -1) break; - } - ret.push(s.substring(start)); - - return ret; -}; - -type ContactType = { - id: string; - username: string; - acct: string; - display_name: string; - note?: string; - noindex?: boolean; - fields?: { - name: string; - value: string; - verified_at: string | null; - }[]; - locked: boolean; - bot: boolean; - created_at: string; - url: string; - followers_count: number; - following_count: number; - statuses_count: number; - last_status_at?: string; - emojis: any; -} | null; - -const getContact = async ( - user: User | null, - emojis: Record, -): Promise => { - if (!user) return null; - - let contact: ContactType = { - id: user.id, - username: user.username, - acct: user.username, - display_name: user.name ?? user.username, - locked: user.isLocked, - bot: user.isBot, - created_at: user.createdAt.toISOString(), - url: `${config.url}/@${user.username}`, - followers_count: user.followersCount, - following_count: user.followingCount, - statuses_count: user.notesCount, - last_status_at: user.lastActiveDate?.toISOString(), - emojis: emojis - ? user.emojis - .filter((e, i, a) => e in emojis && a.indexOf(e) === i) - .map((e) => ({ - shortcode: e, - static_url: emojis[e].publicUrl, - url: emojis[e].originalUrl, - visible_in_picker: true, - })) - : [], - }; - - const [profile] = await Promise.all([ - UserProfiles.findOne({ where: { userId: user.id } }), - loadDriveFiles(contact, "avatar", user.avatarId), - loadDriveFiles(contact, "header", user.bannerId), - ]); - - if (!profile) { - return contact; - } - - contact = { - ...contact, - note: markup(profile.description ?? ""), - noindex: profile.noCrawle, - fields: profile.fields.map((f) => ({ - name: f.name, - value: f.value, - verified_at: null, - })), - }; - - return contact; -}; - -const loadDriveFiles = async ( - contact: any, - key: string, - fileId: string | null, -) => { - if (fileId) { - const file = await DriveFiles.findOneBy({ id: fileId }); - if (file) { - contact[key] = file.webpublicUrl ?? file.url; - contact[`${key}_static`] = contact[key]; - } - } -}; - -const markup = (text: string): string => toHtml(mfm.parse(text)) ?? ""; diff --git a/packages/backend/src/server/api/endpoints/custom-motd.ts b/packages/backend/src/server/api/endpoints/custom-motd.ts deleted file mode 100644 index 098a676a57..0000000000 --- a/packages/backend/src/server/api/endpoints/custom-motd.ts +++ /dev/null @@ -1,33 +0,0 @@ -// import { IsNull } from 'typeorm'; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const meta = await fetchMeta(); - const motd = await Promise.all(meta.customMOTD.map((x) => x)); - return motd; -}); diff --git a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts b/packages/backend/src/server/api/endpoints/custom-splash-icons.ts deleted file mode 100644 index c4833a4eef..0000000000 --- a/packages/backend/src/server/api/endpoints/custom-splash-icons.ts +++ /dev/null @@ -1,33 +0,0 @@ -// import { IsNull } from 'typeorm'; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const meta = await fetchMeta(); - const icons = await Promise.all(meta.customSplashIcons.map((x) => x)); - return icons; -}); diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts deleted file mode 100644 index ce98b53a6b..0000000000 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { DriveFiles } from "@/models/index.js"; -import define from "../define.js"; - -export const meta = { - tags: ["drive", "account"], - - requireCredential: true, - - kind: "read:drive", - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - capacity: { - type: "number", - optional: false, - nullable: false, - }, - usage: { - type: "number", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const instance = await fetchMeta(true); - - // Calculate drive usage - const usage = await DriveFiles.calcDriveUsageOf(user.id); - - return { - capacity: - 1024 * - 1024 * - (user.driveCapacityOverrideMb || instance.localDriveCapacityMb), - usage: usage, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts deleted file mode 100644 index c749e49038..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ /dev/null @@ -1,72 +0,0 @@ -import define from "../../define.js"; -import { DriveFiles } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "read:drive", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFile", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - folderId: { - type: "string", - format: "misskey:id", - nullable: true, - default: null, - }, - type: { - type: "string", - nullable: true, - pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1), - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - DriveFiles.createQueryBuilder("file"), - ps.sinceId, - ps.untilId, - ).andWhere("file.userId = :userId", { userId: user.id }); - - if (ps.folderId) { - query.andWhere("file.folderId = :folderId", { folderId: ps.folderId }); - } else { - query.andWhere("file.folderId IS NULL"); - } - - if (ps.type) { - if (ps.type.endsWith("/*")) { - query.andWhere("file.type like :type", { - type: `${ps.type.replace("/*", "/")}%`, - }); - } else { - query.andWhere("file.type = :type", { type: ps.type }); - } - } - - const files = await query.take(ps.limit).getMany(); - - return await DriveFiles.packMany(files, { detail: false, self: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts deleted file mode 100644 index 9267da5856..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ /dev/null @@ -1,61 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { DriveFiles, Notes } from "@/models/index.js"; - -export const meta = { - tags: ["drive", "notes"], - - requireCredential: true, - - kind: "read:drive", - - description: "Find the notes to which the given file is attached.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - noSuchFile: { - message: "No such file.", - code: "NO_SUCH_FILE", - id: "c118ece3-2e4b-4296-99d1-51756e32d232", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch file - const file = await DriveFiles.findOneBy({ - id: ps.fileId, - userId: user.id, - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - - const notes = await Notes.createQueryBuilder("note") - .where(":file = ANY(note.fileIds)", { file: file.id }) - .getMany(); - - return await Notes.packMany(notes, user, { - detail: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/caption-image.ts b/packages/backend/src/server/api/endpoints/drive/files/caption-image.ts deleted file mode 100644 index 1ab817bd0c..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/caption-image.ts +++ /dev/null @@ -1,42 +0,0 @@ -import define from "../../../define.js"; -import { createWorker } from "tesseract.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "read:drive", - - description: "Return caption of image", - - res: { - type: "string", - optional: false, - nullable: false, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - url: { type: "string" }, - }, - required: ["url"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const worker = createWorker({ - logger: (m) => console.log(m), - }); - - await worker.load(); - await worker.loadLanguage("eng"); - await worker.initialize("eng"); - const { - data: { text }, - } = await worker.recognize(ps.url); - await worker.terminate(); - - return text; -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts deleted file mode 100644 index df89685201..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ /dev/null @@ -1,35 +0,0 @@ -import define from "../../../define.js"; -import { DriveFiles } from "@/models/index.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "read:drive", - - description: "Check if a given file exists.", - - res: { - type: "boolean", - optional: false, - nullable: false, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - md5: { type: "string" }, - }, - required: ["md5"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ - md5: ps.md5, - userId: user.id, - }); - - return file != null; -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts deleted file mode 100644 index 0a167178b2..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { addFile } from "@/services/drive/add-file.js"; -import { DriveFiles } from "@/models/index.js"; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { HOUR } from "@/const.js"; -import define from "../../../define.js"; -import { apiLogger } from "../../../logger.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - limit: { - duration: HOUR, - max: 120, - }, - - requireFile: true, - - kind: "write:drive", - - description: "Upload a new drive file.", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFile", - }, - - errors: { - invalidFileName: { - message: "Invalid file name.", - code: "INVALID_FILE_NAME", - id: "f449b209-0c60-4e51-84d5-29486263bfd4", - }, - - inappropriate: { - message: - "Cannot upload the file because it has been determined that it possibly contains inappropriate content.", - code: "INAPPROPRIATE", - id: "bec5bd69-fba3-43c9-b4fb-2894b66ad5d2", - }, - - noFreeSpace: { - message: - "Cannot upload the file because you have no free space of drive.", - code: "NO_FREE_SPACE", - id: "d08dbc37-a6a9-463a-8c47-96c32ab5f064", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - folderId: { - type: "string", - format: "misskey:id", - nullable: true, - default: null, - }, - name: { type: "string", nullable: true, default: null }, - comment: { - type: "string", - nullable: true, - maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, - default: null, - }, - isSensitive: { type: "boolean", default: false }, - force: { type: "boolean", default: false }, - }, - required: [], -} as const; - -export default define( - meta, - paramDef, - async (ps, user, _, file, cleanup, ip, headers) => { - // 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; - } - - const meta = await fetchMeta(); - - try { - // Create file - const driveFile = await addFile({ - user, - path: file.path, - name, - comment: ps.comment, - folderId: ps.folderId, - force: ps.force, - sensitive: ps.isSensitive, - requestIp: meta.enableIpLogging ? ip : null, - requestHeaders: meta.enableIpLogging ? headers : null, - }); - return await DriveFiles.pack(driveFile, { self: true }); - } catch (e) { - if (e instanceof Error || typeof e === "string") { - apiLogger.error(e); - } - if (e instanceof IdentifiableError) { - if (e.id === "282f77bf-5816-4f72-9264-aa14d8261a21") - throw new ApiError(meta.errors.inappropriate); - if (e.id === "c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6") - throw new ApiError(meta.errors.noFreeSpace); - } - throw new ApiError(); - } finally { - cleanup!(); - } - }, -); diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts deleted file mode 100644 index 4e8b4156f3..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { deleteFile } from "@/services/drive/delete-file.js"; -import { publishDriveStream } from "@/services/stream.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { DriveFiles, Users } from "@/models/index.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "write:drive", - - description: "Delete an existing drive file.", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - - if (!(user.isAdmin || user.isModerator) && file.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } - - // Delete - await deleteFile(file); - - // Publish fileDeleted event - publishDriveStream(user.id, "fileDeleted", file.id); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts b/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts deleted file mode 100644 index ce14f4e09e..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { DriveFiles } from "@/models/index.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "read:drive", - - description: "Search for a drive file by a hash of the contents.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFile", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - md5: { type: "string" }, - }, - required: ["md5"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const files = await DriveFiles.findBy({ - md5: ps.md5, - userId: user.id, - }); - - return await DriveFiles.packMany(files, { self: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts deleted file mode 100644 index c2ad95126f..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ /dev/null @@ -1,51 +0,0 @@ -import define from "../../../define.js"; -import { DriveFiles } from "@/models/index.js"; -import { IsNull } from "typeorm"; - -export const meta = { - requireCredential: true, - - tags: ["drive"], - - kind: "read:drive", - - description: "Search for a drive file by the given parameters.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFile", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string" }, - folderId: { - type: "string", - format: "misskey:id", - nullable: true, - default: null, - }, - }, - required: ["name"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const files = await DriveFiles.findBy({ - name: ps.name, - userId: user.id, - folderId: ps.folderId ?? IsNull(), - }); - - return await Promise.all( - files.map((file) => DriveFiles.pack(file, { self: true })), - ); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts deleted file mode 100644 index 291e3f56bb..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { DriveFiles, Users } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "read:drive", - - description: "Show the properties of a drive file.", - - res: { - type: "object", - optional: false, - nullable: false, - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - anyOf: [ - { - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], - }, - { - properties: { - url: { type: "string" }, - }, - required: ["url"], - }, - ], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - let file: DriveFile | null = null; - - if (ps.fileId) { - file = await DriveFiles.findOneBy({ id: ps.fileId }); - } else if (ps.url) { - file = await DriveFiles.findOne({ - where: [ - { - url: ps.url, - }, - { - webpublicUrl: ps.url, - }, - { - thumbnailUrl: ps.url, - }, - ], - }); - } - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - - if (!(user.isAdmin || user.isModerator) && file.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } - - return await DriveFiles.pack(file, { - detail: true, - withUser: true, - self: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts deleted file mode 100644 index e833fddb58..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { publishDriveStream } from "@/services/stream.js"; -import { DriveFiles, DriveFolders, Users } from "@/models/index.js"; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "write:drive", - - description: "Update the properties of a drive file.", - - errors: { - invalidFileName: { - message: "Invalid file name.", - code: "INVALID_FILE_NAME", - id: "395e7156-f9f0-475e-af89-53c3c23080c2", - }, - - 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", - optional: false, - nullable: false, - ref: "DriveFile", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - folderId: { type: "string", format: "misskey:id", nullable: true }, - name: { type: "string" }, - isSensitive: { type: "boolean" }, - comment: { - type: "string", - nullable: true, - maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, - }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: 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 (!DriveFiles.validateFileName(file.name)) { - throw new ApiError(meta.errors.invalidFileName); - } - - 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.findOneBy({ - id: ps.folderId, - userId: user.id, - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - - file.folderId = folder.id; - } - } - - await DriveFiles.update(file.id, { - name: file.name, - comment: file.comment, - folderId: file.folderId, - isSensitive: file.isSensitive, - }); - - const fileObj = await DriveFiles.pack(file, { self: true }); - - // Publish fileUpdated event - publishDriveStream(user.id, "fileUpdated", fileObj); - - return fileObj; -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts deleted file mode 100644 index 7bb47dbef5..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { uploadFromUrl } from "@/services/drive/upload-from-url.js"; -import define from "../../../define.js"; -import { DriveFiles } from "@/models/index.js"; -import { publishMainStream } from "@/services/stream.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["drive"], - - limit: { - duration: HOUR, - max: 60, - }, - - description: - "Request the server to download a new drive file from the specified URL.", - - requireCredential: true, - - kind: "write:drive", -} as const; - -export const paramDef = { - type: "object", - properties: { - url: { type: "string" }, - folderId: { - type: "string", - format: "misskey:id", - nullable: true, - default: null, - }, - isSensitive: { type: "boolean", default: false }, - comment: { type: "string", nullable: true, maxLength: 512, default: null }, - marker: { type: "string", nullable: true, default: null }, - force: { type: "boolean", default: false }, - }, - required: ["url"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - uploadFromUrl({ - url: ps.url, - user, - folderId: ps.folderId, - sensitive: ps.isSensitive, - force: ps.force, - comment: ps.comment, - }).then((file) => { - DriveFiles.pack(file, { self: true }).then((packedFile) => { - publishMainStream(user.id, "urlUploadFinished", { - marker: ps.marker, - file: packedFile, - }); - }); - }); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts deleted file mode 100644 index ed0d38844b..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ /dev/null @@ -1,57 +0,0 @@ -import define from "../../define.js"; -import { DriveFolders } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "read:drive", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFolder", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - folderId: { - type: "string", - format: "misskey:id", - nullable: true, - default: null, - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - DriveFolders.createQueryBuilder("folder"), - ps.sinceId, - ps.untilId, - ).andWhere("folder.userId = :userId", { userId: user.id }); - - if (ps.folderId) { - query.andWhere("folder.parentId = :parentId", { parentId: ps.folderId }); - } else { - query.andWhere("folder.parentId IS NULL"); - } - - const folders = await query.take(ps.limit).getMany(); - - return await Promise.all(folders.map((folder) => DriveFolders.pack(folder))); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts deleted file mode 100644 index d50f5f2815..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { publishDriveStream } from "@/services/stream.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { DriveFolders } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "write:drive", - - 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", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string", default: "Untitled", maxLength: 200 }, - parentId: { type: "string", format: "misskey:id", nullable: true }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // If the parent folder is specified - let parent = null; - if (ps.parentId) { - // Fetch parent folder - parent = await DriveFolders.findOneBy({ - 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.findOneByOrFail(x.identifiers[0])); - - const folderObj = await DriveFolders.pack(folder); - - // Publish folderCreated event - publishDriveStream(user.id, "folderCreated", folderObj); - - return folderObj; -}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts b/packages/backend/src/server/api/endpoints/drive/folders/delete.ts deleted file mode 100644 index 98895a7320..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ /dev/null @@ -1,60 +0,0 @@ -import define from "../../../define.js"; -import { publishDriveStream } from "@/services/stream.js"; -import { ApiError } from "../../../error.js"; -import { DriveFolders, DriveFiles } from "@/models/index.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "write:drive", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - folderId: { type: "string", format: "misskey:id" }, - }, - required: ["folderId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Get folder - const folder = await DriveFolders.findOneBy({ - id: ps.folderId, - userId: user.id, - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - - const [childFoldersCount, childFilesCount] = await Promise.all([ - DriveFolders.countBy({ parentId: folder.id }), - DriveFiles.countBy({ folderId: folder.id }), - ]); - - if (childFoldersCount !== 0 || childFilesCount !== 0) { - throw new ApiError(meta.errors.hasChildFilesOrFolders); - } - - await DriveFolders.delete(folder.id); - - // Publish folderCreated event - publishDriveStream(user.id, "folderDeleted", folder.id); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/find.ts b/packages/backend/src/server/api/endpoints/drive/folders/find.ts deleted file mode 100644 index 45451fb90b..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts +++ /dev/null @@ -1,47 +0,0 @@ -import define from "../../../define.js"; -import { DriveFolders } from "@/models/index.js"; -import { IsNull } from "typeorm"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "read:drive", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFolder", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string" }, - parentId: { - type: "string", - format: "misskey:id", - nullable: true, - default: null, - }, - }, - required: ["name"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const folders = await DriveFolders.findBy({ - name: ps.name, - userId: user.id, - parentId: ps.parentId ?? IsNull(), - }); - - return await Promise.all(folders.map((folder) => DriveFolders.pack(folder))); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/show.ts b/packages/backend/src/server/api/endpoints/drive/folders/show.ts deleted file mode 100644 index 6a72a22777..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ /dev/null @@ -1,50 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { DriveFolders } from "@/models/index.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "read:drive", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFolder", - }, - - errors: { - noSuchFolder: { - message: "No such folder.", - code: "NO_SUCH_FOLDER", - id: "d74ab9eb-bb09-4bba-bf24-fb58f761e1e9", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - folderId: { type: "string", format: "misskey:id" }, - }, - required: ["folderId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Get folder - const folder = await DriveFolders.findOneBy({ - id: ps.folderId, - userId: user.id, - }); - - if (folder == null) { - throw new ApiError(meta.errors.noSuchFolder); - } - - return await DriveFolders.pack(folder, { - detail: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts deleted file mode 100644 index 929a69bdec..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { publishDriveStream } from "@/services/stream.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { DriveFolders } from "@/models/index.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "write:drive", - - 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", - optional: false, - nullable: false, - ref: "DriveFolder", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - folderId: { type: "string", format: "misskey:id" }, - name: { type: "string", maxLength: 200 }, - parentId: { type: "string", format: "misskey:id", nullable: true }, - }, - required: ["folderId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch folder - const folder = await DriveFolders.findOneBy({ - 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.findOneBy({ - 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: string): Promise { - // Fetch folder - const folder2 = await DriveFolders.findOneBy({ - id: folderId, - }); - - if (folder2!.id === folder!.id) { - return true; - } else if (folder2!.parentId) { - return await checkCircle(folder2!.parentId); - } else { - return false; - } - } - - if (parent.parentId !== null) { - if (await checkCircle(parent.parentId)) { - throw new ApiError(meta.errors.recursiveNesting); - } - } - - folder.parentId = parent.id; - } - } - - // Update - DriveFolders.update(folder.id, { - name: folder.name, - parentId: folder.parentId, - }); - - const folderObj = await DriveFolders.pack(folder); - - // Publish folderUpdated event - publishDriveStream(user.id, "folderUpdated", folderObj); - - return folderObj; -}); diff --git a/packages/backend/src/server/api/endpoints/drive/stream.ts b/packages/backend/src/server/api/endpoints/drive/stream.ts deleted file mode 100644 index 0c9654ca23..0000000000 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ /dev/null @@ -1,59 +0,0 @@ -import define from "../../define.js"; -import { DriveFiles } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["drive"], - - requireCredential: true, - - kind: "read:drive", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "DriveFile", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - type: { - type: "string", - pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1), - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - DriveFiles.createQueryBuilder("file"), - ps.sinceId, - ps.untilId, - ).andWhere("file.userId = :userId", { userId: user.id }); - - if (ps.type) { - if (ps.type.endsWith("/*")) { - query.andWhere("file.type like :type", { - type: `${ps.type.replace("/*", "/")}%`, - }); - } else { - query.andWhere("file.type = :type", { type: ps.type }); - } - } - - const files = await query.take(ps.limit).getMany(); - - return await DriveFiles.packMany(files, { detail: false, self: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/email-address/available.ts b/packages/backend/src/server/api/endpoints/email-address/available.ts deleted file mode 100644 index dc3c5e4b8e..0000000000 --- a/packages/backend/src/server/api/endpoints/email-address/available.ts +++ /dev/null @@ -1,38 +0,0 @@ -import define from "../../define.js"; -import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - available: { - type: "boolean", - optional: false, - nullable: false, - }, - reason: { - type: "string", - optional: false, - nullable: true, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - emailAddress: { type: "string" }, - }, - required: ["emailAddress"], -} as const; - -export default define(meta, paramDef, async (ps) => { - return await validateEmailForAccount(ps.emailAddress); -}); diff --git a/packages/backend/src/server/api/endpoints/emoji.ts b/packages/backend/src/server/api/endpoints/emoji.ts deleted file mode 100644 index ddfad77374..0000000000 --- a/packages/backend/src/server/api/endpoints/emoji.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { IsNull } from "typeorm"; -import { Emojis } from "@/models/index.js"; -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - - requireCredential: false, - allowGet: true, - cacheSec: 3600, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Emoji", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { - type: "string", - }, - }, - required: ["name"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const emoji = await Emojis.findOneOrFail({ - where: { - name: ps.name, - host: IsNull(), - }, - }); - - return Emojis.pack(emoji); -}); diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts deleted file mode 100644 index ad0ce45623..0000000000 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ /dev/null @@ -1,27 +0,0 @@ -import define from "../define.js"; -import endpoints from "../endpoints.js"; - -export const meta = { - requireCredential: false, - - tags: ["meta"], -} as const; - -export const paramDef = { - type: "object", - properties: { - endpoint: { type: "string" }, - }, - required: ["endpoint"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const ep = endpoints.find((x) => x.name === ps.endpoint); - if (ep == null) return null; - return { - params: Object.entries(ep.params.properties || {}).map(([k, v]) => ({ - name: k, - type: v.type.charAt(0).toUpperCase() + v.type.slice(1), - })), - }; -}); diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts deleted file mode 100644 index c5844f8437..0000000000 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ /dev/null @@ -1,35 +0,0 @@ -import define from "../define.js"; -import endpoints from "../endpoints.js"; - -export const meta = { - requireCredential: false, - - tags: ["meta"], - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - example: [ - "admin/abuse-user-reports", - "admin/accounts/create", - "admin/announcements/create", - "...", - ], - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - return endpoints.map((x) => x.name); -}); diff --git a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts b/packages/backend/src/server/api/endpoints/export-custom-emojis.ts deleted file mode 100644 index f4fc43c173..0000000000 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createExportCustomEmojisJob } from "@/queue/index.js"; -import define from "../define.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - createExportCustomEmojisJob(user); -}); diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts deleted file mode 100644 index 4c6d83abdb..0000000000 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ /dev/null @@ -1,46 +0,0 @@ -import define from "../../define.js"; -import { Followings } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["federation"], - - requireCredential: true, - requireAdmin: true, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Following", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - host: { type: "string" }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - }, - required: ["host"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Followings.createQueryBuilder("following"), - ps.sinceId, - ps.untilId, - ).andWhere("following.followeeHost = :host", { host: ps.host }); - - const followings = await query.take(ps.limit).getMany(); - - return await Followings.packMany(followings, me, { populateFollowee: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts deleted file mode 100644 index 88b168600b..0000000000 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ /dev/null @@ -1,46 +0,0 @@ -import define from "../../define.js"; -import { Followings } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["federation"], - - requireCredential: true, - requireAdmin: true, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Following", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - host: { type: "string" }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - }, - required: ["host"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Followings.createQueryBuilder("following"), - ps.sinceId, - ps.untilId, - ).andWhere("following.followerHost = :host", { host: ps.host }); - - const followings = await query.take(ps.limit).getMany(); - - return await Followings.packMany(followings, me, { populateFollowee: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts deleted file mode 100644 index 8f6184b196..0000000000 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ /dev/null @@ -1,171 +0,0 @@ -import config from "@/config/index.js"; -import define from "../../define.js"; -import { Instances } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; - -export const meta = { - tags: ["federation"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "FederationInstance", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - host: { - type: "string", - nullable: true, - description: "Omit or use `null` to not filter by host.", - }, - blocked: { type: "boolean", nullable: true }, - notResponding: { type: "boolean", nullable: true }, - suspended: { type: "boolean", nullable: true }, - federating: { type: "boolean", nullable: true }, - subscribing: { type: "boolean", nullable: true }, - publishing: { type: "boolean", nullable: true }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 30 }, - offset: { type: "integer", default: 0 }, - sort: { type: "string" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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; - - default: - query.orderBy("instance.id", "DESC"); - break; - } - - if (typeof ps.blocked === "boolean") { - const meta = await fetchMeta(true); - if (ps.blocked) { - if (meta.blockedHosts.length === 0) { - return []; - } - query.andWhere("instance.host IN (:...blocks)", { - blocks: meta.blockedHosts, - }); - } else if (meta.blockedHosts.length > 0) { - 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 await Instances.packMany(instances); -}); diff --git a/packages/backend/src/server/api/endpoints/federation/show-instance.ts b/packages/backend/src/server/api/endpoints/federation/show-instance.ts deleted file mode 100644 index 633bb57073..0000000000 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ /dev/null @@ -1,36 +0,0 @@ -import define from "../../define.js"; -import { Instances } from "@/models/index.js"; -import { toPuny } from "@/misc/convert-host.js"; - -export const meta = { - tags: ["federation"], - - requireCredential: true, - requireCredentialPrivateMode: true, - - res: { - oneOf: [ - { - type: "object", - ref: "FederationInstance", - }, - { - type: "null", - }, - ], - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - host: { type: "string" }, - }, - required: ["host"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const instance = await Instances.findOneBy({ host: toPuny(ps.host) }); - - return instance ? await Instances.pack(instance) : null; -}); diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts deleted file mode 100644 index ede7a56c27..0000000000 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { IsNull, MoreThan, Not } from "typeorm"; -import { Followings, Instances } from "@/models/index.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["federation"], - - requireCredential: false, - - allowGet: true, - cacheSec: 60 * 60, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps) => { - const [topSubInstances, topPubInstances, allSubCount, allPubCount] = - await Promise.all([ - Instances.find({ - where: { - followersCount: MoreThan(0), - }, - order: { - followersCount: "DESC", - }, - take: ps.limit, - }), - Instances.find({ - where: { - followingCount: MoreThan(0), - }, - order: { - followingCount: "DESC", - }, - take: ps.limit, - }), - Followings.count({ - where: { - followeeHost: Not(IsNull()), - }, - }), - Followings.count({ - where: { - followerHost: Not(IsNull()), - }, - }), - ]); - - const gotSubCount = topSubInstances - .map((x) => x.followersCount) - .reduce((a, b) => a + b, 0); - const gotPubCount = topPubInstances - .map((x) => x.followingCount) - .reduce((a, b) => a + b, 0); - - return await awaitAll({ - topSubInstances: Instances.packMany(topSubInstances), - otherFollowersCount: Math.max(0, allSubCount - gotSubCount), - topPubInstances: Instances.packMany(topPubInstances), - otherFollowingCount: Math.max(0, allPubCount - gotPubCount), - }); -}); diff --git a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts deleted file mode 100644 index f4c3f6d18c..0000000000 --- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts +++ /dev/null @@ -1,22 +0,0 @@ -import define from "../../define.js"; -import { getRemoteUser } from "../../common/getters.js"; -import { updatePerson } from "@/remote/activitypub/models/person.js"; - -export const meta = { - tags: ["federation"], - - requireCredential: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const user = await getRemoteUser(ps.userId); - await updatePerson(user.uri!); -}); diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts deleted file mode 100644 index ded0a26c5f..0000000000 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../define.js"; -import { Users } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["federation"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailedNotMe", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - host: { type: "string" }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - }, - required: ["host"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Users.createQueryBuilder("user"), - ps.sinceId, - ps.untilId, - ).andWhere("user.host = :host", { host: ps.host }); - - const users = await query.take(ps.limit).getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts deleted file mode 100644 index b73d7262c9..0000000000 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Parser from "rss-parser"; -import { getResponse } from "@/misc/fetch.js"; -import config from "@/config/index.js"; -import define from "../define.js"; - -const rssParser = new Parser(); - -export const meta = { - tags: ["meta"], - - requireCredential: false, - allowGet: true, - cacheSec: 60 * 3, -} as const; - -export const paramDef = { - type: "object", - properties: { - url: { type: "string" }, - }, - required: ["url"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const res = await getResponse({ - url: ps.url, - method: "GET", - headers: Object.assign({ - "User-Agent": config.userAgent, - Accept: "application/rss+xml, */*", - }), - timeout: 5000, - }); - - const text = await res.text(); - - return rssParser.parseString(text); -}); diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts deleted file mode 100644 index e617c1ffb3..0000000000 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ /dev/null @@ -1,107 +0,0 @@ -import create from "@/services/following/create.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; -import { Followings, Users } from "@/models/index.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["following", "users"], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: "write:following", - - 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", - optional: false, - nullable: false, - ref: "UserLite", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, 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.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyFollowing); - } - - try { - await create(follower, followee); - } catch (e) { - if (e instanceof IdentifiableError) { - if (e.id === "710e8fb0-b8c3-4922-be49-d5d93d8e6a6e") - throw new ApiError(meta.errors.blocking); - if (e.id === "3338392a-f764-498d-8855-db939dcf8c48") - throw new ApiError(meta.errors.blocked); - } - throw e; - } - - return await Users.pack(followee.id, user); -}); diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts deleted file mode 100644 index 2eebe8a903..0000000000 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ /dev/null @@ -1,84 +0,0 @@ -import deleteFollowing from "@/services/following/delete.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; -import { Followings, Users } from "@/models/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["following", "users"], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: "write:following", - - 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", - optional: false, - nullable: false, - ref: "UserLite", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, 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.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notFollowing); - } - - await deleteFollowing(follower, followee); - - return await Users.pack(followee.id, user); -}); diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts deleted file mode 100644 index 979d298f7d..0000000000 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ /dev/null @@ -1,84 +0,0 @@ -import deleteFollowing from "@/services/following/delete.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; -import { Followings, Users } from "@/models/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["following", "users"], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: "write:following", - - errors: { - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "5b12c78d-2b28-4dca-99d2-f56139b42ff8", - }, - - followerIsYourself: { - message: "Follower is yourself.", - code: "FOLLOWER_IS_YOURSELF", - id: "07dc03b9-03da-422d-885b-438313707662", - }, - - notFollowing: { - message: "The other use is not following you.", - code: "NOT_FOLLOWING", - id: "5dbf82f5-c92b-40b1-87d1-6c8c0741fd09", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "UserLite", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const followee = user; - - // Check if the follower is yourself - if (user.id === ps.userId) { - throw new ApiError(meta.errors.followerIsYourself); - } - - // Get 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; - }); - - // Check not following - const exist = await Followings.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notFollowing); - } - - await deleteFollowing(follower, followee); - - return await Users.pack(followee.id, user); -}); diff --git a/packages/backend/src/server/api/endpoints/following/requests/accept.ts b/packages/backend/src/server/api/endpoints/following/requests/accept.ts deleted file mode 100644 index a4fc052367..0000000000 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ /dev/null @@ -1,50 +0,0 @@ -import acceptFollowRequest from "@/services/following/requests/accept.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getUser } from "../../../common/getters.js"; - -export const meta = { - tags: ["following", "account"], - - requireCredential: true, - - kind: "write:following", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch follower - const follower = await getUser(ps.userId).catch((e) => { - if (e.id === "15348ddd-432d-49c2-8a5a-8069753becff") - throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - await acceptFollowRequest(user, follower).catch((e) => { - if (e.id === "8884c2dd-5795-4ac9-b27e-6a01d38190f9") - throw new ApiError(meta.errors.noFollowRequest); - throw e; - }); - - return; -}); diff --git a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts b/packages/backend/src/server/api/endpoints/following/requests/cancel.ts deleted file mode 100644 index f309e32999..0000000000 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ /dev/null @@ -1,64 +0,0 @@ -import cancelFollowRequest from "@/services/following/requests/cancel.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getUser } from "../../../common/getters.js"; -import { Users } from "@/models/index.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; - -export const meta = { - tags: ["following", "account"], - - requireCredential: true, - - kind: "write:following", - - 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", - optional: false, - nullable: false, - ref: "UserLite", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, 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 instanceof IdentifiableError) { - if (e.id === "17447091-ce07-46dd-b331-c1fd4f15b1e7") - throw new ApiError(meta.errors.followRequestNotFound); - } - throw e; - } - - return await Users.pack(followee.id, user); -}); diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts deleted file mode 100644 index 6ba23de585..0000000000 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ /dev/null @@ -1,55 +0,0 @@ -import define from "../../../define.js"; -import { FollowRequests } from "@/models/index.js"; - -export const meta = { - tags: ["following", "account"], - - requireCredential: true, - - kind: "read:following", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - follower: { - type: "object", - optional: false, - nullable: false, - ref: "UserLite", - }, - followee: { - type: "object", - optional: false, - nullable: false, - ref: "UserLite", - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const reqs = await FollowRequests.findBy({ - followeeId: user.id, - }); - - return await Promise.all(reqs.map((req) => FollowRequests.pack(req))); -}); diff --git a/packages/backend/src/server/api/endpoints/following/requests/reject.ts b/packages/backend/src/server/api/endpoints/following/requests/reject.ts deleted file mode 100644 index fedc0db487..0000000000 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { rejectFollowRequest } from "@/services/following/reject.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getUser } from "../../../common/getters.js"; - -export const meta = { - tags: ["following", "account"], - - requireCredential: true, - - kind: "write:following", - - errors: { - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "abc2ffa6-25b2-4380-ba99-321ff3a94555", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch follower - const follower = await getUser(ps.userId).catch((e) => { - if (e.id === "15348ddd-432d-49c2-8a5a-8069753becff") - throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - await rejectFollowRequest(user, follower); - - return; -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/featured.ts b/packages/backend/src/server/api/endpoints/gallery/featured.ts deleted file mode 100644 index d478e8e3bf..0000000000 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ /dev/null @@ -1,40 +0,0 @@ -import define from "../../define.js"; -import { GalleryPosts } from "@/models/index.js"; - -export const meta = { - tags: ["gallery"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "GalleryPost", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = GalleryPosts.createQueryBuilder("post") - .andWhere("post.createdAt > :date", { - date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3), - }) - .andWhere("post.likedCount > 0") - .orderBy("post.likedCount", "DESC"); - - const posts = await query.take(10).getMany(); - - return await GalleryPosts.packMany(posts, me); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/popular.ts b/packages/backend/src/server/api/endpoints/gallery/popular.ts deleted file mode 100644 index 5eef68d971..0000000000 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ /dev/null @@ -1,37 +0,0 @@ -import define from "../../define.js"; -import { GalleryPosts } from "@/models/index.js"; - -export const meta = { - tags: ["gallery"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "GalleryPost", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = GalleryPosts.createQueryBuilder("post") - .andWhere("post.likedCount > 0") - .orderBy("post.likedCount", "DESC"); - - const posts = await query.take(10).getMany(); - - return await GalleryPosts.packMany(posts, me); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts.ts b/packages/backend/src/server/api/endpoints/gallery/posts.ts deleted file mode 100644 index f97c161aff..0000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ /dev/null @@ -1,42 +0,0 @@ -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { GalleryPosts } from "@/models/index.js"; - -export const meta = { - tags: ["gallery"], - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "GalleryPost", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - GalleryPosts.createQueryBuilder("post"), - ps.sinceId, - ps.untilId, - ).innerJoinAndSelect("post.user", "user"); - - const posts = await query.take(ps.limit).getMany(); - - return await GalleryPosts.packMany(posts, me); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts deleted file mode 100644 index f3b3768e28..0000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ /dev/null @@ -1,81 +0,0 @@ -import define from "../../../define.js"; -import { DriveFiles, GalleryPosts } from "@/models/index.js"; -import { genId } from "../../../../../misc/gen-id.js"; -import { GalleryPost } from "@/models/entities/gallery-post.js"; -import { ApiError } from "../../../error.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["gallery"], - - requireCredential: true, - - kind: "write:gallery", - - limit: { - duration: HOUR, - max: 300, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "GalleryPost", - }, - - errors: {}, -} as const; - -export const paramDef = { - type: "object", - properties: { - title: { type: "string", minLength: 1 }, - description: { type: "string", nullable: true }, - fileIds: { - type: "array", - uniqueItems: true, - minItems: 1, - maxItems: 32, - items: { - type: "string", - format: "misskey:id", - }, - }, - isSensitive: { type: "boolean", default: false }, - }, - required: ["title", "fileIds"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const files = ( - await Promise.all( - ps.fileIds.map((fileId) => - DriveFiles.findOneBy({ - 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.findOneByOrFail(x.identifiers[0])); - - return await GalleryPosts.pack(post, user); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts deleted file mode 100644 index 9fd9a50099..0000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ /dev/null @@ -1,40 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { GalleryPosts } from "@/models/index.js"; - -export const meta = { - tags: ["gallery"], - - requireCredential: true, - - kind: "write:gallery", - - errors: { - noSuchPost: { - message: "No such post.", - code: "NO_SUCH_POST", - id: "ae52f367-4bd7-4ecd-afc6-5672fff427f5", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - postId: { type: "string", format: "misskey:id" }, - }, - required: ["postId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ - id: ps.postId, - userId: user.id, - }); - - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } - - await GalleryPosts.delete(post.id); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts deleted file mode 100644 index fd46406bdf..0000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ /dev/null @@ -1,61 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { GalleryPosts, GalleryLikes } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; - -export const meta = { - tags: ["gallery"], - - requireCredential: true, - - kind: "write:gallery-likes", - - errors: { - noSuchPost: { - message: "No such post.", - code: "NO_SUCH_POST", - id: "56c06af3-1287-442f-9701-c93f7c4a62ff", - }, - - alreadyLiked: { - message: "The post has already been liked.", - code: "ALREADY_LIKED", - id: "40e9ed56-a59c-473a-bf3f-f289c54fb5a7", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - postId: { type: "string", format: "misskey:id" }, - }, - required: ["postId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ id: ps.postId }); - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } - - // if already liked - const exist = await GalleryLikes.findOneBy({ - postId: post.id, - userId: user.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyLiked); - } - - // Create like - await GalleryLikes.insert({ - id: genId(), - createdAt: new Date(), - postId: post.id, - userId: user.id, - }); - - GalleryPosts.increment({ id: post.id }, "likedCount", 1); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts b/packages/backend/src/server/api/endpoints/gallery/posts/show.ts deleted file mode 100644 index 87e272f018..0000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { GalleryPosts } from "@/models/index.js"; - -export const meta = { - tags: ["gallery"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - errors: { - noSuchPost: { - message: "No such post.", - code: "NO_SUCH_POST", - id: "1137bf14-c5b0-4604-85bb-5b5371b1cd45", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "GalleryPost", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - postId: { type: "string", format: "misskey:id" }, - }, - required: ["postId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const post = await GalleryPosts.findOneBy({ - id: ps.postId, - }); - - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } - - return await GalleryPosts.pack(post, me); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts deleted file mode 100644 index 772dc92028..0000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ /dev/null @@ -1,54 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { GalleryPosts, GalleryLikes } from "@/models/index.js"; - -export const meta = { - tags: ["gallery"], - - requireCredential: true, - - kind: "write:gallery-likes", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - postId: { type: "string", format: "misskey:id" }, - }, - required: ["postId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ id: ps.postId }); - if (post == null) { - throw new ApiError(meta.errors.noSuchPost); - } - - const exist = await GalleryLikes.findOneBy({ - postId: post.id, - userId: user.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notLiked); - } - - // Delete like - await GalleryLikes.delete(exist.id); - - GalleryPosts.decrement({ id: post.id }, "likedCount", 1); -}); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts deleted file mode 100644 index 64e204172e..0000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ /dev/null @@ -1,84 +0,0 @@ -import define from "../../../define.js"; -import { DriveFiles, GalleryPosts } from "@/models/index.js"; -import { GalleryPost } from "@/models/entities/gallery-post.js"; -import { ApiError } from "../../../error.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["gallery"], - - requireCredential: true, - - kind: "write:gallery", - - limit: { - duration: HOUR, - max: 300, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "GalleryPost", - }, - - errors: {}, -} as const; - -export const paramDef = { - type: "object", - properties: { - postId: { type: "string", format: "misskey:id" }, - title: { type: "string", minLength: 1 }, - description: { type: "string", nullable: true }, - fileIds: { - type: "array", - uniqueItems: true, - minItems: 1, - maxItems: 32, - items: { - type: "string", - format: "misskey:id", - }, - }, - isSensitive: { type: "boolean", default: false }, - }, - required: ["postId", "title", "fileIds"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const files = ( - await Promise.all( - ps.fileIds.map((fileId) => - DriveFiles.findOneBy({ - 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.findOneByOrFail({ id: ps.postId }); - - return await GalleryPosts.pack(post, user); -}); diff --git a/packages/backend/src/server/api/endpoints/get-online-users-count.ts b/packages/backend/src/server/api/endpoints/get-online-users-count.ts deleted file mode 100644 index 805674a5b7..0000000000 --- a/packages/backend/src/server/api/endpoints/get-online-users-count.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { MoreThan } from "typeorm"; -import { USER_ONLINE_THRESHOLD } from "@/const.js"; -import { Users } from "@/models/index.js"; -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - - requireCredential: false, - requireCredentialPrivateMode: true, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const count = await Users.countBy({ - lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD)), - }); - - return { - count, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/get-sounds.ts b/packages/backend/src/server/api/endpoints/get-sounds.ts deleted file mode 100644 index f7edd38609..0000000000 --- a/packages/backend/src/server/api/endpoints/get-sounds.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { readdir } from "fs/promises"; -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - requireCredential: false, - requireCredentialPrivateMode: false, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const music_files: (string | null)[] = [null]; - const directory = ( - await readdir("./assets/sounds", { withFileTypes: true }) - ).filter((potentialFolder) => potentialFolder.isDirectory()); - for await (const folder of directory) { - const files = (await readdir(`./assets/sounds/${folder.name}`)).filter( - (potentialSong) => potentialSong.endsWith(".mp3"), - ); - for await (const file of files) { - music_files.push(`${folder.name}/${file.replace(".mp3", "")}`); - } - } - return music_files; -}); diff --git a/packages/backend/src/server/api/endpoints/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts deleted file mode 100644 index df99a1e5a6..0000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ /dev/null @@ -1,112 +0,0 @@ -import define from "../../define.js"; -import { Hashtags } from "@/models/index.js"; - -export const meta = { - tags: ["hashtags"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Hashtag", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - attachedToUserOnly: { type: "boolean", default: false }, - attachedToLocalUserOnly: { type: "boolean", default: false }, - attachedToRemoteUserOnly: { type: "boolean", default: false }, - sort: { - type: "string", - enum: [ - "+mentionedUsers", - "-mentionedUsers", - "+mentionedLocalUsers", - "-mentionedLocalUsers", - "+mentionedRemoteUsers", - "-mentionedRemoteUsers", - "+attachedUsers", - "-attachedUsers", - "+attachedLocalUsers", - "-attachedLocalUsers", - "+attachedRemoteUsers", - "-attachedRemoteUsers", - ], - }, - }, - required: ["sort"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = Hashtags.createQueryBuilder("tag"); - - if (ps.attachedToUserOnly) query.andWhere("tag.attachedUsersCount != 0"); - if (ps.attachedToLocalUserOnly) - query.andWhere("tag.attachedLocalUsersCount != 0"); - if (ps.attachedToRemoteUserOnly) - query.andWhere("tag.attachedRemoteUsersCount != 0"); - - switch (ps.sort) { - case "+mentionedUsers": - query.orderBy("tag.mentionedUsersCount", "DESC"); - break; - case "-mentionedUsers": - query.orderBy("tag.mentionedUsersCount", "ASC"); - break; - case "+mentionedLocalUsers": - query.orderBy("tag.mentionedLocalUsersCount", "DESC"); - break; - case "-mentionedLocalUsers": - query.orderBy("tag.mentionedLocalUsersCount", "ASC"); - break; - case "+mentionedRemoteUsers": - query.orderBy("tag.mentionedRemoteUsersCount", "DESC"); - break; - case "-mentionedRemoteUsers": - query.orderBy("tag.mentionedRemoteUsersCount", "ASC"); - break; - case "+attachedUsers": - query.orderBy("tag.attachedUsersCount", "DESC"); - break; - case "-attachedUsers": - query.orderBy("tag.attachedUsersCount", "ASC"); - break; - case "+attachedLocalUsers": - query.orderBy("tag.attachedLocalUsersCount", "DESC"); - break; - case "-attachedLocalUsers": - query.orderBy("tag.attachedLocalUsersCount", "ASC"); - break; - case "+attachedRemoteUsers": - query.orderBy("tag.attachedRemoteUsersCount", "DESC"); - break; - case "-attachedRemoteUsers": - query.orderBy("tag.attachedRemoteUsersCount", "ASC"); - break; - } - - query.select([ - "tag.name", - "tag.mentionedUsersCount", - "tag.mentionedLocalUsersCount", - "tag.mentionedRemoteUsersCount", - "tag.attachedUsersCount", - "tag.attachedLocalUsersCount", - "tag.attachedRemoteUsersCount", - ]); - - const tags = await query.take(ps.limit).getMany(); - - return Hashtags.packMany(tags); -}); diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts deleted file mode 100644 index 95bc608ece..0000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ /dev/null @@ -1,42 +0,0 @@ -import define from "../../define.js"; -import { Hashtags } from "@/models/index.js"; - -export const meta = { - tags: ["hashtags"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - query: { type: "string" }, - offset: { type: "integer", default: 0 }, - }, - required: ["query"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const hashtags = await Hashtags.createQueryBuilder("tag") - .where("tag.name like :q", { q: `${ps.query.toLowerCase()}%` }) - .orderBy("tag.count", "DESC") - .groupBy("tag.id") - .take(ps.limit) - .skip(ps.offset) - .getMany(); - - return hashtags.map((tag) => tag.name); -}); diff --git a/packages/backend/src/server/api/endpoints/hashtags/show.ts b/packages/backend/src/server/api/endpoints/hashtags/show.ts deleted file mode 100644 index 8cf90e4505..0000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Hashtags } from "@/models/index.js"; -import { normalizeForSearch } from "@/misc/normalize-for-search.js"; - -export const meta = { - tags: ["hashtags"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Hashtag", - }, - - errors: { - noSuchHashtag: { - message: "No such hashtag.", - code: "NO_SUCH_HASHTAG", - id: "110ee688-193e-4a3a-9ecf-c167b2e6981e", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - tag: { type: "string" }, - }, - required: ["tag"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const hashtag = await Hashtags.findOneBy({ - name: normalizeForSearch(ps.tag), - }); - if (hashtag == null) { - throw new ApiError(meta.errors.noSuchHashtag); - } - - return await Hashtags.pack(hashtag); -}); diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts deleted file mode 100644 index e2a8345112..0000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Brackets } from "typeorm"; -import define from "../../define.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Notes } from "@/models/index.js"; -import type { Note } from "@/models/entities/note.js"; -import { safeForSql } from "@/misc/safe-for-sql.js"; -import { normalizeForSearch } from "@/misc/normalize-for-search.js"; - -/* -トレンドに載るためには「『直近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, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - tag: { - type: "string", - optional: false, - nullable: false, - }, - chart: { - type: "array", - optional: false, - nullable: false, - items: { - type: "number", - optional: false, - nullable: false, - }, - }, - usersCount: { - type: "number", - optional: false, - nullable: false, - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, 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/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts deleted file mode 100644 index 532c663070..0000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ /dev/null @@ -1,92 +0,0 @@ -import define from "../../define.js"; -import { Users } from "@/models/index.js"; -import { normalizeForSearch } from "@/misc/normalize-for-search.js"; - -export const meta = { - requireCredential: false, - requireCredentialPrivateMode: true, - - tags: ["hashtags", "users"], - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailed", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - tag: { type: "string" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sort: { - type: "string", - enum: [ - "+follower", - "-follower", - "+createdAt", - "-createdAt", - "+updatedAt", - "-updatedAt", - ], - }, - state: { type: "string", enum: ["all", "alive"], default: "all" }, - origin: { - type: "string", - enum: ["combined", "local", "remote"], - default: "local", - }, - }, - required: ["tag", "sort"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = Users.createQueryBuilder("user").where( - ":tag = ANY(user.tags)", - { tag: normalizeForSearch(ps.tag) }, - ); - - const recent = new Date(Date.now() - 1000 * 60 * 60 * 24 * 5); - - if (ps.state === "alive") { - query.andWhere("user.updatedAt > :date", { date: recent }); - } - - if (ps.origin === "local") { - query.andWhere("user.host IS NULL"); - } else if (ps.origin === "remote") { - query.andWhere("user.host IS NOT NULL"); - } - - switch (ps.sort) { - case "+follower": - query.orderBy("user.followersCount", "DESC"); - break; - case "-follower": - query.orderBy("user.followersCount", "ASC"); - break; - case "+createdAt": - query.orderBy("user.createdAt", "DESC"); - break; - case "-createdAt": - query.orderBy("user.createdAt", "ASC"); - break; - case "+updatedAt": - query.orderBy("user.updatedAt", "DESC"); - break; - case "-updatedAt": - query.orderBy("user.updatedAt", "ASC"); - break; - } - - const users = await query.take(ps.limit).getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts deleted file mode 100644 index 39543442c5..0000000000 --- a/packages/backend/src/server/api/endpoints/i.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Users } from "@/models/index.js"; -import define from "../define.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "MeDetailed", - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user, token) => { - const isSecure = token == null; - - // ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す - return await Users.pack(user.id, user, { - detail: true, - includeSecrets: isSecure, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts deleted file mode 100644 index 1e9892f03b..0000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as speakeasy from "speakeasy"; -import define from "../../../define.js"; -import { UserProfiles } from "@/models/index.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - token: { type: "string" }, - }, - required: ["token"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const token = ps.token.replace(/\s/g, ""); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.twoFactorTempSecret == null) { - throw new Error("二段階認証の設定が開始されていません"); - } - - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorTempSecret, - encoding: "base32", - token: token, - }); - - if (!verified) { - throw new Error("not verified"); - } - - await UserProfiles.update(user.id, { - twoFactorSecret: profile.twoFactorTempSecret, - twoFactorEnabled: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts deleted file mode 100644 index f0581de4b4..0000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { promisify } from "node:util"; -import * as cbor from "cbor"; -import define from "../../../define.js"; -import { - UserProfiles, - UserSecurityKeys, - AttestationChallenges, - Users, -} from "@/models/index.js"; -import config from "@/config/index.js"; -import { procedures, hash } from "../../../2fa.js"; -import { publishMainStream } from "@/services/stream.js"; -import { comparePassword } from "@/misc/password.js"; - -const cborDecodeFirst = promisify(cbor.decodeFirst) as any; -const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8")); - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - clientDataJSON: { type: "string" }, - attestationObject: { type: "string" }, - password: { type: "string" }, - challengeId: { type: "string" }, - name: { type: "string" }, - }, - required: [ - "clientDataJSON", - "attestationObject", - "password", - "challengeId", - "name", - ], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await comparePassword(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]; - - 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.findOneBy({ - 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.insert({ - userId: user.id, - id: credentialIdString, - lastUsed: new Date(), - name: ps.name, - publicKey: verificationData.publicKey.toString("hex"), - }); - - // Publish meUpdated event - publishMainStream( - user.id, - "meUpdated", - await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - }), - ); - - return { - id: credentialIdString, - name: ps.name, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts deleted file mode 100644 index 11b2e9a2e3..0000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts +++ /dev/null @@ -1,22 +0,0 @@ -import define from "../../../define.js"; -import { UserProfiles } from "@/models/index.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - value: { type: "boolean" }, - }, - required: ["value"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - await UserProfiles.update(user.id, { - usePasswordLessLogin: ps.value, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts deleted file mode 100644 index a10dc9b256..0000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ /dev/null @@ -1,61 +0,0 @@ -import define from "../../../define.js"; -import { UserProfiles, AttestationChallenges } from "@/models/index.js"; -import { promisify } from "node:util"; -import * as crypto from "node:crypto"; -import { genId } from "@/misc/gen-id.js"; -import { hash } from "../../../2fa.js"; -import { comparePassword } from "@/misc/password.js"; - -const randomBytes = promisify(crypto.randomBytes); - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - password: { type: "string" }, - }, - required: ["password"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await comparePassword(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.insert({ - userId: user.id, - id: challengeId, - challenge: hash(Buffer.from(challenge, "utf-8")).toString("hex"), - createdAt: new Date(), - registrationChallenge: true, - }); - - return { - challengeId, - challenge, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts deleted file mode 100644 index 533035bc91..0000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as speakeasy from "speakeasy"; -import * as QRCode from "qrcode"; -import config from "@/config/index.js"; -import { UserProfiles } from "@/models/index.js"; -import define from "../../../define.js"; -import { comparePassword } from "@/misc/password.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - password: { type: "string" }, - }, - required: ["password"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await comparePassword(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 url = speakeasy.otpauthURL({ - secret: secret.base32, - encoding: "base32", - label: user.username, - issuer: config.host, - }); - const dataUrl = await QRCode.toDataURL(url); - - return { - qr: dataUrl, - url, - secret: secret.base32, - label: user.username, - issuer: config.host, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts deleted file mode 100644 index 862c971e75..0000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { comparePassword } from "@/misc/password.js"; -import define from "../../../define.js"; -import { UserProfiles, UserSecurityKeys, Users } from "@/models/index.js"; -import { publishMainStream } from "@/services/stream.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - password: { type: "string" }, - credentialId: { type: "string" }, - }, - required: ["password", "credentialId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await comparePassword(ps.password, profile.password!); - - if (!same) { - throw new Error("incorrect password"); - } - - // Make sure we only delete the user's own creds - await UserSecurityKeys.delete({ - userId: user.id, - id: ps.credentialId, - }); - - // Publish meUpdated event - publishMainStream( - user.id, - "meUpdated", - await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - }), - ); - - return {}; -}); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts deleted file mode 100644 index 57d57ff65a..0000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ /dev/null @@ -1,33 +0,0 @@ -import define from "../../../define.js"; -import { UserProfiles } from "@/models/index.js"; -import { comparePassword } from "@/misc/password.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - password: { type: "string" }, - }, - required: ["password"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await comparePassword(ps.password, profile.password!); - - if (!same) { - throw new Error("incorrect password"); - } - - await UserProfiles.update(user.id, { - twoFactorSecret: null, - twoFactorEnabled: false, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts deleted file mode 100644 index b951601949..0000000000 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ /dev/null @@ -1,56 +0,0 @@ -import define from "../../define.js"; -import { AccessTokens } from "@/models/index.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - sort: { - type: "string", - enum: ["+createdAt", "-createdAt", "+lastUsedAt", "-lastUsedAt"], - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = AccessTokens.createQueryBuilder("token").where( - "token.userId = :userId", - { userId: user.id }, - ); - - switch (ps.sort) { - case "+createdAt": - query.orderBy("token.createdAt", "DESC"); - break; - case "-createdAt": - query.orderBy("token.createdAt", "ASC"); - break; - case "+lastUsedAt": - query.orderBy("token.lastUsedAt", "DESC"); - break; - case "-lastUsedAt": - query.orderBy("token.lastUsedAt", "ASC"); - break; - default: - query.orderBy("token.id", "ASC"); - break; - } - - const tokens = await query.getMany(); - - return await Promise.all( - tokens.map((token) => ({ - id: token.id, - name: token.name, - createdAt: token.createdAt, - lastUsedAt: token.lastUsedAt, - permission: token.permission, - })), - ); -}); diff --git a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts b/packages/backend/src/server/api/endpoints/i/authorized-apps.ts deleted file mode 100644 index f759b23037..0000000000 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ /dev/null @@ -1,40 +0,0 @@ -import define from "../../define.js"; -import { AccessTokens, Apps } from "@/models/index.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - sort: { type: "string", enum: ["desc", "asc"], default: "desc" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Get tokens - const tokens = await AccessTokens.find({ - where: { - userId: user.id, - }, - take: ps.limit, - skip: ps.offset, - order: { - id: ps.sort === "asc" ? 1 : -1, - }, - }); - - return await Promise.all( - tokens.map((token) => - Apps.pack(token.appId, user, { - detail: true, - }), - ), - ); -}); diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts deleted file mode 100644 index 8bbb3ad93a..0000000000 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ /dev/null @@ -1,36 +0,0 @@ -import define from "../../define.js"; -import { UserProfiles } from "@/models/index.js"; -import { hashPassword, comparePassword } from "@/misc/password.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - currentPassword: { type: "string" }, - newPassword: { type: "string", minLength: 1 }, - }, - required: ["currentPassword", "newPassword"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await comparePassword(ps.currentPassword, profile.password!); - - if (!same) { - throw new Error("incorrect password"); - } - - // Generate hash of password - const hash = await hashPassword(ps.newPassword); - - await UserProfiles.update(user.id, { - password: hash, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts deleted file mode 100644 index 781abe0b38..0000000000 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { UserProfiles, Users } from "@/models/index.js"; -import { deleteAccount } from "@/services/delete-account.js"; -import define from "../../define.js"; -import { comparePassword } from "@/misc/password.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - password: { type: "string" }, - }, - required: ["password"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const userDetailed = await Users.findOneByOrFail({ id: user.id }); - if (userDetailed.isDeleted) { - return; - } - - // Compare password - const same = await comparePassword(ps.password, profile.password!); - - if (!same) { - throw new Error("incorrect password"); - } - - await deleteAccount(user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/export-blocking.ts b/packages/backend/src/server/api/endpoints/i/export-blocking.ts deleted file mode 100644 index 4517ad5fab..0000000000 --- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts +++ /dev/null @@ -1,22 +0,0 @@ -import define from "../../define.js"; -import { createExportBlockingJob } from "@/queue/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - createExportBlockingJob(user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/export-following.ts b/packages/backend/src/server/api/endpoints/i/export-following.ts deleted file mode 100644 index a228de8f17..0000000000 --- a/packages/backend/src/server/api/endpoints/i/export-following.ts +++ /dev/null @@ -1,25 +0,0 @@ -import define from "../../define.js"; -import { createExportFollowingJob } from "@/queue/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - excludeMuting: { type: "boolean", default: false }, - excludeInactive: { type: "boolean", default: false }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - createExportFollowingJob(user, ps.excludeMuting, ps.excludeInactive); -}); diff --git a/packages/backend/src/server/api/endpoints/i/export-mute.ts b/packages/backend/src/server/api/endpoints/i/export-mute.ts deleted file mode 100644 index 7bddc434d4..0000000000 --- a/packages/backend/src/server/api/endpoints/i/export-mute.ts +++ /dev/null @@ -1,22 +0,0 @@ -import define from "../../define.js"; -import { createExportMuteJob } from "@/queue/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - createExportMuteJob(user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/export-notes.ts b/packages/backend/src/server/api/endpoints/i/export-notes.ts deleted file mode 100644 index 48506ed6d9..0000000000 --- a/packages/backend/src/server/api/endpoints/i/export-notes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import define from "../../define.js"; -import { createExportNotesJob } from "@/queue/index.js"; -import { DAY } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: DAY, - max: 1, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - createExportNotesJob(user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts b/packages/backend/src/server/api/endpoints/i/export-user-lists.ts deleted file mode 100644 index a71b1730b4..0000000000 --- a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts +++ /dev/null @@ -1,22 +0,0 @@ -import define from "../../define.js"; -import { createExportUserListsJob } from "@/queue/index.js"; -import { MINUTE } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: MINUTE, - max: 1, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - createExportUserListsJob(user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts deleted file mode 100644 index f0dbd2de6b..0000000000 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ /dev/null @@ -1,47 +0,0 @@ -import define from "../../define.js"; -import { NoteFavorites } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["account", "notes", "favorites"], - - requireCredential: true, - - kind: "read:favorites", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "NoteFavorite", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - NoteFavorites.createQueryBuilder("favorite"), - ps.sinceId, - ps.untilId, - ) - .andWhere("favorite.userId = :meId", { meId: user.id }) - .leftJoinAndSelect("favorite.note", "note"); - - const favorites = await query.take(ps.limit).getMany(); - - return await NoteFavorites.packMany(favorites, user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts b/packages/backend/src/server/api/endpoints/i/gallery/likes.ts deleted file mode 100644 index d71ee3e5a1..0000000000 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ /dev/null @@ -1,60 +0,0 @@ -import define from "../../../define.js"; -import { GalleryLikes } from "@/models/index.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; - -export const meta = { - tags: ["account", "gallery"], - - requireCredential: true, - - kind: "read:gallery-likes", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - post: { - type: "object", - optional: false, - nullable: false, - ref: "GalleryPost", - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - GalleryLikes.createQueryBuilder("like"), - ps.sinceId, - ps.untilId, - ) - .andWhere("like.userId = :meId", { meId: user.id }) - .leftJoinAndSelect("like.post", "post"); - - const likes = await query.take(ps.limit).getMany(); - - return await GalleryLikes.packMany(likes, user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts deleted file mode 100644 index e471731ae7..0000000000 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { GalleryPosts } from "@/models/index.js"; -import define from "../../../define.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; - -export const meta = { - tags: ["account", "gallery"], - - requireCredential: true, - - kind: "read:gallery", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "GalleryPost", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - GalleryPosts.createQueryBuilder("post"), - ps.sinceId, - ps.untilId, - ).andWhere("post.userId = :meId", { meId: user.id }); - - const posts = await query.take(ps.limit).getMany(); - - return await GalleryPosts.packMany(posts, user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts b/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts deleted file mode 100644 index bd58f9257a..0000000000 --- a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts +++ /dev/null @@ -1,38 +0,0 @@ -import define from "../../define.js"; -import { MutedNotes } from "@/models/index.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "read:account", - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - count: { - type: "number", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - return { - count: await MutedNotes.countBy({ - userId: user.id, - reason: "word", - }), - }; -}); diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts deleted file mode 100644 index e4f1da60cc..0000000000 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ /dev/null @@ -1,60 +0,0 @@ -import define from "../../define.js"; -import { createImportBlockingJob } from "@/queue/index.js"; -import { ApiError } from "../../error.js"; -import { DriveFiles } from "@/models/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - - limit: { - duration: HOUR, - max: 1, - }, - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); - - createImportBlockingJob(user, file.id); -}); diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts deleted file mode 100644 index 1a6c9b565d..0000000000 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ /dev/null @@ -1,59 +0,0 @@ -import define from "../../define.js"; -import { createImportFollowingJob } from "@/queue/index.js"; -import { ApiError } from "../../error.js"; -import { DriveFiles } from "@/models/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duratition: HOUR, - max: 1, - }, - - errors: { - noSuchFile: { - message: "No such file.", - code: "NO_SUCH_FILE", - id: "b98644cf-a5ac-4277-a502-0b8054a709a3", - }, - - unexpectedFileType: { - message: "Must be a CSV or JSON 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: 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 > 2_000_000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); - - createImportFollowingJob(user, file.id); -}); diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts deleted file mode 100644 index 20d240e739..0000000000 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ /dev/null @@ -1,60 +0,0 @@ -import define from "../../define.js"; -import { createImportMutingJob } from "@/queue/index.js"; -import { ApiError } from "../../error.js"; -import { DriveFiles } from "@/models/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - - limit: { - duration: HOUR, - max: 1, - }, - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); - - createImportMutingJob(user, file.id); -}); diff --git a/packages/backend/src/server/api/endpoints/i/import-posts.ts b/packages/backend/src/server/api/endpoints/i/import-posts.ts deleted file mode 100644 index 6fdf562fdb..0000000000 --- a/packages/backend/src/server/api/endpoints/i/import-posts.ts +++ /dev/null @@ -1,44 +0,0 @@ -import define from "../../define.js"; -import { createImportPostsJob } from "@/queue/index.js"; -import { ApiError } from "../../error.js"; -import { DriveFiles } from "@/models/index.js"; -import { DAY } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: DAY, - max: 1, - }, - errors: { - noSuchFile: { - message: "No such file.", - code: "NO_SUCH_FILE", - id: "e674141e-bd2a-ba85-e616-aefb187c9c2a", - }, - - emptyFile: { - message: "That file is empty.", - code: "EMPTY_FILE", - id: "d2f12af1-e7b4-feac-86a3-519548f2728e", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - signatureCheck: { type: "boolean" }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError(meta.errors.noSuchFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); - createImportPostsJob(user, file.id, ps.signatureCheck); -}); diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts deleted file mode 100644 index 03b1dffbbb..0000000000 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ /dev/null @@ -1,59 +0,0 @@ -import define from "../../define.js"; -import { createImportUserListsJob } from "@/queue/index.js"; -import { ApiError } from "../../error.js"; -import { DriveFiles } from "@/models/index.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - fileId: { type: "string", format: "misskey:id" }, - }, - required: ["fileId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile); - if (file.size === 0) throw new ApiError(meta.errors.emptyFile); - - createImportUserListsJob(user, file.id); -}); diff --git a/packages/backend/src/server/api/endpoints/i/known-as.ts b/packages/backend/src/server/api/endpoints/i/known-as.ts deleted file mode 100644 index 5e86e8b955..0000000000 --- a/packages/backend/src/server/api/endpoints/i/known-as.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { User, UserDetailedNotMeOnly } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; -import { resolveUser } from "@/remote/resolve-user.js"; -import acceptAllFollowRequests from "@/services/following/requests/accept-all.js"; -import { publishToFollowers } from "@/services/i/update.js"; -import { publishMainStream } from "@/services/stream.js"; -import { DAY } from "@/const.js"; -import { apiLogger } from "../../logger.js"; -import { UserProfiles } from "@/models/index.js"; -import config from "@/config/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["users"], - - secure: true, - requireCredential: true, - - limit: { - duration: DAY, - max: 30, - }, - - errors: { - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "fcd2eef9-a9b2-4c4f-8624-038099e90aa5", - }, - notRemote: { - message: "User is not remote. You can only migrate to other instances.", - code: "NOT_REMOTE", - id: "4362f8dc-731f-4ad8-a694-be2a88922a24", - }, - uriNull: { - message: "User ActivityPup URI is null.", - code: "URI_NULL", - id: "bf326f31-d430-4f97-9933-5d61e4d48a23", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - alsoKnownAs: { type: "string" }, - }, - required: ["alsoKnownAs"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - if (!ps.alsoKnownAs) throw new ApiError(meta.errors.noSuchUser); - - let unfiltered: string = ps.alsoKnownAs; - const updates = {} as Partial; - - if (!unfiltered) { - updates.alsoKnownAs = null; - } else { - if (unfiltered.startsWith("acct:")) unfiltered = unfiltered.substring(5); - if (unfiltered.startsWith("@")) unfiltered = unfiltered.substring(1); - if (!unfiltered.includes("@")) throw new ApiError(meta.errors.notRemote); - - const userAddress: string[] = unfiltered.split("@"); - const knownAs = await resolveUser(userAddress[0], userAddress[1]).catch( - (e) => { - apiLogger.warn(`failed to resolve remote user: ${e}`); - throw new ApiError(meta.errors.noSuchUser); - }, - ); - - const toUrl: string | null = knownAs.uri; - if (!toUrl) { - throw new ApiError(meta.errors.uriNull); - } - if (updates.alsoKnownAs == null || updates.alsoKnownAs.length === 0) { - updates.alsoKnownAs = [toUrl]; - } else { - updates.alsoKnownAs.push(toUrl); - } - } - - await Users.update(user.id, updates); - - const iObj = await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - }); - - // Publish meUpdated event - publishMainStream(user.id, "meUpdated", iObj); - - if (user.isLocked === false) { - acceptAllFollowRequests(user); - } - - publishToFollowers(user.id); - - return iObj; -}); diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts deleted file mode 100644 index 3d947063ff..0000000000 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import { resolveUser } from "@/remote/resolve-user.js"; -import { DAY } from "@/const.js"; -import DeliverManager from "@/remote/activitypub/deliver-manager.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { apiLogger } from "../../logger.js"; -import deleteFollowing from "@/services/following/delete.js"; -import create from "@/services/following/create.js"; -import { getUser } from "@/server/api/common/getters.js"; -import { Followings, Users } from "@/models/index.js"; -import { UserProfiles } from "@/models/index.js"; -import config from "@/config/index.js"; -import { publishMainStream } from "@/services/stream.js"; - -export const meta = { - tags: ["users"], - - secure: true, - requireCredential: true, - - limit: { - duration: DAY, - max: 5, - }, - - errors: { - noSuchMoveTarget: { - message: "No such move target.", - code: "NO_SUCH_MOVE_TARGET", - id: "b5c90186-4ab0-49c8-9bba-a1f76c202ba4", - }, - remoteAccountForbids: { - message: - "Remote account doesn't have proper 'Known As' alias. Did you remember to set it?", - code: "REMOTE_ACCOUNT_FORBIDS", - id: "b5c90186-4ab0-49c8-9bba-a1f766282ba4", - }, - notRemote: { - message: "User is not remote. You can only migrate to other instances.", - code: "NOT_REMOTE", - id: "4362f8dc-731f-4ad8-a694-be2a88922a24", - }, - adminForbidden: { - message: "Admins cant migrate.", - code: "NOT_ADMIN_FORBIDDEN", - id: "4362e8dc-731f-4ad8-a694-be2a88922a24", - }, - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "fcd2eef9-a9b2-4c4f-8624-038099e90aa5", - }, - uriNull: { - message: "User ActivityPup URI is null.", - code: "URI_NULL", - id: "bf326f31-d430-4f97-9933-5d61e4d48a23", - }, - localUriNull: { - message: "Local User ActivityPup URI is null.", - code: "URI_NULL", - id: "95ba11b9-90e8-43a5-ba16-7acc1ab32e71", - }, - alreadyMoved: { - message: "Account was already moved to another account.", - code: "ALREADY_MOVED", - id: "b234a14e-9ebe-4581-8000-074b3c215962", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - moveToAccount: { type: "string" }, - }, - required: ["moveToAccount"], -} as const; - -function moveActivity(toUrl: string, fromUrl: string) { - const activity = { - id: null, - actor: fromUrl, - type: "Move", - object: fromUrl, - target: toUrl, - } as any; - - return renderActivity(activity); -} - -export default define(meta, paramDef, async (ps, user) => { - if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchMoveTarget); - if (user.isAdmin) throw new ApiError(meta.errors.adminForbidden); - if (user.movedToUri) throw new ApiError(meta.errors.alreadyMoved); - - let unfiltered: string = ps.moveToAccount; - if (!unfiltered) { - throw new ApiError(meta.errors.noSuchMoveTarget); - } - - if (unfiltered.startsWith("acct:")) unfiltered = unfiltered.substring(5); - if (unfiltered.startsWith("@")) unfiltered = unfiltered.substring(1); - if (!unfiltered.includes("@")) throw new ApiError(meta.errors.notRemote); - - const userAddress: string[] = unfiltered.split("@"); - const moveTo: User = await resolveUser(userAddress[0], userAddress[1]).catch( - (e) => { - apiLogger.warn(`failed to resolve remote user: ${e}`); - throw new ApiError(meta.errors.noSuchMoveTarget); - }, - ); - let fromUrl: string | null = user.uri; - if (!fromUrl) { - fromUrl = `${config.url}/users/${user.id}`; - } - - let toUrl: string | null = moveTo.uri; - if (!toUrl) { - throw new ApiError(meta.errors.uriNull); - } - - let allowed = false; - - moveTo.alsoKnownAs?.forEach((element) => { - if (fromUrl!.includes(element)) allowed = true; - }); - - if (!(allowed && toUrl && fromUrl)) - throw new ApiError(meta.errors.remoteAccountForbids); - - const updates = {} as Partial; - - if (!toUrl) toUrl = ""; - updates.movedToUri = toUrl; - - await Users.update(user.id, updates); - const iObj = await Users.pack(user.id, user, { - detail: true, - includeSecrets: true, - }); - - const moveAct = moveActivity(toUrl, fromUrl); - const dm = new DeliverManager(user, moveAct); - dm.addFollowersRecipe(); - dm.execute(); - - // Publish meUpdated event - publishMainStream(user.id, "meUpdated", iObj); - - const followings = await Followings.findBy({ - followeeId: user.id, - }); - - followings.forEach(async (following) => { - //if follower is local - if (!following.followerHost) { - const follower = await getUser(following.followerId).catch((e) => { - if (e.id === "15348ddd-432d-49c2-8a5a-8069753becff") - throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - await deleteFollowing(follower!, user); - try { - await create(follower!, moveTo); - } catch (e) { - /* empty */ - } - } - }); - - return iObj; -}); diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts deleted file mode 100644 index 6e1aabef7d..0000000000 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Brackets } from "typeorm"; -import { - Notifications, - Followings, - Mutings, - Users, - UserProfiles, -} from "@/models/index.js"; -import { notificationTypes } from "@/types.js"; -import read from "@/services/note/read.js"; -import { readNotification } from "../../common/read-notification.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["account", "notifications"], - - requireCredential: true, - - limit: { - duration: 60000, - max: 15, - }, - - kind: "read:notifications", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Notification", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - following: { type: "boolean", default: false }, - unreadOnly: { type: "boolean", default: false }, - markAsRead: { type: "boolean", default: true }, - includeTypes: { - type: "array", - items: { - type: "string", - enum: notificationTypes, - }, - }, - excludeTypes: { - type: "array", - items: { - type: "string", - enum: notificationTypes, - }, - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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 mutingInstanceQuery = UserProfiles.createQueryBuilder("user_profile") - .select("user_profile.mutedInstances") - .where("user_profile.userId = :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("notifier.avatar", "notifierAvatar") - .leftJoinAndSelect("notifier.banner", "notifierBanner") - .leftJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - // muted users - query.andWhere( - new Brackets((qb) => { - qb.where( - `notification.notifierId NOT IN (${mutingQuery.getQuery()})`, - ).orWhere("notification.notifierId IS NULL"); - }), - ); - query.setParameters(mutingQuery.getParameters()); - - // muted instances - query.andWhere( - new Brackets((qb) => { - qb.andWhere("notifier.host IS NULL").orWhere( - `NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`, - ); - }), - ); - query.setParameters(mutingInstanceQuery.getParameters()); - - // suspended users - query.andWhere( - new Brackets((qb) => { - qb.where( - `notification.notifierId NOT IN (${suspendedQuery.getQuery()})`, - ).orWhere("notification.notifierId IS NULL"); - }), - ); - - if (ps.following) { - query.andWhere( - `((notification.notifierId IN (${followingQuery.getQuery()})) OR (notification.notifierId = :meId))`, - { meId: user.id }, - ); - query.setParameters(followingQuery.getParameters()); - } - - if (ps.includeTypes && ps.includeTypes.length > 0) { - query.andWhere("notification.type IN (:...includeTypes)", { - includeTypes: ps.includeTypes, - }); - } else if (ps.excludeTypes && ps.excludeTypes.length > 0) { - query.andWhere("notification.type NOT IN (:...excludeTypes)", { - excludeTypes: ps.excludeTypes, - }); - } - - if (ps.unreadOnly) { - query.andWhere("notification.isRead = false"); - } - - const notifications = await query.take(ps.limit).getMany(); - - // Mark all as read - if (notifications.length > 0 && ps.markAsRead) { - readNotification( - user.id, - notifications.map((x) => x.id), - ); - } - - const notes = notifications - .filter((notification) => - ["mention", "reply", "quote"].includes(notification.type), - ) - .map((notification) => notification.note!); - - if (notes.length > 0) { - read(user.id, notes); - } - - return await Notifications.packMany(notifications, user.id); -}); diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts deleted file mode 100644 index 1be783a061..0000000000 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { PageLikes } from "@/models/index.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["account", "pages"], - - requireCredential: true, - - kind: "read:page-likes", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - page: { - type: "object", - optional: false, - nullable: false, - ref: "Page", - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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 PageLikes.packMany(likes, user); -}); diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts deleted file mode 100644 index 78b72e3bce..0000000000 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Pages } from "@/models/index.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["account", "pages"], - - requireCredential: true, - - kind: "read:pages", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Page", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - Pages.createQueryBuilder("page"), - ps.sinceId, - ps.untilId, - ).andWhere("page.userId = :meId", { meId: user.id }); - - const pages = await query.take(ps.limit).getMany(); - - return await Pages.packMany(pages); -}); diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts deleted file mode 100644 index 40aa579184..0000000000 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { addPinned } from "@/services/i/pin.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Users } from "@/models/index.js"; - -export const meta = { - tags: ["account", "notes"], - - requireCredential: true, - - kind: "write:account", - - 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", - optional: false, - nullable: false, - ref: "MeDetailed", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - await addPinned(user, ps.noteId).catch((e) => { - if (e.id === "70c4e51f-5bea-449c-a030-53bee3cce202") - throw new ApiError(meta.errors.noSuchNote); - if (e.id === "15a018eb-58e5-4da1-93be-330fcc5e4e1a") - throw new ApiError(meta.errors.pinLimitExceeded); - if (e.id === "23f0cf4e-59a3-4276-a91d-61a5891c1514") - throw new ApiError(meta.errors.alreadyPinned); - throw e; - }); - - return await Users.pack(user.id, user, { - detail: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts b/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts deleted file mode 100644 index 0333677275..0000000000 --- a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import define from "../../define.js"; -import { MessagingMessages, UserGroupJoinings } from "@/models/index.js"; - -export const meta = { - tags: ["account", "messaging"], - - requireCredential: true, - - kind: "write:account", -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Update documents - await MessagingMessages.update( - { - recipientId: user.id, - isRead: false, - }, - { - isRead: true, - }, - ); - - const joinings = await UserGroupJoinings.findBy({ userId: user.id }); - - await Promise.all( - joinings.map((j) => - MessagingMessages.createQueryBuilder() - .update() - .set({ - reads: (() => `array_append("reads", '${user.id}')`) as any, - }) - .where("groupId = :groupId", { groupId: j.userGroupId }) - .andWhere("userId != :userId", { userId: user.id }) - .andWhere("NOT (:userId = ANY(reads))", { userId: user.id }) - .execute(), - ), - ); - - publishMainStream(user.id, "readAllMessagingMessages"); -}); diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts deleted file mode 100644 index 8a8857c83c..0000000000 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import define from "../../define.js"; -import { NoteUnreads } from "@/models/index.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "write:account", -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Remove documents - await NoteUnreads.delete({ - userId: user.id, - }); - - // 全て既読になったイベントを発行 - publishMainStream(user.id, "readAllUnreadMentions"); - publishMainStream(user.id, "readAllUnreadSpecifiedNotes"); -}); diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts deleted file mode 100644 index 5218dba871..0000000000 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ /dev/null @@ -1,60 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { genId } from "@/misc/gen-id.js"; -import { AnnouncementReads, Announcements, Users } from "@/models/index.js"; -import { publishMainStream } from "@/services/stream.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchAnnouncement: { - message: "No such announcement.", - code: "NO_SUCH_ANNOUNCEMENT", - id: "184663db-df88-4bc2-8b52-fb85f0681939", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - announcementId: { type: "string", format: "misskey:id" }, - }, - required: ["announcementId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Check if announcement exists - const announcement = await Announcements.findOneBy({ id: ps.announcementId }); - - if (announcement == null) { - throw new ApiError(meta.errors.noSuchAnnouncement); - } - - // Check if already read - const read = await AnnouncementReads.findOneBy({ - announcementId: ps.announcementId, - userId: user.id, - }); - - if (read != null) { - return; - } - - // Create read - await AnnouncementReads.insert({ - id: genId(), - createdAt: new Date(), - announcementId: ps.announcementId, - userId: user.id, - }); - - if (!(await Users.getHasUnreadAnnouncement(user.id))) { - publishMainStream(user.id, "readAllAnnouncements"); - } -}); diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts deleted file mode 100644 index b5b34c0902..0000000000 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - publishInternalEvent, - publishMainStream, - publishUserEvent, -} from "@/services/stream.js"; -import generateUserToken from "../../common/generate-native-user-token.js"; -import define from "../../define.js"; -import { Users, UserProfiles } from "@/models/index.js"; -import { comparePassword } from "@/misc/password.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - password: { type: "string" }, - }, - required: ["password"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const freshUser = await Users.findOneByOrFail({ id: user.id }); - const oldToken = freshUser.token; - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await comparePassword(ps.password, profile.password!); - - if (!same) { - throw new Error("incorrect password"); - } - - const newToken = generateUserToken(); - - await Users.update(user.id, { - token: newToken, - }); - - // Publish event - publishInternalEvent("userTokenRegenerated", { - id: user.id, - oldToken, - newToken, - }); - publishMainStream(user.id, "myTokenRegenerated"); - - // Terminate streaming - setTimeout(() => { - publishUserEvent(user.id, "terminate", {}); - }, 5000); -}); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts deleted file mode 100644 index ee9fe7e9d5..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ /dev/null @@ -1,40 +0,0 @@ -import define from "../../../define.js"; -import { RegistryItems } from "@/models/index.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - scope: { - type: "array", - default: [], - items: { - type: "string", - pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), - }, - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts deleted file mode 100644 index 85900bd74d..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ /dev/null @@ -1,52 +0,0 @@ -import define from "../../../define.js"; -import { RegistryItems } from "@/models/index.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: { - noSuchKey: { - message: "No such key.", - code: "NO_SUCH_KEY", - id: "97a1e8e7-c0f7-47d2-957a-92e61256e01a", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - key: { type: "string" }, - scope: { - type: "array", - default: [], - items: { - type: "string", - pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), - }, - }, - }, - required: ["key"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder("item") - .where("item.domain IS NULL") - .andWhere("item.userId = :userId", { userId: user.id }) - .andWhere("item.key = :key", { key: ps.key }) - .andWhere("item.scope = :scope", { scope: ps.scope }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - return { - updatedAt: item.updatedAt, - value: item.value, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts deleted file mode 100644 index f98c6c929f..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ApiError } from "../../../error.js"; -import define from "../../../define.js"; -import { RegistryItems } from "@/models/index.js"; - -export const meta = { - requireCredential: true, - - secure: false, - - errors: { - noSuchKey: { - message: "No such key.", - code: "NO_SUCH_KEY", - id: "ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - key: { type: "string" }, - scope: { - type: "array", - default: [], - items: { - type: "string", - pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), - }, - }, - }, - required: ["key"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - if (ps.key !== "reactions") return; - const query = RegistryItems.createQueryBuilder("item") - .where("item.domain IS NULL") - .andWhere("item.userId = :userId", { userId: user.id }) - .andWhere("item.key = :key", { key: ps.key }) - .andWhere("item.scope = :scope", { scope: ps.scope }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - return item.value; -}); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts deleted file mode 100644 index b143b7228a..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ /dev/null @@ -1,49 +0,0 @@ -import define from "../../../define.js"; -import { RegistryItems } from "@/models/index.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: { - noSuchKey: { - message: "No such key.", - code: "NO_SUCH_KEY", - id: "ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - key: { type: "string" }, - scope: { - type: "array", - default: [], - items: { - type: "string", - pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), - }, - }, - }, - required: ["key"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder("item") - .where("item.domain IS NULL") - .andWhere("item.userId = :userId", { userId: user.id }) - .andWhere("item.key = :key", { key: ps.key }) - .andWhere("item.scope = :scope", { scope: ps.scope }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - return item.value; -}); diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts deleted file mode 100644 index 23698dc53a..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ /dev/null @@ -1,54 +0,0 @@ -import define from "../../../define.js"; -import { RegistryItems } from "@/models/index.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - scope: { - type: "array", - default: [], - items: { - type: "string", - pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), - }, - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts deleted file mode 100644 index ad7d08c5af..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ /dev/null @@ -1,35 +0,0 @@ -import define from "../../../define.js"; -import { RegistryItems } from "@/models/index.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - scope: { - type: "array", - default: [], - items: { - type: "string", - pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), - }, - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder("item") - .select("item.key") - .where("item.domain IS NULL") - .andWhere("item.userId = :userId", { userId: user.id }) - .andWhere("item.scope = :scope", { scope: ps.scope }); - - const items = await query.getMany(); - - return items.map((x) => x.key); -}); diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts deleted file mode 100644 index d3793b0e20..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ /dev/null @@ -1,49 +0,0 @@ -import define from "../../../define.js"; -import { RegistryItems } from "@/models/index.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: { - noSuchKey: { - message: "No such key.", - code: "NO_SUCH_KEY", - id: "1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - key: { type: "string" }, - scope: { - type: "array", - default: [], - items: { - type: "string", - pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), - }, - }, - }, - required: ["key"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder("item") - .where("item.domain IS NULL") - .andWhere("item.userId = :userId", { userId: user.id }) - .andWhere("item.key = :key", { key: ps.key }) - .andWhere("item.scope = :scope", { scope: ps.scope }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - await RegistryItems.remove(item); -}); diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts deleted file mode 100644 index 3d66359c1d..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import define from "../../../define.js"; -import { RegistryItems } from "@/models/index.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder("item") - .select("item.scope") - .where("item.domain IS NULL") - .andWhere("item.userId = :userId", { userId: user.id }); - - const items = await query.getMany(); - - const res = [] as string[][]; - - for (const item of items) { - if (res.some((scope) => scope.join(".") === item.scope.join("."))) continue; - res.push(item.scope); - } - - return res; -}); diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts deleted file mode 100644 index 7f9eebd5e0..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import define from "../../../define.js"; -import { RegistryItems } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - key: { type: "string", minLength: 1 }, - value: {}, - scope: { - type: "array", - default: [], - items: { - type: "string", - pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), - }, - }, - }, - required: ["key", "value"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = RegistryItems.createQueryBuilder("item") - .where("item.domain IS NULL") - .andWhere("item.userId = :userId", { userId: user.id }) - .andWhere("item.key = :key", { key: ps.key }) - .andWhere("item.scope = :scope", { scope: ps.scope }); - - const existingItem = await query.getOne(); - - if (existingItem) { - await RegistryItems.update(existingItem.id, { - updatedAt: new Date(), - value: ps.value, - }); - } else { - await RegistryItems.insert({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), - userId: user.id, - domain: null, - scope: ps.scope, - key: ps.key, - value: ps.value, - }); - } - - // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする - publishMainStream(user.id, "registryUpdated", { - scope: ps.scope, - key: ps.key, - value: ps.value, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts deleted file mode 100644 index 308442bf7b..0000000000 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ /dev/null @@ -1,31 +0,0 @@ -import define from "../../define.js"; -import { AccessTokens } from "@/models/index.js"; -import { publishUserEvent } from "@/services/stream.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - tokenId: { type: "string", format: "misskey:id" }, - }, - required: ["tokenId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const token = await AccessTokens.findOneBy({ id: ps.tokenId }); - - if (token) { - await AccessTokens.delete({ - id: ps.tokenId, - userId: user.id, - }); - - // Terminate streaming - publishUserEvent(user.id, "terminate"); - } -}); diff --git a/packages/backend/src/server/api/endpoints/i/signin-history.ts b/packages/backend/src/server/api/endpoints/i/signin-history.ts deleted file mode 100644 index 288b750b7b..0000000000 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ /dev/null @@ -1,31 +0,0 @@ -import define from "../../define.js"; -import { Signins } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - Signins.createQueryBuilder("signin"), - ps.sinceId, - ps.untilId, - ).andWhere("signin.userId = :meId", { meId: user.id }); - - const history = await query.take(ps.limit).getMany(); - - return await Promise.all(history.map((record) => Signins.pack(record))); -}); diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts deleted file mode 100644 index c248eb34e5..0000000000 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { removePinned } from "@/services/i/pin.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { Users } from "@/models/index.js"; - -export const meta = { - tags: ["account", "notes"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "454170ce-9d63-4a43-9da1-ea10afe81e21", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "MeDetailed", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - await removePinned(user, ps.noteId).catch((e) => { - if (e.id === "b302d4cf-c050-400a-bbb3-be208681f40c") - throw new ApiError(meta.errors.noSuchNote); - throw e; - }); - - return await Users.pack(user.id, user, { - detail: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts deleted file mode 100644 index 94ad6b3c72..0000000000 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import define from "../../define.js"; -import rndstr from "rndstr"; -import config from "@/config/index.js"; -import { Users, UserProfiles } from "@/models/index.js"; -import { sendEmail } from "@/services/send-email.js"; -import { ApiError } from "../../error.js"; -import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; -import { HOUR } from "@/const.js"; -import { comparePassword } from "@/misc/password.js"; - -export const meta = { - requireCredential: true, - - secure: true, - - limit: { - duration: HOUR, - max: 3, - }, - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - password: { type: "string" }, - email: { type: "string", nullable: true }, - }, - required: ["password"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await comparePassword(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/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts deleted file mode 100644 index 56ed64296b..0000000000 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ /dev/null @@ -1,307 +0,0 @@ -import RE2 from "re2"; -import * as mfm from "mfm-js"; -import { publishMainStream, publishUserEvent } from "@/services/stream.js"; -import acceptAllFollowRequests from "@/services/following/requests/accept-all.js"; -import { publishToFollowers } from "@/services/i/update.js"; -import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js"; -import { extractHashtags } from "@/misc/extract-hashtags.js"; -import { updateUsertags } from "@/services/update-hashtag.js"; -import { Users, DriveFiles, UserProfiles, Pages } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import type { UserProfile } from "@/models/entities/user-profile.js"; -import { notificationTypes } from "@/types.js"; -import { normalizeForSearch } from "@/misc/normalize-for-search.js"; -import { langmap } from "@/misc/langmap.js"; -import { ApiError } from "../../error.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "write:account", - - 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", - }, - - invalidRegexp: { - message: "Invalid Regular Expression.", - code: "INVALID_REGEXP", - id: "0d786918-10df-41cd-8f33-8dec7d9a89a5", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "MeDetailed", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { ...Users.nameSchema, nullable: true }, - description: { ...Users.descriptionSchema, nullable: true }, - location: { ...Users.locationSchema, nullable: true }, - birthday: { ...Users.birthdaySchema, nullable: true }, - lang: { - type: "string", - enum: [null, ...Object.keys(langmap)], - nullable: true, - }, - avatarId: { type: "string", format: "misskey:id", nullable: true }, - bannerId: { type: "string", format: "misskey:id", nullable: true }, - fields: { - type: "array", - minItems: 0, - maxItems: 16, - items: { - type: "object", - properties: { - name: { type: "string" }, - value: { type: "string" }, - }, - required: ["name", "value"], - }, - }, - isLocked: { type: "boolean" }, - isExplorable: { type: "boolean" }, - hideOnlineStatus: { type: "boolean" }, - publicReactions: { type: "boolean" }, - carefulBot: { type: "boolean" }, - autoAcceptFollowed: { type: "boolean" }, - noCrawle: { type: "boolean" }, - isBot: { type: "boolean" }, - isCat: { type: "boolean" }, - speakAsCat: { type: "boolean" }, - showTimelineReplies: { type: "boolean" }, - injectFeaturedNote: { type: "boolean" }, - receiveAnnouncementEmail: { type: "boolean" }, - alwaysMarkNsfw: { type: "boolean" }, - autoSensitive: { type: "boolean" }, - ffVisibility: { type: "string", enum: ["public", "followers", "private"] }, - pinnedPageId: { type: "string", format: "misskey:id", nullable: true }, - mutedWords: { type: "array" }, - mutedInstances: { - type: "array", - items: { - type: "string", - }, - }, - mutingNotificationTypes: { - type: "array", - items: { - type: "string", - enum: notificationTypes, - }, - }, - emailNotificationTypes: { - type: "array", - items: { - type: "string", - }, - }, - }, -} as const; - -export default define(meta, paramDef, async (ps, _user, token) => { - const user = await Users.findOneByOrFail({ id: _user.id }); - const isSecure = token == null; - - const updates = {} as Partial; - const profileUpdates = {} as Partial; - - const profile = await UserProfiles.findOneByOrFail({ userId: 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) { - // validate regular expression syntax - ps.mutedWords - .filter((x) => !Array.isArray(x)) - .forEach((x) => { - const regexp = x.match(/^\/(.+)\/(.*)$/); - if (!regexp) throw new ApiError(meta.errors.invalidRegexp); - - try { - new RE2(regexp[1], regexp[2]); - } catch (err) { - throw new ApiError(meta.errors.invalidRegexp); - } - }); - - profileUpdates.mutedWords = ps.mutedWords; - profileUpdates.enableWordMute = ps.mutedWords.length > 0; - } - if (ps.mutedInstances !== undefined) - profileUpdates.mutedInstances = ps.mutedInstances; - 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.showTimelineReplies === "boolean") - updates.showTimelineReplies = ps.showTimelineReplies; - 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.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat; - 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 (typeof ps.autoSensitive === "boolean") - profileUpdates.autoSensitive = ps.autoSensitive; - if (ps.emailNotificationTypes !== undefined) - profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; - - if (ps.avatarId) { - const avatar = await DriveFiles.findOneBy({ id: 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); - } - - if (ps.bannerId) { - const banner = await DriveFiles.findOneBy({ id: 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); - } - - if (ps.pinnedPageId) { - const page = await Pages.findOneBy({ id: 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.parseSimple(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.findOneBy({ userId: user.id }), - ); - - // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 - if (user.isLocked && ps.isLocked === false) { - acceptAllFollowRequests(user); - } - - // フォロワーにUpdateを配信 - publishToFollowers(user.id); - - return iObj; -}); diff --git a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts b/packages/backend/src/server/api/endpoints/i/user-group-invites.ts deleted file mode 100644 index d0c6caf0e2..0000000000 --- a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts +++ /dev/null @@ -1,60 +0,0 @@ -import define from "../../define.js"; -import { UserGroupInvitations } from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["account", "groups"], - - requireCredential: true, - - kind: "read:user-groups", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - group: { - type: "object", - optional: false, - nullable: false, - ref: "UserGroup", - }, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - UserGroupInvitations.createQueryBuilder("invitation"), - ps.sinceId, - ps.untilId, - ) - .andWhere("invitation.userId = :meId", { meId: user.id }) - .leftJoinAndSelect("invitation.userGroup", "user_group"); - - const invitations = await query.take(ps.limit).getMany(); - - return await UserGroupInvitations.packMany(invitations); -}); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts deleted file mode 100644 index 2b0f1781ea..0000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ /dev/null @@ -1,46 +0,0 @@ -import define from "../../../define.js"; -import { genId } from "@/misc/gen-id.js"; -import { Webhooks } from "@/models/index.js"; -import { publishInternalEvent } from "@/services/stream.js"; -import { webhookEventTypes } from "@/models/entities/webhook.js"; - -export const meta = { - tags: ["webhooks"], - - requireCredential: true, - - kind: "write:account", -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string", minLength: 1, maxLength: 100 }, - url: { type: "string", minLength: 1, maxLength: 1024 }, - secret: { type: "string", minLength: 1, maxLength: 1024 }, - on: { - type: "array", - items: { - type: "string", - enum: webhookEventTypes, - }, - }, - }, - required: ["name", "url", "secret", "on"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - url: ps.url, - secret: ps.secret, - on: ps.on, - }).then((x) => Webhooks.findOneByOrFail(x.identifiers[0])); - - publishInternalEvent("webhookCreated", webhook); - - return webhook; -}); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts b/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts deleted file mode 100644 index 4a2c3d83be..0000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ /dev/null @@ -1,43 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { Webhooks } from "@/models/index.js"; -import { publishInternalEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["webhooks"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchWebhook: { - message: "No such webhook.", - code: "NO_SUCH_WEBHOOK", - id: "bae73e5a-5522-4965-ae19-3a8688e71d82", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - webhookId: { type: "string", format: "misskey:id" }, - }, - required: ["webhookId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.findOneBy({ - id: ps.webhookId, - userId: user.id, - }); - - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } - - await Webhooks.delete(webhook.id); - - publishInternalEvent("webhookDeleted", webhook); -}); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts deleted file mode 100644 index 3afead5996..0000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ /dev/null @@ -1,24 +0,0 @@ -import define from "../../../define.js"; -import { Webhooks } from "@/models/index.js"; - -export const meta = { - tags: ["webhooks", "account"], - - requireCredential: true, - - kind: "read:account", -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const webhooks = await Webhooks.findBy({ - userId: me.id, - }); - - return webhooks; -}); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts deleted file mode 100644 index 96c0457475..0000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ /dev/null @@ -1,40 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { Webhooks } from "@/models/index.js"; - -export const meta = { - tags: ["webhooks"], - - requireCredential: true, - - kind: "read:account", - - errors: { - noSuchWebhook: { - message: "No such webhook.", - code: "NO_SUCH_WEBHOOK", - id: "50f614d9-3047-4f7e-90d8-ad6b2d5fb098", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - webhookId: { type: "string", format: "misskey:id" }, - }, - required: ["webhookId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.findOneBy({ - id: ps.webhookId, - userId: user.id, - }); - - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } - - return webhook; -}); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts deleted file mode 100644 index 161d705e12..0000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ /dev/null @@ -1,61 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { Webhooks } from "@/models/index.js"; -import { publishInternalEvent } from "@/services/stream.js"; -import { webhookEventTypes } from "@/models/entities/webhook.js"; - -export const meta = { - tags: ["webhooks"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchWebhook: { - message: "No such webhook.", - code: "NO_SUCH_WEBHOOK", - id: "fb0fea69-da18-45b1-828d-bd4fd1612518", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - webhookId: { type: "string", format: "misskey:id" }, - name: { type: "string", minLength: 1, maxLength: 100 }, - url: { type: "string", minLength: 1, maxLength: 1024 }, - secret: { type: "string", minLength: 1, maxLength: 1024 }, - on: { - type: "array", - items: { - type: "string", - enum: webhookEventTypes, - }, - }, - active: { type: "boolean" }, - }, - required: ["webhookId", "name", "url", "secret", "on", "active"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const webhook = await Webhooks.findOneBy({ - id: ps.webhookId, - userId: user.id, - }); - - if (webhook == null) { - throw new ApiError(meta.errors.noSuchWebhook); - } - - await Webhooks.update(webhook.id, { - name: ps.name, - url: ps.url, - secret: ps.secret, - on: ps.on, - active: ps.active, - }); - - publishInternalEvent("webhookUpdated", webhook); -}); diff --git a/packages/backend/src/server/api/endpoints/latest-version.ts b/packages/backend/src/server/api/endpoints/latest-version.ts deleted file mode 100644 index 72e84ae044..0000000000 --- a/packages/backend/src/server/api/endpoints/latest-version.ts +++ /dev/null @@ -1,29 +0,0 @@ -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - - requireCredential: false, - requireCredentialPrivateMode: true, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - let tag_name; - await fetch( - "https://codeberg.org/api/v1/repos/calckey/calckey/releases?draft=false&pre-release=false&page=1&limit=1", - ) - .then((response) => response.json()) - .then((data) => { - tag_name = data[0].tag_name; - }); - - return { - tag_name, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/messaging/history.ts b/packages/backend/src/server/api/endpoints/messaging/history.ts deleted file mode 100644 index 7d1df69850..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/history.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Brackets } from "typeorm"; -import type { MessagingMessage } from "@/models/entities/messaging-message.js"; -import { - MessagingMessages, - Mutings, - UserGroupJoinings, -} from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["messaging"], - - requireCredential: true, - - kind: "read:messaging", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "MessagingMessage", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - group: { type: "boolean", default: false }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const mute = await Mutings.findBy({ - muterId: user.id, - }); - - const groups = ps.group - ? await UserGroupJoinings.findBy({ - userId: user.id, - }).then((xs) => xs.map((x) => x.userGroupId)) - : []; - - if (ps.group && groups.length === 0) { - return []; - } - - const history: MessagingMessage[] = []; - - for (let i = 0; i < ps.limit; i++) { - const found = ps.group - ? history.map((m) => m.groupId!) - : history.map((m) => (m.userId === user.id ? m.recipientId! : m.userId!)); - - const query = MessagingMessages.createQueryBuilder("message").orderBy( - "message.createdAt", - "DESC", - ); - - if (ps.group) { - query.where("message.groupId IN (:...groups)", { groups: groups }); - - if (found.length > 0) { - query.andWhere("message.groupId NOT IN (:...found)", { found: found }); - } - } else { - query.where( - new Brackets((qb) => { - qb.where("message.userId = :userId", { userId: user.id }).orWhere( - "message.recipientId = :userId", - { userId: user.id }, - ); - }), - ); - query.andWhere("message.groupId IS NULL"); - - if (found.length > 0) { - query.andWhere("message.userId NOT IN (:...found)", { found: found }); - query.andWhere("message.recipientId NOT IN (:...found)", { - found: found, - }); - } - - if (mute.length > 0) { - query.andWhere("message.userId NOT IN (:...mute)", { - mute: mute.map((m) => m.muteeId), - }); - query.andWhere("message.recipientId NOT IN (:...mute)", { - mute: mute.map((m) => m.muteeId), - }); - } - } - - const message = await query.getOne(); - - if (message) { - history.push(message); - } else { - break; - } - } - - return await Promise.all( - history.map((h) => MessagingMessages.pack(h.id, user)), - ); -}); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages.ts b/packages/backend/src/server/api/endpoints/messaging/messages.ts deleted file mode 100644 index 4b5440383c..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ /dev/null @@ -1,182 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; -import { - MessagingMessages, - UserGroups, - UserGroupJoinings, - Users, -} from "@/models/index.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { Brackets } from "typeorm"; -import { - readUserMessagingMessage, - readGroupMessagingMessage, - deliverReadActivity, -} from "../../common/read-messaging-message.js"; - -export const meta = { - tags: ["messaging"], - - requireCredential: true, - - kind: "read:messaging", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - markAsRead: { type: "boolean", default: true }, - }, - anyOf: [ - { - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], - }, - { - properties: { - groupId: { type: "string", format: "misskey:id" }, - }, - required: ["groupId"], - }, - ], -} as const; - -export default define(meta, paramDef, 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.findOneBy({ id: ps.groupId }); - - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // check joined - const joining = await UserGroupJoinings.findOneBy({ - 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, - }), - ), - ); - } -}); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts deleted file mode 100644 index ed9ae16df0..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ /dev/null @@ -1,165 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getUser } from "../../../common/getters.js"; -import { - MessagingMessages, - DriveFiles, - UserGroups, - UserGroupJoinings, - Blockings, -} from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import type { UserGroup } from "@/models/entities/user-group.js"; -import { createMessage } from "@/services/messages/create.js"; - -export const meta = { - tags: ["messaging"], - - requireCredential: true, - - kind: "write:messaging", - - res: { - type: "object", - optional: false, - nullable: false, - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - text: { type: "string", nullable: true, maxLength: 3000 }, - fileId: { type: "string", format: "misskey:id" }, - }, - anyOf: [ - { - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], - }, - { - properties: { - groupId: { type: "string", format: "misskey:id" }, - }, - required: ["groupId"], - }, - ], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - let recipientUser: User | null; - let recipientGroup: UserGroup | null; - - 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.findOneBy({ - 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.findOneBy({ id: ps.groupId! }); - - if (recipientGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // check joined - const joining = await UserGroupJoinings.findOneBy({ - 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.findOneBy({ - id: ps.fileId, - userId: user.id, - }); - - if (file == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - // テキストが無いかつ添付ファイルも無かったらエラー - if ((ps.text == null || ps.text.trim() === "") && file == null) { - throw new ApiError(meta.errors.contentRequired); - } - - return await createMessage( - user, - recipientUser, - recipientGroup, - ps.text, - file, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts b/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts deleted file mode 100644 index 42ff050d16..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts +++ /dev/null @@ -1,48 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { MessagingMessages } from "@/models/index.js"; -import { deleteMessage } from "@/services/messages/delete.js"; -import { SECOND, HOUR } from "@/const.js"; - -export const meta = { - tags: ["messaging"], - - requireCredential: true, - - kind: "write:messaging", - - limit: { - duration: HOUR, - max: 300, - minInterval: SECOND, - }, - - errors: { - noSuchMessage: { - message: "No such message.", - code: "NO_SUCH_MESSAGE", - id: "54b5b326-7925-42cf-8019-130fda8b56af", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - messageId: { type: "string", format: "misskey:id" }, - }, - required: ["messageId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const message = await MessagingMessages.findOneBy({ - id: ps.messageId, - userId: user.id, - }); - - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } - - await deleteMessage(message); -}); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts b/packages/backend/src/server/api/endpoints/messaging/messages/read.ts deleted file mode 100644 index 0ef013b799..0000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts +++ /dev/null @@ -1,57 +0,0 @@ -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { MessagingMessages } from "@/models/index.js"; -import { - readUserMessagingMessage, - readGroupMessagingMessage, -} from "../../../common/read-messaging-message.js"; - -export const meta = { - tags: ["messaging"], - - requireCredential: true, - - kind: "write:messaging", - - errors: { - noSuchMessage: { - message: "No such message.", - code: "NO_SUCH_MESSAGE", - id: "86d56a2f-a9c3-4afb-b13c-3e9bfef9aa14", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - messageId: { type: "string", format: "misskey:id" }, - }, - required: ["messageId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const message = await MessagingMessages.findOneBy({ id: ps.messageId }); - - if (message == null) { - throw new ApiError(meta.errors.noSuchMessage); - } - - if (message.recipientId) { - await readUserMessagingMessage(user.id, message.userId, [message.id]).catch( - (e) => { - if (e.id === "e140a4bf-49ce-4fb6-b67c-b78dadf6b52f") - throw new ApiError(meta.errors.noSuchMessage); - throw e; - }, - ); - } else if (message.groupId) { - await readGroupMessagingMessage(user.id, message.groupId, [ - message.id, - ]).catch((e) => { - if (e.id === "930a270c-714a-46b2-b776-ad27276dc569") - throw new ApiError(meta.errors.noSuchMessage); - throw e; - }); - } -}); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts deleted file mode 100644 index 4dc1c941e3..0000000000 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ /dev/null @@ -1,530 +0,0 @@ -import { IsNull, MoreThan } from "typeorm"; -import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Ads, Emojis, Users } from "@/models/index.js"; -import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - - requireCredential: false, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - maintainerName: { - type: "string", - optional: false, - nullable: true, - }, - maintainerEmail: { - type: "string", - optional: false, - nullable: true, - }, - version: { - type: "string", - optional: false, - nullable: false, - example: config.version, - }, - name: { - type: "string", - optional: false, - nullable: false, - }, - uri: { - type: "string", - optional: false, - nullable: false, - format: "url", - example: "https://calckey.example.com", - }, - description: { - type: "string", - optional: false, - nullable: true, - }, - langs: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - tosUrl: { - type: "string", - optional: false, - nullable: true, - }, - repositoryUrl: { - type: "string", - optional: false, - nullable: false, - default: "https://codeberg.org/calckey/calckey", - }, - feedbackUrl: { - type: "string", - optional: false, - nullable: false, - default: "https://codeberg.org/calckey/calckey/issues", - }, - defaultDarkTheme: { - type: "string", - optional: false, - nullable: true, - }, - defaultLightTheme: { - type: "string", - optional: false, - nullable: true, - }, - disableRegistration: { - type: "boolean", - optional: false, - nullable: false, - }, - disableLocalTimeline: { - type: "boolean", - optional: false, - nullable: false, - }, - disableRecommendedTimeline: { - type: "boolean", - optional: false, - nullable: false, - }, - disableGlobalTimeline: { - type: "boolean", - optional: false, - nullable: false, - }, - driveCapacityPerLocalUserMb: { - type: "number", - optional: false, - nullable: false, - }, - driveCapacityPerRemoteUserMb: { - type: "number", - optional: false, - nullable: false, - }, - cacheRemoteFiles: { - type: "boolean", - optional: false, - nullable: false, - }, - emailRequiredForSignup: { - type: "boolean", - optional: false, - nullable: false, - }, - enableHcaptcha: { - type: "boolean", - optional: false, - nullable: false, - }, - hcaptchaSiteKey: { - type: "string", - optional: false, - nullable: true, - }, - enableRecaptcha: { - type: "boolean", - optional: false, - nullable: false, - }, - recaptchaSiteKey: { - type: "string", - optional: false, - nullable: true, - }, - swPublickey: { - type: "string", - optional: false, - nullable: true, - }, - mascotImageUrl: { - type: "string", - optional: false, - nullable: false, - default: "/assets/ai.png", - }, - bannerUrl: { - type: "string", - optional: false, - nullable: false, - }, - errorImageUrl: { - type: "string", - optional: false, - nullable: false, - default: "https://xn--931a.moe/aiart/yubitun.png", - }, - iconUrl: { - type: "string", - optional: false, - nullable: true, - }, - maxNoteTextLength: { - type: "number", - optional: false, - nullable: false, - }, - maxCaptionTextLength: { - type: "number", - optional: false, - nullable: false, - }, - emojis: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - aliases: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, - category: { - type: "string", - optional: false, - nullable: true, - }, - host: { - type: "string", - optional: false, - nullable: true, - description: "The local host is represented with `null`.", - }, - url: { - type: "string", - optional: false, - nullable: false, - format: "url", - }, - }, - }, - }, - ads: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - place: { - type: "string", - optional: false, - nullable: false, - }, - url: { - type: "string", - optional: false, - nullable: false, - format: "url", - }, - imageUrl: { - type: "string", - optional: false, - nullable: false, - format: "url", - }, - }, - }, - }, - requireSetup: { - type: "boolean", - optional: false, - nullable: false, - example: false, - }, - enableEmail: { - type: "boolean", - optional: false, - nullable: false, - }, - enableTwitterIntegration: { - type: "boolean", - optional: false, - nullable: false, - }, - enableGithubIntegration: { - type: "boolean", - optional: false, - nullable: false, - }, - enableDiscordIntegration: { - type: "boolean", - optional: false, - nullable: false, - }, - enableServiceWorker: { - type: "boolean", - optional: false, - nullable: false, - }, - translatorAvailable: { - type: "boolean", - optional: false, - nullable: false, - }, - proxyAccountName: { - type: "string", - optional: false, - nullable: true, - }, - features: { - type: "object", - optional: true, - nullable: false, - properties: { - registration: { - type: "boolean", - optional: false, - nullable: false, - }, - localTimeLine: { - type: "boolean", - optional: false, - nullable: false, - }, - recommendedTimeLine: { - type: "boolean", - optional: false, - nullable: false, - }, - globalTimeLine: { - type: "boolean", - optional: false, - nullable: false, - }, - elasticsearch: { - type: "boolean", - optional: false, - nullable: false, - }, - hcaptcha: { - type: "boolean", - optional: false, - nullable: false, - }, - recaptcha: { - type: "boolean", - optional: false, - nullable: false, - }, - objectStorage: { - type: "boolean", - optional: false, - nullable: false, - }, - twitter: { - type: "boolean", - optional: false, - nullable: false, - }, - github: { - type: "boolean", - optional: false, - nullable: false, - }, - discord: { - type: "boolean", - optional: false, - nullable: false, - }, - serviceWorker: { - type: "boolean", - optional: false, - nullable: false, - }, - miauth: { - type: "boolean", - optional: true, - nullable: false, - default: true, - }, - }, - }, - secureMode: { - type: "boolean", - optional: true, - nullable: false, - default: false, - }, - privateMode: { - type: "boolean", - optional: true, - nullable: false, - default: false, - }, - defaultReaction: { - type: "string", - optional: "false", - nullable: false, - default: "⭐", - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - detail: { type: "boolean", default: true }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const instance = await fetchMeta(true); - - const emojis = await Emojis.find({ - where: { - host: IsNull(), - }, - 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, - - secureMode: instance.secureMode, - privateMode: instance.privateMode, - - disableRegistration: instance.disableRegistration, - disableLocalTimeline: instance.disableLocalTimeline, - disableRecommendedTimeline: instance.disableRecommendedTimeline, - 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, - themeColor: instance.themeColor, - mascotImageUrl: instance.mascotImageUrl, - bannerUrl: instance.bannerUrl, - errorImageUrl: instance.errorImageUrl, - iconUrl: instance.iconUrl, - backgroundImageUrl: instance.backgroundImageUrl, - logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため - maxCaptionTextLength: MAX_CAPTION_TEXT_LENGTH, - emojis: instance.privateMode && !me ? [] : await Emojis.packMany(emojis), - defaultLightTheme: instance.defaultLightTheme, - defaultDarkTheme: instance.defaultDarkTheme, - ads: - instance.privateMode && !me - ? [] - : 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, - defaultReaction: instance.defaultReaction, - - ...(ps.detail - ? { - pinnedPages: instance.privateMode && !me ? [] : instance.pinnedPages, - pinnedClipId: - instance.privateMode && !me ? [] : instance.pinnedClipId, - cacheRemoteFiles: instance.cacheRemoteFiles, - requireSetup: - (await Users.countBy({ - host: IsNull(), - isAdmin: true, - })) === 0, - } - : {}), - }; - - if (ps.detail) { - if (!instance.privateMode || me) { - 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, - recommendedTimeline: !instance.disableRecommendedTimeline, - 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, - }; - } - - return response; -}); diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts deleted file mode 100644 index 0525d79a7e..0000000000 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ /dev/null @@ -1,69 +0,0 @@ -import define from "../../define.js"; -import { AccessTokens } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { secureRndstr } from "@/misc/secure-rndstr.js"; - -export const meta = { - tags: ["auth"], - - requireCredential: true, - - secure: true, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - token: { - type: "string", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - session: { type: "string", nullable: true }, - name: { type: "string", nullable: true }, - description: { type: "string", nullable: true }, - iconUrl: { type: "string", nullable: true }, - permission: { - type: "array", - uniqueItems: true, - items: { - type: "string", - }, - }, - }, - required: ["session", "permission"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Generate access token - const accessToken = secureRndstr(32, true); - - const now = new Date(); - - // Insert access token doc - await AccessTokens.insert({ - id: genId(), - createdAt: now, - lastUsedAt: now, - session: ps.session, - userId: user.id, - token: accessToken, - hash: accessToken, - name: ps.name, - description: ps.description, - iconUrl: ps.iconUrl, - permission: ps.permission, - }); - - return { - token: accessToken, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts deleted file mode 100644 index bacab9b458..0000000000 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ /dev/null @@ -1,95 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; -import { genId } from "@/misc/gen-id.js"; -import { Mutings, NoteWatchings } from "@/models/index.js"; -import type { Muting } from "@/models/entities/muting.js"; -import { publishUserEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "write:mutes", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - expiresAt: { - type: "integer", - nullable: true, - description: - "A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.", - }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, 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.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyMuting); - } - - if (ps.expiresAt && ps.expiresAt <= Date.now()) { - return; - } - - // Create mute - await Mutings.insert({ - id: genId(), - createdAt: new Date(), - expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, - muterId: muter.id, - muteeId: mutee.id, - } as Muting); - - publishUserEvent(user.id, "mute", mutee); - - NoteWatchings.delete({ - userId: muter.id, - noteUserId: mutee.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts deleted file mode 100644 index cc67a44c26..0000000000 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ /dev/null @@ -1,74 +0,0 @@ -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; -import { Mutings } from "@/models/index.js"; -import { publishUserEvent } from "@/services/stream.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "write:mutes", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, 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.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notMuting); - } - - // Delete mute - await Mutings.delete({ - id: exist.id, - }); - - publishUserEvent(user.id, "unmute", mutee); -}); diff --git a/packages/backend/src/server/api/endpoints/mute/list.ts b/packages/backend/src/server/api/endpoints/mute/list.ts deleted file mode 100644 index 7bbe29a4c8..0000000000 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { Mutings } from "@/models/index.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "read:mutes", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Muting", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Mutings.createQueryBuilder("muting"), - ps.sinceId, - ps.untilId, - ).andWhere("muting.muterId = :meId", { meId: me.id }); - - const mutings = await query.take(ps.limit).getMany(); - - return await Mutings.packMany(mutings, me); -}); diff --git a/packages/backend/src/server/api/endpoints/my/apps.ts b/packages/backend/src/server/api/endpoints/my/apps.ts deleted file mode 100644 index 8a097c8a04..0000000000 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ /dev/null @@ -1,49 +0,0 @@ -import define from "../../define.js"; -import { Apps } from "@/models/index.js"; - -export const meta = { - tags: ["account", "app"], - - requireCredential: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "App", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = { - userId: user.id, - }; - - const apps = await Apps.find({ - where: query, - take: ps.limit, - skip: ps.offset, - }); - - return await Promise.all( - apps.map((app) => - Apps.pack(app, user, { - detail: true, - }), - ), - ); -}); diff --git a/packages/backend/src/server/api/endpoints/notes.ts b/packages/backend/src/server/api/endpoints/notes.ts deleted file mode 100644 index 9787740ab0..0000000000 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Notes } from "@/models/index.js"; -import define from "../define.js"; -import { makePaginationQuery } from "../common/make-pagination-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredentialPrivateMode: true, - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - local: { type: "boolean", default: false }, - reply: { type: "boolean" }, - renote: { type: "boolean" }, - withFiles: { type: "boolean" }, - poll: { type: "boolean" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - if (ps.local) { - query.andWhere("note.userHost IS NULL"); - } - - if (ps.reply !== undefined) { - query.andWhere( - ps.reply ? "note.replyId IS NOT NULL" : "note.replyId IS NULL", - ); - } - - if (ps.renote !== undefined) { - query.andWhere( - ps.renote ? "note.renoteId IS NOT NULL" : "note.renoteId IS NULL", - ); - } - - if (ps.withFiles !== undefined) { - query.andWhere( - ps.withFiles ? "note.fileIds != '{}'" : "note.fileIds = '{}'", - ); - } - - if (ps.poll !== undefined) { - query.andWhere(ps.poll ? "note.hasPoll = TRUE" : "note.hasPoll = FALSE"); - } - - // TODO - //if (bot != undefined) { - // query.isBot = bot; - //} - - const notes = await query.take(ps.limit).getMany(); - - return await Notes.packMany(notes); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/children.ts b/packages/backend/src/server/api/endpoints/notes/children.ts deleted file mode 100644 index 9047fcce1d..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Brackets } from "typeorm"; -import { Notes } from "@/models/index.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -}; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ) - .andWhere( - "note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))", - { noteId: ps.noteId, depth: ps.depth, limit: ps.limit }, - ) - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner"); - - generateVisibilityQuery(query, user); - if (user) { - generateMutedUserQuery(query, user); - generateBlockedUserQuery(query, user); - } - - const notes = await query.getMany(); - - return await Notes.packMany(notes, user, { detail: false }); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts deleted file mode 100644 index 34b035add2..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { In } from "typeorm"; -import { ClipNotes, Clips } from "@/models/index.js"; -import define from "../../define.js"; -import { getNote } from "../../common/getters.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["clips", "notes"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Clip", - }, - }, - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "47db1a1c-b0af-458d-8fb4-986e4efafe1e", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const note = await getNote(ps.noteId, me).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - const clipNotes = await ClipNotes.findBy({ - noteId: note.id, - }); - - const clips = await Clips.findBy({ - id: In(clipNotes.map((x) => x.clipId)), - isPublic: true, - }); - - return await Promise.all(clips.map((x) => Clips.pack(x))); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts deleted file mode 100644 index 2e8f5ef73b..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Note } from "@/models/entities/note.js"; -import { Notes } from "@/models/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getNote } from "../../common/getters.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "e1035875-9551-45ec-afa8-1ded1fcb53c8", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - const conversation: Note[] = []; - let i = 0; - - async function get(id: any) { - i++; - const p = await getNote(id, user).catch((e) => { - if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") return null; - throw e; - }); - - if (p == null) return; - - if (i > ps.offset!) { - conversation.push(p); - } - - if (conversation.length === ps.limit) { - return; - } - - if (p.replyId) { - await get(p.replyId); - } - } - - if (note.replyId) { - await get(note.replyId); - } - - return await Notes.packMany(conversation, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts deleted file mode 100644 index 41b8ab9796..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { In } from "typeorm"; -import create from "@/services/note/create.js"; -import type { User } from "@/models/entities/user.js"; -import { - Users, - DriveFiles, - Notes, - Channels, - Blockings, -} from "@/models/index.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import type { Note } from "@/models/entities/note.js"; -import type { Channel } from "@/models/entities/channel.js"; -import { MAX_NOTE_TEXT_LENGTH } from "@/const.js"; -import { noteVisibilities } from "../../../../types.js"; -import { ApiError } from "../../error.js"; -import define from "../../define.js"; -import { HOUR } from "@/const.js"; -import { getNote } from "../../common/getters.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - limit: { - duration: HOUR, - max: 300, - }, - - kind: "write:notes", - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - createdNote: { - type: "object", - optional: false, - nullable: false, - 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", - }, - - 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", - }, - - accountLocked: { - message: "You migrated. Your account is now locked.", - code: "ACCOUNT_LOCKED", - id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - visibility: { type: "string", enum: noteVisibilities, default: "public" }, - visibleUserIds: { - type: "array", - uniqueItems: true, - items: { - type: "string", - format: "misskey:id", - }, - }, - text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, - cw: { type: "string", nullable: true, maxLength: 100 }, - localOnly: { type: "boolean", default: false }, - noExtractMentions: { type: "boolean", default: false }, - noExtractHashtags: { type: "boolean", default: false }, - noExtractEmojis: { type: "boolean", default: false }, - fileIds: { - type: "array", - uniqueItems: true, - minItems: 1, - maxItems: 16, - items: { type: "string", format: "misskey:id" }, - }, - mediaIds: { - deprecated: true, - description: - "Use `fileIds` instead. If both are specified, this property is discarded.", - type: "array", - uniqueItems: true, - minItems: 1, - maxItems: 16, - items: { type: "string", format: "misskey:id" }, - }, - replyId: { type: "string", format: "misskey:id", nullable: true }, - renoteId: { type: "string", format: "misskey:id", nullable: true }, - channelId: { type: "string", format: "misskey:id", nullable: true }, - poll: { - type: "object", - nullable: true, - properties: { - choices: { - type: "array", - uniqueItems: true, - minItems: 2, - maxItems: 10, - items: { type: "string", minLength: 1, maxLength: 50 }, - }, - multiple: { type: "boolean", default: false }, - expiresAt: { type: "integer", nullable: true }, - expiredAfter: { type: "integer", nullable: true, minimum: 1 }, - }, - required: ["choices"], - }, - }, - anyOf: [ - { - // (re)note with text, files and poll are optional - properties: { - text: { - type: "string", - minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, - nullable: false, - }, - }, - required: ["text"], - }, - { - // (re)note with files, text and poll are optional - required: ["fileIds"], - }, - { - // (re)note with files, text and poll are optional - required: ["mediaIds"], - }, - { - // (re)note with poll, text and files are optional - properties: { - poll: { type: "object", nullable: false }, - }, - required: ["poll"], - }, - { - // pure renote - required: ["renoteId"], - }, - ], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked); - let visibleUsers: User[] = []; - if (ps.visibleUserIds) { - visibleUsers = await Users.findBy({ - id: In(ps.visibleUserIds), - }); - } - - let files: DriveFile[] = []; - const fileIds = - ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; - if (fileIds != null) { - files = await DriveFiles.createQueryBuilder("file") - .where("file.userId = :userId AND file.id IN (:...fileIds)", { - userId: user.id, - fileIds, - }) - .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') - .setParameters({ fileIds }) - .getMany(); - } - - let renote: Note | null = null; - if (ps.renoteId != null) { - // Fetch renote to note - renote = await getNote(ps.renoteId, user).catch((e) => { - if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchRenoteTarget); - throw e; - }); - - if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { - throw new ApiError(meta.errors.cannotReRenote); - } - - // Check blocking - if (renote.userId !== user.id) { - const block = await Blockings.findOneBy({ - blockerId: renote.userId, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - } - - let reply: Note | null = null; - if (ps.replyId != null) { - // Fetch reply - reply = await getNote(ps.replyId, user).catch((e) => { - if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchReplyTarget); - throw e; - }); - - if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { - throw new ApiError(meta.errors.cannotReplyToPureRenote); - } - - // Check blocking - if (reply.userId !== user.id) { - const block = await Blockings.findOneBy({ - 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; - } - } - - let channel: Channel | null = null; - if (ps.channelId != null) { - channel = await Channels.findOneBy({ id: ps.channelId }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - } - - // Create a post - const note = await create(user, { - createdAt: new Date(), - files: files, - poll: ps.poll - ? { - choices: ps.poll.choices, - multiple: ps.poll.multiple, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - } - : undefined, - text: ps.text || undefined, - reply, - renote, - cw: ps.cw, - localOnly: ps.localOnly, - visibility: ps.visibility, - visibleUsers, - channel, - apMentions: ps.noExtractMentions ? [] : undefined, - apHashtags: ps.noExtractHashtags ? [] : undefined, - apEmojis: ps.noExtractEmojis ? [] : undefined, - }); - - return { - createdNote: await Notes.pack(note, user), - }; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts deleted file mode 100644 index 5fc79db7d1..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ /dev/null @@ -1,57 +0,0 @@ -import deleteNote from "@/services/note/delete.js"; -import { Users } from "@/models/index.js"; -import define from "../../define.js"; -import { getNote } from "../../common/getters.js"; -import { ApiError } from "../../error.js"; -import { SECOND, HOUR } from "@/const.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - kind: "write:notes", - - limit: { - duration: HOUR, - max: 300, - minInterval: SECOND, - }, - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - if (!(user.isAdmin || user.isModerator) && note.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } - - // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため - await deleteNote(await Users.findOneByOrFail({ id: note.userId }), note); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts deleted file mode 100644 index 835594f03a..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { NoteFavorites } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getNote } from "../../../common/getters.js"; - -export const meta = { - tags: ["notes", "favorites"], - - requireCredential: true, - - kind: "write:favorites", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - // if already favorited - const exist = await NoteFavorites.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyFavorited); - } - - // Create favorite - await NoteFavorites.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts deleted file mode 100644 index 9a09767482..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NoteFavorites } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getNote } from "../../../common/getters.js"; - -export const meta = { - tags: ["notes", "favorites"], - - requireCredential: true, - - kind: "write:favorites", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - // if already favorited - const exist = await NoteFavorites.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notFavorited); - } - - // Delete favorite - await NoteFavorites.delete(exist.id); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts deleted file mode 100644 index 47c1e13812..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Notes } from "@/models/index.js"; -import define from "../../define.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - origin: { - type: "string", - enum: ["combined", "local", "remote"], - default: "local", - }, - days: { type: "integer", minimum: 1, maximum: 365, default: 3 }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const max = 30; - const day = 1000 * 60 * 60 * 24 * ps.days; - - const query = Notes.createQueryBuilder("note") - .addSelect("note.score") - .andWhere("note.score > 0") - .andWhere("note.createdAt > :date", { date: new Date(Date.now() - day) }) - .andWhere("note.visibility = 'public'") - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - switch (ps.origin) { - case "local": - query.andWhere("note.userHost IS NULL"); - break; - case "remote": - query.andWhere("note.userHost IS NOT NULL"); - break; - } - - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - let notes = await query.orderBy("note.score", "DESC").take(max).getMany(); - - notes.sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ); - - notes = notes.slice(ps.offset, ps.offset + ps.limit); - - return await Notes.packMany(notes, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts deleted file mode 100644 index 077a1ad5e1..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Notes } from "@/models/index.js"; -import { activeUsersChart } from "@/services/chart/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { generateRepliesQuery } from "../../common/generate-replies-query.js"; -import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; -import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredentialPrivateMode: true, - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - gtlDisabled: { - message: "Global timeline has been disabled.", - code: "GTL_DISABLED", - id: "0332fc13-6ab2-4427-ae80-a9fadffd1a6b", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - withFiles: { - type: "boolean", - default: false, - description: "Only show notes that have attached files.", - }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - generateRepliesQuery(query, user); - if (user) { - generateMutedUserQuery(query, user); - generateMutedNoteQuery(query, user); - generateBlockedUserQuery(query, user); - generateMutedUserRenotesQueryForNotes(query, user); - } - - if (ps.withFiles) { - query.andWhere("note.fileIds != '{}'"); - } - //#endregion - - process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } - }); - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, user))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts deleted file mode 100644 index 3c171278b4..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Followings, Notes } from "@/models/index.js"; -import { activeUsersChart } from "@/services/chart/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { generateRepliesQuery } from "../../common/generate-replies-query.js"; -import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js"; -import { generateChannelQuery } from "../../common/generate-channel-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; -import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - stlDisabled: { - message: "Hybrid timeline has been disabled.", - code: "STL_DISABLED", - id: "620763f4-f621-4533-ab33-0577a1a3c342", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - includeMyRenotes: { type: "boolean", default: true }, - includeRenotedMyNotes: { type: "boolean", default: true }, - includeLocalRenotes: { type: "boolean", default: true }, - withFiles: { - type: "boolean", - default: false, - description: "Only show notes that have attached files.", - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") - .setParameters(followingQuery.getParameters()); - - generateChannelQuery(query, user); - generateRepliesQuery(query, user); - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateMutedNoteQuery(query, user); - generateBlockedUserQuery(query, user); - generateMutedUserRenotesQueryForNotes(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 - - process.nextTick(() => { - activeUsersChart.read(user); - }); - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, user))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts deleted file mode 100644 index cec371c8dc..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Notes, Users } from "@/models/index.js"; -import { activeUsersChart } from "@/services/chart/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateRepliesQuery } from "../../common/generate-replies-query.js"; -import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js"; -import { generateChannelQuery } from "../../common/generate-channel-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; -import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js"; - -export const meta = { - tags: ["notes"], - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - ltlDisabled: { - message: "Local timeline has been disabled.", - code: "LTL_DISABLED", - id: "45a6eb02-7695-4393-b023-dd3be9aaaefd", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - withFiles: { - type: "boolean", - default: false, - description: "Only show notes that have attached files.", - }, - fileType: { - type: "array", - items: { - type: "string", - }, - }, - excludeNsfw: { type: "boolean", default: false }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - 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 (user) generateMutedUserRenotesQueryForNotes(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 - - process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } - }); - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, user))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts deleted file mode 100644 index 68688b504c..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Brackets } from "typeorm"; -import read from "@/services/note/read.js"; -import { Notes, Followings } from "@/models/index.js"; -import define from "../../define.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; -import { generateMutedNoteThreadQuery } from "../../common/generate-muted-note-thread-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - following: { type: "boolean", default: false }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - visibility: { type: "string" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - 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()); - } - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, user))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - read(user.id, found); - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts deleted file mode 100644 index fcd24db992..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Brackets, In } from "typeorm"; -import { Polls, Mutings, Notes, PollVotes } from "@/models/index.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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 - .orderBy("poll.noteId", "DESC") - .take(ps.limit) - .skip(ps.offset) - .getMany(); - - if (polls.length === 0) return []; - - const notes = await Notes.find({ - where: { - id: In(polls.map((poll) => poll.noteId)), - }, - order: { - createdAt: "DESC", - }, - }); - - return await Notes.packMany(notes, user, { - detail: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts deleted file mode 100644 index 0558aa1b8f..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Not } from "typeorm"; -import { publishNoteStream } from "@/services/stream.js"; -import { createNotification } from "@/services/create-notification.js"; -import { deliver } from "@/queue/index.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderVote from "@/remote/activitypub/renderer/vote.js"; -import { deliverQuestionUpdate } from "@/services/note/polls/update.js"; -import { - PollVotes, - NoteWatchings, - Users, - Polls, - Blockings, -} from "@/models/index.js"; -import type { IRemoteUser } from "@/models/entities/user.js"; -import { genId } from "@/misc/gen-id.js"; -import { getNote } from "../../../common/getters.js"; -import { ApiError } from "../../../error.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - kind: "write:votes", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - choice: { type: "integer" }, - }, - required: ["noteId", "choice"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const createdAt = new Date(); - - // Get votee - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - if (!note.hasPoll) { - throw new ApiError(meta.errors.noPoll); - } - - // Check blocking - if (note.userId !== user.id) { - const block = await Blockings.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - - const poll = await Polls.findOneByOrFail({ 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.findBy({ - 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.findOneByOrFail(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.findBy({ - 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.findOneByOrFail({ - id: note.userId, - })) as IRemoteUser; - - deliver( - user, - renderActivity(await renderVote(user, vote, note, poll, pollOwner)), - pollOwner.inbox, - ); - } - - // リモートフォロワーにUpdate配信 - deliverQuestionUpdate(note.id); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts deleted file mode 100644 index 3c8af119ab..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { FindOptionsWhere } from "typeorm"; -import { DeepPartial } from "typeorm"; -import { NoteReactions } from "@/models/index.js"; -import type { NoteReaction } from "@/models/entities/note-reaction.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getNote } from "../../common/getters.js"; - -export const meta = { - tags: ["notes", "reactions"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - allowGet: true, - cacheSec: 60, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "NoteReaction", - }, - }, - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "263fff3d-d0e1-4af4-bea7-8408059b451a", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - type: { type: "string", nullable: true }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // check note visibility - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - const query = { - noteId: ps.noteId, - } as FindOptionsWhere; - - 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, - }, - relations: ["user", "user.avatar", "user.banner", "note"], - }); - - return await NoteReactions.packMany(reactions, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts deleted file mode 100644 index 2c8671070f..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ /dev/null @@ -1,64 +0,0 @@ -import createReaction from "@/services/note/reaction/create.js"; -import define from "../../../define.js"; -import { getNote } from "../../../common/getters.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["reactions", "notes"], - - requireCredential: true, - - kind: "write:reactions", - - 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", - }, - accountLocked: { - message: "You migrated. Your account is now locked.", - code: "ACCOUNT_LOCKED", - id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - reaction: { type: "string" }, - }, - required: ["noteId", "reaction"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked); - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - await createReaction(user, note, ps.reaction).catch((e) => { - if (e.id === "51c42bb4-931a-456b-bff7-e5a8a70dd298") - throw new ApiError(meta.errors.alreadyReacted); - if (e.id === "e70412a4-7197-4726-8e74-f3e0deb92aa7") - throw new ApiError(meta.errors.youHaveBeenBlocked); - throw e; - }); - return; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts deleted file mode 100644 index 59096c4c88..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ /dev/null @@ -1,54 +0,0 @@ -import deleteReaction from "@/services/note/reaction/delete.js"; -import define from "../../../define.js"; -import { getNote } from "../../../common/getters.js"; -import { ApiError } from "../../../error.js"; -import { SECOND, HOUR } from "@/const.js"; - -export const meta = { - tags: ["reactions", "notes"], - - requireCredential: true, - - kind: "write:reactions", - - limit: { - duration: HOUR, - max: 60, - minInterval: 3 * SECOND, - }, - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - await deleteReaction(user, note).catch((e) => { - if (e.id === "60527ec9-b4cb-4a88-a6bd-32d3ad26817d") - throw new ApiError(meta.errors.notReacted); - throw e; - }); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts b/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts deleted file mode 100644 index 56847b1dd2..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/recommended-timeline.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Brackets } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Notes } from "@/models/index.js"; -import { activeUsersChart } from "@/services/chart/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateRepliesQuery } from "../../common/generate-replies-query.js"; -import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js"; -import { generateChannelQuery } from "../../common/generate-channel-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; -import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js"; - -export const meta = { - tags: ["notes"], - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - rtlDisabled: { - message: "Recommended timeline has been disabled.", - code: "RTL_DISABLED", - id: "45a6eb02-7695-4393-b023-dd3be9aaaefe", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - withFiles: { - type: "boolean", - default: false, - description: "Only show notes that have attached files.", - }, - fileType: { - type: "array", - items: { - type: "string", - }, - }, - excludeNsfw: { type: "boolean", default: false }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - }, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); - if (m.disableRecommendedTimeline) { - if (user == null || !(user.isAdmin || user.isModerator)) { - throw new ApiError(meta.errors.rtlDisabled); - } - } - - //#region Construct query - const query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ps.sinceDate, - ps.untilDate, - ) - .andWhere( - `(note.userHost = ANY ('{"${m.recommendedInstances.join('","')}"}'))`, - ) - .andWhere("(note.visibility = 'public')") - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - 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 (user) generateMutedUserRenotesQueryForNotes(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 - - process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } - }); - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, user))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts deleted file mode 100644 index f313616be2..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Notes } from "@/models/index.js"; -import define from "../../define.js"; -import { getNote } from "../../common/getters.js"; -import { ApiError } from "../../error.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "12908022-2e21-46cd-ba6a-3edaf6093f46", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - const query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ) - .andWhere("note.renoteId = :renoteId", { renoteId: note.id }) - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, user))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts deleted file mode 100644 index 5ea4d479c5..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Notes } from "@/models/index.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - generateVisibilityQuery(query, user); - if (user) generateMutedUserQuery(query, user); - if (user) generateBlockedUserQuery(query, user); - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, user))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts deleted file mode 100644 index f988acaa51..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { Brackets } from "typeorm"; -import { Notes } from "@/models/index.js"; -import { safeForSql } from "@/misc/safe-for-sql.js"; -import { normalizeForSearch } from "@/misc/normalize-for-search.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; - -export const meta = { - tags: ["notes", "hashtags"], - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - reply: { type: "boolean", nullable: true, default: null }, - renote: { type: "boolean", nullable: true, default: null }, - withFiles: { - type: "boolean", - default: false, - description: "Only show notes that have attached files.", - }, - poll: { type: "boolean", nullable: true, default: null }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - }, - anyOf: [ - { - properties: { - tag: { type: "string", minLength: 1 }, - }, - required: ["tag"], - }, - { - properties: { - query: { - type: "array", - description: - "The outer arrays are chained with OR, the inner arrays are chained with AND.", - items: { - type: "array", - items: { - type: "string", - minLength: 1, - }, - minItems: 1, - }, - minItems: 1, - }, - }, - required: ["query"], - }, - ], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ) - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me); - if (me) generateBlockedUserQuery(query, me); - - try { - if (ps.tag) { - if (!safeForSql(normalizeForSearch(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(normalizeForSearch(ps.tag))) - throw "Injection"; - qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); - } - }), - ); - } - }), - ); - } - } catch (e) { - if (e.message === "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"); - } - } - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, me))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts deleted file mode 100644 index 8f563c384f..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { In } from "typeorm"; -import { Notes } from "@/models/index.js"; -import { Note } from "@/models/entities/note.js"; -import config from "@/config/index.js"; -import es from "../../../../db/elasticsearch.js"; -import sonic from "../../../../db/sonic.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: {}, -} as const; - -export const paramDef = { - type: "object", - properties: { - query: { type: "string" }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - host: { - type: "string", - nullable: true, - description: "The local host is represented with `null`.", - }, - userId: { - type: "string", - format: "misskey:id", - nullable: true, - default: null, - }, - channelId: { - type: "string", - format: "misskey:id", - nullable: true, - default: null, - }, - }, - required: ["query"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - if (es == null && sonic == 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me); - if (me) generateBlockedUserQuery(query, me); - - const notes: Note[] = await query.take(ps.limit).getMany(); - - return await Notes.packMany(notes, me); - } else if (sonic) { - let start = 0; - const chunkSize = 100; - - // Use sonic to fetch and step through all search results that could match the requirements - const ids = []; - while (true) { - const results = await sonic.search.query( - sonic.collection, - sonic.bucket, - ps.query, - { - limit: chunkSize, - offset: start, - }, - ); - - start += chunkSize; - - if (results.length === 0) { - break; - } - - const res = results - .map((k) => JSON.parse(k)) - .filter((key) => { - if (ps.userId && key.userId !== ps.userId) { - return false; - } - if (ps.channelId && key.channelId !== ps.channelId) { - return false; - } - if (ps.sinceId && key.id <= ps.sinceId) { - return false; - } - if (ps.untilId && key.id >= ps.untilId) { - return false; - } - return true; - }) - .map((key) => key.id); - - ids.push(...res); - } - - // Sort all the results by note id DESC (newest first) - ids.sort((a, b) => b - a); - - // Fetch the notes from the database until we have enough to satisfy the limit - start = 0; - const found = []; - while (found.length < ps.limit && start < ids.length) { - const chunk = ids.slice(start, start + chunkSize); - const notes: Note[] = await Notes.find({ - where: { - id: In(chunk), - }, - order: { - id: "DESC", - }, - }); - - // The notes are checked for visibility and muted/blocked users when packed - found.push(...(await Notes.packMany(notes, me))); - start += chunkSize; - } - - // If we have more results than the limit, trim them - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; - } else { - const userQuery = - ps.userId != null - ? [ - { - term: { - userId: ps.userId, - }, - }, - ] - : []; - - const hostQuery = - ps.userId == null - ? ps.host === null - ? [ - { - bool: { - must_not: { - exists: { - field: "userHost", - }, - }, - }, - }, - ] - : ps.host !== undefined - ? [ - { - term: { - userHost: ps.host, - }, - }, - ] - : [] - : []; - - const result = await es.search({ - index: config.elasticsearch.index || "misskey_note", - body: { - size: ps.limit, - from: ps.offset, - query: { - bool: { - must: [ - { - simple_query_string: { - fields: ["text"], - query: ps.query.toLowerCase(), - default_operator: "and", - }, - }, - ...hostQuery, - ...userQuery, - ], - }, - }, - sort: [ - { - _doc: "desc", - }, - ], - }, - }); - - const hits = result.body.hits.hits.map((hit: any) => hit._id); - - if (hits.length === 0) return []; - - // Fetch found notes - const notes = await Notes.find({ - where: { - id: In(hits), - }, - order: { - id: -1, - }, - }); - - return await Notes.packMany(notes, me); - } -}); diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts deleted file mode 100644 index 39d128134f..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Notes } from "@/models/index.js"; -import define from "../../define.js"; -import { getNote } from "../../common/getters.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "24fcbfc6-2e37-42b6-8388-c29b3861a08d", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - return await Notes.pack(note, user, { - // FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774) - detail: true, - }).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts deleted file mode 100644 index 630b2a8007..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - NoteFavorites, - Notes, - NoteThreadMutings, - NoteWatchings, -} from "@/models/index.js"; -import { getNote } from "../../common/getters.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - isFavorited: { - type: "boolean", - optional: false, - nullable: false, - }, - isWatching: { - type: "boolean", - optional: false, - nullable: false, - }, - isMutedThread: { - type: "boolean", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user); - - const [favorite, watching, threadMuting] = await Promise.all([ - NoteFavorites.count({ - where: { - userId: user.id, - noteId: note.id, - }, - take: 1, - }), - NoteWatchings.count({ - where: { - userId: user.id, - noteId: note.id, - }, - take: 1, - }), - NoteThreadMutings.count({ - where: { - userId: user.id, - threadId: note.threadId || note.id, - }, - take: 1, - }), - ]); - - return { - isFavorited: favorite !== 0, - isWatching: watching !== 0, - isMutedThread: threadMuting !== 0, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts deleted file mode 100644 index e4803cc291..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Notes, NoteThreadMutings } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import readNote from "@/services/note/read.js"; -import define from "../../../define.js"; -import { getNote } from "../../../common/getters.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "5ff67ada-ed3b-2e71-8e87-a1a421e177d2", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - const mutedNotes = await Notes.find({ - where: [ - { - id: note.threadId || note.id, - }, - { - threadId: note.threadId || note.id, - }, - ], - }); - - await readNote(user.id, mutedNotes); - - await NoteThreadMutings.insert({ - id: genId(), - createdAt: new Date(), - threadId: note.threadId || note.id, - userId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts deleted file mode 100644 index c06fd59ba5..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NoteThreadMutings } from "@/models/index.js"; -import define from "../../../define.js"; -import { getNote } from "../../../common/getters.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "bddd57ac-ceb3-b29d-4334-86ea5fae481a", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - await NoteThreadMutings.delete({ - threadId: note.threadId || note.id, - userId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts deleted file mode 100644 index f85c0cfd32..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Brackets } from "typeorm"; -import { Notes, Followings } from "@/models/index.js"; -import { activeUsersChart } from "@/services/chart/index.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { generateRepliesQuery } from "../../common/generate-replies-query.js"; -import { generateMutedNoteQuery } from "../../common/generate-muted-note-query.js"; -import { generateChannelQuery } from "../../common/generate-channel-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; -import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - includeMyRenotes: { type: "boolean", default: true }, - includeRenotedMyNotes: { type: "boolean", default: true }, - includeLocalRenotes: { type: "boolean", default: true }, - withFiles: { - type: "boolean", - default: false, - description: "Only show notes that have attached files.", - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") - .setParameters(followingQuery.getParameters()); - - generateChannelQuery(query, user); - generateRepliesQuery(query, user); - generateVisibilityQuery(query, user); - generateMutedUserQuery(query, user); - generateMutedNoteQuery(query, user); - generateBlockedUserQuery(query, user); - generateMutedUserRenotesQueryForNotes(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 - - process.nextTick(() => { - activeUsersChart.read(user); - }); - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, user))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts deleted file mode 100644 index c6415ceef2..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { URLSearchParams } from "node:url"; -import fetch from "node-fetch"; -import config from "@/config/index.js"; -import { getAgentByUrl } from "@/misc/fetch.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Notes } from "@/models/index.js"; -import { ApiError } from "../../error.js"; -import { getNote } from "../../common/getters.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "object", - optional: false, - nullable: false, - }, - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "bea9b03f-36e0-49c5-a4db-627a029f8971", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - targetLang: { type: "string" }, - }, - required: ["noteId", "targetLang"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - 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, - // TODO - //timeout: 10000, - agent: getAgentByUrl, - }); - - const json = (await res.json()) as { - translations: { - detected_source_language: string; - text: string; - }[]; - }; - - return { - sourceLang: json.translations[0].detected_source_language, - text: json.translations[0].text, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts deleted file mode 100644 index a30a19f190..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ /dev/null @@ -1,53 +0,0 @@ -import deleteNote from "@/services/note/delete.js"; -import { Notes, Users } from "@/models/index.js"; -import define from "../../define.js"; -import { getNote } from "../../common/getters.js"; -import { ApiError } from "../../error.js"; -import { SECOND, HOUR } from "@/const.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - kind: "write:notes", - - limit: { - duration: HOUR, - max: 300, - minInterval: SECOND, - }, - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "efd4a259-2442-496b-8dd7-b255aa1a160f", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - const renotes = await Notes.findBy({ - userId: user.id, - renoteId: note.id, - }); - - for (const note of renotes) { - deleteNote(await Users.findOneByOrFail({ id: user.id }), note); - } -}); diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts deleted file mode 100644 index 03f5cee3f3..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Brackets } from "typeorm"; -import { UserLists, UserListJoinings, Notes } from "@/models/index.js"; -import { activeUsersChart } from "@/services/chart/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; - -export const meta = { - tags: ["notes", "lists"], - - requireCredential: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - noSuchList: { - message: "No such list.", - code: "NO_SUCH_LIST", - id: "8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - listId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - includeMyRenotes: { type: "boolean", default: true }, - includeRenotedMyNotes: { type: "boolean", default: true }, - includeLocalRenotes: { type: "boolean", default: true }, - withFiles: { - type: "boolean", - default: false, - description: "Only show notes that have attached files.", - }, - }, - required: ["listId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const list = await UserLists.findOneBy({ - id: ps.listId, - userId: user.id, - }); - - if (list == null) { - throw new ApiError(meta.errors.noSuchList); - } - - //#region Construct query - const query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ) - .innerJoin( - UserListJoinings.metadata.targetName, - "userListJoining", - "userListJoining.userId = note.userId", - ) - .innerJoinAndSelect("note.user", "user") - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner") - .andWhere("userListJoining.userListId = :userListId", { - userListId: list.id, - }); - - 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 - - process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } - }); - - // We fetch more than requested because some may be filtered out, and if there's less than - // requested, the pagination stops. - const found = []; - const take = Math.floor(ps.limit * 1.5); - let skip = 0; - while (found.length < ps.limit) { - const notes = await query.take(take).skip(skip).getMany(); - found.push(...(await Notes.packMany(notes, user))); - skip += take; - if (notes.length < take) break; - } - - if (found.length > ps.limit) { - found.length = ps.limit; - } - - return found; -}); diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts deleted file mode 100644 index f8921099a1..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/watching/create.ts +++ /dev/null @@ -1,38 +0,0 @@ -import watch from "@/services/note/watch.js"; -import define from "../../../define.js"; -import { getNote } from "../../../common/getters.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "ea0e37a6-90a3-4f58-ba6b-c328ca206fc7", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - await watch(user.id, note); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts deleted file mode 100644 index b441ad74b9..0000000000 --- a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts +++ /dev/null @@ -1,38 +0,0 @@ -import unwatch from "@/services/note/unwatch.js"; -import define from "../../../define.js"; -import { getNote } from "../../../common/getters.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - kind: "write:account", - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "09b3695c-f72c-4731-a428-7cff825fc82e", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - await unwatch(user.id, note); -}); diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts deleted file mode 100644 index bc5723369c..0000000000 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createNotification } from "@/services/create-notification.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["notifications"], - - requireCredential: true, - - kind: "write:notifications", - - errors: {}, -} as const; - -export const paramDef = { - type: "object", - properties: { - body: { type: "string" }, - header: { type: "string", nullable: true }, - icon: { type: "string", nullable: true }, - }, - required: ["body"], -} as const; - -export default define(meta, paramDef, async (ps, user, token) => { - createNotification(user.id, "app", { - appAccessTokenId: token ? token.id : null, - customBody: ps.body, - customHeader: ps.header, - customIcon: ps.icon, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts deleted file mode 100644 index e0888ad752..0000000000 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import { pushNotification } from "@/services/push-notification.js"; -import { Notifications } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["notifications", "account"], - - requireCredential: true, - - kind: "write:notifications", -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Update documents - await Notifications.update( - { - notifieeId: user.id, - isRead: false, - }, - { - isRead: true, - }, - ); - - // 全ての通知を読みましたよというイベントを発行 - publishMainStream(user.id, "readAllNotifications"); - pushNotification(user.id, "readAllNotifications", undefined); -}); diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts deleted file mode 100644 index 9efb2fcc0b..0000000000 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ /dev/null @@ -1,49 +0,0 @@ -import define from "../../define.js"; -import { readNotification } from "../../common/read-notification.js"; - -export const meta = { - tags: ["notifications", "account"], - - requireCredential: true, - - kind: "write:notifications", - - description: "Mark a notification as read.", - - errors: { - noSuchNotification: { - message: "No such notification.", - code: "NO_SUCH_NOTIFICATION", - id: "efa929d5-05b5-47d1-beec-e6a4dbed011e", - }, - }, -} as const; - -export const paramDef = { - oneOf: [ - { - type: "object", - properties: { - notificationId: { type: "string", format: "misskey:id" }, - }, - required: ["notificationId"], - }, - { - type: "object", - properties: { - notificationIds: { - type: "array", - items: { type: "string", format: "misskey:id" }, - maxItems: 100, - }, - }, - required: ["notificationIds"], - }, - ], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - if ("notificationId" in ps) - return readNotification(user.id, [ps.notificationId]); - return readNotification(user.id, ps.notificationIds); -}); diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts deleted file mode 100644 index a0f1e912fc..0000000000 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import { Users, Pages } from "@/models/index.js"; -import define from "../define.js"; -import { ApiError } from "../error.js"; - -export const meta = { - requireCredential: true, - secure: true, - - errors: { - noSuchPage: { - message: "No such page.", - code: "NO_SUCH_PAGE", - id: "4a13ad31-6729-46b4-b9af-e86b265c2e74", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - pageId: { type: "string", format: "misskey:id" }, - event: { type: "string" }, - var: {}, - }, - required: ["pageId", "event"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - publishMainStream(page.userId, "pageEvent", { - pageId: ps.pageId, - event: ps.event, - var: ps.var, - userId: user.id, - user: await Users.pack( - user.id, - { id: page.userId }, - { - detail: true, - }, - ), - }); -}); diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts deleted file mode 100644 index 716d3265cc..0000000000 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Pages, DriveFiles } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { Page } from "@/models/entities/page.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["pages"], - - requireCredential: true, - - kind: "write:pages", - - limit: { - duration: HOUR, - max: 300, - }, - - res: { - type: "object", - optional: false, - nullable: false, - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - title: { type: "string" }, - name: { type: "string", minLength: 1 }, - summary: { type: "string", nullable: true }, - content: { - type: "array", - items: { - type: "object", - additionalProperties: true, - }, - }, - variables: { - type: "array", - items: { - type: "object", - additionalProperties: true, - }, - }, - script: { type: "string" }, - eyeCatchingImageId: { - type: "string", - format: "misskey:id", - nullable: true, - }, - font: { - type: "string", - enum: ["serif", "sans-serif"], - default: "sans-serif", - }, - alignCenter: { type: "boolean", default: false }, - isPublic: { type: "boolean", default: true }, - hideTitleWhenPinned: { type: "boolean", default: false }, - }, - required: ["title", "name", "content", "variables", "script"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - let eyeCatchingImage = null; - if (ps.eyeCatchingImageId != null) { - eyeCatchingImage = await DriveFiles.findOneBy({ - id: ps.eyeCatchingImageId, - userId: user.id, - }); - - if (eyeCatchingImage == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - await Pages.findBy({ - userId: user.id, - name: ps.name, - }).then((result) => { - if (result.length > 0) { - throw new ApiError(meta.errors.nameAlreadyExists); - } - }); - - const page = await Pages.insert( - 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, - isPublic: ps.isPublic, - }), - ).then((x) => Pages.findOneByOrFail(x.identifiers[0])); - - return await Pages.pack(page); -}); diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts deleted file mode 100644 index 98b035f7c7..0000000000 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Pages } from "@/models/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["pages"], - - requireCredential: true, - - kind: "write:pages", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - pageId: { type: "string", format: "misskey:id" }, - }, - required: ["pageId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - if (page.userId !== user.id) { - throw new ApiError(meta.errors.accessDenied); - } - - await Pages.delete(page.id); -}); diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts deleted file mode 100644 index a763465897..0000000000 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Pages } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["pages"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Page", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const query = Pages.createQueryBuilder("page") - .where("page.visibility = 'public'") - .andWhere("page.likedCount > 0") - .orderBy("page.likedCount", "DESC"); - - const pages = await query.take(10).getMany(); - - return await Pages.packMany(pages, me); -}); diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts deleted file mode 100644 index f14ed39eb0..0000000000 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Pages, PageLikes } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["pages"], - - requireCredential: true, - - kind: "write:page-likes", - - errors: { - noSuchPage: { - message: "No such page.", - code: "NO_SUCH_PAGE", - id: "cc98a8a2-0dc3-4123-b198-62c71df18ed3", - }, - - alreadyLiked: { - message: "The page has already been liked.", - code: "ALREADY_LIKED", - id: "cc98a8a2-0dc3-4123-b198-62c71df18ed3", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - pageId: { type: "string", format: "misskey:id" }, - }, - required: ["pageId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - // if already liked - const exist = await PageLikes.findOneBy({ - pageId: page.id, - userId: user.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyLiked); - } - - // Create like - await PageLikes.insert({ - id: genId(), - createdAt: new Date(), - pageId: page.id, - userId: user.id, - }); - - Pages.increment({ id: page.id }, "likedCount", 1); -}); diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts deleted file mode 100644 index a25eb30b6d..0000000000 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { IsNull } from "typeorm"; -import { Pages, Users } from "@/models/index.js"; -import type { Page } from "@/models/entities/page.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["pages"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "object", - optional: false, - nullable: false, - ref: "Page", - }, - - errors: { - noSuchPage: { - message: "No such page.", - code: "NO_SUCH_PAGE", - id: "222120c0-3ead-4528-811b-b96f233388d7", - }, - }, -} as const; - -export const paramDef = { - type: "object", - anyOf: [ - { - properties: { - pageId: { type: "string", format: "misskey:id" }, - }, - required: ["pageId"], - }, - { - properties: { - name: { type: "string" }, - username: { type: "string" }, - }, - required: ["name", "username"], - }, - ], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - let page: Page | null = null; - - if (ps.pageId) { - page = await Pages.findOneBy({ id: ps.pageId }); - } else if (ps.name && ps.username) { - const author = await Users.findOneBy({ - host: IsNull(), - usernameLower: ps.username.toLowerCase(), - }); - if (author) { - page = await Pages.findOneBy({ - name: ps.name, - userId: author.id, - }); - } - } - - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - if (!page.isPublic && (user == null || page.userId !== user.id)) { - throw new ApiError(meta.errors.noSuchPage); - } - - return await Pages.pack(page, user); -}); diff --git a/packages/backend/src/server/api/endpoints/pages/unlike.ts b/packages/backend/src/server/api/endpoints/pages/unlike.ts deleted file mode 100644 index 07bf3fbf48..0000000000 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Pages, PageLikes } from "@/models/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["pages"], - - requireCredential: true, - - kind: "write:page-likes", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - pageId: { type: "string", format: "misskey:id" }, - }, - required: ["pageId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) { - throw new ApiError(meta.errors.noSuchPage); - } - - const exist = await PageLikes.findOneBy({ - pageId: page.id, - userId: user.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notLiked); - } - - // Delete like - await PageLikes.delete(exist.id); - - Pages.decrement({ id: page.id }, "likedCount", 1); -}); diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts deleted file mode 100644 index 65e1b3b2d2..0000000000 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Not } from "typeorm"; -import { Pages, DriveFiles } from "@/models/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["pages"], - - requireCredential: true, - - kind: "write:pages", - - limit: { - duration: HOUR, - max: 300, - }, - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - pageId: { type: "string", format: "misskey:id" }, - title: { type: "string" }, - name: { type: "string", minLength: 1 }, - summary: { type: "string", nullable: true }, - content: { - type: "array", - items: { - type: "object", - additionalProperties: true, - }, - }, - variables: { - type: "array", - items: { - type: "object", - additionalProperties: true, - }, - }, - script: { type: "string" }, - eyeCatchingImageId: { - type: "string", - format: "misskey:id", - nullable: true, - }, - font: { type: "string", enum: ["serif", "sans-serif"] }, - alignCenter: { type: "boolean" }, - hideTitleWhenPinned: { type: "boolean" }, - isPublic: { type: "boolean" }, - }, - required: ["pageId", "title", "name", "content", "variables", "script"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: 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.findOneBy({ - id: ps.eyeCatchingImageId, - userId: user.id, - }); - - if (eyeCatchingImage == null) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - await Pages.findBy({ - 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, - isPublic: ps.isPublic, - alignCenter: - ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, - hideTitleWhenPinned: - ps.hideTitleWhenPinned === undefined - ? page.hideTitleWhenPinned - : ps.hideTitleWhenPinned, - font: ps.font === undefined ? page.font : ps.font, - eyeCatchingImageId: - ps.eyeCatchingImageId === null - ? null - : ps.eyeCatchingImageId === undefined - ? page.eyeCatchingImageId - : eyeCatchingImage!.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/patrons.ts b/packages/backend/src/server/api/endpoints/patrons.ts deleted file mode 100644 index d6ac6c3971..0000000000 --- a/packages/backend/src/server/api/endpoints/patrons.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - description: "Get list of Calckey patrons from Codeberg", - - requireCredential: false, - requireCredentialPrivateMode: false, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - let patrons; - await fetch( - "https://codeberg.org/calckey/calckey/raw/branch/develop/patrons.json", - ) - .then((response) => response.json()) - .then((data) => { - patrons = data["patrons"]; - }); - - return patrons; -}); diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts deleted file mode 100644 index c1f7e110bc..0000000000 --- a/packages/backend/src/server/api/endpoints/ping.ts +++ /dev/null @@ -1,32 +0,0 @@ -import define from "../define.js"; - -export const meta = { - requireCredential: false, - - tags: ["meta"], - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - pong: { - type: "number", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - return { - pong: Date.now(), - }; -}); diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts deleted file mode 100644 index 22020068ce..0000000000 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { IsNull } from "typeorm"; -import { Users } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import * as Acct from "@/misc/acct.js"; -import type { User } from "@/models/entities/user.js"; -import define from "../define.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - requireCredentialPrivateMode: false, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailed", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const meta = await fetchMeta(); - - const users = await Promise.all( - meta.pinnedUsers - .map((acct) => Acct.parse(acct)) - .map((acct) => - Users.findOneBy({ - usernameLower: acct.username.toLowerCase(), - host: acct.host ?? IsNull(), - }), - ), - ); - - return await Users.packMany( - users.filter((x) => x !== undefined) as User[], - me, - { detail: true }, - ); -}); diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts deleted file mode 100644 index 09c8cb6fab..0000000000 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { PromoReads } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getNote } from "../../common/getters.js"; - -export const meta = { - tags: ["notes"], - - requireCredential: true, - - errors: { - noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "d785b897-fcd3-4fe9-8fc3-b85c26e6c932", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - noteId: { type: "string", format: "misskey:id" }, - }, - required: ["noteId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch((err) => { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") - throw new ApiError(meta.errors.noSuchNote); - throw err; - }); - - const exist = await PromoReads.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist != null) { - return; - } - - await PromoReads.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/recommended-instances.ts b/packages/backend/src/server/api/endpoints/recommended-instances.ts deleted file mode 100644 index 8407afb1d3..0000000000 --- a/packages/backend/src/server/api/endpoints/recommended-instances.ts +++ /dev/null @@ -1,33 +0,0 @@ -// import { IsNull } from 'typeorm'; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "string", - optional: false, - nullable: false, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const meta = await fetchMeta(); - const instances = await Promise.all(meta.recommendedInstances.map((x) => x)); - return instances; -}); diff --git a/packages/backend/src/server/api/endpoints/release.ts b/packages/backend/src/server/api/endpoints/release.ts deleted file mode 100644 index e5ebbb79a6..0000000000 --- a/packages/backend/src/server/api/endpoints/release.ts +++ /dev/null @@ -1,28 +0,0 @@ -import define from "../define.js"; - -export const meta = { - tags: ["meta"], - description: "Get release notes from Codeberg", - - requireCredential: false, - requireCredentialPrivateMode: false, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - let release; - - await fetch( - "https://codeberg.org/calckey/calckey/raw/branch/develop/release.json", - ) - .then((response) => response.json()) - .then((data) => { - release = data; - }); - return release; -}); diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts deleted file mode 100644 index 857cbd9756..0000000000 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { genId } from "@/misc/gen-id.js"; -import { RenoteMutings } from "@/models/index.js"; -import { RenoteMuting } from "@/models/entities/renote-muting.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "write:mutes", - - errors: { - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "6fef56f3-e765-4957-88e5-c6f65329b8a5", - }, - - alreadyMuting: { - message: "You are already muting that user.", - code: "ALREADY_MUTING", - id: "7e7359cb-160c-4956-b08f-4d1c653cd007", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const muter = user; - - // 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 RenoteMutings.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, - }); - - if (exist != null) { - throw new ApiError(meta.errors.alreadyMuting); - } - - // Create mute - await RenoteMutings.insert({ - id: genId(), - createdAt: new Date(), - muterId: muter.id, - muteeId: mutee.id, - } as RenoteMuting); - - // publishUserEvent(user.id, "mute", mutee); -}); diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts deleted file mode 100644 index fb4c972af0..0000000000 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { RenoteMutings } from "@/models/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "write:mutes", - - errors: { - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "b851d00b-8ab1-4a56-8b1b-e24187cb48ef", - }, - - notMuting: { - message: "You are not muting that user.", - code: "NOT_MUTING", - id: "5467d020-daa9-4553-81e1-135c0c35a96d", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const muter = user; - - // 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 RenoteMutings.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, - }); - - if (exist == null) { - throw new ApiError(meta.errors.notMuting); - } - - // Delete mute - await RenoteMutings.delete({ - id: exist.id, - }); - - // publishUserEvent(user.id, "unmute", mutee); -}); diff --git a/packages/backend/src/server/api/endpoints/renote-mute/list.ts b/packages/backend/src/server/api/endpoints/renote-mute/list.ts deleted file mode 100644 index 9149dd9753..0000000000 --- a/packages/backend/src/server/api/endpoints/renote-mute/list.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { RenoteMutings } from "@/models/index.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - kind: "read:mutes", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "RenoteMuting", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 30 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - RenoteMutings.createQueryBuilder("muting"), - ps.sinceId, - ps.untilId, - ).andWhere("muting.muterId = :meId", { meId: me.id }); - - const mutings = await query.take(ps.limit).getMany(); - - return await RenoteMutings.packMany(mutings, me); -}); diff --git a/packages/backend/src/server/api/endpoints/request-reset-password.ts b/packages/backend/src/server/api/endpoints/request-reset-password.ts deleted file mode 100644 index bac564c1d6..0000000000 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ /dev/null @@ -1,76 +0,0 @@ -import rndstr from "rndstr"; -import { IsNull } from "typeorm"; -import { publishMainStream } from "@/services/stream.js"; -import config from "@/config/index.js"; -import { Users, UserProfiles, PasswordResetRequests } from "@/models/index.js"; -import { sendEmail } from "@/services/send-email.js"; -import { genId } from "@/misc/gen-id.js"; -import { ApiError } from "../error.js"; -import define from "../define.js"; -import { HOUR } from "@/const.js"; - -export const meta = { - tags: ["reset password"], - - requireCredential: false, - - description: "Request a users password to be reset.", - - limit: { - duration: HOUR, - max: 3, - }, - - errors: {}, -} as const; - -export const paramDef = { - type: "object", - properties: { - username: { type: "string" }, - email: { type: "string" }, - }, - required: ["username", "email"], -} as const; - -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneBy({ - usernameLower: ps.username.toLowerCase(), - host: IsNull(), - }); - - // 合致するユーザーが登録されていなかったら無視 - if (user == null) { - return; - } - - const profile = await UserProfiles.findOneByOrFail({ userId: 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/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts deleted file mode 100644 index c64db7bca8..0000000000 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { resetDb } from "@/db/postgre.js"; -import define from "../define.js"; -import { ApiError } from "../error.js"; - -export const meta = { - tags: ["non-productive"], - - requireCredential: false, - - description: - "Only available when running with NODE_ENV=testing. Reset the database and flush Redis.", - - errors: {}, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - if (process.env.NODE_ENV !== "test") - throw new Error("NODE_ENV is not a test"); - - await resetDb(); - - await new Promise((resolve) => setTimeout(resolve, 1000)); -}); diff --git a/packages/backend/src/server/api/endpoints/reset-password.ts b/packages/backend/src/server/api/endpoints/reset-password.ts deleted file mode 100644 index f695ae41f1..0000000000 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import { Users, UserProfiles, PasswordResetRequests } from "@/models/index.js"; -import define from "../define.js"; -import { ApiError } from "../error.js"; -import { hashPassword } from "@/misc/password.js"; - -export const meta = { - tags: ["reset password"], - - requireCredential: false, - - description: "Complete the password reset that was previously requested.", - - errors: {}, -} as const; - -export const paramDef = { - type: "object", - properties: { - token: { type: "string" }, - password: { type: "string" }, - }, - required: ["token", "password"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const req = await PasswordResetRequests.findOneByOrFail({ - token: ps.token, - }); - - // 発行してから30分以上経過していたら無効 - if (Date.now() - req.createdAt.getTime() > 1000 * 60 * 30) { - throw new Error(); // TODO - } - - // Generate hash of password - const hash = await hashPassword(ps.password); - - await UserProfiles.update(req.userId, { - password: hash, - }); - - PasswordResetRequests.delete(req.id); -}); diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts deleted file mode 100644 index 1ce27e2621..0000000000 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as os from "node:os"; -import si from "systeminformation"; -import define from "../define.js"; - -export const meta = { - requireCredential: false, - requireCredentialPrivateMode: true, - - tags: ["meta"], -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const memStats = await si.mem(); - const fsStats = await si.fsSize(); - - return { - machine: os.hostname(), - cpu: { - model: os.cpus()[0].model, - cores: os.cpus().length, - }, - mem: { - total: memStats.total, - }, - fs: { - total: fsStats[0].size, - used: fsStats[0].used, - }, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/stats.ts b/packages/backend/src/server/api/endpoints/stats.ts deleted file mode 100644 index 8bd5597689..0000000000 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Instances, NoteReactions, Notes, Users } from "@/models/index.js"; -import define from "../define.js"; -import {} from "@/services/chart/index.js"; -import { IsNull } from "typeorm"; - -export const meta = { - requireCredential: false, - requireCredentialPrivateMode: true, - - tags: ["meta"], - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - notesCount: { - type: "number", - optional: false, - nullable: false, - }, - originalNotesCount: { - type: "number", - optional: false, - nullable: false, - }, - usersCount: { - type: "number", - optional: false, - nullable: false, - }, - originalUsersCount: { - type: "number", - optional: false, - nullable: false, - }, - instances: { - type: "number", - optional: false, - nullable: false, - }, - driveUsageLocal: { - type: "number", - optional: false, - nullable: false, - }, - driveUsageRemote: { - type: "number", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async () => { - const [ - notesCount, - originalNotesCount, - usersCount, - originalUsersCount, - reactionsCount, - //originalReactionsCount, - instances, - ] = await Promise.all([ - Notes.count({ cache: 3600000 }), // 1 hour - Notes.count({ where: { userHost: IsNull() }, cache: 3600000 }), - Users.count({ cache: 3600000 }), - Users.count({ where: { host: IsNull() }, cache: 3600000 }), - NoteReactions.count({ cache: 3600000 }), // 1 hour - //NoteReactions.count({ where: { userHost: IsNull() }, cache: 3600000 }), - Instances.count({ cache: 3600000 }), - ]); - - return { - notesCount, - originalNotesCount, - usersCount, - originalUsersCount, - reactionsCount, - //originalReactionsCount, - instances, - driveUsageLocal: 0, - driveUsageRemote: 0, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts deleted file mode 100644 index 42a35fb685..0000000000 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { genId } from "@/misc/gen-id.js"; -import { SwSubscriptions } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - description: "Register to receive push notifications.", - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - state: { - type: "string", - optional: true, - nullable: false, - enum: ["already-subscribed", "subscribed"], - }, - key: { - type: "string", - optional: false, - nullable: true, - }, - userId: { - type: "string", - optional: false, - nullable: false, - }, - endpoint: { - type: "string", - optional: false, - nullable: false, - }, - sendReadMessage: { - type: "boolean", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - endpoint: { type: "string" }, - auth: { type: "string" }, - publickey: { type: "string" }, - sendReadMessage: { type: "boolean", default: false }, - }, - required: ["endpoint", "auth", "publickey"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // if already subscribed - const exist = await SwSubscriptions.findOneBy({ - userId: me.id, - endpoint: ps.endpoint, - auth: ps.auth, - publickey: ps.publickey, - }); - - const instance = await fetchMeta(true); - - if (exist != null) { - return { - state: "already-subscribed" as const, - key: instance.swPublicKey, - userId: me.id, - endpoint: exist.endpoint, - sendReadMessage: exist.sendReadMessage, - }; - } - - await SwSubscriptions.insert({ - id: genId(), - createdAt: new Date(), - userId: me.id, - endpoint: ps.endpoint, - auth: ps.auth, - publickey: ps.publickey, - sendReadMessage: ps.sendReadMessage, - }); - - return { - state: "subscribed" as const, - key: instance.swPublicKey, - userId: me.id, - endpoint: ps.endpoint, - sendReadMessage: ps.sendReadMessage, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/sw/show-registration.ts b/packages/backend/src/server/api/endpoints/sw/show-registration.ts deleted file mode 100644 index c7a9609cff..0000000000 --- a/packages/backend/src/server/api/endpoints/sw/show-registration.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { SwSubscriptions } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - description: "Check push notification registration exists.", - - res: { - type: "object", - optional: false, - nullable: true, - properties: { - userId: { - type: "string", - optional: false, - nullable: false, - }, - endpoint: { - type: "string", - optional: false, - nullable: false, - }, - sendReadMessage: { - type: "boolean", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - endpoint: { type: "string" }, - }, - required: ["endpoint"], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const exist = await SwSubscriptions.findOneBy({ - userId: me.id, - endpoint: ps.endpoint, - }); - - if (exist != null) { - return { - userId: exist.userId, - endpoint: exist.endpoint, - sendReadMessage: exist.sendReadMessage, - }; - } - - return null; -}); diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts deleted file mode 100644 index e2a40f51cb..0000000000 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { SwSubscriptions } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["account"], - - requireCredential: false, - - description: "Unregister from receiving push notifications.", -} as const; - -export const paramDef = { - type: "object", - properties: { - endpoint: { type: "string" }, - }, - required: ["endpoint"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - await SwSubscriptions.delete({ - ...(me ? { userId: me.id } : {}), - endpoint: ps.endpoint, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/sw/update-registration.ts b/packages/backend/src/server/api/endpoints/sw/update-registration.ts deleted file mode 100644 index 5ba53ee8a7..0000000000 --- a/packages/backend/src/server/api/endpoints/sw/update-registration.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { SwSubscriptions } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["account"], - - requireCredential: true, - - description: "Unregister from receiving push notifications.", -} as const; - -export const paramDef = { - type: "object", - properties: { - endpoint: { type: "string" }, - sendReadMessage: { type: "boolean" }, - }, - required: ["endpoint"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const swSubscription = await SwSubscriptions.findOneBy({ - userId: me.id, - endpoint: ps.endpoint, - }); - - if (swSubscription === null) { - throw new Error("No such registration"); - } - - if (ps.sendReadMessage !== undefined) { - swSubscription.sendReadMessage = ps.sendReadMessage; - } - - await SwSubscriptions.update(swSubscription.id, { - sendReadMessage: swSubscription.sendReadMessage, - }); - - return { - userId: swSubscription.userId, - endpoint: swSubscription.endpoint, - sendReadMessage: swSubscription.sendReadMessage, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/test.ts b/packages/backend/src/server/api/endpoints/test.ts deleted file mode 100644 index 2c43c61152..0000000000 --- a/packages/backend/src/server/api/endpoints/test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import define from "../define.js"; - -export const meta = { - tags: ["non-productive"], - - description: "Endpoint for testing input validation.", - - requireCredential: false, -} as const; - -export const paramDef = { - type: "object", - properties: { - required: { type: "boolean" }, - string: { type: "string" }, - default: { type: "string", default: "hello" }, - nullableDefault: { type: "string", nullable: true, default: "hello" }, - id: { type: "string", format: "misskey:id" }, - }, - required: ["required"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - return ps; -}); diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts deleted file mode 100644 index f5aa4ed1ea..0000000000 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { IsNull } from "typeorm"; -import { Users, UsedUsernames } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - available: { - type: "boolean", - optional: false, - nullable: false, - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - username: Users.localUsernameSchema, - }, - required: ["username"], -} as const; - -export default define(meta, paramDef, async (ps) => { - // Get exist - const exist = await Users.countBy({ - host: IsNull(), - usernameLower: ps.username.toLowerCase(), - }); - - const exist2 = await UsedUsernames.countBy({ - username: ps.username.toLowerCase(), - }); - - return { - available: exist === 0 && exist2 === 0, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts deleted file mode 100644 index f0a8670902..0000000000 --- a/packages/backend/src/server/api/endpoints/users.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Users } from "@/models/index.js"; -import define from "../define.js"; -import { generateMutedUserQueryForUsers } from "../common/generate-muted-user-query.js"; -import { generateBlockQueryForUsers } from "../common/generate-block-query.js"; - -export const meta = { - tags: ["users"], - - requireCredential: true, - requireCredentialPrivateMode: true, - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailed", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - sort: { - type: "string", - enum: [ - "+follower", - "-follower", - "+createdAt", - "-createdAt", - "+updatedAt", - "-updatedAt", - ], - }, - state: { - type: "string", - enum: ["all", "admin", "moderator", "adminOrModerator", "alive"], - default: "all", - }, - origin: { - type: "string", - enum: ["combined", "local", "remote"], - default: "local", - }, - hostname: { - type: "string", - nullable: true, - default: null, - description: "The local host is represented with `null`.", - }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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; - } - - if (ps.hostname) { - query.andWhere("user.host = :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 - .andWhere("user.updatedAt IS NOT NULL") - .orderBy("user.updatedAt", "DESC"); - break; - case "-updatedAt": - query - .andWhere("user.updatedAt IS NOT NULL") - .orderBy("user.updatedAt", "ASC"); - break; - default: - query.orderBy("user.id", "ASC"); - break; - } - - if (me) generateMutedUserQueryForUsers(query, me); - if (me) generateBlockQueryForUsers(query, me); - - query.take(ps.limit); - query.skip(ps.offset); - - const users = await query.getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/clips.ts b/packages/backend/src/server/api/endpoints/users/clips.ts deleted file mode 100644 index 0dc90b8f99..0000000000 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Clips } from "@/models/index.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["users", "clips"], - requireCredentialPrivateMode: true, - - description: "Show all clips this user owns.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Clip", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - Clips.createQueryBuilder("clip"), - ps.sinceId, - ps.untilId, - ) - .andWhere("clip.userId = :userId", { userId: ps.userId }) - .andWhere("clip.isPublic = true"); - - const clips = await query.take(ps.limit).getMany(); - - return await Clips.packMany(clips); -}); diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts deleted file mode 100644 index 138343d9f4..0000000000 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { IsNull } from "typeorm"; -import { Users, Followings, UserProfiles } from "@/models/index.js"; -import { toPunyNullable } from "@/misc/convert-host.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - description: "Show everyone that follows this user.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - }, - anyOf: [ - { - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], - }, - { - properties: { - username: { type: "string" }, - host: { - type: "string", - nullable: true, - description: "The local host is represented with `null`.", - }, - }, - required: ["username", "host"], - }, - ], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy( - ps.userId != null - ? { id: ps.userId } - : { - usernameLower: ps.username!.toLowerCase(), - host: toPunyNullable(ps.host) ?? IsNull(), - }, - ); - - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } - - const profile = await UserProfiles.findOneByOrFail({ userId: 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.findOneBy({ - followeeId: user.id, - followerId: me.id, - }); - if (following == null) { - throw new ApiError(meta.errors.forbidden); - } - } - } - - const query = makePaginationQuery( - Followings.createQueryBuilder("following"), - ps.sinceId, - ps.untilId, - ) - .andWhere("following.followeeId = :userId", { userId: user.id }) - .innerJoinAndSelect("following.follower", "follower"); - - const followings = await query.take(ps.limit).getMany(); - - return await Followings.packMany(followings, me, { populateFollower: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts deleted file mode 100644 index 967379d0c4..0000000000 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { IsNull } from "typeorm"; -import { Users, Followings, UserProfiles } from "@/models/index.js"; -import { toPunyNullable } from "@/misc/convert-host.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - description: "Show everyone that this user is following.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - }, - anyOf: [ - { - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], - }, - { - properties: { - username: { type: "string" }, - host: { - type: "string", - nullable: true, - description: "The local host is represented with `null`.", - }, - }, - required: ["username", "host"], - }, - ], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy( - ps.userId != null - ? { id: ps.userId } - : { - usernameLower: ps.username!.toLowerCase(), - host: toPunyNullable(ps.host) ?? IsNull(), - }, - ); - - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } - - const profile = await UserProfiles.findOneByOrFail({ userId: 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.findOneBy({ - followeeId: user.id, - followerId: me.id, - }); - if (following == null) { - throw new ApiError(meta.errors.forbidden); - } - } - } - - const query = makePaginationQuery( - Followings.createQueryBuilder("following"), - ps.sinceId, - ps.untilId, - ) - .andWhere("following.followerId = :userId", { userId: user.id }) - .innerJoinAndSelect("following.followee", "followee"); - - const followings = await query.take(ps.limit).getMany(); - - return await Followings.packMany(followings, me, { populateFollowee: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts b/packages/backend/src/server/api/endpoints/users/gallery/posts.ts deleted file mode 100644 index 5d64fb4727..0000000000 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ /dev/null @@ -1,45 +0,0 @@ -import define from "../../../define.js"; -import { GalleryPosts } from "@/models/index.js"; -import { makePaginationQuery } from "../../../common/make-pagination-query.js"; - -export const meta = { - tags: ["users", "gallery"], - requireCredentialPrivateMode: true, - - description: "Show all gallery posts by the given user.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "GalleryPost", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - GalleryPosts.createQueryBuilder("post"), - ps.sinceId, - ps.untilId, - ).andWhere("post.userId = :userId", { userId: ps.userId }); - - const posts = await query.take(ps.limit).getMany(); - - return await GalleryPosts.packMany(posts, user); -}); diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts deleted file mode 100644 index 9722804c8d..0000000000 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Not, In, IsNull } from "typeorm"; -import { maximum } from "@/prelude/array.js"; -import { Notes, Users } from "@/models/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - description: - "Get a list of other users that the specified user frequently replies to.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - properties: { - user: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailed", - }, - weight: { - type: "number", - optional: false, - nullable: false, - }, - }, - }, - }, - - errors: { - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "e6965129-7b2a-40a4-bae2-cd84cd434822", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Lookup user - const user = await getUser(ps.userId).catch((e) => { - if (e.id === "15348ddd-432d-49c2-8a5a-8069753becff") - throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Fetch recent notes - const recentNotes = await Notes.find({ - where: { - userId: user.id, - replyId: Not(IsNull()), - }, - order: { - id: -1, - }, - take: 1000, - select: ["replyId"], - }); - - // 投稿が少なかったら中断 - if (recentNotes.length === 0) { - return []; - } - - // TODO ミュートを考慮 - const replyTargetNotes = await Notes.find({ - where: { - id: In(recentNotes.map((p) => p.replyId)), - }, - select: ["userId"], - }); - - const repliedUsers: any = {}; - - // Extract replies from recent notes - for (const userId of replyTargetNotes.map((x) => x.userId.toString())) { - if (repliedUsers[userId]) { - repliedUsers[userId]++; - } else { - repliedUsers[userId] = 1; - } - } - - // Calc peak - const peak = maximum(Object.values(repliedUsers)); - - // Sort replies by frequency - const repliedUsersSorted = Object.keys(repliedUsers).sort( - (a, b) => repliedUsers[b] - repliedUsers[a], - ); - - // Extract top replied users - const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit); - - // Make replies object (includes weights) - const repliesObj = await Promise.all( - topRepliedUsers.map(async (user) => ({ - user: await Users.pack(user, me, { detail: true }), - weight: repliedUsers[user] / peak, - })), - ); - - return repliesObj; -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts deleted file mode 100644 index 76bd78c49f..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/create.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { UserGroups, UserGroupJoinings } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { UserGroup } from "@/models/entities/user-group.js"; -import type { UserGroupJoining } from "@/models/entities/user-group-joining.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["groups"], - - requireCredential: true, - - kind: "write:user-groups", - - description: "Create a new group.", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "UserGroup", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string", minLength: 1, maxLength: 100 }, - }, - required: ["name"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const userGroup = await UserGroups.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - } as UserGroup).then((x) => UserGroups.findOneByOrFail(x.identifiers[0])); - - // Push the owner - await UserGroupJoinings.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: userGroup.id, - } as UserGroupJoining); - - return await UserGroups.pack(userGroup); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/delete.ts b/packages/backend/src/server/api/endpoints/users/groups/delete.ts deleted file mode 100644 index 81c15ad38e..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { UserGroups } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["groups"], - - requireCredential: true, - - kind: "write:user-groups", - - description: "Delete an existing group.", - - errors: { - noSuchGroup: { - message: "No such group.", - code: "NO_SUCH_GROUP", - id: "63dbd64c-cd77-413f-8e08-61781e210b38", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - groupId: { type: "string", format: "misskey:id" }, - }, - required: ["groupId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - userId: user.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - await UserGroups.delete(userGroup.id); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts deleted file mode 100644 index 5cb3a7bad3..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { UserGroupJoinings, UserGroupInvitations } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { UserGroupJoining } from "@/models/entities/user-group-joining.js"; -import { ApiError } from "../../../../error.js"; -import define from "../../../../define.js"; - -export const meta = { - tags: ["groups", "users"], - - requireCredential: true, - - kind: "write:user-groups", - - description: "Join a group the authenticated user has been invited to.", - - errors: { - noSuchInvitation: { - message: "No such invitation.", - code: "NO_SUCH_INVITATION", - id: "98c11eca-c890-4f42-9806-c8c8303ebb5e", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - invitationId: { type: "string", format: "misskey:id" }, - }, - required: ["invitationId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch the invitation - const invitation = await UserGroupInvitations.findOneBy({ - id: ps.invitationId, - }); - - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - if (invitation.userId !== user.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - // Push the user - await UserGroupJoinings.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - userGroupId: invitation.userGroupId, - } as UserGroupJoining); - - UserGroupInvitations.delete(invitation.id); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts b/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts deleted file mode 100644 index c04ebed23b..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { UserGroupInvitations } from "@/models/index.js"; -import define from "../../../../define.js"; -import { ApiError } from "../../../../error.js"; - -export const meta = { - tags: ["groups", "users"], - - requireCredential: true, - - kind: "write:user-groups", - - description: - "Delete an existing group invitation for the authenticated user without joining the group.", - - errors: { - noSuchInvitation: { - message: "No such invitation.", - code: "NO_SUCH_INVITATION", - id: "ad7471d4-2cd9-44b4-ac68-e7136b4ce656", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - invitationId: { type: "string", format: "misskey:id" }, - }, - required: ["invitationId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch the invitation - const invitation = await UserGroupInvitations.findOneBy({ - id: ps.invitationId, - }); - - if (invitation == null) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - if (invitation.userId !== user.id) { - throw new ApiError(meta.errors.noSuchInvitation); - } - - await UserGroupInvitations.delete(invitation.id); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/invite.ts b/packages/backend/src/server/api/endpoints/users/groups/invite.ts deleted file mode 100644 index 10cc215861..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - UserGroups, - UserGroupJoinings, - UserGroupInvitations, -} from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { UserGroupInvitation } from "@/models/entities/user-group-invitation.js"; -import { createNotification } from "@/services/create-notification.js"; -import { getUser } from "../../../common/getters.js"; -import { ApiError } from "../../../error.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["groups", "users"], - - requireCredential: true, - - kind: "write:user-groups", - - description: "Invite a user to an existing group.", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - groupId: { type: "string", format: "misskey:id" }, - userId: { type: "string", format: "misskey:id" }, - }, - required: ["groupId", "userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - 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.findOneBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (joining) { - throw new ApiError(meta.errors.alreadyAdded); - } - - const existInvitation = await UserGroupInvitations.findOneBy({ - 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.findOneByOrFail(x.identifiers[0]), - ); - - // 通知を作成 - createNotification(user.id, "groupInvited", { - notifierId: me.id, - userGroupInvitationId: invitation.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/joined.ts b/packages/backend/src/server/api/endpoints/users/groups/joined.ts deleted file mode 100644 index 8422cf586d..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Not, In } from "typeorm"; -import { UserGroups, UserGroupJoinings } from "@/models/index.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["groups", "account"], - - requireCredential: true, - - kind: "read:user-groups", - - description: "List the groups that the authenticated user is a member of.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "UserGroup", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const ownedGroups = await UserGroups.findBy({ - userId: me.id, - }); - - const joinings = await UserGroupJoinings.findBy({ - userId: me.id, - ...(ownedGroups.length > 0 - ? { - userGroupId: Not(In(ownedGroups.map((x) => x.id))), - } - : {}), - }); - - return await Promise.all(joinings.map((x) => UserGroups.pack(x.userGroupId))); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/leave.ts b/packages/backend/src/server/api/endpoints/users/groups/leave.ts deleted file mode 100644 index d963b1826e..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { UserGroups, UserGroupJoinings } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["groups", "users"], - - requireCredential: true, - - kind: "write:user-groups", - - description: - "Leave a group. The owner of a group can not leave. They must transfer ownership or delete the group instead.", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - groupId: { type: "string", format: "misskey:id" }, - }, - required: ["groupId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - if (me.id === userGroup.userId) { - throw new ApiError(meta.errors.youAreOwner); - } - - await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: me.id }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/owned.ts b/packages/backend/src/server/api/endpoints/users/groups/owned.ts deleted file mode 100644 index d86185ff02..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { UserGroups } from "@/models/index.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["groups", "account"], - - requireCredential: true, - - kind: "read:user-groups", - - description: "List the groups that the authenticated user is the owner of.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "UserGroup", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const userGroups = await UserGroups.findBy({ - userId: me.id, - }); - - return await Promise.all(userGroups.map((x) => UserGroups.pack(x))); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/pull.ts b/packages/backend/src/server/api/endpoints/users/groups/pull.ts deleted file mode 100644 index 1f79a2d2b7..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { UserGroups, UserGroupJoinings } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getUser } from "../../../common/getters.js"; - -export const meta = { - tags: ["groups", "users"], - - requireCredential: true, - - kind: "write:user-groups", - - description: - "Removes a specified user from a group. The owner can not be removed.", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - groupId: { type: "string", format: "misskey:id" }, - userId: { type: "string", format: "misskey:id" }, - }, - required: ["groupId", "userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - // Fetch the user - const user = await getUser(ps.userId).catch((e) => { - if (e.id === "15348ddd-432d-49c2-8a5a-8069753becff") - throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - if (user.id === userGroup.userId) { - throw new ApiError(meta.errors.isOwner); - } - - // Pull the user - await UserGroupJoinings.delete({ - userGroupId: userGroup.id, - userId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/show.ts b/packages/backend/src/server/api/endpoints/users/groups/show.ts deleted file mode 100644 index 46f4410c84..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { UserGroups, UserGroupJoinings } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["groups", "account"], - - requireCredential: true, - - kind: "read:user-groups", - - description: "Show the properties of a group.", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "UserGroup", - }, - - errors: { - noSuchGroup: { - message: "No such group.", - code: "NO_SUCH_GROUP", - id: "ea04751e-9b7e-487b-a509-330fb6bd6b9b", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - groupId: { type: "string", format: "misskey:id" }, - }, - required: ["groupId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - const joining = await UserGroupJoinings.findOneBy({ - userId: me.id, - userGroupId: userGroup.id, - }); - - if (joining == null && userGroup.userId !== me.id) { - throw new ApiError(meta.errors.noSuchGroup); - } - - return await UserGroups.pack(userGroup); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts b/packages/backend/src/server/api/endpoints/users/groups/transfer.ts deleted file mode 100644 index 0322441574..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { UserGroups, UserGroupJoinings } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getUser } from "../../../common/getters.js"; - -export const meta = { - tags: ["groups", "users"], - - requireCredential: true, - - kind: "write:user-groups", - - description: - "Transfer ownership of a group from the authenticated user to another user.", - - res: { - type: "object", - optional: false, - nullable: false, - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - groupId: { type: "string", format: "misskey:id" }, - userId: { type: "string", format: "misskey:id" }, - }, - required: ["groupId", "userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - 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.findOneBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (joining == null) { - throw new ApiError(meta.errors.noSuchGroupMember); - } - - await UserGroups.update(userGroup.id, { - userId: ps.userId, - }); - - return await UserGroups.pack(userGroup.id); -}); diff --git a/packages/backend/src/server/api/endpoints/users/groups/update.ts b/packages/backend/src/server/api/endpoints/users/groups/update.ts deleted file mode 100644 index fa720c9c45..0000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { UserGroups } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["groups"], - - requireCredential: true, - - kind: "write:user-groups", - - description: "Update the properties of a group.", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "UserGroup", - }, - - errors: { - noSuchGroup: { - message: "No such group.", - code: "NO_SUCH_GROUP", - id: "9081cda3-7a9e-4fac-a6ce-908d70f282f6", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - groupId: { type: "string", format: "misskey:id" }, - name: { type: "string", minLength: 1, maxLength: 100 }, - }, - required: ["groupId", "name"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the group - const userGroup = await UserGroups.findOneBy({ - id: ps.groupId, - userId: me.id, - }); - - if (userGroup == null) { - throw new ApiError(meta.errors.noSuchGroup); - } - - await UserGroups.update(userGroup.id, { - name: ps.name, - }); - - return await UserGroups.pack(userGroup.id); -}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts deleted file mode 100644 index 6bbbf603e5..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { UserLists } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { UserList } from "@/models/entities/user-list.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["lists"], - - requireCredential: true, - - kind: "write:account", - - description: "Create a new list of users.", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "UserList", - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - name: { type: "string", minLength: 1, maxLength: 100 }, - }, - required: ["name"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const userList = await UserLists.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - } as UserList).then((x) => UserLists.findOneByOrFail(x.identifiers[0])); - - return await UserLists.pack(userList); -}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete-all.ts b/packages/backend/src/server/api/endpoints/users/lists/delete-all.ts deleted file mode 100644 index 49c4cf6f63..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/delete-all.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { UserLists } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["lists"], - - requireCredential: true, - - kind: "write:account", - - description: "Delete all lists of users.", - - errors: { - noSuchList: { - message: "No such list.", - code: "NO_SUCH_LIST", - id: "78436795-db79-42f5-b1e2-55ea2cf19166", - }, - }, -} as const; - -export const paramDef = { - type: "object", -} as const; - -export default define(meta, paramDef, async (ps, user) => { - while ((await UserLists.findOneBy({ userId: user.id })) != null) { - const userList = await UserLists.findOneBy({ userId: user.id }); - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - await UserLists.delete(userList.id); - } -}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts deleted file mode 100644 index 4566295676..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { UserLists } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["lists"], - - requireCredential: true, - - kind: "write:account", - - description: "Delete an existing list of users.", - - errors: { - noSuchList: { - message: "No such list.", - code: "NO_SUCH_LIST", - id: "78436795-db79-42f5-b1e2-55ea2cf19166", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - listId: { type: "string", format: "misskey:id" }, - }, - required: ["listId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: user.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - await UserLists.delete(userList.id); -}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts deleted file mode 100644 index 5d590ee0ec..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { UserLists } from "@/models/index.js"; -import define from "../../../define.js"; - -export const meta = { - tags: ["lists", "account"], - - requireCredential: true, - - kind: "read:account", - - description: "Show all lists that the authenticated user has created.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "UserList", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: {}, - required: [], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const userLists = await UserLists.findBy({ - userId: me.id, - }); - - return await Promise.all(userLists.map((x) => UserLists.pack(x))); -}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts deleted file mode 100644 index 07fae20675..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { publishUserListStream } from "@/services/stream.js"; -import { UserLists, UserListJoinings, Users } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getUser } from "../../../common/getters.js"; - -export const meta = { - tags: ["lists", "users"], - - requireCredential: true, - - kind: "write:account", - - description: "Remove a user from a list.", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - listId: { type: "string", format: "misskey:id" }, - userId: { type: "string", format: "misskey:id" }, - }, - required: ["listId", "userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the list - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: me.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - // Fetch the user - const user = await getUser(ps.userId).catch((e) => { - if (e.id === "15348ddd-432d-49c2-8a5a-8069753becff") - throw new ApiError(meta.errors.noSuchUser); - throw e; - }); - - // Pull the user - await UserListJoinings.delete({ userListId: userList.id, userId: user.id }); - - publishUserListStream(userList.id, "userRemoved", await Users.pack(user)); -}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts deleted file mode 100644 index a14195bbc3..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { pushUserToUserList } from "@/services/user-list/push.js"; -import { UserLists, UserListJoinings, Blockings } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; -import { getUser } from "../../../common/getters.js"; - -export const meta = { - tags: ["lists", "users"], - - requireCredential: true, - - kind: "write:account", - - description: "Add a user to an existing list.", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - listId: { type: "string", format: "misskey:id" }, - userId: { type: "string", format: "misskey:id" }, - }, - required: ["listId", "userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the list - const userList = await UserLists.findOneBy({ - 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.findOneBy({ - blockerId: user.id, - blockeeId: me.id, - }); - if (block) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - - const exist = await UserListJoinings.findOneBy({ - userListId: userList.id, - userId: user.id, - }); - - if (exist) { - throw new ApiError(meta.errors.alreadyAdded); - } - - // Push the user - await pushUserToUserList(user, userList); -}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts deleted file mode 100644 index 716fd405dc..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { UserLists } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["lists", "account"], - - requireCredential: true, - - kind: "read:account", - - description: "Show the properties of a list.", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "UserList", - }, - - errors: { - noSuchList: { - message: "No such list.", - code: "NO_SUCH_LIST", - id: "7bc05c21-1d7a-41ae-88f1-66820f4dc686", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - listId: { type: "string", format: "misskey:id" }, - }, - required: ["listId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - // Fetch the list - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: me.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - return await UserLists.pack(userList); -}); diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts deleted file mode 100644 index 0ac788fd37..0000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { UserLists } from "@/models/index.js"; -import define from "../../../define.js"; -import { ApiError } from "../../../error.js"; - -export const meta = { - tags: ["lists"], - - requireCredential: true, - - kind: "write:account", - - description: "Update the properties of a list.", - - res: { - type: "object", - optional: false, - nullable: false, - ref: "UserList", - }, - - errors: { - noSuchList: { - message: "No such list.", - code: "NO_SUCH_LIST", - id: "796666fe-3dff-4d39-becb-8a5932c1d5b7", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - listId: { type: "string", format: "misskey:id" }, - name: { type: "string", minLength: 1, maxLength: 100 }, - }, - required: ["listId", "name"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - // Fetch the list - const userList = await UserLists.findOneBy({ - id: ps.listId, - userId: user.id, - }); - - if (userList == null) { - throw new ApiError(meta.errors.noSuchList); - } - - await UserLists.update(userList.id, { - name: ps.name, - }); - - return await UserLists.pack(userList.id); -}); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts deleted file mode 100644 index 724cfc9af1..0000000000 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Brackets } from "typeorm"; -import { Notes } from "@/models/index.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; -import { getUser } from "../../common/getters.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js"; -import { generateBlockedUserQuery } from "../../common/generate-block-query.js"; - -export const meta = { - tags: ["users", "notes"], - - requireCredentialPrivateMode: true, - description: "Show all notes that this user created.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Note", - }, - }, - - errors: { - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "27e494ba-2ac2-48e8-893b-10d4d8c2387b", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - includeReplies: { type: "boolean", default: true }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - includeMyRenotes: { type: "boolean", default: true }, - withFiles: { type: "boolean", default: false }, - fileType: { - type: "array", - items: { - type: "string", - }, - }, - excludeNsfw: { type: "boolean", default: false }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, 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("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - generateVisibilityQuery(query, me); - if (me) { - generateMutedUserQuery(query, me, user); - generateBlockedUserQuery(query, me); - } - - if (ps.withFiles) { - query.andWhere("note.fileIds != '{}'"); - } - - if (ps.fileType != null) { - query.andWhere("note.fileIds != '{}'"); - query.andWhere( - new Brackets((qb) => { - for (const type of ps.fileType!) { - const i = ps.fileType!.indexOf(type); - qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { - [`type${i}`]: type, - }); - } - }), - ); - - if (ps.excludeNsfw) { - query.andWhere("note.cw IS NULL"); - query.andWhere( - '0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)', - ); - } - } - - if (!ps.includeReplies) { - query.andWhere("note.replyId IS NULL"); - } - - if (ps.includeMyRenotes === false) { - query.andWhere( - new Brackets((qb) => { - qb.orWhere("note.userId != :userId", { userId: user.id }); - qb.orWhere("note.renoteId IS NULL"); - qb.orWhere("note.text IS NOT NULL"); - qb.orWhere("note.fileIds != '{}'"); - qb.orWhere( - '0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)', - ); - }), - ); - } - - //#endregion - - const timeline = await query.take(ps.limit).getMany(); - - return await Notes.packMany(timeline, me); -}); diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts deleted file mode 100644 index c08258b19d..0000000000 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Pages } from "@/models/index.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; - -export const meta = { - tags: ["users", "pages"], - requireCredentialPrivateMode: true, - - description: "Show all pages this user created.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "Page", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery( - Pages.createQueryBuilder("page"), - ps.sinceId, - ps.untilId, - ) - .andWhere("page.userId = :userId", { userId: ps.userId }) - .andWhere("page.visibility = 'public'") - .andWhere("page.isPublic = true"); - - const pages = await query.take(ps.limit).getMany(); - - return await Pages.packMany(pages); -}); diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts deleted file mode 100644 index 17b7a04a06..0000000000 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { NoteReactions, UserProfiles } from "@/models/index.js"; -import define from "../../define.js"; -import { makePaginationQuery } from "../../common/make-pagination-query.js"; -import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["users", "reactions"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - description: "Show all reactions this user made.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "NoteReaction", - }, - }, - - errors: { - reactionsNotPublic: { - message: "Reactions of the user is not public.", - code: "REACTIONS_NOT_PUBLIC", - id: "673a7dd2-6924-1093-e0c0-e68456ceae5c", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: "string", format: "misskey:id" }, - untilId: { type: "string", format: "misskey:id" }, - sinceDate: { type: "integer" }, - untilDate: { type: "integer" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const profile = await UserProfiles.findOneByOrFail({ userId: 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 NoteReactions.packMany(reactions, me, { withNote: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts deleted file mode 100644 index 615cca7856..0000000000 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Users, Followings } from "@/models/index.js"; -import define from "../../define.js"; -import { generateMutedUserQueryForUsers } from "../../common/generate-muted-user-query.js"; -import { - generateBlockedUserQuery, - generateBlockQueryForUsers, -} from "../../common/generate-block-query.js"; -import { DAY } from "@/const.js"; - -export const meta = { - tags: ["users"], - - requireCredential: true, - - kind: "read:account", - - description: - "Show users that the authenticated user might be interested to follow.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "UserDetailed", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - offset: { type: "integer", default: 0 }, - }, - required: [], -} as const; - -export default define(meta, paramDef, 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() - 7 * DAY), - }) - .andWhere("user.id != :meId", { meId: me.id }) - .orderBy("user.followersCount", "DESC"); - - generateMutedUserQueryForUsers(query, me); - generateBlockQueryForUsers(query, me); - generateBlockedUserQuery(query, me); - - const followingQuery = Followings.createQueryBuilder("following") - .select("following.followeeId") - .where("following.followerId = :followerId", { followerId: me.id }); - - query.andWhere(`user.id NOT IN (${followingQuery.getQuery()})`); - - query.setParameters(followingQuery.getParameters()); - - const users = await query.take(ps.limit).skip(ps.offset).getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts deleted file mode 100644 index 5580eaea0b..0000000000 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Users } from "@/models/index.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["users"], - - requireCredential: true, - - description: - "Show the different kinds of relations between the authenticated user and the specified user(s).", - - res: { - optional: false, - nullable: false, - oneOf: [ - { - type: "object", - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - isFollowing: { - type: "boolean", - optional: false, - nullable: false, - }, - hasPendingFollowRequestFromYou: { - type: "boolean", - optional: false, - nullable: false, - }, - hasPendingFollowRequestToYou: { - type: "boolean", - optional: false, - nullable: false, - }, - isFollowed: { - type: "boolean", - optional: false, - nullable: false, - }, - isBlocking: { - type: "boolean", - optional: false, - nullable: false, - }, - isBlocked: { - type: "boolean", - optional: false, - nullable: false, - }, - isMuted: { - type: "boolean", - optional: false, - nullable: false, - }, - isRenoteMuted: { - type: "boolean", - optional: false, - nullable: false, - }, - }, - }, - { - type: "array", - items: { - type: "object", - optional: false, - nullable: false, - properties: { - id: { - type: "string", - optional: false, - nullable: false, - format: "id", - }, - isFollowing: { - type: "boolean", - optional: false, - nullable: false, - }, - hasPendingFollowRequestFromYou: { - type: "boolean", - optional: false, - nullable: false, - }, - hasPendingFollowRequestToYou: { - type: "boolean", - optional: false, - nullable: false, - }, - isFollowed: { - type: "boolean", - optional: false, - nullable: false, - }, - isBlocking: { - type: "boolean", - optional: false, - nullable: false, - }, - isBlocked: { - type: "boolean", - optional: false, - nullable: false, - }, - isMuted: { - type: "boolean", - optional: false, - nullable: false, - }, - isRenoteMuted: { - type: "boolean", - optional: false, - nullable: false, - }, - }, - }, - }, - ], - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { - anyOf: [ - { type: "string", format: "misskey:id" }, - { - type: "array", - items: { type: "string", format: "misskey:id" }, - }, - ], - }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; - - const relations = await Promise.all( - ids.map((id) => Users.getRelation(me.id, id)), - ); - - return Array.isArray(ps.userId) ? relations : relations[0]; -}); diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts deleted file mode 100644 index 44d3f9b500..0000000000 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ /dev/null @@ -1,106 +0,0 @@ -import * as sanitizeHtml from "sanitize-html"; -import { publishAdminStream } from "@/services/stream.js"; -import { AbuseUserReports, Users } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { sendEmail } from "@/services/send-email.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { getUser } from "../../common/getters.js"; -import { ApiError } from "../../error.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["users"], - - requireCredential: true, - - description: "File a report.", - - 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", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - comment: { type: "string", minLength: 1, maxLength: 2048 }, - }, - required: ["userId", "comment"], -} as const; - -export default define(meta, paramDef, 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.insert({ - id: genId(), - createdAt: new Date(), - targetUserId: user.id, - targetUserHost: user.host, - reporterId: me.id, - reporterHost: null, - comment: ps.comment, - }).then((x) => AbuseUserReports.findOneByOrFail(x.identifiers[0])); - - // Publish event to moderators - setImmediate(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, - }); - } - - const meta = await fetchMeta(); - if (meta.email) { - sendEmail( - meta.email, - "New abuse report", - sanitizeHtml(ps.comment), - sanitizeHtml(ps.comment), - ); - } - }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts deleted file mode 100644 index 99aa2f1af3..0000000000 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { Brackets } from "typeorm"; -import { Followings, Users } from "@/models/index.js"; -import { USER_ACTIVE_THRESHOLD } from "@/const.js"; -import type { User } from "@/models/entities/user.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - description: "Search for a user by username and/or host.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "User", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - username: { type: "string", nullable: true }, - host: { type: "string", nullable: true }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - detail: { type: "boolean", default: true }, - }, - anyOf: [{ required: ["username"] }, { required: ["host"] }], -} as const; - -// TODO: avatar,bannerをJOINしたいけどエラーになる - -export default define(meta, paramDef, 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 }); - } - - return []; -}); diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts deleted file mode 100644 index db687a1075..0000000000 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Brackets } from "typeorm"; -import { UserProfiles, Users } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import define from "../../define.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - description: "Search for users.", - - res: { - type: "array", - optional: false, - nullable: false, - items: { - type: "object", - optional: false, - nullable: false, - ref: "User", - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - query: { type: "string" }, - offset: { type: "integer", default: 0 }, - limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }, - origin: { - type: "string", - enum: ["local", "remote", "combined"], - default: "combined", - }, - detail: { type: "boolean", default: true }, - }, - required: ["query"], -} as const; - -export default define(meta, paramDef, 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( - new Brackets((qb) => { - qb.where("user.name ILIKE :query", { query: `%${ps.query}%` }); - - // Also search username if it qualifies as username - if (Users.validateLocalUsername(ps.query)) { - qb.orWhere("user.usernameLower LIKE :username", { - username: `%${ps.query.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") { - nameQuery.andWhere("user.host IS NULL"); - } else if (ps.origin === "remote") { - nameQuery.andWhere("user.host IS NOT NULL"); - } - - users = await nameQuery - .orderBy("user.updatedAt", "DESC", "NULLS LAST") - .take(ps.limit) - .skip(ps.offset) - .getMany(); - - if (users.length < ps.limit) { - const profQuery = UserProfiles.createQueryBuilder("prof") - .select("prof.userId") - .where("prof.description ILIKE :query", { - query: `%${ps.query}%`, - }); - - if (ps.origin === "local") { - profQuery.andWhere("prof.userHost IS NULL"); - } else if (ps.origin === "remote") { - profQuery.andWhere("prof.userHost IS NOT NULL"); - } - - const query = Users.createQueryBuilder("user") - .where(`user.id IN (${profQuery.getQuery()})`) - .andWhere( - new Brackets((qb) => { - qb.where("user.updatedAt IS NULL").orWhere( - "user.updatedAt > :activeThreshold", - { activeThreshold: activeThreshold }, - ); - }), - ) - .andWhere("user.isSuspended = FALSE") - .setParameters(profQuery.getParameters()); - - users = users.concat( - await query - .orderBy("user.updatedAt", "DESC", "NULLS LAST") - .take(ps.limit) - .skip(ps.offset) - .getMany(), - ); - } - } - - return await Users.packMany(users, me, { detail: ps.detail }); -}); diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts deleted file mode 100644 index 49cac81fdb..0000000000 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { FindOptionsWhere } from "typeorm"; -import { In, IsNull } from "typeorm"; -import { resolveUser } from "@/remote/resolve-user.js"; -import { Users } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import define from "../../define.js"; -import { apiLogger } from "../../logger.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - description: "Show the properties of a user.", - - res: { - optional: false, - nullable: false, - oneOf: [ - { - type: "object", - ref: "UserDetailed", - }, - { - type: "array", - items: { - type: "object", - ref: "UserDetailed", - }, - }, - ], - }, - - errors: { - failedToResolveRemoteUser: { - message: "Failed to resolve remote user.", - code: "FAILED_TO_RESOLVE_REMOTE_USER", - id: "ef7b9be4-9cba-4e6f-ab41-90ed171c7d3c", - kind: "server", - }, - - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "4362f8dc-731f-4ad8-a694-be5a88922a24", - }, - }, -} as const; - -export const paramDef = { - type: "object", - anyOf: [ - { - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], - }, - { - properties: { - userIds: { - type: "array", - uniqueItems: true, - items: { - type: "string", - format: "misskey:id", - }, - }, - }, - required: ["userIds"], - }, - { - properties: { - username: { type: "string" }, - host: { - type: "string", - nullable: true, - description: "The local host is represented with `null`.", - }, - }, - required: ["username"], - }, - ], -} as const; - -export default define(meta, paramDef, 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.findBy( - 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: FindOptionsWhere = - ps.userId != null - ? { id: ps.userId } - : { usernameLower: ps.username!.toLowerCase(), host: IsNull() }; - - user = await Users.findOneBy(q); - } - - if (user == null || (!isAdminOrModerator && user.isSuspended)) { - throw new ApiError(meta.errors.noSuchUser); - } - - return await Users.pack(user, me, { - detail: true, - }); - } -}); diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts deleted file mode 100644 index 83e821f498..0000000000 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { - DriveFiles, - Followings, - NoteFavorites, - NoteReactions, - Notes, - PageLikes, - PollVotes, - Users, -} from "@/models/index.js"; -import { awaitAll } from "@/prelude/await-all.js"; -import define from "../../define.js"; -import { ApiError } from "../../error.js"; - -export const meta = { - tags: ["users"], - - requireCredential: false, - requireCredentialPrivateMode: true, - - description: "Show statistics about a user.", - - errors: { - noSuchUser: { - message: "No such user.", - code: "NO_SUCH_USER", - id: "9e638e45-3b25-4ef7-8f95-07e8498f1819", - }, - }, - - res: { - type: "object", - optional: false, - nullable: false, - properties: { - notesCount: { - type: "integer", - optional: false, - nullable: false, - }, - repliesCount: { - type: "integer", - optional: false, - nullable: false, - }, - renotesCount: { - type: "integer", - optional: false, - nullable: false, - }, - repliedCount: { - type: "integer", - optional: false, - nullable: false, - }, - renotedCount: { - type: "integer", - optional: false, - nullable: false, - }, - pollVotesCount: { - type: "integer", - optional: false, - nullable: false, - }, - pollVotedCount: { - type: "integer", - optional: false, - nullable: false, - }, - localFollowingCount: { - type: "integer", - optional: false, - nullable: false, - }, - remoteFollowingCount: { - type: "integer", - optional: false, - nullable: false, - }, - localFollowersCount: { - type: "integer", - optional: false, - nullable: false, - }, - remoteFollowersCount: { - type: "integer", - optional: false, - nullable: false, - }, - followingCount: { - type: "integer", - optional: false, - nullable: false, - }, - followersCount: { - type: "integer", - optional: false, - nullable: false, - }, - sentReactionsCount: { - type: "integer", - optional: false, - nullable: false, - }, - receivedReactionsCount: { - type: "integer", - optional: false, - nullable: false, - }, - noteFavoritesCount: { - type: "integer", - optional: false, - nullable: false, - }, - pageLikesCount: { - type: "integer", - optional: false, - nullable: false, - }, - pageLikedCount: { - type: "integer", - optional: false, - nullable: false, - }, - driveFilesCount: { - type: "integer", - optional: false, - nullable: false, - }, - driveUsage: { - type: "integer", - optional: false, - nullable: false, - description: "Drive usage in bytes", - }, - }, - }, -} as const; - -export const paramDef = { - type: "object", - properties: { - userId: { type: "string", format: "misskey:id" }, - }, - required: ["userId"], -} as const; - -export default define(meta, paramDef, async (ps, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - if (user == null) { - throw new ApiError(meta.errors.noSuchUser); - } - - const result = await awaitAll({ - notesCount: Notes.createQueryBuilder("note") - .where("note.userId = :userId", { userId: user.id }) - .getCount(), - repliesCount: Notes.createQueryBuilder("note") - .where("note.userId = :userId", { userId: user.id }) - .andWhere("note.replyId IS NOT NULL") - .getCount(), - renotesCount: Notes.createQueryBuilder("note") - .where("note.userId = :userId", { userId: user.id }) - .andWhere("note.renoteId IS NOT NULL") - .getCount(), - repliedCount: Notes.createQueryBuilder("note") - .where("note.replyUserId = :userId", { userId: user.id }) - .getCount(), - renotedCount: Notes.createQueryBuilder("note") - .where("note.renoteUserId = :userId", { userId: user.id }) - .getCount(), - pollVotesCount: PollVotes.createQueryBuilder("vote") - .where("vote.userId = :userId", { userId: user.id }) - .getCount(), - pollVotedCount: PollVotes.createQueryBuilder("vote") - .innerJoin("vote.note", "note") - .where("note.userId = :userId", { userId: user.id }) - .getCount(), - localFollowingCount: Followings.createQueryBuilder("following") - .where("following.followerId = :userId", { userId: user.id }) - .andWhere("following.followeeHost IS NULL") - .getCount(), - remoteFollowingCount: Followings.createQueryBuilder("following") - .where("following.followerId = :userId", { userId: user.id }) - .andWhere("following.followeeHost IS NOT NULL") - .getCount(), - localFollowersCount: Followings.createQueryBuilder("following") - .where("following.followeeId = :userId", { userId: user.id }) - .andWhere("following.followerHost IS NULL") - .getCount(), - remoteFollowersCount: Followings.createQueryBuilder("following") - .where("following.followeeId = :userId", { userId: user.id }) - .andWhere("following.followerHost IS NOT NULL") - .getCount(), - sentReactionsCount: NoteReactions.createQueryBuilder("reaction") - .where("reaction.userId = :userId", { userId: user.id }) - .getCount(), - receivedReactionsCount: NoteReactions.createQueryBuilder("reaction") - .innerJoin("reaction.note", "note") - .where("note.userId = :userId", { userId: user.id }) - .getCount(), - noteFavoritesCount: NoteFavorites.createQueryBuilder("favorite") - .where("favorite.userId = :userId", { userId: user.id }) - .getCount(), - pageLikesCount: PageLikes.createQueryBuilder("like") - .where("like.userId = :userId", { userId: user.id }) - .getCount(), - pageLikedCount: PageLikes.createQueryBuilder("like") - .innerJoin("like.page", "page") - .where("page.userId = :userId", { userId: user.id }) - .getCount(), - driveFilesCount: DriveFiles.createQueryBuilder("file") - .where("file.userId = :userId", { userId: user.id }) - .getCount(), - driveUsage: DriveFiles.calcDriveUsageOf(user), - }); - - result.followingCount = - result.localFollowingCount + result.remoteFollowingCount; - result.followersCount = - result.localFollowersCount + result.remoteFollowersCount; - - return result; -}); diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts deleted file mode 100644 index c58561a04e..0000000000 --- a/packages/backend/src/server/api/error.ts +++ /dev/null @@ -1,36 +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/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts deleted file mode 100644 index b8e4231655..0000000000 --- a/packages/backend/src/server/api/index.ts +++ /dev/null @@ -1,230 +0,0 @@ -/** - * API Server - */ - -import Koa from "koa"; -import Router from "@koa/router"; -import multer from "@koa/multer"; -import bodyParser from "koa-bodyparser"; -import cors from "@koa/cors"; -import { - apiMastodonCompatible, - getClient, -} from "./mastodon/ApiMastodonCompatibleService.js"; -import { Instances, AccessTokens, Users } from "@/models/index.js"; -import config from "@/config/index.js"; -import fs from "fs"; -import endpoints from "./endpoints.js"; -import compatibility from "./compatibility.js"; -import handler from "./api-handler.js"; -import signup from "./private/signup.js"; -import signin from "./private/signin.js"; -import signupPending from "./private/signup-pending.js"; -import discord from "./service/discord.js"; -import github from "./service/github.js"; -import twitter from "./service/twitter.js"; -import { koaBody } from "koa-body"; -import { - convertId, - IdConvertType as IdType, -} from "../../../native-utils/built/index.js"; - -// re-export native rust id conversion (function and enum) -export { IdType, convertId }; - -// 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(); -}); - -// Init router -const router = new Router(); -const mastoRouter = new Router(); -const mastoFileRouter = new Router(); -const errorRouter = new Router(); - -// Init multer instance -const upload = multer({ - storage: multer.diskStorage({}), - limits: { - fileSize: config.maxFileSize || 262144000, - files: 1, - }, -}); - -router.use( - bodyParser({ - // リクエストが multipart/form-data でない限りはJSONだと見なす - detectJSON: (ctx) => - !( - ctx.is("multipart/form-data") || - ctx.is("application/x-www-form-urlencoded") - ), - }), -); - -mastoRouter.use( - koaBody({ - multipart: true, - urlencoded: true, - }), -); - -mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - let multipartData = await ctx.file; - if (!multipartData) { - ctx.body = { error: "No image" }; - ctx.status = 401; - return; - } - const data = await client.uploadMedia(multipartData); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } -}); -mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - let multipartData = await ctx.file; - if (!multipartData) { - ctx.body = { error: "No image" }; - ctx.status = 401; - return; - } - const data = await client.uploadMedia(multipartData); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } -}); - -mastoRouter.use(async (ctx, next) => { - if (ctx.request.query) { - if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) { - ctx.request.body = ctx.request.query; - } else { - ctx.request.body = { ...ctx.request.body, ...ctx.request.query }; - } - } - await next(); -}); - -apiMastodonCompatible(mastoRouter); - -/** - * Register endpoint handlers - */ -for (const endpoint of [...endpoints, ...compatibility]) { - 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), - ); - - if (endpoint.meta.allowGet) { - router.get( - `/${endpoint.name.replace(/-/g, "_")}`, - handler.bind(null, endpoint), - ); - } else { - router.get(`/${endpoint.name.replace(/-/g, "_")}`, async (ctx) => { - ctx.status = 405; - }); - } - } - - router.post(`/${endpoint.name}`, handler.bind(null, endpoint)); - - if (endpoint.meta.allowGet) { - router.get(`/${endpoint.name}`, handler.bind(null, endpoint)); - } else { - router.get(`/${endpoint.name}`, async (ctx) => { - ctx.status = 405; - }); - } - } -} - -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"], - where: { - isSuspended: false, - }, - }); - - ctx.body = instances.map((instance) => instance.host); -}); - -router.post("/miauth/:session/check", async (ctx) => { - const token = await AccessTokens.findOneBy({ - session: ctx.params.session, - }); - - if (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 -errorRouter.all("(.*)", async (ctx) => { - ctx.status = 404; -}); - -// Register router -app.use(mastoFileRouter.routes()); -app.use(mastoRouter.routes()); -app.use(mastoRouter.allowedMethods()); -app.use(router.routes()); -app.use(errorRouter.routes()); - -export default app; diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts deleted file mode 100644 index dd005ad136..0000000000 --- a/packages/backend/src/server/api/limiter.ts +++ /dev/null @@ -1,85 +0,0 @@ -import Limiter from "ratelimiter"; -import { CacheableLocalUser, User } from "@/models/entities/user.js"; -import Logger from "@/services/logger.js"; -import { redisClient } from "../../db/redis.js"; -import type { IEndpointMeta } from "./endpoints.js"; - -const logger = new Logger("limiter"); - -export const limiter = ( - limitation: IEndpointMeta["limit"] & { key: NonNullable }, - actor: string, -) => - new Promise((ok, reject) => { - if (process.env.NODE_ENV === "test") ok(); - - const hasShortTermLimit = typeof limitation.minInterval === "number"; - - const hasLongTermLimit = - typeof limitation.duration === "number" && - typeof limitation.max === "number"; - - if (hasShortTermLimit) { - min(); - } else if (hasLongTermLimit) { - max(); - } else { - ok(); - } - - // Short-term limit - function min(): void { - const minIntervalLimiter = new Limiter({ - id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval, - max: 1, - db: redisClient, - }); - - minIntervalLimiter.get((err, info) => { - if (err) { - return reject("ERR"); - } - - logger.debug( - `${actor} ${limitation.key} min remaining: ${info.remaining}`, - ); - - if (info.remaining === 0) { - reject("BRIEF_REQUEST_INTERVAL"); - } else { - if (hasLongTermLimit) { - max(); - } else { - ok(); - } - } - }); - } - - // Long term limit - function max(): void { - const limiter = new Limiter({ - id: `${actor}:${limitation.key}`, - duration: limitation.duration, - max: limitation.max, - db: redisClient, - }); - - limiter.get((err, info) => { - if (err) { - return reject("ERR"); - } - - logger.debug( - `${actor} ${limitation.key} max remaining: ${info.remaining}`, - ); - - if (info.remaining === 0) { - reject("RATE_LIMIT_EXCEEDED"); - } else { - ok(); - } - }); - } - }); diff --git a/packages/backend/src/server/api/logger.ts b/packages/backend/src/server/api/logger.ts deleted file mode 100644 index 083888ed77..0000000000 --- a/packages/backend/src/server/api/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from "@/services/logger.js"; - -export const apiLogger = new Logger("api"); diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts deleted file mode 100644 index e8dfe52812..0000000000 --- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts +++ /dev/null @@ -1,140 +0,0 @@ -import Router from "@koa/router"; -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; -import { apiAuthMastodon } from "./endpoints/auth.js"; -import { apiAccountMastodon } from "./endpoints/account.js"; -import { apiStatusMastodon } from "./endpoints/status.js"; -import { apiFilterMastodon } from "./endpoints/filter.js"; -import { apiTimelineMastodon } from "./endpoints/timeline.js"; -import { apiNotificationsMastodon } from "./endpoints/notifications.js"; -import { apiSearchMastodon } from "./endpoints/search.js"; -import { getInstance } from "./endpoints/meta.js"; - -export function getClient( - BASE_URL: string, - authorization: string | undefined, -): MegalodonInterface { - const accessTokenArr = authorization?.split(" ") ?? [null]; - const accessToken = accessTokenArr[accessTokenArr.length - 1]; - const generator = (megalodon as any).default; - const client = generator( - "misskey", - BASE_URL, - accessToken, - ) as MegalodonInterface; - return client; -} - -export function apiMastodonCompatible(router: Router): void { - apiAuthMastodon(router); - apiAccountMastodon(router); - apiStatusMastodon(router); - apiFilterMastodon(router); - apiTimelineMastodon(router); - apiNotificationsMastodon(router); - apiSearchMastodon(router); - - router.get("/v1/custom_emojis", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getInstanceCustomEmojis(); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.get("/v1/instance", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt - // displayed without being logged in - try { - const data = await client.getInstance(); - ctx.body = await getInstance(data.data); - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.get("/v1/announcements", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getInstanceAnnouncements(); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.post<{ Params: { id: string } }>( - "/v1/announcements/:id/dismiss", - async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.dismissInstanceAnnouncement(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - - router.get("/v1/filters", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt - // displayed without being logged in - try { - const data = await client.getFilters(); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.get("/v1/trends", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt - // displayed without being logged in - try { - const data = await client.getInstanceTrends(); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.get("/v1/preferences", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt - // displayed without being logged in - try { - const data = await client.getPreferences(); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts deleted file mode 100644 index 70bdb74f34..0000000000 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ /dev/null @@ -1,545 +0,0 @@ -import { Users } from "@/models/index.js"; -import { resolveUser } from "@/remote/resolve-user.js"; -import Router from "@koa/router"; -import { FindOptionsWhere, IsNull } from "typeorm"; -import { getClient } from "../ApiMastodonCompatibleService.js"; -import { argsToBools, limitToInt } from "./timeline.js"; -import { convertId, IdType } from "../../index.js"; - -const relationshipModel = { - id: "", - following: false, - followed_by: false, - delivery_following: false, - blocking: false, - blocked_by: false, - muting: false, - muting_notifications: false, - requested: false, - domain_blocking: false, - showing_reblogs: false, - endorsed: false, - notifying: false, - note: "", -}; - -export function apiAccountMastodon(router: Router): void { - router.get("/v1/accounts/verify_credentials", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.verifyAccountCredentials(); - let acct = data.data; - acct.id = convertId(acct.id, IdType.MastodonId); - acct.display_name = acct.display_name || acct.username; - acct.url = `${BASE_URL}/@${acct.url}`; - acct.note = acct.note || ""; - acct.avatar_static = acct.avatar; - acct.header = acct.header || "https://http.cat/404"; - acct.header_static = acct.header || "https://http.cat/404"; - acct.source = { - note: acct.note, - fields: acct.fields, - privacy: "public", - sensitive: false, - language: "", - }; - console.log(acct); - ctx.body = acct; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.patch("/v1/accounts/update_credentials", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.updateCredentials( - (ctx.request as any).body as any, - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get("/v1/accounts/lookup", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.search( - (ctx.request.query as any).acct, - "accounts", - ); - let resp = data.data.accounts[0]; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const calcId = convertId(ctx.params.id, IdType.CalckeyId); - const data = await client.getAccount(calcId); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>( - "/v1/accounts/:id/statuses", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountStatuses( - convertId(ctx.params.id, IdType.CalckeyId), - argsToBools(limitToInt(ctx.query as any)), - ); - let resp = data.data; - for (let statIdx = 0; statIdx < resp.length; statIdx++) { - resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); - resp[statIdx].in_reply_to_account_id = resp[statIdx] - .in_reply_to_account_id - ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) - : null; - resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id - ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) - : null; - let mentions = resp[statIdx].mentions; - for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) { - resp[statIdx].mentions[mtnIdx].id = convertId( - mentions[mtnIdx].id, - IdType.MastodonId, - ); - } - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>( - "/v1/accounts/:id/followers", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountFollowers( - convertId(ctx.params.id, IdType.CalckeyId), - limitToInt(ctx.query as any), - ); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>( - "/v1/accounts/:id/following", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountFollowing( - convertId(ctx.params.id, IdType.CalckeyId), - limitToInt(ctx.query as any), - ); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>( - "/v1/accounts/:id/lists", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountLists(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/follow", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.followAccount( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let acct = data.data; - acct.following = true; - acct.id = convertId(acct.id, IdType.MastodonId); - ctx.body = acct; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/unfollow", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unfollowAccount( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let acct = data.data; - acct.id = convertId(acct.id, IdType.MastodonId); - acct.following = false; - ctx.body = acct; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/block", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.blockAccount( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/unblock", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unblockAccount( - convertId(ctx.params.id, IdType.MastodonId), - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/mute", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.muteAccount( - convertId(ctx.params.id, IdType.CalckeyId), - (ctx.request as any).body as any, - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/accounts/:id/unmute", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unmuteAccount( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get("/v1/accounts/relationships", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - let users; - try { - // TODO: this should be body - let ids = ctx.request.query ? ctx.request.query["id[]"] : null; - if (typeof ids === "string") { - ids = [ids]; - } - users = ids; - relationshipModel.id = ids?.toString() || "1"; - if (!ids) { - ctx.body = [relationshipModel]; - return; - } - - let reqIds = []; - for (let i = 0; i < ids.length; i++) { - reqIds.push(convertId(ids[i], IdType.CalckeyId)); - } - - const data = await client.getRelationships(reqIds); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - let data = e.response.data; - data.users = users; - console.error(data); - ctx.status = 401; - ctx.body = data; - } - }); - router.get("/v1/bookmarks", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = (await client.getBookmarks( - limitToInt(ctx.query as any), - )) as any; - let resp = data.data; - for (let statIdx = 0; statIdx < resp.length; statIdx++) { - resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); - resp[statIdx].in_reply_to_account_id = resp[statIdx] - .in_reply_to_account_id - ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) - : null; - resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id - ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) - : null; - let mentions = resp[statIdx].mentions; - for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) { - resp[statIdx].mentions[mtnIdx].id = convertId( - mentions[mtnIdx].id, - IdType.MastodonId, - ); - } - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get("/v1/favourites", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getFavourites(limitToInt(ctx.query as any)); - let resp = data.data; - for (let statIdx = 0; statIdx < resp.length; statIdx++) { - resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); - resp[statIdx].in_reply_to_account_id = resp[statIdx] - .in_reply_to_account_id - ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) - : null; - resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id - ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) - : null; - let mentions = resp[statIdx].mentions; - for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) { - resp[statIdx].mentions[mtnIdx].id = convertId( - mentions[mtnIdx].id, - IdType.MastodonId, - ); - } - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get("/v1/mutes", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getMutes(limitToInt(ctx.query as any)); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get("/v1/blocks", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getBlocks(limitToInt(ctx.query as any)); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get("/v1/follow_requests", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getFollowRequests( - ((ctx.query as any) || { limit: 20 }).limit, - ); - let resp = data.data; - for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { - resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); - } - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>( - "/v1/follow_requests/:id/authorize", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.acceptFollowRequest( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/follow_requests/:id/reject", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.rejectFollowRequest( - convertId(ctx.params.id, IdType.CalckeyId), - ); - let resp = data.data; - resp.id = convertId(resp.id, IdType.MastodonId); - ctx.body = resp; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/auth.ts b/packages/backend/src/server/api/mastodon/endpoints/auth.ts deleted file mode 100644 index e2cfc47aff..0000000000 --- a/packages/backend/src/server/api/mastodon/endpoints/auth.ts +++ /dev/null @@ -1,81 +0,0 @@ -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; -import Router from "@koa/router"; -import { koaBody } from "koa-body"; -import { getClient } from "../ApiMastodonCompatibleService.js"; -import bodyParser from "koa-bodyparser"; - -const readScope = [ - "read:account", - "read:drive", - "read:blocks", - "read:favorites", - "read:following", - "read:messaging", - "read:mutes", - "read:notifications", - "read:reactions", - "read:pages", - "read:page-likes", - "read:user-groups", - "read:channels", - "read:gallery", - "read:gallery-likes", -]; -const writeScope = [ - "write:account", - "write:drive", - "write:blocks", - "write:favorites", - "write:following", - "write:messaging", - "write:mutes", - "write:notes", - "write:notifications", - "write:reactions", - "write:votes", - "write:pages", - "write:page-likes", - "write:user-groups", - "write:channels", - "write:gallery", - "write:gallery-likes", -]; - -export function apiAuthMastodon(router: Router): void { - router.post("/v1/apps", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const client = getClient(BASE_URL, ""); - const body: any = ctx.request.body || ctx.request.query; - try { - let scope = body.scopes; - if (typeof scope === "string") scope = scope.split(" "); - const pushScope = new Set(); - for (const s of scope) { - if (s.match(/^read/)) for (const r of readScope) pushScope.add(r); - if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r); - } - const scopeArr = Array.from(pushScope); - - const red = body.redirect_uris; - const appData = await client.registerApp(body.client_name, { - scopes: scopeArr, - redirect_uris: red, - website: body.website, - }); - const returns = { - id: Math.floor(Math.random() * 100).toString(), - name: appData.name, - website: body.website, - redirect_uri: red, - client_id: Buffer.from(appData.url || "").toString("base64"), - client_secret: appData.clientSecret, - }; - console.log(returns); - ctx.body = returns; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts deleted file mode 100644 index d21bc1d330..0000000000 --- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts +++ /dev/null @@ -1,84 +0,0 @@ -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; -import Router from "@koa/router"; -import { getClient } from "../ApiMastodonCompatibleService.js"; - -export function apiFilterMastodon(router: Router): void { - router.get("/v1/filters", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const data = await client.getFilters(); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.get("/v1/filters/:id", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const data = await client.getFilter(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.post("/v1/filters", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const data = await client.createFilter(body.phrase, body.context, body); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.post("/v1/filters/:id", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const data = await client.updateFilter( - ctx.params.id, - body.phrase, - body.context, - ); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.delete("/v1/filters/:id", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const data = await client.deleteFilter(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts deleted file mode 100644 index d362d1b9e5..0000000000 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Entity } from "@calckey/megalodon"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Users, Notes } from "@/models/index.js"; -import { IsNull, MoreThan } from "typeorm"; - -// TODO: add calckey features -export async function getInstance(response: Entity.Instance) { - const meta = await fetchMeta(true); - const totalUsers = Users.count({ where: { host: IsNull() } }); - const totalStatuses = Notes.count({ where: { userHost: IsNull() } }); - return { - uri: response.uri, - title: response.title || "Calckey", - short_description: - response.description.substring(0, 50) || "See real server website", - description: - response.description || - "This is a vanilla Calckey Instance. It doesnt seem to have a description. BTW you are using the Mastodon api to access this server :)", - email: response.email || "", - version: "3.0.0 compatible (3.5+ Calckey)", //I hope this version string is correct, we will need to test it. - urls: response.urls, - stats: { - user_count: await totalUsers, - status_count: await totalStatuses, - domain_count: response.stats.domain_count, - }, - thumbnail: response.thumbnail || "https://http.cat/404", - languages: meta.langs, - registrations: !meta.disableRegistration || response.registrations, - approval_required: !response.registrations, - invites_enabled: response.registrations, - configuration: { - accounts: { - max_featured_tags: 20, - }, - statuses: { - max_characters: 3000, - max_media_attachments: 4, - characters_reserved_per_url: response.uri.length, - }, - media_attachments: { - supported_mime_types: [ - "image/jpeg", - "image/png", - "image/gif", - "image/heic", - "image/heif", - "image/webp", - "image/avif", - "video/webm", - "video/mp4", - "video/quicktime", - "video/ogg", - "audio/wave", - "audio/wav", - "audio/x-wav", - "audio/x-pn-wave", - "audio/vnd.wave", - "audio/ogg", - "audio/vorbis", - "audio/mpeg", - "audio/mp3", - "audio/webm", - "audio/flac", - "audio/aac", - "audio/m4a", - "audio/x-m4a", - "audio/mp4", - "audio/3gpp", - "video/x-ms-asf", - ], - image_size_limit: 10485760, - image_matrix_limit: 16777216, - video_size_limit: 41943040, - video_frame_rate_limit: 60, - video_matrix_limit: 2304000, - }, - polls: { - max_options: 8, - max_characters_per_option: 50, - min_expiration: 300, - max_expiration: 2629746, - }, - }, - contact_account: { - id: "1", - username: "admin", - acct: "admin", - display_name: "admin", - locked: true, - bot: true, - discoverable: false, - group: false, - created_at: new Date().toISOString(), - note: "

Please refer to the original instance for the actual admin contact.

", - url: `${response.uri}/`, - avatar: `${response.uri}/static-assets/badges/info.png`, - avatar_static: `${response.uri}/static-assets/badges/info.png`, - header: "https://http.cat/404", - header_static: "https://http.cat/404", - followers_count: -1, - following_count: 0, - statuses_count: 0, - last_status_at: new Date().toISOString(), - noindex: true, - emojis: [], - fields: [], - }, - rules: [], - }; -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts deleted file mode 100644 index 8508f1d486..0000000000 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ /dev/null @@ -1,90 +0,0 @@ -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; -import Router from "@koa/router"; -import { koaBody } from "koa-body"; -import { getClient } from "../ApiMastodonCompatibleService.js"; -import { toTextWithReaction } from "./timeline.js"; -function toLimitToInt(q: any) { - if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10); - return q; -} - -export function apiNotificationsMastodon(router: Router): void { - router.get("/v1/notifications", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const data = await client.getNotifications(toLimitToInt(ctx.query)); - const notfs = data.data; - const ret = notfs.map((n) => { - if (n.type !== "follow" && n.type !== "follow_request") { - if (n.type === "reaction") n.type = "favourite"; - n.status = toTextWithReaction( - n.status ? [n.status] : [], - ctx.hostname, - )[0]; - return n; - } else { - return n; - } - }); - ctx.body = ret; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.get("/v1/notification/:id", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const dataRaw = await client.getNotification(ctx.params.id); - const data = dataRaw.data; - if (data.type !== "follow" && data.type !== "follow_request") { - if (data.type === "reaction") data.type = "favourite"; - ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0]; - } else { - ctx.body = data; - } - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.post("/v1/notifications/clear", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const data = await client.dismissNotifications(); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - - router.post("/v1/notification/:id/dismiss", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const data = await client.dismissNotification(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts deleted file mode 100644 index e4990811ae..0000000000 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ /dev/null @@ -1,135 +0,0 @@ -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; -import Router from "@koa/router"; -import { getClient } from "../ApiMastodonCompatibleService.js"; -import axios from "axios"; -import { Converter } from "@calckey/megalodon"; -import { limitToInt } from "./timeline.js"; - -export function apiSearchMastodon(router: Router): void { - router.get("/v1/search", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.request.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const body: any = ctx.request.body; - try { - const query: any = limitToInt(ctx.query); - const type = query.type || ""; - const data = await client.search(query.q, type, query); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get("/v2/search", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const query: any = limitToInt(ctx.query); - const type = query.type; - if (type) { - const data = await client.search(query.q, type, query); - ctx.body = data.data; - } else { - const acct = await client.search(query.q, "accounts", query); - const stat = await client.search(query.q, "statuses", query); - const tags = await client.search(query.q, "hashtags", query); - ctx.body = { - accounts: acct.data.accounts, - statuses: stat.data.statuses, - hashtags: tags.data.hashtags, - }; - } - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get("/v1/trends/statuses", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.headers.authorization; - try { - const data = await getHighlight( - BASE_URL, - ctx.request.hostname, - accessTokens, - ); - ctx.body = data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get("/v2/suggestions", async (ctx) => { - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const accessTokens = ctx.headers.authorization; - try { - const query: any = ctx.query; - const data = await getFeaturedUser( - BASE_URL, - ctx.request.hostname, - accessTokens, - query.limit || 20, - ); - console.log(data); - ctx.body = data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); -} -async function getHighlight( - BASE_URL: string, - domain: string, - accessTokens: string | undefined, -) { - const accessTokenArr = accessTokens?.split(" ") ?? [null]; - const accessToken = accessTokenArr[accessTokenArr.length - 1]; - try { - const api = await axios.post(`${BASE_URL}/api/notes/featured`, { - i: accessToken, - }); - const data: MisskeyEntity.Note[] = api.data; - return data.map((note) => Converter.note(note, domain)); - } catch (e: any) { - console.log(e); - console.log(e.response.data); - return []; - } -} -async function getFeaturedUser( - BASE_URL: string, - host: string, - accessTokens: string | undefined, - limit: number, -) { - const accessTokenArr = accessTokens?.split(" ") ?? [null]; - const accessToken = accessTokenArr[accessTokenArr.length - 1]; - try { - const api = await axios.post(`${BASE_URL}/api/users`, { - i: accessToken, - limit, - origin: "local", - sort: "+follower", - state: "alive", - }); - const data: MisskeyEntity.UserDetail[] = api.data; - console.log(data); - return data.map((u) => { - return { - source: "past_interactions", - account: Converter.userDetail(u, host), - }; - }); - } catch (e: any) { - console.log(e); - console.log(e.response.data); - return []; - } -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts deleted file mode 100644 index fcfbd6aaaf..0000000000 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ /dev/null @@ -1,445 +0,0 @@ -import Router from "@koa/router"; -import { getClient } from "../ApiMastodonCompatibleService.js"; -import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js"; -import axios from "axios"; -import querystring from "node:querystring"; -import qs from "qs"; -import { limitToInt } from "./timeline.js"; - -function normalizeQuery(data: any) { - const str = querystring.stringify(data); - return qs.parse(str); -} - -export function apiStatusMastodon(router: Router): void { - router.post("/v1/statuses", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - let body: any = ctx.request.body; - if ( - (!body.poll && body["poll[options][]"]) || - (!body.media_ids && body["media_ids[]"]) - ) { - body = normalizeQuery(body); - } - const text = body.status; - const removed = text.replace(/@\S+/g, "").replace(/\s|​/g, ""); - const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed); - const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed); - if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) { - const a = await client.createEmojiReaction( - body.in_reply_to_id, - removed, - ); - ctx.body = a.data; - } - if (body.in_reply_to_id && removed === "/unreact") { - try { - const id = body.in_reply_to_id; - const post = await client.getStatus(id); - const react = post.data.emoji_reactions.filter((e) => e.me)[0].name; - const data = await client.deleteEmojiReaction(id, react); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - } - if (!body.media_ids) body.media_ids = undefined; - if (body.media_ids && !body.media_ids.length) body.media_ids = undefined; - const { sensitive } = body; - body.sensitive = - typeof sensitive === "string" ? sensitive === "true" : sensitive; - const data = await client.postStatus(text, body); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>("/v1/statuses/:id", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.delete<{ Params: { id: string } }>("/v1/statuses/:id", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.deleteStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e.response.data, request.params.id); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - interface IReaction { - id: string; - createdAt: string; - user: MisskeyEntity.User; - type: string; - } - router.get<{ Params: { id: string } }>( - "/v1/statuses/:id/context", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const id = ctx.params.id; - const data = await client.getStatusContext( - id, - limitToInt(ctx.query as any), - ); - const status = await client.getStatus(id); - let reqInstance = axios.create({ - headers: { - Authorization: ctx.headers.authorization, - }, - }); - const reactionsAxios = await reqInstance.get( - `${BASE_URL}/api/notes/reactions?noteId=${id}`, - ); - const reactions: IReaction[] = reactionsAxios.data; - const text = reactions - .map((r) => `${r.type.replace("@.", "")} ${r.user.username}`) - .join("
"); - data.data.descendants.unshift( - statusModel( - status.data.id, - status.data.account.id, - status.data.emojis, - text, - ), - ); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>( - "/v1/statuses/:id/reblogged_by", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getStatusRebloggedBy(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>( - "/v1/statuses/:id/favourited_by", - async (ctx) => { - ctx.body = []; - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/statuses/:id/favourite", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const react = await getFirstReaction(BASE_URL, accessTokens); - try { - const a = (await client.createEmojiReaction( - ctx.params.id, - react, - )) as any; - //const data = await client.favouriteStatus(ctx.params.id) as any; - ctx.body = a.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/statuses/:id/unfavourite", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - const react = await getFirstReaction(BASE_URL, accessTokens); - try { - const data = await client.deleteEmojiReaction(ctx.params.id, react); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - - router.post<{ Params: { id: string } }>( - "/v1/statuses/:id/reblog", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.reblogStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - - router.post<{ Params: { id: string } }>( - "/v1/statuses/:id/unreblog", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unreblogStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - - router.post<{ Params: { id: string } }>( - "/v1/statuses/:id/bookmark", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.bookmarkStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - - router.post<{ Params: { id: string } }>( - "/v1/statuses/:id/unbookmark", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = (await client.unbookmarkStatus(ctx.params.id)) as any; - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - - router.post<{ Params: { id: string } }>( - "/v1/statuses/:id/pin", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.pinStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - - router.post<{ Params: { id: string } }>( - "/v1/statuses/:id/unpin", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.unpinStatus(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getMedia(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.put<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.updateMedia( - ctx.params.id, - ctx.request.body as any, - ); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>("/v1/polls/:id", async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getPoll(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.post<{ Params: { id: string } }>( - "/v1/polls/:id/votes", - async (ctx) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.votePoll( - ctx.params.id, - (ctx.request.body as any).choices, - ); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); -} - -async function getFirstReaction( - BASE_URL: string, - accessTokens: string | undefined, -) { - const accessTokenArr = accessTokens?.split(" ") ?? [null]; - const accessToken = accessTokenArr[accessTokenArr.length - 1]; - let react = "⭐"; - try { - const api = await axios.post(`${BASE_URL}/api/i/registry/get-unsecure`, { - scope: ["client", "base"], - key: "reactions", - i: accessToken, - }); - const reactRaw = api.data; - react = Array.isArray(reactRaw) ? api.data[0] : "⭐"; - console.log(api.data); - return react; - } catch (e) { - return react; - } -} - -export function statusModel( - id: string | null, - acctId: string | null, - emojis: MastodonEntity.Emoji[], - content: string, -) { - const now = Math.floor(new Date().getTime() / 1000); - return { - id: "9atm5frjhb", - uri: "https://http.cat/404", // "" - url: "https://http.cat/404", // "", - account: { - id: "9arzuvv0sw", - username: "Reactions", - acct: "Reactions", - display_name: "Reactions to this post", - locked: false, - created_at: now, - followers_count: 0, - following_count: 0, - statuses_count: 0, - note: "", - url: "https://http.cat/404", - avatar: "/static-assets/badges/info.png", - avatar_static: "/static-assets/badges/info.png", - header: "https://http.cat/404", // "" - header_static: "https://http.cat/404", // "" - emojis: [], - fields: [], - moved: null, - bot: false, - }, - in_reply_to_id: id, - in_reply_to_account_id: acctId, - reblog: null, - content: `

${content}

`, - plain_content: null, - created_at: now, - emojis: emojis, - replies_count: 0, - reblogs_count: 0, - favourites_count: 0, - favourited: false, - reblogged: false, - muted: false, - sensitive: false, - spoiler_text: "", - visibility: "public" as const, - media_attachments: [], - mentions: [], - tags: [], - card: null, - poll: null, - application: null, - language: null, - pinned: false, - emoji_reactions: [], - bookmarked: false, - quote: null, - }; -} diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts deleted file mode 100644 index ce3a4dc958..0000000000 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ /dev/null @@ -1,336 +0,0 @@ -import Router from "@koa/router"; -import megalodon, { Entity, MegalodonInterface } from "@calckey/megalodon"; -import { getClient } from "../ApiMastodonCompatibleService.js"; -import { statusModel } from "./status.js"; -import Autolinker from "autolinker"; -import { ParsedUrlQuery } from "querystring"; - -export function limitToInt(q: ParsedUrlQuery) { - let object: any = q; - if (q.limit) - if (typeof q.limit === "string") object.limit = parseInt(q.limit, 10); - if (q.offset) - if (typeof q.offset === "string") object.offset = parseInt(q.offset, 10); - return object; -} - -export function argsToBools(q: ParsedUrlQuery) { - // Values taken from https://docs.joinmastodon.org/client/intro/#boolean - const toBoolean = (value: string) => - !["0", "f", "F", "false", "FALSE", "off", "OFF"].includes(value); - - let object: any = q; - if (q.only_media) - if (typeof q.only_media === "string") - object.only_media = toBoolean(q.only_media); - if (q.exclude_replies) - if (typeof q.exclude_replies === "string") - object.exclude_replies = toBoolean(q.exclude_replies); - return q; -} - -export function toTextWithReaction(status: Entity.Status[], host: string) { - return status.map((t) => { - if (!t) return statusModel(null, null, [], "no content"); - t.quote = null as any; - if (!t.emoji_reactions) return t; - if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0]; - const reactions = t.emoji_reactions.map((r) => { - const emojiNotation = r.url ? `:${r.name.replace("@.", "")}:` : r.name; - return `${emojiNotation} (${r.count}${r.me ? `* ` : ""})`; - }); - const reaction = t.emoji_reactions as Entity.Reaction[]; - const emoji = t.emojis || []; - for (const r of reaction) { - if (!r.url) continue; - emoji.push({ - shortcode: r.name, - url: r.url, - static_url: r.url, - visible_in_picker: true, - category: "", - }); - } - const isMe = reaction.findIndex((r) => r.me) > -1; - const total = reaction.reduce((sum, reaction) => sum + reaction.count, 0); - t.favourited = isMe; - t.favourites_count = total; - t.emojis = emoji; - t.content = `

${autoLinker(t.content, host)}

${reactions.join( - ", ", - )}

`; - return t; - }); -} -export function autoLinker(input: string, host: string) { - return Autolinker.link(input, { - hashtag: "twitter", - mention: "twitter", - email: false, - stripPrefix: false, - replaceFn: function (match) { - switch (match.type) { - case "url": - return true; - case "mention": - console.log("Mention: ", match.getMention()); - console.log("Mention Service Name: ", match.getServiceName()); - return `@${match.getMention()}`; - case "hashtag": - console.log("Hashtag: ", match.getHashtag()); - return `#${match.getHashtag()}`; - } - return false; - }, - }); -} - -export function apiTimelineMastodon(router: Router): void { - router.get("/v1/timelines/public", async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const query: any = ctx.query; - const data = query.local - ? await client.getLocalTimeline(argsToBools(limitToInt(query))) - : await client.getPublicTimeline(argsToBools(limitToInt(query))); - ctx.body = toTextWithReaction(data.data, ctx.hostname); - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get<{ Params: { hashtag: string } }>( - "/v1/timelines/tag/:hashtag", - async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getTagTimeline( - ctx.params.hashtag, - argsToBools(limitToInt(ctx.query)), - ); - ctx.body = toTextWithReaction(data.data, ctx.hostname); - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get("/v1/timelines/home", async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getHomeTimeline(limitToInt(ctx.query)); - ctx.body = toTextWithReaction(data.data, ctx.hostname); - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get<{ Params: { listId: string } }>( - "/v1/timelines/list/:listId", - async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getListTimeline( - ctx.params.listId, - limitToInt(ctx.query), - ); - ctx.body = toTextWithReaction(data.data, ctx.hostname); - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get("/v1/conversations", async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getConversationTimeline(limitToInt(ctx.query)); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get("/v1/lists", async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getLists(); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.get<{ Params: { id: string } }>( - "/v1/lists/:id", - async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getList(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post("/v1/lists", async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.createList((ctx.query as any).title); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }); - router.put<{ Params: { id: string } }>( - "/v1/lists/:id", - async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.updateList(ctx.params.id, ctx.query as any); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.delete<{ Params: { id: string } }>( - "/v1/lists/:id", - async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.deleteList(ctx.params.id); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.get<{ Params: { id: string } }>( - "/v1/lists/:id/accounts", - async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.getAccountsInList( - ctx.params.id, - ctx.query as any, - ); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.post<{ Params: { id: string } }>( - "/v1/lists/:id/accounts", - async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.addAccountsToList( - ctx.params.id, - (ctx.query as any).account_ids, - ); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); - router.delete<{ Params: { id: string } }>( - "/v1/lists/:id/accounts", - async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); - try { - const data = await client.deleteAccountsFromList( - ctx.params.id, - (ctx.query as any).account_ids, - ); - ctx.body = data.data; - } catch (e: any) { - console.error(e); - console.error(e.response.data); - ctx.status = 401; - ctx.body = e.response.data; - } - }, - ); -} -function escapeHTML(str: string) { - if (!str) { - return ""; - } - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} -function nl2br(str: string) { - if (!str) { - return ""; - } - str = str.replace(/\r\n/g, "
"); - str = str.replace(/(\n|\r)/g, "
"); - return str; -} diff --git a/packages/backend/src/server/api/openapi/errors.ts b/packages/backend/src/server/api/openapi/errors.ts deleted file mode 100644 index 0fe229d88e..0000000000 --- a/packages/backend/src/server/api/openapi/errors.ts +++ /dev/null @@ -1,71 +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_CALC: { - value: { - error: { - message: - "You sent a request to Calc, Calckey's resident stoner furry, instead of the server.", - code: "I_AM_CALC", - id: "60c46cd1-f23a-46b1-bebe-5d2b73951a84", - }, - }, - }, - }, - "429": { - RATE_LIMIT_EXCEEDED: { - value: { - error: { - message: "Rate limit exceeded. Please try again later.", - code: "RATE_LIMIT_EXCEEDED", - id: "d5826d14-3982-4d2e-8011-b9e9f02499ef", - }, - }, - }, - }, - "500": { - INTERNAL_ERROR: { - value: { - error: { - message: - "Internal error occurred. Please contact us if the error persists.", - code: "INTERNAL_ERROR", - id: "5d37dbcb-891e-41ca-a3d6-e690c97775ac", - }, - }, - }, - }, -}; diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts deleted file mode 100644 index dfaacf9e50..0000000000 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ /dev/null @@ -1,226 +0,0 @@ -import endpoints from "../endpoints.js"; -import config from "@/config/index.js"; -import { errors as basicErrors } from "./errors.js"; -import { schemas, convertSchemaToOpenApiSchema } from "./schemas.js"; - -export function genOpenapiSpec() { - const spec = { - openapi: "3.0.0", - - info: { - version: "v1", - title: "Calckey API", - "x-logo": { url: "/static-assets/api-doc.png" }, - }, - - externalDocs: { - description: "Repository", - url: "https://codeberg.org/calckey/calckey", - }, - - servers: [ - { - url: config.apiUrl, - }, - ], - - paths: {} as any, - - components: { - schemas: schemas, - - securitySchemes: { - ApiKeyAuth: { - type: "apiKey", - in: "body", - name: "i", - }, - // TODO: change this to oauth2 when the remaining oauth stuff is set up - Bearer: { - type: "http", - scheme: "bearer", - }, - }, - }, - }; - - for (const endpoint of endpoints.filter((ep) => !ep.meta.secure)) { - const errors = {} as any; - - if (endpoint.meta.errors) { - for (const e of Object.values(endpoint.meta.errors)) { - errors[e.code] = { - value: { - error: e, - }, - }; - } - } - - const resSchema = endpoint.meta.res - ? convertSchemaToOpenApiSchema(endpoint.meta.res) - : {}; - - let desc = - (endpoint.meta.description - ? endpoint.meta.description - : "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 requestType = endpoint.meta.requireFile - ? "multipart/form-data" - : "application/json"; - const schema = endpoint.params; - - if (endpoint.meta.requireFile) { - schema.properties.file = { - type: "string", - format: "binary", - description: "The file contents.", - }; - schema.required.push("file"); - } - - const security = [ - { - ApiKeyAuth: [], - }, - { - Bearer: [], - }, - ]; - if (!endpoint.meta.requireCredential) { - // add this to make authentication optional - security.push({}); - } - - const info = { - operationId: endpoint.name, - summary: endpoint.name, - description: desc, - externalDocs: { - description: "Source code", - url: `https://codeberg.org/calckey/calckey/src/branch/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`, - }, - tags: endpoint.meta.tags || undefined, - security, - requestBody: { - required: true, - content: { - [requestType]: { - schema, - }, - }, - }, - 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 Calc", - 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"], - }, - }, - }, - }, - }; - - const path = { - post: info, - }; - if (endpoint.meta.allowGet) { - path.get = { ...info }; - // API Key authentication is not permitted for GET requests - path.get.security = path.get.security.filter( - (elem) => !Object.prototype.hasOwnProperty.call(elem, "ApiKeyAuth"), - ); - } - - spec.paths[`/${endpoint.name}`] = path; - } - - return spec; -} diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts deleted file mode 100644 index 68b15d5677..0000000000 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { Schema } from "@/misc/schema.js"; -import { refs } from "@/misc/schema.js"; - -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.anyOf) res.anyOf = schema.anyOf.map(convertSchemaToOpenApiSchema); - if (schema.oneOf) res.oneOf = schema.oneOf.map(convertSchemaToOpenApiSchema); - if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema); - - if (schema.ref) { - res.$ref = `#/components/schemas/${schema.ref}`; - } - - return res; -} - -export const schemas = { - Error: { - type: "object", - properties: { - error: { - type: "object", - description: "An error object.", - properties: { - code: { - type: "string", - description: "An error code. Unique within the endpoint.", - }, - message: { - type: "string", - description: "An error message.", - }, - id: { - type: "string", - format: "uuid", - description: "An error ID. This ID is static.", - }, - }, - required: ["code", "id", "message"], - }, - }, - required: ["error"], - }, - - ...Object.fromEntries( - Object.entries(refs).map(([key, schema]) => [ - key, - convertSchemaToOpenApiSchema(schema), - ]), - ), -}; diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts deleted file mode 100644 index ef5b137813..0000000000 --- a/packages/backend/src/server/api/private/signin.ts +++ /dev/null @@ -1,270 +0,0 @@ -import type Koa from "koa"; -import * as speakeasy from "speakeasy"; -import signin from "../common/signin.js"; -import config from "@/config/index.js"; -import { - Users, - Signins, - UserProfiles, - UserSecurityKeys, - AttestationChallenges, -} from "@/models/index.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import { genId } from "@/misc/gen-id.js"; -import { - comparePassword, - hashPassword, - isOldAlgorithm, -} from "@/misc/password.js"; -import { verifyLogin, hash } from "../2fa.js"; -import { randomBytes } from "node:crypto"; -import { IsNull } from "typeorm"; -import { limiter } from "../limiter.js"; -import { getIpHash } from "@/misc/get-ip-hash.js"; - -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 }; - } - - try { - // not more than 1 attempt per second and not more than 10 attempts per hour - await limiter( - { key: "signin", duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, - getIpHash(ctx.ip), - ); - } catch (err) { - ctx.status = 429; - ctx.body = { - error: { - message: "Too many failed attempts to sign in. Try again later.", - code: "TOO_MANY_AUTHENTICATION_FAILURES", - id: "22d05606-fbcf-421a-a2db-b32610dcfd1b", - }, - }; - return; - } - - 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.findOneBy({ - usernameLower: username.toLowerCase(), - host: IsNull(), - })) 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.findOneByOrFail({ userId: user.id }); - - // Compare password - const same = await comparePassword(password, profile.password!); - - if (same && isOldAlgorithm(profile.password!)) { - profile.password = await hashPassword(password); - await UserProfiles.save(profile); - } - - 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.findOneBy({ - 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.findOneBy({ - 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.findBy({ - userId: user.id, - }); - - if (keys.length === 0) { - await fail(403, { - id: "f27fd449-9af4-4841-9249-1f989b9fa4a4", - }); - return; - } - - // 32 byte challenge - const challenge = randomBytes(32) - .toString("base64") - .replace(/=/g, "") - .replace(/\+/g, "-") - .replace(/\//g, "_"); - - const challengeId = genId(); - - await AttestationChallenges.insert({ - userId: user.id, - id: challengeId, - challenge: hash(Buffer.from(challenge, "utf-8")).toString("hex"), - createdAt: new Date(), - registrationChallenge: false, - }); - - ctx.body = { - challenge, - challengeId, - securityKeys: keys.map((key) => ({ - id: key.id, - })), - }; - ctx.status = 200; - return; - } - // never get here -}; diff --git a/packages/backend/src/server/api/private/signup-pending.ts b/packages/backend/src/server/api/private/signup-pending.ts deleted file mode 100644 index c7fdcea221..0000000000 --- a/packages/backend/src/server/api/private/signup-pending.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type Koa from "koa"; -import { Users, UserPendings, UserProfiles } from "@/models/index.js"; -import { signup } from "../common/signup.js"; -import signin from "../common/signin.js"; - -export default async (ctx: Koa.Context) => { - const body = ctx.request.body; - - const code = body["code"]; - - try { - const pendingUser = await UserPendings.findOneByOrFail({ code }); - - const { account, secret } = await signup({ - username: pendingUser.username, - passwordHash: pendingUser.password, - }); - - UserPendings.delete({ - id: pendingUser.id, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: account.id }); - - await UserProfiles.update( - { userId: profile.userId }, - { - email: pendingUser.email, - emailVerified: true, - emailVerifyCode: null, - }, - ); - - signin(ctx, account); - } catch (e) { - ctx.throw(400, e); - } -}; diff --git a/packages/backend/src/server/api/private/signup.ts b/packages/backend/src/server/api/private/signup.ts deleted file mode 100644 index 754d86c3b8..0000000000 --- a/packages/backend/src/server/api/private/signup.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type Koa from "koa"; -import rndstr from "rndstr"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { verifyHcaptcha, verifyRecaptcha } from "@/misc/captcha.js"; -import { Users, RegistrationTickets, UserPendings } from "@/models/index.js"; -import { signup } from "../common/signup.js"; -import config from "@/config/index.js"; -import { sendEmail } from "@/services/send-email.js"; -import { genId } from "@/misc/gen-id.js"; -import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; -import { hashPassword } from "@/misc/password.js"; - -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.findOneBy({ - 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 hash = await hashPassword(password); - - 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/packages/backend/src/server/api/service/discord.ts b/packages/backend/src/server/api/service/discord.ts deleted file mode 100644 index 9906d2f7ca..0000000000 --- a/packages/backend/src/server/api/service/discord.ts +++ /dev/null @@ -1,333 +0,0 @@ -import type Koa from "koa"; -import Router from "@koa/router"; -import { OAuth2 } from "oauth"; -import { v4 as uuid } from "uuid"; -import { IsNull } from "typeorm"; -import { getJson } from "@/misc/fetch.js"; -import config from "@/config/index.js"; -import { publishMainStream } from "@/services/stream.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Users, UserProfiles } from "@/models/index.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import { redisClient } from "../../../db/redis.js"; -import signin from "../common/signin.js"; - -function getUserToken(ctx: Koa.BaseContext): string | null { - return ((ctx.headers["cookie"] || "").match(/igi=(\w+)/) || [null, null])[1]; -} - -function compareOrigin(ctx: Koa.BaseContext): boolean { - function normalizeUrl(url?: string): 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.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - profile.integrations.discord = undefined; - - 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 || typeof code !== "string") { - 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}`, - }, - )) as Record; - - if ( - typeof id !== "string" || - typeof username !== "string" || - typeof discriminator !== "string" - ) { - 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.findOneBy({ id: profile.userId })) as ILocalUser, - true, - ); - } else { - const code = ctx.query.code; - - if (!code || typeof code !== "string") { - 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}`, - }, - )) as Record; - if ( - typeof id !== "string" || - typeof username !== "string" || - typeof discriminator !== "string" - ) { - ctx.throw(400, "invalid session"); - return; - } - - const user = await Users.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - await UserProfiles.update(user.id, { - integrations: { - ...profile.integrations, - discord: { - accessToken: accessToken, - refreshToken: refreshToken, - expiresDate: expiresDate, - id: id, - username: username, - discriminator: discriminator, - }, - }, - }); - - ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream( - user.id, - "meUpdated", - await Users.pack(user, user, { - detail: true, - includeSecrets: true, - }), - ); - } -}); - -export default router; diff --git a/packages/backend/src/server/api/service/github.ts b/packages/backend/src/server/api/service/github.ts deleted file mode 100644 index f77c5f795d..0000000000 --- a/packages/backend/src/server/api/service/github.ts +++ /dev/null @@ -1,296 +0,0 @@ -import type Koa from "koa"; -import Router from "@koa/router"; -import { OAuth2 } from "oauth"; -import { v4 as uuid } from "uuid"; -import { IsNull } from "typeorm"; -import { getJson } from "@/misc/fetch.js"; -import config from "@/config/index.js"; -import { publishMainStream } from "@/services/stream.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Users, UserProfiles } from "@/models/index.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import { redisClient } from "../../../db/redis.js"; -import signin from "../common/signin.js"; - -function getUserToken(ctx: Koa.BaseContext): string | null { - return ((ctx.headers["cookie"] || "").match(/igi=(\w+)/) || [null, null])[1]; -} - -function compareOrigin(ctx: Koa.BaseContext): boolean { - function normalizeUrl(url?: string): 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.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - profile.integrations.github = undefined; - - 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 || typeof code !== "string") { - 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}`, - }, - )) as Record; - if (typeof login !== "string" || typeof id !== "string") { - 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.findOneBy({ id: link.userId })) as ILocalUser, - true, - ); - } else { - const code = ctx.query.code; - - if (!code || typeof code !== "string") { - 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}`, - }, - )) as Record; - - if (typeof login !== "string" || typeof id !== "string") { - ctx.throw(400, "invalid session"); - return; - } - - const user = await Users.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - await UserProfiles.update(user.id, { - integrations: { - ...profile.integrations, - github: { - accessToken: accessToken, - id: id, - login: login, - }, - }, - }); - - ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream( - user.id, - "meUpdated", - await Users.pack(user, user, { - detail: true, - includeSecrets: true, - }), - ); - } -}); - -export default router; diff --git a/packages/backend/src/server/api/service/twitter.ts b/packages/backend/src/server/api/service/twitter.ts deleted file mode 100644 index 3695592410..0000000000 --- a/packages/backend/src/server/api/service/twitter.ts +++ /dev/null @@ -1,226 +0,0 @@ -import type Koa from "koa"; -import Router from "@koa/router"; -import { v4 as uuid } from "uuid"; -import autwh from "autwh"; -import { IsNull } from "typeorm"; -import { publishMainStream } from "@/services/stream.js"; -import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Users, UserProfiles } from "@/models/index.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import signin from "../common/signin.js"; -import { redisClient } from "../../../db/redis.js"; - -function getUserToken(ctx: Koa.BaseContext): string | null { - return ((ctx.headers["cookie"] || "").match(/igi=(\w+)/) || [null, null])[1]; -} - -function compareOrigin(ctx: Koa.BaseContext): boolean { - function normalizeUrl(url?: string): string { - return url == null - ? "" - : 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.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - profile.integrations.twitter = undefined; - - 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 verifier = ctx.query.oauth_verifier; - if (!verifier || typeof verifier !== "string") { - ctx.throw(400, "invalid session"); - return; - } - - const result = await twAuth!.done(JSON.parse(twCtx), 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.findOneBy({ id: link.userId })) as ILocalUser, - true, - ); - } else { - const verifier = ctx.query.oauth_verifier; - - if (!verifier || typeof verifier !== "string") { - 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.findOneByOrFail({ - host: IsNull(), - token: userToken, - }); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - await UserProfiles.update(user.id, { - integrations: { - ...profile.integrations, - twitter: { - accessToken: result.accessToken, - accessTokenSecret: result.accessTokenSecret, - userId: result.userId, - screenName: result.screenName, - }, - }, - }); - - ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream( - user.id, - "meUpdated", - await Users.pack(user, user, { - detail: true, - includeSecrets: true, - }), - ); - } -}); - -export default router; diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts deleted file mode 100644 index fc8e0ce35e..0000000000 --- a/packages/backend/src/server/api/stream/channel.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type Connection from "."; -import type { Note } from "@/models/entities/note.js"; -import { Notes } from "@/models/index.js"; -import type { Packed } from "@/misc/schema.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; - -/** - * 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 renoteMuting() { - return this.connection.renoteMuting; - } - - 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; - } - - 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, - }); - } - - protected withPackedNote( - callback: (note: Packed<"Note">) => void, - ): (Note) => void { - return async (note: Note) => { - try { - // because `note` was previously JSON.stringify'ed, the fields that - // were objects before are now strings and have to be restored or - // removed from the object - note.createdAt = new Date(note.createdAt); - note.reply = undefined; - note.renote = undefined; - note.user = undefined; - note.channel = undefined; - - const packed = await Notes.pack(note, this.user, { detail: true }); - - callback(packed); - } catch (err) { - if ( - err instanceof IdentifiableError && - err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24" - ) { - // skip: note not visible to user - return; - } else { - throw err; - } - } - }; - } - - public abstract init(params: any): void; - public dispose?(): void; - public onMessage?(type: string, body: any): void; -} diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts deleted file mode 100644 index 59ae228250..0000000000 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Channel from "../channel.js"; - -export default class extends Channel { - public readonly chName = "admin"; - public static shouldShare = true; - public static requireCredential = true; - - public async init(params: any) { - // Subscribe admin stream - this.subscriber.on(`adminStream:${this.user!.id}`, (data) => { - this.send(data); - }); - } -} diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts deleted file mode 100644 index 050a8d1019..0000000000 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ /dev/null @@ -1,63 +0,0 @@ -import Channel from "../channel.js"; -import { Notes } from "@/models/index.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import type { StreamMessages } from "../types.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; - -export default class extends Channel { - public readonly chName = "antenna"; - public static shouldShare = false; - public static requireCredential = false; - private antennaId: string; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onEvent = this.onEvent.bind(this); - } - - public async init(params: any) { - this.antennaId = params.antennaId as string; - - // Subscribe stream - this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); - } - - private async onEvent(data: StreamMessages["antenna"]["payload"]) { - if (data.type === "note") { - try { - const note = await Notes.pack(data.body.id, this.user, { - detail: true, - }); - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) - return; - - this.connection.cacheNote(note); - - this.send("note", note); - } catch (e) { - if ( - e instanceof IdentifiableError && - e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24" - ) { - // skip: note not visible to user - return; - } else { - throw e; - } - } - } else { - this.send(data.type, data.body); - } - } - - public dispose() { - // Unsubscribe events - this.subscriber.off(`antennaStream:${this.antennaId}`, this.onEvent); - } -} diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts deleted file mode 100644 index d046579f42..0000000000 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ /dev/null @@ -1,84 +0,0 @@ -import Channel from "../channel.js"; -import { Users } from "@/models/index.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import type { User } from "@/models/entities/user.js"; -import type { StreamMessages } from "../types.js"; -import type { Packed } from "@/misc/schema.js"; - -export default class extends Channel { - public readonly chName = "channel"; - public static shouldShare = false; - public static requireCredential = false; - private channelId: string; - private typers: Map = new Map(); - private emitTypersIntervalId: ReturnType; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onNote = this.onNote.bind(this); - this.emitTypers = this.emitTypers.bind(this); - } - - 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); - } - - private async onNote(note: Packed<"Note">) { - if (note.channelId !== this.channelId) return; - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) - return; - - this.connection.cacheNote(note); - - this.send("note", note); - } - - private onEvent(data: StreamMessages["channel"]["payload"]) { - if (data.type === "typing") { - const id = data.body; - const begin = !this.typers.has(id); - this.typers.set(id, new Date()); - if (begin) { - this.emitTypers(); - } - } - } - - 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) this.typers.delete(userId); - } - - const userIds = Array.from(this.typers.keys()); - const users = await Users.packMany(userIds, null, { - detail: false, - }); - - this.send({ - type: "typers", - body: users, - }); - } - - public dispose() { - // Unsubscribe events - this.subscriber.off("notesStream", this.onNote); - this.subscriber.off(`channelStream:${this.channelId}`, this.onEvent); - - clearInterval(this.emitTypersIntervalId); - } -} diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts deleted file mode 100644 index 275730eae5..0000000000 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Channel from "../channel.js"; - -export default class extends Channel { - public readonly chName = "drive"; - public static shouldShare = true; - public static requireCredential = true; - - public async init(params: any) { - // Subscribe drive stream - this.subscriber.on(`driveStream:${this.user!.id}`, (data) => { - this.send(data); - }); - } -} diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts deleted file mode 100644 index aa3844c7e3..0000000000 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ /dev/null @@ -1,82 +0,0 @@ -import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { getWordMute } from "@/misc/check-word-mute.js"; -import { isInstanceMuted } from "@/misc/is-instance-muted.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import type { Packed } from "@/misc/schema.js"; - -export default class extends Channel { - public readonly chName = "globalTimeline"; - public static shouldShare = true; - public static requireCredential = false; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); - } - - 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); - } - - private async onNote(note: Packed<"Note">) { - if (note.visibility !== "public") return; - if (note.channelId != null) return; - - // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if ( - reply.userId !== this.user!.id && - note.userId !== this.user!.id && - reply.userId !== note.userId - ) - return; - } - - // Ignore notes from instances the user has muted - if ( - isInstanceMuted( - note, - new Set(this.userProfile?.mutedInstances ?? []), - ) - ) - return; - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) - return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる - if ( - this.userProfile && - (await getWordMute(note, this.user, this.userProfile.mutedWords)).muted - ) - return; - - this.connection.cacheNote(note); - - this.send("note", note); - } - - public dispose() { - // Unsubscribe events - this.subscriber.off("notesStream", this.onNote); - } -} diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts deleted file mode 100644 index a2e5481abb..0000000000 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ /dev/null @@ -1,52 +0,0 @@ -import Channel from "../channel.js"; -import { normalizeForSearch } from "@/misc/normalize-for-search.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import type { Packed } from "@/misc/schema.js"; - -export default class extends Channel { - public readonly chName = "hashtag"; - public static shouldShare = false; - public static requireCredential = false; - private q: string[][]; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); - } - - public async init(params: any) { - this.q = params.q; - - if (this.q == null) return; - - // Subscribe stream - this.subscriber.on("notesStream", this.onNote); - } - - 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; - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) - return; - - this.connection.cacheNote(note); - - this.send("note", note); - } - - public dispose() { - // Unsubscribe events - this.subscriber.off("notesStream", this.onNote); - } -} diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts deleted file mode 100644 index fa4a8a3901..0000000000 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ /dev/null @@ -1,80 +0,0 @@ -import Channel from "../channel.js"; -import { getWordMute } from "@/misc/check-word-mute.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import { isInstanceMuted } from "@/misc/is-instance-muted.js"; -import type { Packed } from "@/misc/schema.js"; - -export default class extends Channel { - public readonly chName = "homeTimeline"; - public static shouldShare = true; - public static requireCredential = true; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); - } - - public async init(params: any) { - // Subscribe events - this.subscriber.on("notesStream", this.onNote); - } - - 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; - } - - // Ignore notes from instances the user has muted - if ( - isInstanceMuted( - note, - new Set(this.userProfile?.mutedInstances ?? []), - ) - ) - return; - - // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if ( - reply.userId !== this.user!.id && - note.userId !== this.user!.id && - reply.userId !== note.userId - ) - return; - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) - return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる - if ( - this.userProfile && - (await getWordMute(note, this.user, this.userProfile.mutedWords)).muted - ) - return; - - this.connection.cacheNote(note); - - this.send("note", note); - } - - public dispose() { - // Unsubscribe events - this.subscriber.off("notesStream", this.onNote); - } -} diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts deleted file mode 100644 index 557bb96827..0000000000 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ /dev/null @@ -1,97 +0,0 @@ -import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { getWordMute } from "@/misc/check-word-mute.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import { isInstanceMuted } from "@/misc/is-instance-muted.js"; -import type { Packed } from "@/misc/schema.js"; - -export default class extends Channel { - public readonly chName = "hybridTimeline"; - public static shouldShare = true; - public static requireCredential = true; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); - } - - 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); - } - - 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; - - // Ignore notes from instances the user has muted - if ( - isInstanceMuted( - note, - new Set(this.userProfile?.mutedInstances ?? []), - ) - ) - return; - - // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if ( - reply.userId !== this.user!.id && - note.userId !== this.user!.id && - reply.userId !== note.userId - ) - return; - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) - return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる - if ( - this.userProfile && - (await getWordMute(note, this.user, this.userProfile.mutedWords)).muted - ) - return; - - this.connection.cacheNote(note); - - this.send("note", note); - } - - public dispose() { - // Unsubscribe events - this.subscriber.off("notesStream", this.onNote); - } -} diff --git a/packages/backend/src/server/api/stream/channels/index.ts b/packages/backend/src/server/api/stream/channels/index.ts deleted file mode 100644 index d1127be47c..0000000000 --- a/packages/backend/src/server/api/stream/channels/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import main from "./main.js"; -import homeTimeline from "./home-timeline.js"; -import localTimeline from "./local-timeline.js"; -import hybridTimeline from "./hybrid-timeline.js"; -import recommendedTimeline from "./recommended-timeline.js"; -import globalTimeline from "./global-timeline.js"; -import serverStats from "./server-stats.js"; -import queueStats from "./queue-stats.js"; -import userList from "./user-list.js"; -import antenna from "./antenna.js"; -import messaging from "./messaging.js"; -import messagingIndex from "./messaging-index.js"; -import drive from "./drive.js"; -import hashtag from "./hashtag.js"; -import channel from "./channel.js"; -import admin from "./admin.js"; - -export default { - main, - homeTimeline, - localTimeline, - recommendedTimeline, - hybridTimeline, - globalTimeline, - serverStats, - queueStats, - userList, - antenna, - messaging, - messagingIndex, - drive, - hashtag, - channel, - admin, -}; diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts deleted file mode 100644 index dc3aab8d7d..0000000000 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ /dev/null @@ -1,74 +0,0 @@ -import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { getWordMute } from "@/misc/check-word-mute.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import type { Packed } from "@/misc/schema.js"; - -export default class extends Channel { - public readonly chName = "localTimeline"; - public static shouldShare = true; - public static requireCredential = false; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); - } - - 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); - } - - 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; - - // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if ( - reply.userId !== this.user!.id && - note.userId !== this.user!.id && - reply.userId !== note.userId - ) - return; - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) - return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる - if ( - this.userProfile && - (await getWordMute(note, this.user, this.userProfile.mutedWords)).muted - ) - return; - - this.connection.cacheNote(note); - - this.send("note", note); - } - - public dispose() { - // Unsubscribe events - this.subscriber.off("notesStream", this.onNote); - } -} diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts deleted file mode 100644 index b8c72442ff..0000000000 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Channel from "../channel.js"; -import { - isInstanceMuted, - isUserFromMutedInstance, -} from "@/misc/is-instance-muted.js"; - -export default class extends Channel { - public readonly chName = "main"; - public static shouldShare = true; - public static requireCredential = true; - - public async init(params: any) { - // Subscribe main stream channel - this.subscriber.on(`mainStream:${this.user!.id}`, async (data) => { - switch (data.type) { - case "notification": { - // Ignore notifications from instances the user has muted - if ( - isUserFromMutedInstance( - data.body, - new Set(this.userProfile?.mutedInstances ?? []), - ) - ) - return; - if (data.body.userId && this.muting.has(data.body.userId)) return; - - break; - } - case "mention": { - if ( - isInstanceMuted( - data.body, - new Set(this.userProfile?.mutedInstances ?? []), - ) - ) - return; - - if (this.muting.has(data.body.userId)) return; - break; - } - } - - this.send(data.type, data.body); - }); - } -} diff --git a/packages/backend/src/server/api/stream/channels/messaging-index.ts b/packages/backend/src/server/api/stream/channels/messaging-index.ts deleted file mode 100644 index 8165172d73..0000000000 --- a/packages/backend/src/server/api/stream/channels/messaging-index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Channel from "../channel.js"; - -export default class extends Channel { - public readonly chName = "messagingIndex"; - public static shouldShare = true; - public static requireCredential = true; - - public async init(params: any) { - // Subscribe messaging index stream - this.subscriber.on(`messagingIndexStream:${this.user!.id}`, (data) => { - this.send(data); - }); - } -} diff --git a/packages/backend/src/server/api/stream/channels/messaging.ts b/packages/backend/src/server/api/stream/channels/messaging.ts deleted file mode 100644 index 0622bd4649..0000000000 --- a/packages/backend/src/server/api/stream/channels/messaging.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - readUserMessagingMessage, - readGroupMessagingMessage, - deliverReadActivity, -} from "../../common/read-messaging-message.js"; -import Channel from "../channel.js"; -import { UserGroupJoinings, Users, MessagingMessages } from "@/models/index.js"; -import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; -import type { UserGroup } from "@/models/entities/user-group.js"; -import type { StreamMessages } from "../types.js"; - -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: Map = new Map(); - private emitTypersIntervalId: ReturnType; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onEvent = this.onEvent.bind(this); - this.onMessage = this.onMessage.bind(this); - this.emitTypers = this.emitTypers.bind(this); - } - - public async init(params: any) { - this.otherpartyId = params.otherparty; - this.otherparty = this.otherpartyId - ? await Users.findOneByOrFail({ id: this.otherpartyId }) - : null; - this.groupId = params.group; - - // Check joining - if (this.groupId) { - const joining = await UserGroupJoinings.findOneBy({ - 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); - } - - private onEvent( - data: - | StreamMessages["messaging"]["payload"] - | StreamMessages["groupMessaging"]["payload"], - ) { - if (data.type === "typing") { - const id = data.body; - const begin = !this.typers.has(id); - this.typers.set(id, new Date()); - if (begin) { - this.emitTypers(); - } - } else { - this.send(data); - } - } - - 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.findOneBy({ id: 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; - } - } - - private async emitTypers() { - const now = new Date(); - - // Remove not typing users - for (const [userId, date] of this.typers.entries()) { - if (now.getTime() - date.getTime() > 5000) this.typers.delete(userId); - } - - const userIds = Array.from(this.typers.keys()); - const users = await Users.packMany(userIds, null, { - detail: false, - }); - - this.send({ - type: "typers", - body: users, - }); - } - - public dispose() { - this.subscriber.off(this.subCh, this.onEvent); - - clearInterval(this.emitTypersIntervalId); - } -} diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts deleted file mode 100644 index a5a93c332e..0000000000 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Xev from "xev"; -import Channel from "../channel.js"; - -const ev = new Xev(); - -export default class extends Channel { - public readonly chName = "queueStats"; - public static shouldShare = true; - public static requireCredential = false; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onStats = this.onStats.bind(this); - this.onMessage = this.onMessage.bind(this); - } - - public async init(params: any) { - ev.addListener("queueStats", this.onStats); - } - - private onStats(stats: any) { - this.send("stats", stats); - } - - 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; - } - } - - public dispose() { - ev.removeListener("queueStats", this.onStats); - } -} diff --git a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts deleted file mode 100644 index 6baec77442..0000000000 --- a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts +++ /dev/null @@ -1,95 +0,0 @@ -import Channel from "../channel.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { getWordMute } from "@/misc/check-word-mute.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import { isInstanceMuted } from "@/misc/is-instance-muted.js"; -import type { Packed } from "@/misc/schema.js"; - -export default class extends Channel { - public readonly chName = "recommendedTimeline"; - public static shouldShare = true; - public static requireCredential = true; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onNote = this.withPackedNote(this.onNote.bind(this)); - } - - public async init(params: any) { - const meta = await fetchMeta(); - if ( - meta.disableRecommendedTimeline && - !this.user!.isAdmin && - !this.user!.isModerator - ) - return; - - // Subscribe events - this.subscriber.on("notesStream", this.onNote); - } - - private async onNote(note: Packed<"Note">) { - // チャンネルの投稿ではなく、自分自身の投稿 または - // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または - // チャンネルの投稿ではなく、全体公開のローカルの投稿 または - // フォローしているチャンネルの投稿 の場合だけ - const meta = await fetchMeta(); - if ( - !( - note.user.host != null && - meta.recommendedInstances.includes(note.user.host) && - note.visibility === "public" - ) - ) - return; - - // Ignore notes from instances the user has muted - if ( - isInstanceMuted( - note, - new Set(this.userProfile?.mutedInstances ?? []), - ) - ) - return; - - // 関係ない返信は除外 - if (note.reply && !this.user!.showTimelineReplies) { - const reply = note.reply; - // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 - if ( - reply.userId !== this.user!.id && - note.userId !== this.user!.id && - reply.userId !== note.userId - ) - return; - } - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) - return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる - if ( - this.userProfile && - (await getWordMute(note, this.user, this.userProfile.mutedWords)).muted - ) - return; - - this.connection.cacheNote(note); - - this.send("note", note); - } - - public dispose() { - // Unsubscribe events - this.subscriber.off("notesStream", this.onNote); - } -} diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts deleted file mode 100644 index 58659138de..0000000000 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Xev from "xev"; -import Channel from "../channel.js"; - -const ev = new Xev(); - -export default class extends Channel { - public readonly chName = "serverStats"; - public static shouldShare = true; - public static requireCredential = false; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.onStats = this.onStats.bind(this); - this.onMessage = this.onMessage.bind(this); - } - - public async init(params: any) { - ev.addListener("serverStats", this.onStats); - } - - private onStats(stats: any) { - this.send("stats", stats); - } - - 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; - } - } - - public dispose() { - ev.removeListener("serverStats", this.onStats); - } -} diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts deleted file mode 100644 index 105c45955c..0000000000 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ /dev/null @@ -1,72 +0,0 @@ -import Channel from "../channel.js"; -import { UserListJoinings, UserLists } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import type { Packed } from "@/misc/schema.js"; - -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; - - constructor(id: string, connection: Channel["connection"]) { - super(id, connection); - this.updateListUsers = this.updateListUsers.bind(this); - this.onNote = this.withPackedNote(this.onNote.bind(this)); - } - - public async init(params: any) { - this.listId = params.listId as string; - - // Check existence and owner - const list = await UserLists.findOneBy({ - 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); - } - - private async updateListUsers() { - const users = await UserListJoinings.find({ - where: { - userListId: this.listId, - }, - select: ["userId"], - }); - - this.listUsers = users.map((x) => x.userId); - } - - private async onNote(note: Packed<"Note">) { - if (!this.listUsers.includes(note.userId)) return; - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - - if (note.renote && !note.text && isUserRelated(note, this.renoteMuting)) - return; - - this.send("note", note); - } - - public dispose() { - // Unsubscribe events - this.subscriber.off(`userListStream:${this.listId}`, this.send); - this.subscriber.off("notesStream", this.onNote); - - clearInterval(this.listUsersClock); - } -} diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts deleted file mode 100644 index 055fe200b7..0000000000 --- a/packages/backend/src/server/api/stream/index.ts +++ /dev/null @@ -1,621 +0,0 @@ -import type { EventEmitter } from "events"; -import type * as websocket from "websocket"; -import readNote from "@/services/note/read.js"; -import type { User } from "@/models/entities/user.js"; -import type { Channel as ChannelModel } from "@/models/entities/channel.js"; -import { - Users, - Followings, - Mutings, - RenoteMutings, - UserProfiles, - ChannelFollowings, - Blockings, -} from "@/models/index.js"; -import type { AccessToken } from "@/models/entities/access-token.js"; -import type { UserProfile } from "@/models/entities/user-profile.js"; -import { - publishChannelStream, - publishGroupMessagingStream, - publishMessagingStream, -} from "@/services/stream.js"; -import type { UserGroup } from "@/models/entities/user-group.js"; -import type { Packed } from "@/misc/schema.js"; -import { readNotification } from "../common/read-notification.js"; -import channels from "./channels/index.js"; -import type Channel from "./channel.js"; -import type { StreamEventEmitter, StreamMessages } from "./types.js"; -import { Converter } from "@calckey/megalodon"; -import { getClient } from "../mastodon/ApiMastodonCompatibleService.js"; -import { toTextWithReaction } from "../mastodon/endpoints/timeline.js"; - -/** - * Main stream connection - */ -export default class Connection { - public user?: User; - public userProfile?: UserProfile | null; - public following: Set = new Set(); - public muting: Set = new Set(); - public renoteMuting: 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: Map = new Map(); - private cachedNotes: Packed<"Note">[] = []; - private isMastodonCompatible: boolean = false; - private host: string; - private accessToken: string; - private currentSubscribe: string[][] = []; - - constructor( - wsConnection: websocket.connection, - subscriber: EventEmitter, - user: User | null | undefined, - token: AccessToken | null | undefined, - host: string, - accessToken: string, - prepareStream: string | undefined, - ) { - console.log("constructor", prepareStream); - this.wsConnection = wsConnection; - this.subscriber = subscriber; - if (user) this.user = user; - if (token) this.token = token; - if (host) this.host = host; - if (accessToken) this.accessToken = accessToken; - - this.onWsConnectionMessage = this.onWsConnectionMessage.bind(this); - this.onUserEvent = this.onUserEvent.bind(this); - this.onNoteStreamMessage = this.onNoteStreamMessage.bind(this); - this.onBroadcastMessage = this.onBroadcastMessage.bind(this); - - this.wsConnection.on("message", this.onWsConnectionMessage); - - this.subscriber.on("broadcast", (data) => { - this.onBroadcastMessage(data); - }); - - if (this.user) { - this.updateFollowing(); - this.updateMuting(); - this.updateRenoteMuting(); - this.updateBlocking(); - this.updateFollowingChannels(); - this.updateUserProfile(); - - this.subscriber.on(`user:${this.user.id}`, this.onUserEvent); - } - console.log("prepare", prepareStream); - if (prepareStream) { - this.onWsConnectionMessage({ - type: "utf8", - utf8Data: JSON.stringify({ stream: prepareStream, type: "subscribe" }), - }); - } - } - - 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: renote mute events - // 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; - } - } - - /** - * クライアントからメッセージ受信時 - */ - private async onWsConnectionMessage(data: websocket.Message) { - if (data.type !== "utf8") return; - if (data.utf8Data == null) return; - - let objs: Record[]; - - try { - objs = [JSON.parse(data.utf8Data)]; - } catch (e) { - return; - } - - const simpleObj = objs[0]; - if (simpleObj.stream) { - // is Mastodon Compatible - this.isMastodonCompatible = true; - if (simpleObj.type === "subscribe") { - let forSubscribe = []; - if (simpleObj.stream === "user") { - this.currentSubscribe.push(["user"]); - objs = [ - { - type: "connect", - body: { - channel: "main", - id: simpleObj.stream, - }, - }, - { - type: "connect", - body: { - channel: "homeTimeline", - id: simpleObj.stream, - }, - }, - ]; - const client = getClient(this.host, this.accessToken); - try { - const tl = await client.getHomeTimeline(); - for (const t of tl.data) forSubscribe.push(t.id); - } catch (e: any) { - console.log(e); - console.error(e.response.data); - } - } else if (simpleObj.stream === "public:local") { - this.currentSubscribe.push(["public:local"]); - objs = [ - { - type: "connect", - body: { - channel: "localTimeline", - id: simpleObj.stream, - }, - }, - ]; - const client = getClient(this.host, this.accessToken); - const tl = await client.getLocalTimeline(); - for (const t of tl.data) forSubscribe.push(t.id); - } else if (simpleObj.stream === "public") { - this.currentSubscribe.push(["public"]); - objs = [ - { - type: "connect", - body: { - channel: "globalTimeline", - id: simpleObj.stream, - }, - }, - ]; - const client = getClient(this.host, this.accessToken); - const tl = await client.getPublicTimeline(); - for (const t of tl.data) forSubscribe.push(t.id); - } else if (simpleObj.stream === "list") { - this.currentSubscribe.push(["list", simpleObj.list]); - objs = [ - { - type: "connect", - body: { - channel: "list", - id: simpleObj.stream, - params: { - listId: simpleObj.list, - }, - }, - }, - ]; - const client = getClient(this.host, this.accessToken); - const tl = await client.getListTimeline(simpleObj.list); - for (const t of tl.data) forSubscribe.push(t.id); - } - for (const s of forSubscribe) { - objs.push({ - type: "s", - body: { - id: s, - }, - }); - } - } - } - - for (const obj of objs) { - const { type, body } = obj; - console.log(type, body); - switch (type) { - 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; - } - } - } - - private onBroadcastMessage(data: StreamMessages["broadcast"]["payload"]) { - this.sendMessageToWs(data.type, data.body); - } - - 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); - } - - 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, - }); - } - } - - private onReadNotification(payload: any) { - if (!payload.id) return; - readNotification(this.user!.id, [payload.id]); - } - - /** - * 投稿購読要求時 - */ - private onSubscribeNote(payload: any) { - if (!payload.id) return; - - const current = this.subscribingNotes.get(payload.id) || 0; - this.subscribingNotes.set(payload.id, current + 1); - - if (!current) { - this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); - } - } - - /** - * 投稿購読解除要求時 - */ - private onUnsubscribeNote(payload: any) { - if (!payload.id) return; - - const current = this.subscribingNotes.get(payload.id) || 0; - if (current <= 1) { - this.subscribingNotes.delete(payload.id); - this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage); - return; - } - this.subscribingNotes.set(payload.id, current - 1); - } - - private async onNoteStreamMessage(data: StreamMessages["note"]["payload"]) { - this.sendMessageToWs("noteUpdated", { - id: data.body.id, - type: data.type, - body: data.body.body, - }); - } - - /** - * チャンネル接続要求時 - */ - private onChannelConnectRequested(payload: any) { - const { channel, id, params, pong } = payload; - this.connectChannel(id, params, channel, pong); - } - - /** - * チャンネル切断要求時 - */ - private onChannelDisconnectRequested(payload: any) { - const { id } = payload; - this.disconnectChannel(id); - } - - /** - * クライアントにメッセージ送信 - */ - public sendMessageToWs(type: string, payload: any) { - console.log(payload, this.isMastodonCompatible); - if (this.isMastodonCompatible) { - if (payload.type === "note") { - this.wsConnection.send( - JSON.stringify({ - stream: [payload.id], - event: "update", - payload: JSON.stringify( - toTextWithReaction( - [Converter.note(payload.body, this.host)], - this.host, - )[0], - ), - }), - ); - this.onSubscribeNote({ - id: payload.body.id, - }); - } else if (payload.type === "reacted" || payload.type === "unreacted") { - // reaction - const client = getClient(this.host, this.accessToken); - client.getStatus(payload.id).then((data) => { - const newPost = toTextWithReaction([data.data], this.host); - const targetPost = newPost[0]; - for (const stream of this.currentSubscribe) { - this.wsConnection.send( - JSON.stringify({ - stream, - event: "status.update", - payload: JSON.stringify(targetPost), - }), - ); - } - }); - } else if (payload.type === "deleted") { - // delete - for (const stream of this.currentSubscribe) { - this.wsConnection.send( - JSON.stringify({ - stream, - event: "delete", - payload: payload.id, - }), - ); - } - } else if (payload.type === "unreadNotification") { - if (payload.id === "user") { - const body = Converter.notification(payload.body, this.host); - if (body.type === "reaction") body.type = "favourite"; - body.status = toTextWithReaction( - body.status ? [body.status] : [], - "", - )[0]; - this.wsConnection.send( - JSON.stringify({ - stream: ["user"], - event: "notification", - payload: JSON.stringify(body), - }), - ); - } - } - } else { - this.wsConnection.send( - JSON.stringify({ - type: type, - body: payload, - }), - ); - } - } - - /** - * チャンネルに接続 - */ - 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 - */ - 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 メッセージ - */ - private onChannelMessageRequested(data: any) { - const channel = this.channels.find((c) => c.id === data.id); - if (channel?.onMessage != null) { - channel.onMessage(data.type, data.body); - } - } - - private typingOnChannel(channel: ChannelModel["id"]) { - if (this.user) { - publishChannelStream(channel, "typing", this.user.id); - } - } - - 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); - } - } - } - - private async updateFollowing() { - const followings = await Followings.find({ - where: { - followerId: this.user!.id, - }, - select: ["followeeId"], - }); - - this.following = new Set(followings.map((x) => x.followeeId)); - } - - private async updateMuting() { - const mutings = await Mutings.find({ - where: { - muterId: this.user!.id, - }, - select: ["muteeId"], - }); - - this.muting = new Set(mutings.map((x) => x.muteeId)); - } - - private async updateRenoteMuting() { - const renoteMutings = await RenoteMutings.find({ - where: { - muterId: this.user!.id, - }, - select: ["muteeId"], - }); - - this.renoteMuting = new Set(renoteMutings.map((x) => x.muteeId)); - } - - 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)); - } - - private async updateFollowingChannels() { - const followings = await ChannelFollowings.find({ - where: { - followerId: this.user!.id, - }, - select: ["followeeId"], - }); - - this.followingChannels = new Set( - followings.map((x) => x.followeeId), - ); - } - - private async updateUserProfile() { - this.userProfile = await UserProfiles.findOneBy({ - userId: this.user!.id, - }); - } - - /** - * ストリームが切れたとき - */ - public dispose() { - for (const c of this.channels.filter((c) => c.dispose)) { - if (c.dispose) c.dispose(); - } - } -} diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts deleted file mode 100644 index 9becf9f642..0000000000 --- a/packages/backend/src/server/api/stream/types.ts +++ /dev/null @@ -1,292 +0,0 @@ -import type { EventEmitter } from "events"; -import type Emitter from "strict-event-emitter-types"; -import type { Channel } from "@/models/entities/channel.js"; -import type { User } from "@/models/entities/user.js"; -import type { UserProfile } from "@/models/entities/user-profile.js"; -import type { Note } from "@/models/entities/note.js"; -import type { Antenna } from "@/models/entities/antenna.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import type { DriveFolder } from "@/models/entities/drive-folder.js"; -import type { UserList } from "@/models/entities/user-list.js"; -import type { MessagingMessage } from "@/models/entities/messaging-message.js"; -import type { UserGroup } from "@/models/entities/user-group.js"; -import type { AbuseUserReport } from "@/models/entities/abuse-user-report.js"; -import type { Signin } from "@/models/entities/signin.js"; -import type { Page } from "@/models/entities/page.js"; -import type { Packed } from "@/misc/schema.js"; -import type { Webhook } from "@/models/entities/webhook"; - -//#region Stream type-body definitions -export interface InternalStreamTypes { - userChangeSuspendedState: { - id: User["id"]; - isSuspended: User["isSuspended"]; - }; - userChangeSilencedState: { - id: User["id"]; - isSilenced: User["isSilenced"]; - }; - userChangeModeratorState: { - id: User["id"]; - isModerator: User["isModerator"]; - }; - userTokenRegenerated: { - id: User["id"]; - oldToken: User["token"]; - newToken: User["token"]; - }; - localUserUpdated: { - id: User["id"]; - }; - remoteUserUpdated: { - id: User["id"]; - }; - webhookCreated: Webhook; - webhookDeleted: Webhook; - webhookUpdated: Webhook; - antennaCreated: Antenna; - antennaDeleted: Antenna; - antennaUpdated: Antenna; -} - -export interface BroadcastTypes { - emojiAdded: { - emoji: Packed<"Emoji">; - }; -} - -export interface UserStreamTypes { - terminate: Record; - followChannel: Channel; - unfollowChannel: Channel; - updateUserProfile: UserProfile; - mute: User; - unmute: User; - follow: Packed<"UserDetailedNotMe">; - unfollow: Packed<"User">; - userAdded: Packed<"User">; -} - -export interface MainStreamTypes { - notification: Packed<"Notification">; - mention: Packed<"Note">; - reply: Packed<"Note">; - renote: Packed<"Note">; - follow: Packed<"UserDetailedNotMe">; - 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; - signin: Signin; - registryUpdated: { - scope?: string[]; - key: string; - value: any | null; - }; - driveFileCreated: Packed<"DriveFile">; - readAntenna: Antenna; - receiveFollowRequest: Packed<"User">; -} - -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?: { - name: string; - url: string; - } | null; - userId: User["id"]; - }; - unreacted: { - reaction: string; - userId: User["id"]; - }; - replied: { - id: Note["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 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> = 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; - }; - admin: { - name: `adminStream:${User["id"]}`; - payload: EventUnionFromDictionary; - }; - notes: { - name: "notesStream"; - payload: Note; - }; -}; - -// API event definitions -// ストリームごとのEmitterの辞書を用意 -type EventEmitterDictionary = { - [x in keyof StreamMessages]: Emitter< - EventEmitter, - { - [y in StreamMessages[x]["name"]]: ( - e: StreamMessages[x]["payload"], - ) => void; - } - >; -}; -// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( - k: infer I, -) => void - ? I - : never; -// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする -export type StreamEventEmitter = UnionToIntersection< - EventEmitterDictionary[keyof StreamMessages] ->; -// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる - -// provide stream channels union -export type StreamChannels = StreamMessages[keyof StreamMessages]["name"]; diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts deleted file mode 100644 index 14e07b7487..0000000000 --- a/packages/backend/src/server/api/streaming.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type * as http from "node:http"; -import { EventEmitter } from "events"; -import type { ParsedUrlQuery } from "querystring"; -import * as websocket from "websocket"; - -import { subscriber as redisClient } from "@/db/redis.js"; -import { Users } from "@/models/index.js"; -import MainStreamConnection from "./stream/index.js"; -import authenticate from "./authenticate.js"; - -export const initializeStreamingServer = (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; - const headers = request.httpRequest.headers["sec-websocket-protocol"] || ""; - const cred = q.i || q.access_token || headers; - const accessToken = cred.toString(); - - const [user, app] = await authenticate( - request.httpRequest.headers.authorization, - accessToken, - ).catch((err) => { - request.reject(403, err.message); - return []; - }); - if (typeof user === "undefined") { - return; - } - - 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 host = `https://${request.host}`; - const prepareStream = q.stream?.toString(); - console.log("start", q); - - const main = new MainStreamConnection( - connection, - ev, - user, - app, - host, - accessToken, - prepareStream, - ); - - 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.type === "utf8" && data.utf8Data === "ping") { - connection.send("pong"); - } - }); - }); -}; diff --git a/packages/backend/src/server/file/assets/bad-egg.png b/packages/backend/src/server/file/assets/bad-egg.png deleted file mode 100644 index e96ba0dcc1c9e074f27bf75c291017899015bdf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1676 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 LKV5Xv_4@w+k2RB# diff --git a/packages/backend/src/server/file/assets/dummy.png b/packages/backend/src/server/file/assets/dummy.png deleted file mode 100644 index 39332b0c1beeda1edb90d78d25c16e7372aff030..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6285 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 MskKSzCCsmX1IGX9?*IS* diff --git a/packages/backend/src/server/file/assets/thumbnail-not-available.png b/packages/backend/src/server/file/assets/thumbnail-not-available.png deleted file mode 100644 index 07cad9919c5a1c6a9398799fc75c989c602386ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5705 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 SV^ec~{A;>KIwe<}9{m^SoJF+& diff --git a/packages/backend/src/server/file/index.ts b/packages/backend/src/server/file/index.ts deleted file mode 100644 index 26df1de51d..0000000000 --- a/packages/backend/src/server/file/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * File Server - */ - -import * as fs from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; -import Koa from "koa"; -import cors from "@koa/cors"; -import Router from "@koa/router"; -import sendDriveFile from "./send-drive-file.js"; - -const _filename = fileURLToPath(import.meta.url); -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'; img-src 'self'; media-src 'self'; 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()); - -export default app; diff --git a/packages/backend/src/server/file/send-drive-file.ts b/packages/backend/src/server/file/send-drive-file.ts deleted file mode 100644 index 0877369025..0000000000 --- a/packages/backend/src/server/file/send-drive-file.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as fs from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; -import type Koa from "koa"; -import send from "koa-send"; -import rename from "rename"; -import { serverLogger } from "../index.js"; -import { contentDisposition } from "@/misc/content-disposition.js"; -import { DriveFiles } from "@/models/index.js"; -import { InternalStorage } from "@/services/drive/internal-storage.js"; -import { createTemp } from "@/misc/create-temp.js"; -import { downloadUrl } from "@/misc/download-url.js"; -import { detectType } from "@/misc/get-file-info.js"; -import { convertToWebp } from "@/services/drive/image-processor.js"; -import { GenerateVideoThumbnail } from "@/services/drive/generate-video-thumbnail.js"; -import { StatusError } from "@/misc/fetch.js"; -import { FILE_TYPE_BROWSERSAFE } from "@/const.js"; - -const _filename = fileURLToPath(import.meta.url); -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 createTemp(); - - try { - await downloadUrl(file.uri, path); - - const { mime, ext } = await detectType(path); - - const convertFile = async () => { - if (isThumbnail) { - if ( - [ - "image/jpeg", - "image/webp", - "image/png", - "image/svg+xml", - "image/avif", - ].includes(mime) - ) { - return await convertToWebp(path, 996, 560); - } else if (mime.startsWith("video/")) { - return await GenerateVideoThumbnail(path); - } - } - - if (isWebpublic) { - if (["image/svg+xml"].includes(mime)) { - return await convertToWebp(path, 2048, 2048, 100); - } - } - - return { - data: fs.readFileSync(path), - ext, - type: mime, - }; - }; - - const image = await convertFile(); - ctx.body = image.data; - ctx.set( - "Content-Type", - FILE_TYPE_BROWSERSAFE.includes(image.type) - ? image.type - : "application/octet-stream", - ); - 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", - FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : "application/octet-stream", - ); - 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_BROWSERSAFE.includes(file.type) - ? file.type - : "application/octet-stream", - ); - ctx.set("Cache-Control", "max-age=31536000, immutable"); - ctx.set("Content-Disposition", contentDisposition("inline", file.name)); - } -} diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts deleted file mode 100644 index 7d274f7d28..0000000000 --- a/packages/backend/src/server/index.ts +++ /dev/null @@ -1,286 +0,0 @@ -/** - * Core Server - */ - -import cluster from "node:cluster"; -import * as fs from "node:fs"; -import * as http from "node:http"; -import Koa from "koa"; -import Router from "@koa/router"; -import mount from "koa-mount"; -import koaLogger from "koa-logger"; -import * as slow from "koa-slow"; -import { IsNull } from "typeorm"; -import config from "@/config/index.js"; -import Logger from "@/services/logger.js"; -import { UserProfiles, Users } from "@/models/index.js"; -import { genIdenticon } from "@/misc/gen-identicon.js"; -import { createTemp } from "@/misc/create-temp.js"; -import { publishMainStream } from "@/services/stream.js"; -import * as Acct from "@/misc/acct.js"; -import { envOption } from "@/env.js"; -import megalodon, { MegalodonInterface } from "@calckey/megalodon"; -import activityPub from "./activitypub.js"; -import nodeinfo from "./nodeinfo.js"; -import wellKnown from "./well-known.js"; -import apiServer from "./api/index.js"; -import fileServer from "./file/index.js"; -import proxyServer from "./proxy/index.js"; -import webServer from "./web/index.js"; -import { initializeStreamingServer } from "./api/streaming.js"; -import { koaBody } from "koa-body"; -import { v4 as uuid } from "uuid"; - -export const serverLogger = new Logger("server", "gray", false); - -// Init app -const app = new Koa(); -app.proxy = true; - -// Replace trailing slashes -app.use(async (ctx, next) => { - if (ctx.request.path !== "/" && ctx.request.path.endsWith("/")) - return ctx.redirect(ctx.request.path.replace(/\/$/, "")); - else await next(); -}); - -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", fileServer)); -app.use(mount("/proxy", proxyServer)); - -// Init router -const router = new Router(); -const mastoRouter = new Router(); - -mastoRouter.use( - koaBody({ - urlencoded: true, - multipart: true, - }), -); - -mastoRouter.use(async (ctx, next) => { - if (ctx.request.query) { - if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) { - ctx.request.body = ctx.request.query; - } else { - ctx.request.body = { ...ctx.request.body, ...ctx.request.query }; - } - } - await next(); -}); - -// Routing -router.use(activityPub.routes()); -router.use(nodeinfo.routes()); -router.use(wellKnown.routes()); - -router.get("/avatar/@:acct", async (ctx) => { - const { username, host } = Acct.parse(ctx.params.acct); - const user = await Users.findOne({ - where: { - usernameLower: username.toLowerCase(), - host: host == null || host === config.host ? IsNull() : host, - isSuspended: false, - }, - relations: ["avatar"], - }); - - if (user) { - ctx.redirect(Users.getAvatarUrlSync(user)); - } else { - ctx.redirect("/static-assets/user-unknown.png"); - } -}); - -router.get("/identicon/:x", async (ctx) => { - const [temp, cleanup] = await createTemp(); - await genIdenticon(ctx.params.x, fs.createWriteStream(temp)); - ctx.set("Content-Type", "image/png"); - ctx.body = fs.createReadStream(temp).on("close", () => cleanup()); -}); - -router.get("/verify-email/:code", async (ctx) => { - const profile = await UserProfiles.findOneBy({ - 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; - } -}); - -mastoRouter.get("/oauth/authorize", async (ctx) => { - const { client_id, state, redirect_uri } = ctx.request.query; - console.log(ctx.request.req); - let param = "mastodon=true"; - if (state) param += `&state=${state}`; - if (redirect_uri) param += `&redirect_uri=${redirect_uri}`; - const client = client_id ? client_id : ""; - ctx.redirect( - `${Buffer.from(client.toString(), "base64").toString()}?${param}`, - ); -}); - -mastoRouter.post("/oauth/token", async (ctx) => { - const body: any = ctx.request.body || ctx.request.query; - console.log("token-request", body); - console.log("token-query", ctx.request.query); - if (body.grant_type === "client_credentials") { - const ret = { - access_token: uuid(), - token_type: "Bearer", - scope: "read", - created_at: Math.floor(new Date().getTime() / 1000), - }; - ctx.body = ret; - return; - } - let client_id: any = body.client_id; - const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; - const generator = (megalodon as any).default; - const client = generator("misskey", BASE_URL, null) as MegalodonInterface; - let m = null; - let token = null; - if (body.code) { - //m = body.code.match(/^([a-zA-Z0-9]{8})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{12})/); - //if (!m.length) { - // ctx.body = { error: "Invalid code" }; - // return; - //} - //token = `${m[1]}-${m[2]}-${m[3]}-${m[4]}-${m[5]}` - console.log(body.code, token); - token = body.code; - } - if (client_id instanceof Array) { - client_id = client_id.toString(); - } else if (!client_id) { - client_id = null; - } - try { - const atData = await client.fetchAccessToken( - client_id, - body.client_secret, - token ? token : "", - ); - const ret = { - access_token: atData.accessToken, - token_type: "Bearer", - scope: body.scope || "read write follow push", - created_at: Math.floor(new Date().getTime() / 1000), - }; - console.log("token-response", ret); - ctx.body = ret; - } catch (err: any) { - console.error(err); - ctx.status = 401; - ctx.body = err.response.data; - } -}); - -// Register router -app.use(mastoRouter.routes()); -app.use(router.routes()); - -app.use(mount(webServer)); - -function createServer() { - return http.createServer(app.callback()); -} - -// For testing -export const startServer = () => { - const server = createServer(); - - initializeStreamingServer(server); - - server.listen(config.port); - - return server; -}; - -export default () => - new Promise((resolve) => { - const server = createServer(); - - initializeStreamingServer(server); - - server.on("error", (e) => { - switch ((e as any).code) { - case "EACCES": - serverLogger.error( - `You do not have permission to listen on port ${config.port}.`, - ); - break; - case "EADDRINUSE": - serverLogger.error( - `Port ${config.port} is already in use by another process.`, - ); - break; - default: - serverLogger.error(e); - break; - } - - if (cluster.isWorker) { - process.send!("listenFailed"); - } else { - // disableClustering - process.exit(1); - } - }); - - // @ts-ignore - server.listen(config.port, resolve); - }); diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts deleted file mode 100644 index 8563573d47..0000000000 --- a/packages/backend/src/server/nodeinfo.ts +++ /dev/null @@ -1,119 +0,0 @@ -import Router from "@koa/router"; -import config from "@/config/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { Users, Notes } from "@/models/index.js"; -import { IsNull, MoreThan } from "typeorm"; -import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; -import { Cache } from "@/misc/cache.js"; - -const router = new Router(); - -const nodeinfo2_1path = "/nodeinfo/2.1"; -const nodeinfo2_0path = "/nodeinfo/2.0"; - -// to cleo: leave this http or bonks -export const links = [ - { - 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 now = Date.now(); - const [meta, total, activeHalfyear, activeMonth, localPosts] = - await Promise.all([ - fetchMeta(true), - Users.count({ where: { host: IsNull() } }), - Users.count({ - where: { - host: IsNull(), - lastActiveDate: MoreThan(new Date(now - 15552000000)), - }, - }), - Users.count({ - where: { - host: IsNull(), - lastActiveDate: MoreThan(new Date(now - 2592000000)), - }, - }), - Notes.count({ where: { userHost: IsNull() } }), - ]); - - const proxyAccount = meta.proxyAccountId - ? await Users.pack(meta.proxyAccountId).catch(() => null) - : null; - - return { - software: { - name: "calckey", - version: config.version, - repository: meta.repositoryUrl, - homepage: "https://calckey.cloud", - }, - protocols: ["activitypub"], - services: { - inbound: [] as string[], - outbound: ["atom1.0", "rss2.0"], - }, - openRegistrations: !meta.disableRegistration, - usage: { - users: { total, activeHalfyear, activeMonth }, - localPosts, - localComments: 0, - }, - 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, - disableRecommendedTimeline: meta.disableRecommendedTimeline, - disableGlobalTimeline: meta.disableGlobalTimeline, - emailRequiredForSignup: meta.emailRequiredForSignup, - enableHcaptcha: meta.enableHcaptcha, - enableRecaptcha: meta.enableRecaptcha, - maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, - maxCaptionTextLength: MAX_CAPTION_TEXT_LENGTH, - enableTwitterIntegration: meta.enableTwitterIntegration, - enableGithubIntegration: meta.enableGithubIntegration, - enableDiscordIntegration: meta.enableDiscordIntegration, - enableEmail: meta.enableEmail, - enableServiceWorker: meta.enableServiceWorker, - proxyAccountName: proxyAccount ? proxyAccount.username : null, - themeColor: meta.themeColor || "#31748f", - }, - }; -}; - -const cache = new Cache>>(1000 * 60 * 10); - -router.get(nodeinfo2_1path, async (ctx) => { - const base = await cache.fetch(null, () => nodeinfo2()); - - ctx.body = { version: "2.1", ...base }; - ctx.set("Cache-Control", "public, max-age=600"); -}); - -router.get(nodeinfo2_0path, async (ctx) => { - const base = await cache.fetch(null, () => nodeinfo2()); - - // @ts-ignore - base.software.repository = undefined; - - ctx.body = { version: "2.0", ...base }; - ctx.set("Cache-Control", "public, max-age=600"); -}); - -export default router; diff --git a/packages/backend/src/server/proxy/index.ts b/packages/backend/src/server/proxy/index.ts deleted file mode 100644 index 004b3779fb..0000000000 --- a/packages/backend/src/server/proxy/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Media Proxy - */ - -import Koa from "koa"; -import cors from "@koa/cors"; -import Router from "@koa/router"; -import { proxyMedia } from "./proxy-media.js"; - -// Init app -const app = new Koa(); -app.use(cors()); -app.use(async (ctx, next) => { - ctx.set( - "Content-Security-Policy", - `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`, - ); - await next(); -}); - -// Init router -const router = new Router(); - -router.get("/:url*", proxyMedia); - -// Register router -app.use(router.routes()); - -export default app; diff --git a/packages/backend/src/server/proxy/proxy-media.ts b/packages/backend/src/server/proxy/proxy-media.ts deleted file mode 100644 index a9c257bfeb..0000000000 --- a/packages/backend/src/server/proxy/proxy-media.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as fs from "node:fs"; -import type Koa from "koa"; -import sharp from "sharp"; -import type { IImage } from "@/services/drive/image-processor.js"; -import { convertToWebp } from "@/services/drive/image-processor.js"; -import { createTemp } from "@/misc/create-temp.js"; -import { downloadUrl } from "@/misc/download-url.js"; -import { detectType } from "@/misc/get-file-info.js"; -import { StatusError } from "@/misc/fetch.js"; -import { FILE_TYPE_BROWSERSAFE } from "@/const.js"; -import { serverLogger } from "../index.js"; -import { isMimeImage } from "@/misc/is-mime-image.js"; - -export async function proxyMedia(ctx: Koa.Context) { - const url = "url" in ctx.query ? ctx.query.url : `https://${ctx.params.url}`; - - if (typeof url !== "string") { - ctx.status = 400; - return; - } - - // Create temp file - const [path, cleanup] = await createTemp(); - - try { - await downloadUrl(url, path); - - const { mime, ext } = await detectType(path); - const isConvertibleImage = isMimeImage(mime, "sharp-convertible-image"); - - let image: IImage; - - if ("static" in ctx.query && isConvertibleImage) { - image = await convertToWebp(path, 996, 560); - } else if ("preview" in ctx.query && isConvertibleImage) { - image = await convertToWebp(path, 400, 400); - } else if ("badge" in ctx.query) { - if (!isConvertibleImage) { - // 画像でないなら404でお茶を濁す - throw new StatusError("Unexpected mime", 404); - } - - const mask = sharp(path) - .resize(96, 96, { - fit: "inside", - withoutEnlargement: false, - }) - .greyscale() - .normalise() - .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast - .flatten({ background: "#000" }) - .toColorspace("b-w"); - - const stats = await mask.clone().stats(); - - if (stats.entropy < 0.1) { - // エントロピーがあまりない場合は404にする - throw new StatusError("Skip to provide badge", 404); - } - - const data = sharp({ - create: { - width: 96, - height: 96, - channels: 4, - background: { r: 0, g: 0, b: 0, alpha: 0 }, - }, - }) - .pipelineColorspace("b-w") - .boolean(await mask.png().toBuffer(), "eor"); - - image = { - data: await data.png().toBuffer(), - ext: "png", - type: "image/png", - }; - } else if (mime === "image/svg+xml") { - image = await convertToWebp(path, 2048, 2048, 1); - } else if ( - !(mime.startsWith("image/") && FILE_TYPE_BROWSERSAFE.includes(mime)) - ) { - throw new StatusError("Rejected type", 403, "Rejected type"); - } 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.statusCode === 302 || e.isClientError)) { - ctx.status = e.statusCode; - } else { - ctx.status = 500; - } - } finally { - cleanup(); - } -} diff --git a/packages/backend/src/server/web/bios.css b/packages/backend/src/server/web/bios.css deleted file mode 100644 index e79fc0657b..0000000000 --- a/packages/backend/src/server/web/bios.css +++ /dev/null @@ -1,147 +0,0 @@ -main > .tabs { - padding: 16px; - border-bottom: 4px solid #908caa; -} -#lsEditor > .adder { - margin: 16px; - padding: 16px; - border: 2px solid #908caa; -} -#lsEditor > .adder > textarea { - display: block; - width: 100%; - min-height: 5em; - box-sizing: border-box; -} -#lsEditor > .record { - padding: 16px; - border-bottom: 1px solid #908caa; -} -#lsEditor > .record > header { - font-weight: 700; -} -#lsEditor > .record > textarea { - display: block; - width: 100%; - min-height: 5em; - box-sizing: border-box; -} - -html { - background: #191724; -} -main { - background: #1f1d2e; - border-radius: 10px; -} -#tl > div { - padding: 16px; - border-bottom: 1px solid #908caa; -} -#tl > div > header { - font-weight: 700; -} - -* { - font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; -} -#calckey_app { - display: none !important; -} -body, -html { - background-color: #191724; - color: #e0def4; - justify-content: center; - margin: auto; - padding: 10px; - text-align: center; -} -button { - border-radius: 999px; - padding: 0px 12px 0px 12px; - border: none; - cursor: pointer; - margin-bottom: 12px; - background: linear-gradient(90deg, rgb(156, 207, 216), rgb(49, 116, 143)); - line-height: 50px; - color: #191724; - font-weight: bold; - font-size: 20px; - padding: 12px; -} -button { - border-radius: 999px; - padding: 0px 12px 0px 12px; - border: none; - cursor: pointer; - margin-bottom: 12px; -} -button { - background: #444; - line-height: 40px; - color: rgb(156, 207, 216); - font-size: 16px; - padding: 0 20px; - margin-right: 5px; - margin-left: 5px; -} -button:hover { - background: #555; -} -#ls { - background: linear-gradient(90deg, rgb(156, 207, 216), rgb(49, 116, 143)); - line-height: 30px; - color: #191724; - font-weight: bold; - font-size: 18px; - padding: 12px; -} -#ls:hover { - background: rgb(156, 207, 216); -} -a { - color: rgb(156, 207, 216); - text-decoration: none; -} -p, -li { - font-size: 16px; -} - -h1 { - font-size: 32px; -} -code { - font-family: Fira, FiraCode, monospace; -} -textarea { - background-color: #444; - border: solid #aaa; - border-radius: 10px; - color: #e0def4; - margin-top: 1rem; - margin-bottom: 1rem; - width: 20rem; - height: 7.5rem; - padding: 0.5rem; -} - -textarea:focus { - border: solid #eee; -} -input { - background-color: #666; - border: solid #aaa; - border-radius: 10px; - color: #e0def4; - margin-top: 1rem; - margin-bottom: 1rem; - width: 10rem; - height: 1rem; - padding: 0.5rem; -} - -input:focus { - border: solid #eee; -} diff --git a/packages/backend/src/server/web/bios.js b/packages/backend/src/server/web/bios.js deleted file mode 100644 index e715a01068..0000000000 --- a/packages/backend/src/server/web/bios.js +++ /dev/null @@ -1,89 +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/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js deleted file mode 100644 index e7e859d20c..0000000000 --- a/packages/backend/src/server/web/boot.js +++ /dev/null @@ -1,319 +0,0 @@ -/** - * BOOT LOADER - * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。 - * - 翻訳ファイルをフェッチする。 - * - バージョンに基づいて適切なメインスクリプトを読み込む。 - * - キャッシュされたコンパイル済みテーマを適用する。 - * - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。 - * テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。 - * 注: webpackは介さないため、このファイルではrequireやimportは使えません。 - */ - -"use strict"; - -// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので -(async () => { - window.onerror = (e) => { - console.error(e); - renderError("SOMETHING_HAPPENED", e); - }; - window.onunhandledrejection = (e) => { - console.error(e); - renderError("SOMETHING_HAPPENED_IN_PROMISE", e); - }; - - //#region Detect language & fetch translations - const v = localStorage.getItem("v") || VERSION; - - 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 { - await checkUpdate(); - renderError("LOCALE_FETCH"); - return; - } - //#endregion - - //#region Script - function importAppScript() { - import(`/assets/${CLIENT_ENTRY}`).catch(async (e) => { - await checkUpdate(); - console.error(e); - renderError("APP_IMPORT", e); - }); - } - - // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある - if (document.readyState !== "loading") { - importAppScript(); - } else { - window.addEventListener("DOMContentLoaded", () => { - importAppScript(); - }); - } - //#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; - } - } - } - } - } - const colorSchema = localStorage.getItem("colorSchema"); - if (colorSchema) { - document.documentElement.style.setProperty("color-schema", colorSchema); - } - //#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); - } - - async function addStyle(styleText) { - let css = document.createElement("style"); - css.appendChild(document.createTextNode(styleText)); - document.head.appendChild(css); - } - - function renderError(code, details) { - let errorsElement = document.getElementById("errors"); - - if (!errorsElement) { - document.body.innerHTML = ` - - - - - -

An error has occurred!

- -

Don't worry, it's (probably) not your fault.

-

Please make sure your browser is up-to-date and any AdBlockers are off.

-

If the problem persists after refreshing, please contact your instance's administrator.
You may also try the following options:

- - - -
- - - -
- - - -
-
- `; - errorsElement = document.getElementById("errors"); - } - const detailsElement = document.createElement("details"); - detailsElement.innerHTML = ` -
- - ERROR CODE: ${code} - - ${JSON.stringify(details)}`; - errorsElement.appendChild(detailsElement); - addStyle(` - * { - font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; - } - - #calckey_app, - #splash { - display: none !important; - } - - body, - html { - background-color: #191724; - color: #e0def4; - justify-content: center; - margin: auto; - padding: 10px; - text-align: center; - } - - button { - border-radius: 999px; - padding: 0px 12px 0px 12px; - border: none; - cursor: pointer; - margin-bottom: 12px; - } - - .button-big { - background: linear-gradient(90deg, rgb(196, 167, 231), rgb(235, 188, 186)); - line-height: 50px; - } - - .button-big:hover { - background: rgb(49, 116, 143); - } - - .button-small { - background: #444; - line-height: 40px; - } - - .button-small:hover { - background: #555; - } - - .button-label-big { - color: #191724; - font-weight: bold; - font-size: 20px; - padding: 12px; - } - - .button-label-small { - color: rgb(156, 207, 216); - font-size: 16px; - padding: 12px; - } - - a { - color: rgb(156, 207, 216); - text-decoration: none; - } - - p, - li { - font-size: 16px; - } - - .dont-worry, - #msg { - font-size: 18px; - } - - .icon-warning { - color: #f6c177; - height: 4rem; - padding-top: 2rem; - } - - h1 { - font-size: 32px; - } - - code { - font-family: Fira, FiraCode, monospace; - } - - details { - background: #1f1d2e; - margin-bottom: 2rem; - padding: 0.5rem 1rem; - width: 40rem; - border-radius: 10px; - justify-content: center; - margin: auto; - } - - summary { - cursor: pointer; - } - - summary > * { - display: inline; - } - - @media screen and (max-width: 500px) { - details { - width: 50%; - } - `); - } - - async function checkUpdate() { - try { - 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(); - } - } catch (e) { - console.error(e); - renderError("UPDATE_CHECK", e); - throw e; - } - } - - function refresh() { - // Clear cache (service worker) - try { - navigator.serviceWorker.controller.postMessage("clear"); - navigator.serviceWorker.getRegistrations().then((registrations) => { - registrations.forEach((registration) => registration.unregister()); - }); - } catch (e) { - console.error(e); - } - - location.reload(); - } -})(); diff --git a/packages/backend/src/server/web/cli.css b/packages/backend/src/server/web/cli.css deleted file mode 100644 index 460a7b7f31..0000000000 --- a/packages/backend/src/server/web/cli.css +++ /dev/null @@ -1,92 +0,0 @@ -html { - background: #191724; -} -main { - background: #1f1d2e; - border-radius: 10px; -} -#tl > div { - border: 1px solid #908caa; - border-radius: 10px; - margin: 10px; - padding: 10px; - width: fit-content; -} -#tl > div > header { - font-weight: 700; - display: inline-flex; -} - -img { - border-radius: 10px; - margin-right: 10px; -} - -#form { - text-align: center; -} - -#calckey_app { - display: none !important; -} - -body, -html { - font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; - background-color: #191724; - color: #e0def4; - justify-content: center; - margin: auto; - padding: 10px; -} -button { - border-radius:999px; - padding:0 40px; - margin-top: 1rem; - border:none; - cursor:pointer; - margin-bottom:12px; - background:linear-gradient(90deg,#9ccfd8,#31748f); - line-height:50px; - color:#191724; - font-weight:700; - font-size:20px; - } -button:hover { - background: rgb(156, 207, 216); -} -a { - color: rgb(156, 207, 216); - text-decoration: none; -} -p, -li { - font-size: 16px; -} - -h1 { - font-size: 32px; -} -code { - font-family: Fira, FiraCode, monospace; -} -#text { - background-color: #444; - border: solid #aaa; - border-radius: 10px; - color: #e0def4; - margin-top: 3rem; - width: 20rem; - height: 5rem; - padding: 0.5rem; -} - -#text:focus { - border: solid #eee; -} - -@media screen and (max-width: 500px) { - #text { - width: 80% - } -} diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/src/server/web/cli.js deleted file mode 100644 index 85a61a2446..0000000000 --- a/packages/backend/src/server/web/cli.js +++ /dev/null @@ -1,72 +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 header = document.createElement("header"); - const name = document.createElement("p"); - const avatar = document.createElement("img"); - name.textContent = `${note.user.name} @${note.user.username}`; - avatar.src = note.user.avatarUrl; - avatar.style = "height: 40px"; - const text = document.createElement("div"); - text.textContent = `${note.text}`; - el.appendChild(header); - header.appendChild(avatar); - header.appendChild(name); - if (note.text) { - el.appendChild(text); - } - if (note.files) { - for (const file of note.files) { - const img = document.createElement("img"); - img.src = file.properties.thumbnailUrl; - el.appendChild(img); - } - } - tl.appendChild(el); - } - }); -}; diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts deleted file mode 100644 index 9cbeb28ae1..0000000000 --- a/packages/backend/src/server/web/feed.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Feed } from "feed"; -import { In, IsNull } from "typeorm"; -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; -import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js"; - -export default async function (user: User) { - const author = { - link: `${config.url}/@${user.username}`, - name: user.name || user.username, - }; - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - const notes = await Notes.find({ - where: { - userId: user.id, - renoteId: IsNull(), - 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: "Calckey", - description: `${user.notesCount} Notes, ${ - profile.ffVisibility === "public" ? user.followingCount : "?" - } Following, ${ - profile.ffVisibility === "public" ? user.followersCount : "?" - } Followers${profile.description ? ` · ${profile.description}` : ""}`, - link: author.link, - image: await Users.getAvatarUrl(user), - 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.findBy({ - id: In(note.fileIds), - }) - : []; - const file = files.find((file) => file.type.startsWith("image/")); - - feed.addItem({ - title: `New note by ${author.name}`, - link: `${config.url}/notes/${note.id}`, - date: note.createdAt, - description: note.cw || undefined, - content: note.text || undefined, - image: file ? DriveFiles.getPublicUrl(file) || undefined : undefined, - }); - } - - return feed; -} diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts deleted file mode 100644 index 028170cd75..0000000000 --- a/packages/backend/src/server/web/index.ts +++ /dev/null @@ -1,677 +0,0 @@ -/** - * Web Client Server - */ - -import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { readFileSync } from "node:fs"; -import Koa from "koa"; -import Router from "@koa/router"; -import send from "koa-send"; -import views from "koa-views"; -import sharp from "sharp"; -import { createBullBoard } from "@bull-board/api"; -import { BullAdapter } from "@bull-board/api/bullAdapter.js"; -import { KoaAdapter } from "@bull-board/koa"; -import { In, IsNull } from "typeorm"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import config from "@/config/index.js"; -import { - Users, - Notes, - UserProfiles, - Pages, - Channels, - Clips, - GalleryPosts, -} from "@/models/index.js"; -import * as Acct from "@/misc/acct.js"; -import { getNoteSummary } from "@/misc/get-note-summary.js"; -import { queues } from "@/queue/queues.js"; -import { genOpenapiSpec } from "../api/openapi/gen-spec.js"; -import { urlPreviewHandler } from "./url-preview.js"; -import { manifestHandler } from "./manifest.js"; -import packFeed from "./feed.js"; -import { MINUTE, DAY } from "@/const.js"; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const staticAssets = `${_dirname}/../../../assets/`; -const clientAssets = `${_dirname}/../../../../client/assets/`; -const assets = `${_dirname}/../../../../../built/_client_dist_/`; -const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; - -// Init app -const app = new Koa(); - -//#region Bull Dashboard -const bullBoardPath = "/queue"; - -// Authenticate -app.use(async (ctx, next) => { - if (ctx.path === bullBoardPath || ctx.path.startsWith(`${bullBoardPath}/`)) { - const token = ctx.cookies.get("token"); - if (token == null) { - ctx.status = 401; - return; - } - const user = await Users.findOneBy({ token }); - if (user == null || !(user.isAdmin || user.isModerator)) { - ctx.status = 403; - return; - } - } - await next(); -}); - -const serverAdapter = new KoaAdapter(); - -createBullBoard({ - queues: queues.map((q) => new BullAdapter(q)), - serverAdapter, -}); - -serverAdapter.setBasePath(bullBoardPath); -app.use(serverAdapter.registerPlugin()); -//#endregion - -// Init renderer -app.use( - views(`${_dirname}/views`, { - extension: "pug", - options: { - version: config.version, - getClientEntry: () => - process.env.NODE_ENV === "production" - ? config.clientEntry - : JSON.parse( - readFileSync( - `${_dirname}/../../../../../built/_client_dist_/manifest.json`, - "utf-8", - ), - )["src/init.ts"], - config, - }, - }), -); - -// Favicon Router -app.use(async (ctx, next) => { - if (ctx.path != "/favicon.ico") return next(); - const meta = await fetchMeta(); - if (meta.iconUrl === "") - ctx.body = readFileSync(`${_dirname}/../../../assets/favicon.ico`); - else ctx.redirect(meta.iconUrl); -}); - -// 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: 7 * DAY, - }); -}); - -router.get("/client-assets/(.*)", async (ctx) => { - await send(ctx as any, ctx.path.replace("/client-assets/", ""), { - root: clientAssets, - maxage: 7 * DAY, - }); -}); - -router.get("/assets/(.*)", async (ctx) => { - await send(ctx as any, ctx.path.replace("/assets/", ""), { - root: assets, - maxage: 7 * DAY, - }); -}); - -// 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: 30 * DAY, - }); -}); - -router.get("/twemoji-badge/(.*)", async (ctx) => { - const path = ctx.path.replace("/twemoji-badge/", ""); - - if (!path.match(/^[0-9a-f-]+\.png$/)) { - ctx.status = 404; - return; - } - - const mask = await sharp( - `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/${path.replace( - ".png", - "", - )}.svg`, - { density: 1000 }, - ) - .resize(488, 488) - .greyscale() - .normalise() - .linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast - .flatten({ background: "#000" }) - .extend({ - top: 12, - bottom: 12, - left: 12, - right: 12, - background: "#000", - }) - .toColorspace("b-w") - .png() - .toBuffer(); - - const buffer = await sharp({ - create: { - width: 512, - height: 512, - channels: 4, - background: { r: 0, g: 0, b: 0, alpha: 0 }, - }, - }) - .pipelineColorspace("b-w") - .boolean(mask, "eor") - .resize(96, 96) - .png() - .toBuffer(); - - ctx.set( - "Content-Security-Policy", - "default-src 'none'; style-src 'unsafe-inline'", - ); - ctx.set("Cache-Control", "max-age=2592000"); - ctx.set("Content-Type", "image/png"); - ctx.body = buffer; -}); - -// ServiceWorker -router.get("/sw.js", async (ctx) => { - await send(ctx as any, "/sw.js", { - root: swAssets, - maxage: 10 * MINUTE, - }); -}); - -// Manifest -router.get("/manifest.json", manifestHandler); - -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", urlPreviewHandler); - -router.get("/api.json", async (ctx) => { - ctx.body = genOpenapiSpec(); -}); - -const getFeed = async (acct: string) => { - const meta = await fetchMeta(); - if (meta.privateMode) { - return; - } - const { username, host } = Acct.parse(acct); - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: host ?? IsNull(), - isSuspended: false, - }); - - return user && (await packFeed(user)); -}; - -// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them. -const reUser = new RegExp( - "^/@(?[^/]+?)(?:.(?json|rss|atom))?(?:/(?[^/]+))?$", -); -router.get(reUser, async (ctx, next) => { - const groups = reUser.exec(ctx.originalUrl)?.groups; - if (!groups) { - await next(); - return; - } - - ctx.params = groups; - - console.log(ctx, ctx.params); - if (groups.feed) { - if (groups.sub) { - await next(); - return; - } - - switch (groups.feed) { - case "json": - await jsonFeed(ctx, next); - break; - case "rss": - await rssFeed(ctx, next); - break; - case "atom": - await atomFeed(ctx, next); - break; - } - return; - } - - await userPage(ctx, next); -}); - -// Atom -const atomFeed: Router.Middleware = 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 -const rssFeed: Router.Middleware = 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 -const jsonFeed: Router.Middleware = 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 -const userPage: Router.Middleware = async (ctx, next) => { - const userParam = ctx.params.user; - const subParam = ctx.params.sub; - const { username, host } = Acct.parse(userParam); - - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: host ?? IsNull(), - isSuspended: false, - }); - - if (user === null) { - await next(); - return; - } - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const meta = await fetchMeta(); - const me = profile.fields - ? profile.fields - .filter((filed) => filed.value?.match(/^https?:/)) - .map((field) => field.value) - : []; - - const userDetail = { - user, - profile, - me, - avatarUrl: await Users.getAvatarUrl(user), - sub: subParam, - instanceName: meta.name || "Calckey", - icon: meta.iconUrl, - themeColor: meta.themeColor, - privateMode: meta.privateMode, - }; - - await ctx.render("user", userDetail); - ctx.set("Cache-Control", "public, max-age=15"); -}; - -router.get("/users/:user", async (ctx) => { - const user = await Users.findOneBy({ - id: ctx.params.user, - host: IsNull(), - 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.findOneBy({ - id: ctx.params.note, - visibility: In(["public", "home"]), - }); - - if (note) { - const _note = await Notes.pack(note); - const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); - const meta = await fetchMeta(); - await ctx.render("note", { - note: _note, - profile, - avatarUrl: await Users.getAvatarUrl( - await Users.findOneByOrFail({ id: note.userId }), - ), - // TODO: Let locale changeable by instance setting - summary: getNoteSummary(_note), - instanceName: meta.name || "Calckey", - icon: meta.iconUrl, - privateMode: meta.privateMode, - themeColor: meta.themeColor, - }); - - ctx.set("Cache-Control", "public, max-age=15"); - - return; - } - - await next(); -}); - -router.get("/posts/:note", async (ctx, next) => { - const note = await Notes.findOneBy({ - id: ctx.params.note, - visibility: In(["public", "home"]), - }); - - if (note) { - const _note = await Notes.pack(note); - const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); - const meta = await fetchMeta(); - await ctx.render("note", { - note: _note, - profile, - avatarUrl: await Users.getAvatarUrl( - await Users.findOneByOrFail({ id: note.userId }), - ), - // TODO: Let locale changeable by instance setting - summary: getNoteSummary(_note), - instanceName: meta.name || "Calckey", - icon: meta.iconUrl, - privateMode: meta.privateMode, - themeColor: meta.themeColor, - }); - - ctx.set("Cache-Control", "public, max-age=15"); - - return; - } - - await next(); -}); - -// Page -router.get("/@:user/pages/:page", async (ctx, next) => { - const { username, host } = Acct.parse(ctx.params.user); - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: host ?? IsNull(), - }); - - if (user == null) return; - - const page = await Pages.findOneBy({ - name: ctx.params.page, - userId: user.id, - }); - - if (page) { - const _page = await Pages.pack(page); - const profile = await UserProfiles.findOneByOrFail({ userId: page.userId }); - const meta = await fetchMeta(); - await ctx.render("page", { - page: _page, - profile, - avatarUrl: await Users.getAvatarUrl( - await Users.findOneByOrFail({ id: page.userId }), - ), - instanceName: meta.name || "Calckey", - icon: meta.iconUrl, - themeColor: meta.themeColor, - privateMode: meta.privateMode, - }); - - if (["public"].includes(page.visibility)) { - ctx.set("Cache-Control", "public, max-age=15"); - } else { - ctx.set("Cache-Control", "private, max-age=0, must-revalidate"); - } - - return; - } - - await next(); -}); - -// Clip -// TODO: handling of private clips -router.get("/clips/:clip", async (ctx, next) => { - const clip = await Clips.findOneBy({ - id: ctx.params.clip, - }); - - if (clip) { - const _clip = await Clips.pack(clip); - const profile = await UserProfiles.findOneByOrFail({ userId: clip.userId }); - const meta = await fetchMeta(); - await ctx.render("clip", { - clip: _clip, - profile, - avatarUrl: await Users.getAvatarUrl( - await Users.findOneByOrFail({ id: clip.userId }), - ), - instanceName: meta.name || "Calckey", - privateMode: meta.privateMode, - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - ctx.set("Cache-Control", "public, max-age=15"); - - return; - } - - await next(); -}); - -// Gallery post -router.get("/gallery/:post", async (ctx, next) => { - const post = await GalleryPosts.findOneBy({ id: ctx.params.post }); - - if (post) { - const _post = await GalleryPosts.pack(post); - const profile = await UserProfiles.findOneByOrFail({ userId: post.userId }); - const meta = await fetchMeta(); - await ctx.render("gallery-post", { - post: _post, - profile, - avatarUrl: await Users.getAvatarUrl( - await Users.findOneByOrFail({ id: post.userId }), - ), - instanceName: meta.name || "Calckey", - icon: meta.iconUrl, - themeColor: meta.themeColor, - privateMode: meta.privateMode, - }); - - ctx.set("Cache-Control", "public, max-age=15"); - - return; - } - - await next(); -}); - -// Channel -router.get("/channels/:channel", async (ctx, next) => { - const channel = await Channels.findOneBy({ - 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 || "Calckey", - icon: meta.iconUrl, - themeColor: meta.themeColor, - privateMode: meta.privateMode, - }); - - ctx.set("Cache-Control", "public, max-age=15"); - - return; - } - - await next(); -}); -//#endregion - -router.get("/_info_card_", async (ctx) => { - const meta = await fetchMeta(true); - if (meta.privateMode) { - ctx.status = 403; - return; - } - - ctx.remove("X-Frame-Options"); - - await ctx.render("info-card", { - version: config.version, - host: config.host, - meta: meta, - originalUsersCount: await Users.countBy({ host: IsNull() }), - originalNotesCount: await Notes.countBy({ userHost: IsNull() }), - }); -}); - -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 = 0) => - [ - undefined, - ...target.split("/").filter((x) => x), - ...source - .split("/") - .filter((x) => x) - .splice(depth), - ].join("/"); - -router.get("/flush", async (ctx) => { - await ctx.render("flush"); -}); - -// If a non-WebSocket request comes in to streaming and base html is returned with cache, the path will be cached by Proxy, etc. and it will be wrong. -router.get("/streaming", async (ctx) => { - ctx.status = 503; - ctx.set("Cache-Control", "private, max-age=0"); -}); -router.get("/api/v1/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(); - let motd = ["Loading..."]; - if (meta.customMOTD.length > 0) { - motd = meta.customMOTD; - } - let splashIconUrl = meta.iconUrl; - if (meta.customSplashIcons.length > 0) { - splashIconUrl = - meta.customSplashIcons[ - Math.floor(Math.random() * meta.customSplashIcons.length) - ]; - } - await ctx.render("base", { - img: meta.bannerUrl, - title: meta.name || "Calckey", - instanceName: meta.name || "Calckey", - desc: meta.description, - icon: meta.iconUrl, - splashIcon: splashIconUrl, - themeColor: meta.themeColor, - randomMOTD: motd[Math.floor(Math.random() * motd.length)], - privateMode: meta.privateMode, - }); - ctx.set("Cache-Control", "public, max-age=3"); -}); - -// Register router -app.use(router.routes()); - -export default app; diff --git a/packages/backend/src/server/web/manifest.json b/packages/backend/src/server/web/manifest.json deleted file mode 100644 index 1e662fb202..0000000000 --- a/packages/backend/src/server/web/manifest.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "short_name": "Calckey", - "name": "Calckey", - "description": "An open source, decentralized social media platform that's free forever!", - "start_url": "/", - "display": "standalone", - "background_color": "#1f1d2e", - "theme_color": "#31748f", - "orientation": "portrait-primary", - "icons": [ - { - "src": "/static-assets/icons/192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any" - }, - { - "src": "/static-assets/icons/512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any" - }, - { - "src": "/static-assets/icons/maskable.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/static-assets/icons/monochrome.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "monochrome" - } - ], - "share_target": { - "action": "/share/", - "params": { - "title": "title", - "text": "text", - "url": "url" - } - }, - "screenshots" : [ - { - "src": "/static-assets/screenshots/1.webp", - "sizes": "1195x579", - "type": "image/webp", - "platform": "narrow", - "label": "Profile page" - }, - { - "src": "/static-assets/screenshots/2.webp", - "sizes": "1195x579", - "type": "image/webp", - "platform": "narrow", - "label": "Posts" - } - ], - "shortcuts" : [ - { - "name": "Notifications", - "short_name": "Notifs", - "url": "/my/notifications" - }, - { - "name": "Chats", - "url": "/my/messaging" - } - ], - "categories": [ - "social" - ] -} diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts deleted file mode 100644 index 31acc42f6b..0000000000 --- a/packages/backend/src/server/web/manifest.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type Koa from "koa"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import manifest from "./manifest.json" assert { type: "json" }; - -export const manifestHandler = async (ctx: Koa.Context) => { - // TODO - //const res = structuredClone(manifest); - const res = JSON.parse(JSON.stringify(manifest)); - - const instance = await fetchMeta(true); - - res.short_name = instance.name || "Calckey"; - res.name = instance.name || "Calckey"; - if (instance.themeColor) res.theme_color = instance.themeColor; - - ctx.set("Cache-Control", "max-age=300"); - ctx.body = res; -}; diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css deleted file mode 100644 index ee42b9deba..0000000000 --- a/packages/backend/src/server/web/style.css +++ /dev/null @@ -1,127 +0,0 @@ -html, body { - 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.2s ease; -} - -#splashIcon { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - width: 64px; - height: 64px; - pointer-events: none; - animation-duration: 1s; - animation-iteration-count: infinite; - animation-name: tada; -} - -#splashSpinner { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - display: inline-block; - width: 28px; - height: 28px; - transform: translateY(110px); - display: none; - color: var(--accent); -} -#splashSpinner > .spinner { - position: absolute; - top: 0; - left: 0; - width: 28px; - height: 28px; - fill-rule: evenodd; - clip-rule: evenodd; - stroke-linecap: round; - stroke-linejoin: round; - stroke-miterlimit: 1.5; -} -#splashSpinner > .spinner.bg { - opacity: 0.275; -} -#splashSpinner > .spinner.fg { - animation: splashSpinner 0.5s linear infinite; -} - -@keyframes splashSpinner { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -@keyframes tada { - 0% { - 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); - } - - 100% { - transform: scale3d(1, 1, 1); - } -} - -@media(prefers-reduced-motion) { - #splashSpinner { - display: block; - } - - #splashIcon { - animation: none; - } -} - -#splashText { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - display: inline-block; - width: 70%; - height: 0; - text-align: center; - padding-top: 100px; - font-family: sans-serif; -} diff --git a/packages/backend/src/server/web/url-preview.ts b/packages/backend/src/server/web/url-preview.ts deleted file mode 100644 index c9f3b6cac9..0000000000 --- a/packages/backend/src/server/web/url-preview.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type Koa from "koa"; -import summaly from "summaly"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import Logger from "@/services/logger.js"; -import config from "@/config/index.js"; -import { query } from "@/prelude/url.js"; -import { getJson } from "@/misc/fetch.js"; - -const logger = new Logger("url-preview"); - -export const urlPreviewHandler = async (ctx: Koa.Context) => { - const url = ctx.query.url; - if (typeof url !== "string") { - ctx.status = 400; - return; - } - - const lang = ctx.query.lang; - if (Array.isArray(lang)) { - ctx.status = 400; - return; - } - - const meta = await fetchMeta(); - - logger.info( - meta.summalyProxy - ? `(Proxy) Getting preview of ${url}@${lang} ...` - : `Getting preview of ${url}@${lang} ...`, - ); - - try { - const summary = meta.summalyProxy - ? await getJson( - `${meta.summalyProxy}?${query({ - url: url, - lang: lang ?? "en-US", - })}`, - ) - : await summaly.default(url, { - followRedirects: false, - lang: lang ?? "en-US", - }); - - logger.succ(`Got preview of ${url}: ${summary.title}`); - - if ( - summary.url && - !(summary.url.startsWith("http://") || summary.url.startsWith("https://")) - ) { - throw new Error("unsupported schema included"); - } - - if ( - summary.player?.url && - !( - summary.player.url.startsWith("http://") || - summary.player.url.startsWith("https://") - ) - ) { - throw new Error("unsupported schema included"); - } - - summary.icon = wrap(summary.icon); - summary.thumbnail = wrap(summary.thumbnail); - - // Cache 7days - ctx.set("Cache-Control", "max-age=604800, immutable"); - - ctx.body = summary; - } catch (err) { - logger.warn(`Failed to get preview of ${url}: ${err}`); - 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.webp?${query({ - url, - preview: "1", - })}` - : url - : null; -} diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug deleted file mode 100644 index b5841883bd..0000000000 --- a/packages/backend/src/server/web/views/base.pug +++ /dev/null @@ -1,96 +0,0 @@ -block vars - -block loadClientEntry - - const clientEntry = getClientEntry(); - -doctype html - -// - - - ___ _ _ - / __\__ _| | ___| | _____ _ _ - / / / _` | |/ __| |/ / _ \ | | | - / /__| (_| | | (__| < __/ |_| | - \____/\__,_|_|\___|_|\_\___|\__, | - (___/ - - Thank you for using Calckey! - If you are reading this message... how about joining the development? - https://codeberg.org/calckey/calckey - -html - - - var timestamp = Date.now(); - - head - meta(charset='utf-8') - meta(name='application-name' content='Calckey') - meta(name='referrer' content='origin') - meta(name='darkreader-lock' content='') - meta(name='theme-color' content= themeColor || '#31748f') - meta(name='theme-color-orig' content= themeColor || '#31748f') - meta(property='twitter:card' content='summary') - meta(property='og:site_name' content= instanceName || 'Calckey') - meta(name='viewport' content='width=device-width, initial-scale=1') - link(rel='icon' href= icon || `/favicon.ico?${ timestamp }`) - link(rel='apple-touch-icon' href= icon || `/apple-touch-icon.png?${ timestamp }`) - link(rel='manifest' href='/manifest.json') - link(rel='prefetch' href=`/static-assets/badges/info.png?${ timestamp }`) - link(rel='prefetch' href=`/static-assets/badges/not-found.png?${ timestamp }`) - link(rel='prefetch' href=`/static-assets/badges/error.png?${ timestamp }`) - link(rel='stylesheet' href=`/static-assets/instance.css?${ timestamp }`) - link(rel='modulepreload' href=`/assets/${clientEntry.file}`) - - if Array.isArray(clientEntry.css) - each href in clientEntry.css - link(rel='stylesheet' href=`/assets/${href}`) - - title - block title - = title || 'Calckey' - - block desc - meta(name='description' content=desc || 'An open source, decentralized social media platform that\'s free forever! 🚀') - - block meta - if privateMode - meta(name='robots' content='noindex') - - block og - meta(property='og:title' content=title || 'Calckey') - meta(property='og:description' content=desc || 'An open source, decentralized social media platform that\'s free forever! 🚀') - meta(property='og:image' content=img) - meta(property='og:image:alt' content=alt || 'Pfp') - - style - include ../style.css - - script. - var VERSION = "#{version}"; - var CLIENT_ENTRY = "#{clientEntry.file}"; - - script - include ../boot.js - - body - noscript: p - | JavaScriptを有効にしてください - br - | Please turn on your JavaScript - div#splash - img#splashIcon(src= splashIcon || `/static-assets/splash.png?${ timestamp }`) - span#splashText - block randomMOTD - = randomMOTD - div#splashSpinner - - - - - - - - - - - block content diff --git a/packages/backend/src/server/web/views/bios.pug b/packages/backend/src/server/web/views/bios.pug deleted file mode 100644 index 408ebb27b1..0000000000 --- a/packages/backend/src/server/web/views/bios.pug +++ /dev/null @@ -1,21 +0,0 @@ -doctype html - -html - - head - meta(charset='utf-8') - meta(name='application-name' content='Calckey') - meta(name='viewport' content='width=device-width, initial-scale=1.0') - title Calckey Repair Tool - style - include ../bios.css - script - include ../bios.js - - body - header - h1 Calckey Repair Tool v#{version} - main - div.tabs - button#ls Edit local storage - div#content diff --git a/packages/backend/src/server/web/views/channel.pug b/packages/backend/src/server/web/views/channel.pug deleted file mode 100644 index c4594b7666..0000000000 --- a/packages/backend/src/server/web/views/channel.pug +++ /dev/null @@ -1,20 +0,0 @@ -extends ./base - -block vars - - const title = privateMode ? instanceName : channel.name; - - const url = `${config.url}/channels/${channel.id}`; - -block title - = `${title} | ${instanceName}` - -block desc - unless privateMode - meta(name='description' content=channel.description) - -block og - unless privateMode - 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) diff --git a/packages/backend/src/server/web/views/cli.pug b/packages/backend/src/server/web/views/cli.pug deleted file mode 100644 index 2abd5ae950..0000000000 --- a/packages/backend/src/server/web/views/cli.pug +++ /dev/null @@ -1,23 +0,0 @@ -doctype html - -html - - head - meta(charset='utf-8') - meta(name='application-name' content='Calckey') - meta(name='viewport' content='width=device-width, initial-scale=1.0') - title Calckey Cli - style - include ../cli.css - script - include ../cli.js - - body - header - h1 Calckey Simple Client v#{version} - main - div#form - textarea#text - br - button#submit Post - div#tl diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug deleted file mode 100644 index 2432470c10..0000000000 --- a/packages/backend/src/server/web/views/clip.pug +++ /dev/null @@ -1,34 +0,0 @@ -extends ./base - -block vars - - const user = clip.user; - - const title = privateMode ? instanceName : clip.name; - - const url = `${config.url}/clips/${clip.id}`; - -block title - = `${title} | ${instanceName}` - -block desc - unless privateMode - meta(name='description' content= clip.description) - -block og - unless privateMode - 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= avatarUrl) - -block meta - unless privateMode - 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) - - // todo - if user.twitter - meta(name='twitter:creator' content=`@${user.twitter.screenName}`) diff --git a/packages/backend/src/server/web/views/flush.pug b/packages/backend/src/server/web/views/flush.pug deleted file mode 100644 index c3de247270..0000000000 --- a/packages/backend/src/server/web/views/flush.pug +++ /dev/null @@ -1,71 +0,0 @@ -doctype html - -html - head - meta(charset='utf-8') - meta(name='application-name' content='Calckey') - meta(name='viewport' content='width=device-width, initial-scale=1.0') - title Flush Calckey - style. - * { - font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; - } - body, - html { - background-color: #191724; - color: #e0def4; - justify-content: center; - margin: auto; - padding: 10px; - text-align: center; - } - a { - color: rgb(156, 207, 216); - text-decoration: none; - } - - body - #msg - script. - const msg = document.getElementById('msg'); - const successText = `\nSuccess Flush! Back to Calckey\n成功しました。Calckeyを開き直してください。`; - - 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 new 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/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug deleted file mode 100644 index 1b1c2fbfbd..0000000000 --- a/packages/backend/src/server/web/views/gallery-post.pug +++ /dev/null @@ -1,36 +0,0 @@ -extends ./base - -block vars - - const user = post.user; - - const title = privateMode ? instanceName : post.title; - - const url = `${config.url}/gallery/${post.id}`; - -block title - = `${title} | ${instanceName}` - -block desc - unless privateMode - meta(name='description' content= post.description) - -block og - unless privateMode - 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 - unless privateMode - 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) - - // todo - if user.twitter - meta(name='twitter:creator' content=`@${user.twitter.screenName}`) - - if !user.host - link(rel='alternate' href=url type='application/activity+json') diff --git a/packages/backend/src/server/web/views/info-card.pug b/packages/backend/src/server/web/views/info-card.pug deleted file mode 100644 index be52d0c39f..0000000000 --- a/packages/backend/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='Calckey') - title= meta.name || host - style. - html, body { - margin: 0; - padding: 0; - min-height: 100vh; - background: #fff; - } - - #a { - display: block; - } - - #banner { - background-size: cover; - background-position: center center; - } - - #title { - display: inline-block; - margin: 24px; - padding: 0.5em 0.8em; - color: #fff; - background: rgba(0, 0, 0, 0.5); - font-weight: bold; - font-size: 1.3em; - } - - #content { - overflow: auto; - color: #353c3e; - } - - #description { - margin: 24px; - } - - body - a#a(href=`https://${host}` target="_blank") - header#banner(style=`background-image: url(${meta.bannerUrl})`) - div#title= meta.name || host - div#content - div#description= meta.description diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug deleted file mode 100644 index 476d42d9e8..0000000000 --- a/packages/backend/src/server/web/views/note.pug +++ /dev/null @@ -1,56 +0,0 @@ -extends ./base - -block vars - - const user = note.user; - - const title = privateMode ? instanceName : (user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}`); - - const url = `${config.url}/notes/${note.id}`; - - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null; - - const isImage = note.files.length !== 0 && note.files[0].type.startsWith('image'); - - const isVideo = note.files.length !== 0 && note.files[0].type.startsWith('video'); - - const imageUrl = isImage ? note.files[0].url : isVideo ? note.files[0].thumbnailUrl : avatarUrl; - -block title - = `${title} | ${instanceName}` - -block desc - unless privateMode - meta(name='description' content= summary) - -block og - unless privateMode - 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= imageUrl) - if isImage && !note.files[0].isSensitive - meta(property='og:image:width' content=note.files[0].properties.width) - meta(property='og:image:height' content=note.files[0].properties.height) - meta(property='og:image:type' content=note.files[0].type) - meta(property='twitter:card' content="summary_large_image") - if isVideo - meta(property='og:video:type' content=note.files[0].type) - meta(property='og:video' content=note.files[0].url) - -block meta - unless privateMode - if user.host || isRenote || 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) - - // todo - if user.twitter - meta(name='twitter:creator' content=`@${user.twitter.screenName}`) - - if note.prev - link(rel='prev' href=`${config.url}/notes/${note.prev}`) - if note.next - link(rel='next' href=`${config.url}/notes/${note.next}`) - - if !user.host - link(rel='alternate' href=url type='application/activity+json') - if note.uri - link(rel='alternate' href=note.uri type='application/activity+json') diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug deleted file mode 100644 index 1095282131..0000000000 --- a/packages/backend/src/server/web/views/page.pug +++ /dev/null @@ -1,34 +0,0 @@ -extends ./base - -block vars - - const user = page.user; - - const title = privateMode ? instanceName : page.title; - - const url = `${config.url}/@${user.username}/${page.name}`; - -block title - = `${title} | ${instanceName}` - -block desc - unless privateMode - meta(name='description' content= page.summary) - -block og - unless privateMode - 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 : avatarUrl) - -block meta - unless privateMode - 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) - - // todo - if user.twitter - meta(name='twitter:creator' content=`@${user.twitter.screenName}`) diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug deleted file mode 100644 index cc14dedb3a..0000000000 --- a/packages/backend/src/server/web/views/user.pug +++ /dev/null @@ -1,42 +0,0 @@ -extends ./base - -block vars - - const title = privateMode ? instanceName : (user.name ? `${user.name} (@${user.username})` : `@${user.username}`); - - const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`; - -block title - = `${title} | ${instanceName}` - -block desc - unless privateMode - meta(name='description' content= profile.description) - -block og - unless privateMode - 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= avatarUrl) - -block meta - unless privateMode - 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) - - if profile.twitter - meta(name='twitter:creator' content=`@${profile.twitter.screenName}`) - - if !sub - if !user.host - link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json') - if user.uri - link(rel='alternate' href=user.uri type='application/activity+json') - if profile.url - link(rel='alternate' href=profile.url type='text/html') - - each m in me - link(rel='me' href=`${m}`) diff --git a/packages/backend/src/server/well-known.ts b/packages/backend/src/server/well-known.ts deleted file mode 100644 index 5af3b2c84b..0000000000 --- a/packages/backend/src/server/well-known.ts +++ /dev/null @@ -1,191 +0,0 @@ -import Router from "@koa/router"; - -import config from "@/config/index.js"; -import * as Acct from "@/misc/acct.js"; -import { links } from "./nodeinfo.js"; -import { escapeAttribute, escapeValue } from "@/prelude/xml.js"; -import { Users } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import type { FindOptionsWhere } from "typeorm"; -import { IsNull } from "typeorm"; - -// 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: { - rel: "lrdd", - 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}`, - }, - ], - }; -}); - -if (config.twa != null) { - router.get("/.well-known/assetlinks.json", async (ctx) => { - ctx.set("Content-Type", "application/json"); - ctx.body = [ - { - relation: ["delegate_permission/common.handle_all_urls"], - target: { - namespace: config.twa.nameSpace, - package_name: config.twa.packageName, - sha256_cert_fingerprints: config.twa.sha256CertFingerprints, - }, - }, - ]; - }); -} - -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"]): FindOptionsWhere => ({ - id, - host: IsNull(), - isSuspended: false, - }); - - const generateQuery = (resource: string): FindOptionsWhere | number => - resource.startsWith(`${config.url.toLowerCase()}/users/`) - ? fromId(resource.split("/").pop()!) - : fromAcct( - Acct.parse( - resource.startsWith(`${config.url.toLowerCase()}/@`) - ? resource.split("/").pop()! - : resource.startsWith("acct:") - ? resource.slice("acct:".length) - : resource, - ), - ); - - const fromAcct = (acct: Acct.Acct): FindOptionsWhere | number => - !acct.host || acct.host === config.host.toLowerCase() - ? { - usernameLower: acct.username, - host: IsNull(), - 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.findOneBy(query); - - if (user == null) { - ctx.status = 404; - return; - } - - const subject = `acct:${user.username}@${config.host}`; - const self = { - rel: "self", - type: "application/activity+json", - href: `${config.url}/users/${user.id}`, - }; - const profilePage = { - rel: "http://webfinger.net/rel/profile-page", - type: "text/html", - href: `${config.url}/@${user.username}`, - }; - const subscribe = { - rel: "http://ostatus.org/schema/1.0/subscribe", - template: `${config.url}/authorize-follow?acct={uri}`, - }; - - if (ctx.accepts(jrd, xrd) === xrd) { - ctx.body = XRD( - { element: "Subject", value: subject }, - { element: "Link", attributes: self }, - { element: "Link", attributes: profilePage }, - { element: "Link", attributes: subscribe }, - ); - ctx.type = xrd; - } else { - ctx.body = { - subject, - links: [self, profilePage, subscribe], - }; - ctx.type = jrd; - } - - ctx.vary("Accept"); - ctx.set("Cache-Control", "public, max-age=180"); -}); - -// Return 404 for other .well-known -router.all(allPath, async (ctx) => { - ctx.status = 404; -}); - -export default router; diff --git a/packages/backend/src/services/add-note-to-antenna.ts b/packages/backend/src/services/add-note-to-antenna.ts deleted file mode 100644 index 38979acb40..0000000000 --- a/packages/backend/src/services/add-note-to-antenna.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Antenna } from "@/models/entities/antenna.js"; -import type { Note } from "@/models/entities/note.js"; -import { AntennaNotes, Mutings, Notes } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { isUserRelated } from "@/misc/is-user-related.js"; -import { publishAntennaStream, publishMainStream } from "@/services/stream.js"; -import type { User } from "@/models/entities/user.js"; - -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.findOneByOrFail({ id: note.replyId }); - } - if (note.renoteId != null) { - _note.renote = await Notes.findOneByOrFail({ id: note.renoteId }); - } - - if (isUserRelated(_note, new Set(mutings.map((x) => x.muteeId)))) { - return; - } - - // 2秒経っても既読にならなかったら通知 - setTimeout(async () => { - const unread = await AntennaNotes.findOneBy({ - antennaId: antenna.id, - read: false, - }); - if (unread) { - publishMainStream(antenna.userId, "unreadAntenna", antenna); - } - }, 2000); - } -} diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts deleted file mode 100644 index 60bd6e9431..0000000000 --- a/packages/backend/src/services/blocking/create.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { publishMainStream, publishUserEvent } from "@/services/stream.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderFollow from "@/remote/activitypub/renderer/follow.js"; -import renderUndo from "@/remote/activitypub/renderer/undo.js"; -import { renderBlock } from "@/remote/activitypub/renderer/block.js"; -import { deliver } from "@/queue/index.js"; -import renderReject from "@/remote/activitypub/renderer/reject.js"; -import type { Blocking } from "@/models/entities/blocking.js"; -import type { User } from "@/models/entities/user.js"; -import { - Blockings, - Users, - FollowRequests, - Followings, - UserListJoinings, - UserLists, -} from "@/models/index.js"; -import { perUserFollowingChart } from "@/services/chart/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import { getActiveWebhooks } from "@/misc/webhook-cache.js"; -import { webhookDeliver } from "@/queue/index.js"; - -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), - ]); - - const blocking = { - id: genId(), - createdAt: new Date(), - blocker, - blockerId: blocker.id, - blockee, - blockeeId: blockee.id, - } as Blocking; - - await Blockings.insert(blocking); - - if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { - const content = renderActivity(renderBlock(blocking)); - deliver(blocker, content, blockee.inbox); - } -} - -async function cancelRequest(follower: User, followee: User) { - const request = await FollowRequests.findOneBy({ - 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(async (packed) => { - publishUserEvent(follower.id, "unfollow", packed); - publishMainStream(follower.id, "unfollow", packed); - - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === follower.id && x.on.includes("unfollow"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "unfollow", { - user: 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.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (following == null) { - return; - } - - await Promise.all([ - Followings.delete(following.id), - Users.decrement({ id: follower.id }, "followingCount", 1), - Users.decrement({ id: followee.id }, "followersCount", 1), - perUserFollowingChart.update(follower, followee, false), - ]); - - // Publish unfollow event - if (Users.isLocalUser(follower)) { - Users.pack(followee, follower, { - detail: true, - }).then(async (packed) => { - publishUserEvent(follower.id, "unfollow", packed); - publishMainStream(follower.id, "unfollow", packed); - - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === follower.id && x.on.includes("unfollow"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "unfollow", { - user: 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.findBy({ - userId: listOwner.id, - }); - - for (const userList of userLists) { - await UserListJoinings.delete({ - userListId: userList.id, - userId: user.id, - }); - } -} diff --git a/packages/backend/src/services/blocking/delete.ts b/packages/backend/src/services/blocking/delete.ts deleted file mode 100644 index 67f1e76f0e..0000000000 --- a/packages/backend/src/services/blocking/delete.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { renderBlock } from "@/remote/activitypub/renderer/block.js"; -import renderUndo from "@/remote/activitypub/renderer/undo.js"; -import { deliver } from "@/queue/index.js"; -import Logger from "../logger.js"; -import type { CacheableUser } from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; -import { Blockings, Users } from "@/models/index.js"; - -const logger = new Logger("blocking/delete"); - -export default async function (blocker: CacheableUser, blockee: CacheableUser) { - const blocking = await Blockings.findOneBy({ - blockerId: blocker.id, - blockeeId: blockee.id, - }); - - if (blocking == null) { - logger.warn( - "ブロック解除がリクエストされましたがブロックしていませんでした", - ); - return; - } - - // Since we already have the blocker and blockee, we do not need to fetch - // them in the query above and can just manually insert them here. - blocking.blocker = blocker; - blocking.blockee = blockee; - - Blockings.delete(blocking.id); - - // deliver if remote bloking - if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { - const content = renderActivity(renderUndo(renderBlock(blocking), blocker)); - deliver(blocker, content, blockee.inbox); - } -} diff --git a/packages/backend/src/services/chart/charts/active-users.ts b/packages/backend/src/services/chart/charts/active-users.ts deleted file mode 100644 index 7a0c45cfaf..0000000000 --- a/packages/backend/src/services/chart/charts/active-users.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import type { User } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; -import { name, schema } from "./entities/active-users.js"; - -const week = 1000 * 60 * 60 * 24 * 7; -const month = 1000 * 60 * 60 * 24 * 30; -const year = 1000 * 60 * 60 * 24 * 365; - -/** - * アクティブユーザーに関するチャート - */ - -export default class ActiveUsersChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async read(user: { - id: User["id"]; - host: null; - createdAt: User["createdAt"]; - }): Promise { - await this.commit({ - read: [user.id], - registeredWithinWeek: - Date.now() - user.createdAt.getTime() < week ? [user.id] : [], - registeredWithinMonth: - Date.now() - user.createdAt.getTime() < month ? [user.id] : [], - registeredWithinYear: - Date.now() - user.createdAt.getTime() < year ? [user.id] : [], - registeredOutsideWeek: - Date.now() - user.createdAt.getTime() > week ? [user.id] : [], - registeredOutsideMonth: - Date.now() - user.createdAt.getTime() > month ? [user.id] : [], - registeredOutsideYear: - Date.now() - user.createdAt.getTime() > year ? [user.id] : [], - }); - } - - public async write(user: { - id: User["id"]; - host: null; - createdAt: User["createdAt"]; - }): Promise { - await this.commit({ - write: [user.id], - }); - } -} diff --git a/packages/backend/src/services/chart/charts/ap-request.ts b/packages/backend/src/services/chart/charts/ap-request.ts deleted file mode 100644 index 6e56be0b36..0000000000 --- a/packages/backend/src/services/chart/charts/ap-request.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { name, schema } from "./entities/ap-request.js"; - -/** - * Chart about ActivityPub requests - */ - -export default class ApRequestChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async deliverSucc(): Promise { - await this.commit({ - deliverSucceeded: 1, - }); - } - - public async deliverFail(): Promise { - await this.commit({ - deliverFailed: 1, - }); - } - - public async inbox(): Promise { - await this.commit({ - inboxReceived: 1, - }); - } -} diff --git a/packages/backend/src/services/chart/charts/drive.ts b/packages/backend/src/services/chart/charts/drive.ts deleted file mode 100644 index 9793ff79dc..0000000000 --- a/packages/backend/src/services/chart/charts/drive.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { DriveFiles } from "@/models/index.js"; -import { Not, IsNull } from "typeorm"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { name, schema } from "./entities/drive.js"; - -/** - * ドライブに関するチャート - */ - -export default class DriveChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(file: DriveFile, isAdditional: boolean): Promise { - const fileSizeKb = file.size / 1000; - await this.commit( - file.userHost === null - ? { - "local.incCount": isAdditional ? 1 : 0, - "local.incSize": isAdditional ? fileSizeKb : 0, - "local.decCount": isAdditional ? 0 : 1, - "local.decSize": isAdditional ? 0 : fileSizeKb, - } - : { - "remote.incCount": isAdditional ? 1 : 0, - "remote.incSize": isAdditional ? fileSizeKb : 0, - "remote.decCount": isAdditional ? 0 : 1, - "remote.decSize": isAdditional ? 0 : fileSizeKb, - }, - ); - } -} diff --git a/packages/backend/src/services/chart/charts/entities/active-users.ts b/packages/backend/src/services/chart/charts/entities/active-users.ts deleted file mode 100644 index 4e5fa37a27..0000000000 --- a/packages/backend/src/services/chart/charts/entities/active-users.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "activeUsers"; - -export const schema = { - readWrite: { intersection: ["read", "write"], range: "small" }, - read: { uniqueIncrement: true, range: "small" }, - write: { uniqueIncrement: true, range: "small" }, - registeredWithinWeek: { uniqueIncrement: true, range: "small" }, - registeredWithinMonth: { uniqueIncrement: true, range: "small" }, - registeredWithinYear: { uniqueIncrement: true, range: "small" }, - registeredOutsideWeek: { uniqueIncrement: true, range: "small" }, - registeredOutsideMonth: { uniqueIncrement: true, range: "small" }, - registeredOutsideYear: { uniqueIncrement: true, range: "small" }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/ap-request.ts b/packages/backend/src/services/chart/charts/entities/ap-request.ts deleted file mode 100644 index eb02201742..0000000000 --- a/packages/backend/src/services/chart/charts/entities/ap-request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "apRequest"; - -export const schema = { - deliverFailed: {}, - deliverSucceeded: {}, - inboxReceived: {}, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/drive.ts b/packages/backend/src/services/chart/charts/entities/drive.ts deleted file mode 100644 index 0280ec655f..0000000000 --- a/packages/backend/src/services/chart/charts/entities/drive.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "drive"; - -export const schema = { - "local.incCount": {}, - "local.incSize": {}, // in kilobyte - "local.decCount": {}, - "local.decSize": {}, // in kilobyte - "remote.incCount": {}, - "remote.incSize": {}, // in kilobyte - "remote.decCount": {}, - "remote.decSize": {}, // in kilobyte -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/federation.ts b/packages/backend/src/services/chart/charts/entities/federation.ts deleted file mode 100644 index b77e020961..0000000000 --- a/packages/backend/src/services/chart/charts/entities/federation.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "federation"; - -export const schema = { - deliveredInstances: { uniqueIncrement: true, range: "small" }, - inboxInstances: { uniqueIncrement: true, range: "small" }, - stalled: { uniqueIncrement: true, range: "small" }, - sub: { accumulate: true, range: "small" }, - pub: { accumulate: true, range: "small" }, - pubsub: { accumulate: true, range: "small" }, - subActive: { accumulate: true, range: "small" }, - pubActive: { accumulate: true, range: "small" }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/hashtag.ts b/packages/backend/src/services/chart/charts/entities/hashtag.ts deleted file mode 100644 index 77964b4ca1..0000000000 --- a/packages/backend/src/services/chart/charts/entities/hashtag.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "hashtag"; - -export const schema = { - "local.users": { uniqueIncrement: true }, - "remote.users": { uniqueIncrement: true }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/instance.ts b/packages/backend/src/services/chart/charts/entities/instance.ts deleted file mode 100644 index a75dea475b..0000000000 --- a/packages/backend/src/services/chart/charts/entities/instance.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "instance"; - -export const schema = { - "requests.failed": { range: "small" }, - "requests.succeeded": { range: "small" }, - "requests.received": { range: "small" }, - "notes.total": { accumulate: true }, - "notes.inc": {}, - "notes.dec": {}, - "notes.diffs.normal": {}, - "notes.diffs.reply": {}, - "notes.diffs.renote": {}, - "notes.diffs.withFile": {}, - "users.total": { accumulate: true }, - "users.inc": { range: "small" }, - "users.dec": { range: "small" }, - "following.total": { accumulate: true }, - "following.inc": { range: "small" }, - "following.dec": { range: "small" }, - "followers.total": { accumulate: true }, - "followers.inc": { range: "small" }, - "followers.dec": { range: "small" }, - "drive.totalFiles": { accumulate: true }, - "drive.incFiles": {}, - "drive.decFiles": {}, - "drive.incUsage": {}, // in kilobyte - "drive.decUsage": {}, // in kilobyte -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/notes.ts b/packages/backend/src/services/chart/charts/entities/notes.ts deleted file mode 100644 index 04e75a2f29..0000000000 --- a/packages/backend/src/services/chart/charts/entities/notes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "notes"; - -export const schema = { - "local.total": { accumulate: true }, - "local.inc": {}, - "local.dec": {}, - "local.diffs.normal": {}, - "local.diffs.reply": {}, - "local.diffs.renote": {}, - "local.diffs.withFile": {}, - "remote.total": { accumulate: true }, - "remote.inc": {}, - "remote.dec": {}, - "remote.diffs.normal": {}, - "remote.diffs.reply": {}, - "remote.diffs.renote": {}, - "remote.diffs.withFile": {}, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/per-user-drive.ts b/packages/backend/src/services/chart/charts/entities/per-user-drive.ts deleted file mode 100644 index d9dcd3d35e..0000000000 --- a/packages/backend/src/services/chart/charts/entities/per-user-drive.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "perUserDrive"; - -export const schema = { - totalCount: { accumulate: true }, - totalSize: { accumulate: true }, // in kilobyte - incCount: { range: "small" }, - incSize: {}, // in kilobyte - decCount: { range: "small" }, - decSize: {}, // in kilobyte -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/per-user-following.ts b/packages/backend/src/services/chart/charts/entities/per-user-following.ts deleted file mode 100644 index 3cbeec1114..0000000000 --- a/packages/backend/src/services/chart/charts/entities/per-user-following.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "perUserFollowing"; - -export const schema = { - "local.followings.total": { accumulate: true }, - "local.followings.inc": { range: "small" }, - "local.followings.dec": { range: "small" }, - "local.followers.total": { accumulate: true }, - "local.followers.inc": { range: "small" }, - "local.followers.dec": { range: "small" }, - "remote.followings.total": { accumulate: true }, - "remote.followings.inc": { range: "small" }, - "remote.followings.dec": { range: "small" }, - "remote.followers.total": { accumulate: true }, - "remote.followers.inc": { range: "small" }, - "remote.followers.dec": { range: "small" }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/per-user-notes.ts b/packages/backend/src/services/chart/charts/entities/per-user-notes.ts deleted file mode 100644 index 30c22e2f46..0000000000 --- a/packages/backend/src/services/chart/charts/entities/per-user-notes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "perUserNotes"; - -export const schema = { - total: { accumulate: true }, - inc: { range: "small" }, - dec: { range: "small" }, - "diffs.normal": { range: "small" }, - "diffs.reply": { range: "small" }, - "diffs.renote": { range: "small" }, - "diffs.withFile": { range: "small" }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/per-user-reactions.ts b/packages/backend/src/services/chart/charts/entities/per-user-reactions.ts deleted file mode 100644 index f281531c0c..0000000000 --- a/packages/backend/src/services/chart/charts/entities/per-user-reactions.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "perUserReaction"; - -export const schema = { - "local.count": { range: "small" }, - "remote.count": { range: "small" }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/test-grouped.ts b/packages/backend/src/services/chart/charts/entities/test-grouped.ts deleted file mode 100644 index 428f2bb36c..0000000000 --- a/packages/backend/src/services/chart/charts/entities/test-grouped.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "testGrouped"; - -export const schema = { - "foo.total": { accumulate: true }, - "foo.inc": {}, - "foo.dec": {}, -} as const; - -export const entity = Chart.schemaToEntity(name, schema, true); diff --git a/packages/backend/src/services/chart/charts/entities/test-intersection.ts b/packages/backend/src/services/chart/charts/entities/test-intersection.ts deleted file mode 100644 index 30d8753d7a..0000000000 --- a/packages/backend/src/services/chart/charts/entities/test-intersection.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "testIntersection"; - -export const schema = { - a: { uniqueIncrement: true }, - b: { uniqueIncrement: true }, - aAndB: { intersection: ["a", "b"] }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/test-unique.ts b/packages/backend/src/services/chart/charts/entities/test-unique.ts deleted file mode 100644 index 03b8a7653e..0000000000 --- a/packages/backend/src/services/chart/charts/entities/test-unique.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "testUnique"; - -export const schema = { - foo: { uniqueIncrement: true }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/test.ts b/packages/backend/src/services/chart/charts/entities/test.ts deleted file mode 100644 index a11d53e32e..0000000000 --- a/packages/backend/src/services/chart/charts/entities/test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "test"; - -export const schema = { - "foo.total": { accumulate: true }, - "foo.inc": {}, - "foo.dec": {}, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/entities/users.ts b/packages/backend/src/services/chart/charts/entities/users.ts deleted file mode 100644 index a9544d77b1..0000000000 --- a/packages/backend/src/services/chart/charts/entities/users.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Chart from "../../core.js"; - -export const name = "users"; - -export const schema = { - "local.total": { accumulate: true }, - "local.inc": { range: "small" }, - "local.dec": { range: "small" }, - "remote.total": { accumulate: true }, - "remote.inc": { range: "small" }, - "remote.dec": { range: "small" }, -} as const; - -export const entity = Chart.schemaToEntity(name, schema); diff --git a/packages/backend/src/services/chart/charts/federation.ts b/packages/backend/src/services/chart/charts/federation.ts deleted file mode 100644 index 1a03d574df..0000000000 --- a/packages/backend/src/services/chart/charts/federation.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { Followings, Instances } from "@/models/index.js"; -import { name, schema } from "./entities/federation.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; - -/** - * フェデレーションに関するチャート - */ - -export default class FederationChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - const meta = await fetchMeta(); - - const suspendedInstancesQuery = Instances.createQueryBuilder("instance") - .select("instance.host") - .where("instance.isSuspended = true"); - - const pubsubSubQuery = Followings.createQueryBuilder("f") - .select("f.followerHost") - .where("f.followerHost IS NOT NULL"); - - const subInstancesQuery = Followings.createQueryBuilder("f") - .select("f.followeeHost") - .where("f.followeeHost IS NOT NULL"); - - const pubInstancesQuery = Followings.createQueryBuilder("f") - .select("f.followerHost") - .where("f.followerHost IS NOT NULL"); - - const [sub, pub, pubsub, subActive, pubActive] = await Promise.all([ - Followings.createQueryBuilder("following") - .select("COUNT(DISTINCT following.followeeHost)") - .where("following.followeeHost IS NOT NULL") - .andWhere( - meta.blockedHosts.length === 0 - ? "1=1" - : "following.followeeHost NOT IN (:...blocked)", - { blocked: meta.blockedHosts }, - ) - .andWhere( - `following.followeeHost NOT IN (${suspendedInstancesQuery.getQuery()})`, - ) - .getRawOne() - .then((x) => parseInt(x.count, 10)), - Followings.createQueryBuilder("following") - .select("COUNT(DISTINCT following.followerHost)") - .where("following.followerHost IS NOT NULL") - .andWhere( - meta.blockedHosts.length === 0 - ? "1=1" - : "following.followerHost NOT IN (:...blocked)", - { blocked: meta.blockedHosts }, - ) - .andWhere( - `following.followerHost NOT IN (${suspendedInstancesQuery.getQuery()})`, - ) - .getRawOne() - .then((x) => parseInt(x.count, 10)), - Followings.createQueryBuilder("following") - .select("COUNT(DISTINCT following.followeeHost)") - .where("following.followeeHost IS NOT NULL") - .andWhere( - meta.blockedHosts.length === 0 - ? "1=1" - : "following.followeeHost NOT IN (:...blocked)", - { blocked: meta.blockedHosts }, - ) - .andWhere( - `following.followeeHost NOT IN (${suspendedInstancesQuery.getQuery()})`, - ) - .andWhere(`following.followeeHost IN (${pubsubSubQuery.getQuery()})`) - .setParameters(pubsubSubQuery.getParameters()) - .getRawOne() - .then((x) => parseInt(x.count, 10)), - Instances.createQueryBuilder("instance") - .select("COUNT(instance.id)") - .where(`instance.host IN (${subInstancesQuery.getQuery()})`) - .andWhere( - meta.blockedHosts.length === 0 - ? "1=1" - : "instance.host NOT IN (:...blocked)", - { blocked: meta.blockedHosts }, - ) - .andWhere("instance.isSuspended = false") - .andWhere("instance.lastCommunicatedAt > :gt", { - gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30), - }) - .getRawOne() - .then((x) => parseInt(x.count, 10)), - Instances.createQueryBuilder("instance") - .select("COUNT(instance.id)") - .where(`instance.host IN (${pubInstancesQuery.getQuery()})`) - .andWhere( - meta.blockedHosts.length === 0 - ? "1=1" - : "instance.host NOT IN (:...blocked)", - { blocked: meta.blockedHosts }, - ) - .andWhere("instance.isSuspended = false") - .andWhere("instance.lastCommunicatedAt > :gt", { - gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30), - }) - .getRawOne() - .then((x) => parseInt(x.count, 10)), - ]); - - return { - sub: sub, - pub: pub, - pubsub: pubsub, - subActive: subActive, - pubActive: pubActive, - }; - } - - public async deliverd(host: string, succeeded: boolean): Promise { - await this.commit( - succeeded - ? { - deliveredInstances: [host], - } - : { - stalled: [host], - }, - ); - } - - public async inbox(host: string): Promise { - await this.commit({ - inboxInstances: [host], - }); - } -} diff --git a/packages/backend/src/services/chart/charts/hashtag.ts b/packages/backend/src/services/chart/charts/hashtag.ts deleted file mode 100644 index 0211df857f..0000000000 --- a/packages/backend/src/services/chart/charts/hashtag.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import type { User } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; -import { name, schema } from "./entities/hashtag.js"; - -/** - * ハッシュタグに関するチャート - */ - -export default class HashtagChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update( - hashtag: string, - user: { id: User["id"]; host: User["host"] }, - ): Promise { - await this.commit( - { - "local.users": Users.isLocalUser(user) ? [user.id] : [], - "remote.users": Users.isLocalUser(user) ? [] : [user.id], - }, - hashtag, - ); - } -} diff --git a/packages/backend/src/services/chart/charts/instance.ts b/packages/backend/src/services/chart/charts/instance.ts deleted file mode 100644 index d6e3483d8e..0000000000 --- a/packages/backend/src/services/chart/charts/instance.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { DriveFiles, Followings, Users, Notes } from "@/models/index.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import type { Note } from "@/models/entities/note.js"; -import { toPuny } from "@/misc/convert-host.js"; -import { name, schema } from "./entities/instance.js"; - -/** - * インスタンスごとのチャート - */ - -export default class InstanceChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor( - group: string, - ): Promise>> { - const [notesCount, usersCount, followingCount, followersCount, driveFiles] = - await Promise.all([ - Notes.countBy({ userHost: group }), - Users.countBy({ host: group }), - Followings.countBy({ followerHost: group }), - Followings.countBy({ followeeHost: group }), - DriveFiles.countBy({ userHost: group }), - ]); - - return { - "notes.total": notesCount, - "users.total": usersCount, - "following.total": followingCount, - "followers.total": followersCount, - "drive.totalFiles": driveFiles, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async requestReceived(host: string): Promise { - await this.commit( - { - "requests.received": 1, - }, - toPuny(host), - ); - } - - public async requestSent(host: string, isSucceeded: boolean): Promise { - await this.commit( - { - "requests.succeeded": isSucceeded ? 1 : 0, - "requests.failed": isSucceeded ? 0 : 1, - }, - toPuny(host), - ); - } - - public async newUser(host: string): Promise { - await this.commit( - { - "users.total": 1, - "users.inc": 1, - }, - toPuny(host), - ); - } - - public async updateNote( - host: string, - note: Note, - isAdditional: boolean, - ): Promise { - await this.commit( - { - "notes.total": isAdditional ? 1 : -1, - "notes.inc": isAdditional ? 1 : 0, - "notes.dec": isAdditional ? 0 : 1, - "notes.diffs.normal": - note.replyId == null && note.renoteId == null - ? isAdditional - ? 1 - : -1 - : 0, - "notes.diffs.renote": - note.renoteId != null ? (isAdditional ? 1 : -1) : 0, - "notes.diffs.reply": note.replyId != null ? (isAdditional ? 1 : -1) : 0, - "notes.diffs.withFile": - note.fileIds.length > 0 ? (isAdditional ? 1 : -1) : 0, - }, - toPuny(host), - ); - } - - public async updateFollowing( - host: string, - isAdditional: boolean, - ): Promise { - await this.commit( - { - "following.total": isAdditional ? 1 : -1, - "following.inc": isAdditional ? 1 : 0, - "following.dec": isAdditional ? 0 : 1, - }, - toPuny(host), - ); - } - - public async updateFollowers( - host: string, - isAdditional: boolean, - ): Promise { - await this.commit( - { - "followers.total": isAdditional ? 1 : -1, - "followers.inc": isAdditional ? 1 : 0, - "followers.dec": isAdditional ? 0 : 1, - }, - toPuny(host), - ); - } - - public async updateDrive( - file: DriveFile, - isAdditional: boolean, - ): Promise { - const fileSizeKb = file.size / 1000; - await this.commit( - { - "drive.totalFiles": isAdditional ? 1 : -1, - "drive.incFiles": isAdditional ? 1 : 0, - "drive.incUsage": isAdditional ? fileSizeKb : 0, - "drive.decFiles": isAdditional ? 1 : 0, - "drive.decUsage": isAdditional ? fileSizeKb : 0, - }, - file.userHost, - ); - } -} diff --git a/packages/backend/src/services/chart/charts/notes.ts b/packages/backend/src/services/chart/charts/notes.ts deleted file mode 100644 index 9ec347b470..0000000000 --- a/packages/backend/src/services/chart/charts/notes.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { Notes } from "@/models/index.js"; -import { Not, IsNull } from "typeorm"; -import type { Note } from "@/models/entities/note.js"; -import { name, schema } from "./entities/notes.js"; - -/** - * ノートに関するチャート - */ - -export default class NotesChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - const [localCount, remoteCount] = await Promise.all([ - Notes.countBy({ userHost: IsNull() }), - Notes.countBy({ userHost: Not(IsNull()) }), - ]); - - return { - "local.total": localCount, - "remote.total": remoteCount, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(note: Note, isAdditional: boolean): Promise { - const prefix = note.userHost === null ? "local" : "remote"; - - await this.commit({ - [`${prefix}.total`]: isAdditional ? 1 : -1, - [`${prefix}.inc`]: isAdditional ? 1 : 0, - [`${prefix}.dec`]: isAdditional ? 0 : 1, - [`${prefix}.diffs.normal`]: - note.replyId == null && note.renoteId == null - ? isAdditional - ? 1 - : -1 - : 0, - [`${prefix}.diffs.renote`]: - note.renoteId != null ? (isAdditional ? 1 : -1) : 0, - [`${prefix}.diffs.reply`]: - note.replyId != null ? (isAdditional ? 1 : -1) : 0, - [`${prefix}.diffs.withFile`]: - note.fileIds.length > 0 ? (isAdditional ? 1 : -1) : 0, - }); - } -} diff --git a/packages/backend/src/services/chart/charts/per-user-drive.ts b/packages/backend/src/services/chart/charts/per-user-drive.ts deleted file mode 100644 index 18589bb84a..0000000000 --- a/packages/backend/src/services/chart/charts/per-user-drive.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { DriveFiles } from "@/models/index.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { name, schema } from "./entities/per-user-drive.js"; - -/** - * ユーザーごとのドライブに関するチャート - */ - -export default class PerUserDriveChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor( - group: string, - ): Promise>> { - const [count, size] = await Promise.all([ - DriveFiles.countBy({ userId: group }), - DriveFiles.calcDriveUsageOf(group), - ]); - - return { - totalCount: count, - totalSize: size, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update(file: DriveFile, isAdditional: boolean): Promise { - const fileSizeKb = file.size / 1000; - await this.commit( - { - totalCount: isAdditional ? 1 : -1, - totalSize: isAdditional ? fileSizeKb : -fileSizeKb, - incCount: isAdditional ? 1 : 0, - incSize: isAdditional ? fileSizeKb : 0, - decCount: isAdditional ? 0 : 1, - decSize: isAdditional ? 0 : fileSizeKb, - }, - file.userId, - ); - } -} diff --git a/packages/backend/src/services/chart/charts/per-user-following.ts b/packages/backend/src/services/chart/charts/per-user-following.ts deleted file mode 100644 index 3e8b576f20..0000000000 --- a/packages/backend/src/services/chart/charts/per-user-following.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { Followings, Users } from "@/models/index.js"; -import { Not, IsNull } from "typeorm"; -import type { User } from "@/models/entities/user.js"; -import { name, schema } from "./entities/per-user-following.js"; - -/** - * ユーザーごとのフォローに関するチャート - */ - -export default class PerUserFollowingChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor( - group: string, - ): Promise>> { - const [ - localFollowingsCount, - localFollowersCount, - remoteFollowingsCount, - remoteFollowersCount, - ] = await Promise.all([ - Followings.countBy({ followerId: group, followeeHost: IsNull() }), - Followings.countBy({ followeeId: group, followerHost: IsNull() }), - Followings.countBy({ followerId: group, followeeHost: Not(IsNull()) }), - Followings.countBy({ followeeId: group, followerHost: Not(IsNull()) }), - ]); - - return { - "local.followings.total": localFollowingsCount, - "local.followers.total": localFollowersCount, - "remote.followings.total": remoteFollowingsCount, - "remote.followers.total": remoteFollowersCount, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update( - follower: { id: User["id"]; host: User["host"] }, - followee: { id: User["id"]; host: User["host"] }, - isFollow: boolean, - ): Promise { - const prefixFollower = Users.isLocalUser(follower) ? "local" : "remote"; - const prefixFollowee = Users.isLocalUser(followee) ? "local" : "remote"; - - this.commit( - { - [`${prefixFollower}.followings.total`]: isFollow ? 1 : -1, - [`${prefixFollower}.followings.inc`]: isFollow ? 1 : 0, - [`${prefixFollower}.followings.dec`]: isFollow ? 0 : 1, - }, - follower.id, - ); - this.commit( - { - [`${prefixFollowee}.followers.total`]: isFollow ? 1 : -1, - [`${prefixFollowee}.followers.inc`]: isFollow ? 1 : 0, - [`${prefixFollowee}.followers.dec`]: isFollow ? 0 : 1, - }, - followee.id, - ); - } -} diff --git a/packages/backend/src/services/chart/charts/per-user-notes.ts b/packages/backend/src/services/chart/charts/per-user-notes.ts deleted file mode 100644 index d0190cefd0..0000000000 --- a/packages/backend/src/services/chart/charts/per-user-notes.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import type { User } from "@/models/entities/user.js"; -import { Notes } from "@/models/index.js"; -import type { Note } from "@/models/entities/note.js"; -import { name, schema } from "./entities/per-user-notes.js"; - -/** - * ユーザーごとのノートに関するチャート - */ - -export default class PerUserNotesChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor( - group: string, - ): Promise>> { - const [count] = await Promise.all([Notes.countBy({ userId: group })]); - - return { - total: count, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update( - user: { id: User["id"] }, - note: Note, - isAdditional: boolean, - ): Promise { - await this.commit( - { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - "diffs.normal": - note.replyId == null && note.renoteId == null - ? isAdditional - ? 1 - : -1 - : 0, - "diffs.renote": note.renoteId != null ? (isAdditional ? 1 : -1) : 0, - "diffs.reply": note.replyId != null ? (isAdditional ? 1 : -1) : 0, - "diffs.withFile": note.fileIds.length > 0 ? (isAdditional ? 1 : -1) : 0, - }, - user.id, - ); - } -} diff --git a/packages/backend/src/services/chart/charts/per-user-reactions.ts b/packages/backend/src/services/chart/charts/per-user-reactions.ts deleted file mode 100644 index 75def3de04..0000000000 --- a/packages/backend/src/services/chart/charts/per-user-reactions.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import type { User } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -import { Users } from "@/models/index.js"; -import { name, schema } from "./entities/per-user-reactions.js"; - -/** - * ユーザーごとのリアクションに関するチャート - */ - -export default class PerUserReactionsChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor( - group: string, - ): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update( - user: { id: User["id"]; host: User["host"] }, - note: Note, - ): Promise { - const prefix = Users.isLocalUser(user) ? "local" : "remote"; - this.commit( - { - [`${prefix}.count`]: 1, - }, - note.userId, - ); - } -} diff --git a/packages/backend/src/services/chart/charts/test-grouped.ts b/packages/backend/src/services/chart/charts/test-grouped.ts deleted file mode 100644 index 6520099fe6..0000000000 --- a/packages/backend/src/services/chart/charts/test-grouped.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { name, schema } from "./entities/test-grouped.js"; - -/** - * For testing - */ - -export default class TestGroupedChart extends Chart { - private total = {} as Record; - - constructor() { - super(name, schema, true); - } - - protected async tickMajor( - group: string, - ): Promise>> { - return { - "foo.total": this.total[group], - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async increment(group: string): Promise { - if (this.total[group] == null) this.total[group] = 0; - - this.total[group]++; - - await this.commit( - { - "foo.total": 1, - "foo.inc": 1, - }, - group, - ); - } -} diff --git a/packages/backend/src/services/chart/charts/test-intersection.ts b/packages/backend/src/services/chart/charts/test-intersection.ts deleted file mode 100644 index 0fa973861f..0000000000 --- a/packages/backend/src/services/chart/charts/test-intersection.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { name, schema } from "./entities/test-intersection.js"; - -/** - * For testing - */ - -export default class TestIntersectionChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async addA(key: string): Promise { - await this.commit({ - a: [key], - }); - } - - public async addB(key: string): Promise { - await this.commit({ - b: [key], - }); - } -} diff --git a/packages/backend/src/services/chart/charts/test-unique.ts b/packages/backend/src/services/chart/charts/test-unique.ts deleted file mode 100644 index 095021622c..0000000000 --- a/packages/backend/src/services/chart/charts/test-unique.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { name, schema } from "./entities/test-unique.js"; - -/** - * For testing - */ - -export default class TestUniqueChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return {}; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async uniqueIncrement(key: string): Promise { - await this.commit({ - foo: [key], - }); - } -} diff --git a/packages/backend/src/services/chart/charts/test.ts b/packages/backend/src/services/chart/charts/test.ts deleted file mode 100644 index afdb3bf14e..0000000000 --- a/packages/backend/src/services/chart/charts/test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { name, schema } from "./entities/test.js"; - -/** - * For testing - */ - -export default class TestChart extends Chart { - public total = 0; // publicにするのはテストのため - - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - return { - "foo.total": this.total, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async increment(): Promise { - this.total++; - - await this.commit({ - "foo.total": 1, - "foo.inc": 1, - }); - } - - public async decrement(): Promise { - this.total--; - - await this.commit({ - "foo.total": -1, - "foo.dec": 1, - }); - } -} diff --git a/packages/backend/src/services/chart/charts/users.ts b/packages/backend/src/services/chart/charts/users.ts deleted file mode 100644 index 6fef9ecf7b..0000000000 --- a/packages/backend/src/services/chart/charts/users.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { KVs } from "../core.js"; -import Chart from "../core.js"; -import { Users } from "@/models/index.js"; -import { Not, IsNull } from "typeorm"; -import type { User } from "@/models/entities/user.js"; -import { name, schema } from "./entities/users.js"; - -/** - * ユーザー数に関するチャート - */ - -export default class UsersChart extends Chart { - constructor() { - super(name, schema); - } - - protected async tickMajor(): Promise>> { - const [localCount, remoteCount] = await Promise.all([ - Users.countBy({ host: IsNull() }), - Users.countBy({ host: Not(IsNull()) }), - ]); - - return { - "local.total": localCount, - "remote.total": remoteCount, - }; - } - - protected async tickMinor(): Promise>> { - return {}; - } - - public async update( - user: { id: User["id"]; host: User["host"] }, - isAdditional: boolean, - ): Promise { - const prefix = Users.isLocalUser(user) ? "local" : "remote"; - - await this.commit({ - [`${prefix}.total`]: isAdditional ? 1 : -1, - [`${prefix}.inc`]: isAdditional ? 1 : 0, - [`${prefix}.dec`]: isAdditional ? 0 : 1, - }); - } -} diff --git a/packages/backend/src/services/chart/core.ts b/packages/backend/src/services/chart/core.ts deleted file mode 100644 index 36fe373269..0000000000 --- a/packages/backend/src/services/chart/core.ts +++ /dev/null @@ -1,922 +0,0 @@ -/** - * チャートエンジン - * - * Tests located in test/chart - */ - -import * as nestedProperty from "nested-property"; -import Logger from "../logger.js"; -import type { Repository } from "typeorm"; -import { EntitySchema, LessThan, Between } from "typeorm"; -import { - dateUTC, - isTimeSame, - isTimeBefore, - subtractTime, - addTime, -} from "@/prelude/time.js"; -import { getChartInsertLock } from "@/misc/app-lock.js"; -import { db } from "@/db/postgre.js"; -import promiseLimit from "promise-limit"; - -const logger = new Logger("chart", "white", process.env.NODE_ENV !== "test"); - -const columnPrefix = "___" as const; -const uniqueTempColumnPrefix = "unique_temp___" as const; -const columnDot = "_" as const; - -type Schema = Record< - string, - { - uniqueIncrement?: boolean; - - intersection?: string[] | ReadonlyArray; - - range?: "big" | "small" | "medium"; - - // previousな値を引き継ぐかどうか - accumulate?: boolean; - } ->; - -type KeyToColumnName = T extends `${infer R1}.${infer R2}` - ? `${R1}${typeof columnDot}${KeyToColumnName}` - : T; - -type Columns = { - [K in - keyof S as `${typeof columnPrefix}${KeyToColumnName}`]: number; -}; - -type TempColumnsForUnique = { - [K in - keyof S as `${typeof uniqueTempColumnPrefix}${KeyToColumnName< - string & K - >}`]: S[K]["uniqueIncrement"] extends true ? string[] : never; -}; - -type RawRecord = { - id: number; - - /** - * 集計のグループ - */ - group?: string | null; - - /** - * 集計日時のUnixタイムスタンプ(秒) - */ - date: number; -} & TempColumnsForUnique & - Columns; - -const camelToSnake = (str: string): string => { - return str.replace(/([A-Z])/g, (s) => `_${s.charAt(0).toLowerCase()}`); -}; - -const removeDuplicates = (array: any[]) => Array.from(new Set(array)); - -type Commit = { - [K in keyof S]?: S[K]["uniqueIncrement"] extends true ? string[] : number; -}; - -export type KVs = { - [K in keyof S]: number; -}; - -type ChartResult = { - [P in keyof T]: number[]; -}; - -type UnionToIntersection = (T extends any ? (x: T) => any : never) extends ( - x: infer R, -) => any - ? R - : never; - -type UnflattenSingleton = K extends `${infer A}.${infer B}` - ? { - [_ in A]: UnflattenSingleton; - } - : { - [_ in K]: V; - }; - -type Unflatten> = UnionToIntersection< - { - [K in Extract]: UnflattenSingleton; - }[Extract] ->; - -type ToJsonSchema = { - type: "object"; - properties: { - [K in keyof S]: S[K] extends number[] - ? { type: "array"; items: { type: "number" } } - : ToJsonSchema; - }; - required: (keyof S)[]; -}; - -export function getJsonSchema( - schema: S, -): ToJsonSchema>> { - const jsonSchema = { - type: "object", - properties: {} as Record, - required: [], - }; - - for (const k in schema) { - jsonSchema.properties[k] = { - type: "array", - items: { type: "number" }, - }; - } - - return jsonSchema as ToJsonSchema>>; -} - -/** - * 様々なチャートの管理を司るクラス - */ - -export default abstract class Chart { - public schema: T; - - private name: string; - private buffer: { - diff: Commit; - group: string | null; - }[] = []; - // ↓にしたいけどfindOneとかで型エラーになる - //private repositoryForHour: Repository>; - //private repositoryForDay: Repository>; - private repositoryForHour: Repository<{ - id: number; - group?: string | null; - date: number; - }>; - private repositoryForDay: Repository<{ - id: number; - group?: string | null; - date: number; - }>; - - /** - * 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用) - */ - protected abstract tickMajor(group: string | null): Promise>>; - - /** - * 少なくとも最小スパン内に1回は実行されて欲しい計算処理を入れる - */ - protected abstract tickMinor(group: string | null): Promise>>; - - private static convertSchemaToColumnDefinitions( - schema: Schema, - ): Record { - const columns = {} as Record< - string, - { type: string; array?: boolean; default?: any } - >; - for (const [k, v] of Object.entries(schema)) { - const name = k.replaceAll(".", columnDot); - const type = - v.range === "big" - ? "bigint" - : v.range === "small" - ? "smallint" - : "integer"; - if (v.uniqueIncrement) { - columns[uniqueTempColumnPrefix + name] = { - type: "varchar", - array: true, - default: "{}", - }; - columns[columnPrefix + name] = { - type, - default: 0, - }; - } else { - columns[columnPrefix + name] = { - type, - default: 0, - }; - } - } - return columns; - } - - private static dateToTimestamp(x: Date): number { - return Math.floor(x.getTime() / 1000); - } - - 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]; - } - - private static getCurrentDate() { - return Chart.parseDate(new Date()); - } - - public static schemaToEntity( - name: string, - schema: Schema, - grouped = false, - ): { - hour: EntitySchema; - day: EntitySchema; - } { - const createEntity = (span: "hour" | "day"): EntitySchema => - new EntitySchema({ - name: - span === "hour" - ? `__chart__${camelToSnake(name)}` - : span === "day" - ? `__chart_day__${camelToSnake(name)}` - : (new Error("not happen") as never), - columns: { - id: { - type: "integer", - primary: true, - generated: true, - }, - date: { - type: "integer", - }, - ...(grouped - ? { - group: { - type: "varchar", - length: 128, - }, - } - : {}), - ...Chart.convertSchemaToColumnDefinitions(schema), - }, - indices: [ - { - columns: grouped ? ["date", "group"] : ["date"], - unique: true, - }, - ], - uniques: [ - { - columns: grouped ? ["date", "group"] : ["date"], - }, - ], - relations: { - /* TODO - group: { - target: () => Foo, - type: 'many-to-one', - onDelete: 'CASCADE', - }, - */ - }, - }); - - return { - hour: createEntity("hour"), - day: createEntity("day"), - }; - } - - constructor(name: string, schema: T, grouped = false) { - this.name = name; - this.schema = schema; - - const { hour, day } = Chart.schemaToEntity(name, schema, grouped); - this.repositoryForHour = db.getRepository<{ - id: number; - group?: string | null; - date: number; - }>(hour); - this.repositoryForDay = db.getRepository<{ - id: number; - group?: string | null; - date: number; - }>(day); - } - - private convertRawRecord(x: RawRecord): KVs { - const kvs = {} as Record; - for (const k of Object.keys(x).filter((k) => - k.startsWith(columnPrefix), - ) as (keyof Columns)[]) { - kvs[ - (k as string).substr(columnPrefix.length).split(columnDot).join(".") - ] = x[k]; - } - return kvs as KVs; - } - - private getNewLog(latest: KVs | null): KVs { - const log = {} as Record; - for (const [k, v] of Object.entries(this.schema) as [ - keyof typeof this["schema"], - this["schema"][string], - ][]) { - if (v.accumulate && latest) { - log[k] = latest[k]; - } else { - log[k] = 0; - } - } - return log as KVs; - } - - private getLatestLog( - group: string | null, - span: "hour" | "day", - ): Promise | null> { - const repository = - span === "hour" - ? this.repositoryForHour - : span === "day" - ? this.repositoryForDay - : (new Error("not happen") as never); - - return repository - .findOne({ - where: group - ? { - group: group, - } - : {}, - order: { - date: -1, - }, - }) - .then((x) => x ?? null) as Promise | null>; - } - - /** - * 現在(=今のHour or Day)のログをデータベースから探して、あればそれを返し、なければ作成して返します。 - */ - private async claimCurrentLog( - group: string | null, - span: "hour" | "day", - ): Promise> { - const [y, m, d, h] = Chart.getCurrentDate(); - - const current = dateUTC( - span === "hour" - ? [y, m, d, h] - : span === "day" - ? [y, m, d] - : (new Error("not happen") as never), - ); - - const repository = - span === "hour" - ? this.repositoryForHour - : span === "day" - ? this.repositoryForDay - : (new Error("not happen") as never); - - // 現在(=今のHour or Day)のログ - const currentLog = (await repository.findOneBy({ - date: Chart.dateToTimestamp(current), - ...(group ? { group: group } : {}), - })) as RawRecord | undefined; - - // ログがあればそれを返して終了 - if (currentLog != null) { - return currentLog; - } - - let log: RawRecord; - let data: KVs; - - // 集計期間が変わってから、初めてのチャート更新なら - // 最も最近のログを持ってくる - // * 例えば集計期間が「日」である場合で考えると、 - // * 昨日何もチャートを更新するような出来事がなかった場合は、 - // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、 - // * 「昨日の」と決め打ちせずに「もっとも最近の」とします - const latest = await this.getLatestLog(group, span); - - if (latest != null) { - // 空ログデータを作成 - data = this.getNewLog(this.convertRawRecord(latest)); - } else { - // ログが存在しなかったら - // (Misskeyインスタンスを建てて初めてのチャート更新時など) - - // 初期ログデータを作成 - data = this.getNewLog(null); - - logger.info( - `${ - this.name + (group ? `:${group}` : "") - }(${span}): Initial commit created`, - ); - } - - const date = Chart.dateToTimestamp(current); - const lockKey = group - ? `${this.name}:${date}:${span}:${group}` - : `${this.name}:${date}:${span}`; - - const unlock = await getChartInsertLock(lockKey); - try { - // ロック内でもう1回チェックする - const currentLog = (await repository.findOneBy({ - date: date, - ...(group ? { group: group } : {}), - })) as RawRecord | undefined; - - // ログがあればそれを返して終了 - if (currentLog != null) return currentLog; - - const columns = {} as Record; - for (const [k, v] of Object.entries(data)) { - const name = k.replaceAll(".", columnDot); - columns[columnPrefix + name] = v; - } - - // 新規ログ挿入 - log = (await repository - .insert({ - date: date, - ...(group ? { group: group } : {}), - ...columns, - }) - .then((x) => - repository.findOneByOrFail(x.identifiers[0]), - )) as RawRecord; - - logger.info( - `${ - this.name + (group ? `:${group}` : "") - }(${span}): New commit created`, - ); - - return log; - } finally { - unlock(); - } - } - - protected commit(diff: Commit, group: string | null = null): void { - for (const [k, v] of Object.entries(diff)) { - if (v == null || v === 0 || (Array.isArray(v) && v.length === 0)) - // rome-ignore lint/performance/noDelete: needs to be deleted not just set to undefined - delete diff[k]; - } - this.buffer.push({ - diff, - group, - }); - } - - public async save(): Promise { - 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 ( - logHour: RawRecord, - logDay: RawRecord, - ): Promise => { - const finalDiffs = {} as Record; - - for (const diff of this.buffer - .filter((q) => q.group == null || q.group === logHour.group) - .map((q) => q.diff)) { - for (const [k, v] of Object.entries(diff)) { - if (finalDiffs[k] == null) { - finalDiffs[k] = v; - } else { - if (typeof finalDiffs[k] === "number") { - (finalDiffs[k] as number) += v as number; - } else { - (finalDiffs[k] as string[]) = (finalDiffs[k] as string[]).concat( - v, - ); - } - } - } - } - - const queryForHour: Record, number | (() => string)> = - {} as any; - const queryForDay: Record, number | (() => string)> = - {} as any; - for (const [k, v] of Object.entries(finalDiffs)) { - if (typeof v === "number") { - const name = (columnPrefix + - k.replaceAll(".", columnDot)) as keyof Columns; - if (v > 0) queryForHour[name] = () => `"${name}" + ${v}`; - if (v < 0) queryForHour[name] = () => `"${name}" - ${Math.abs(v)}`; - if (v > 0) queryForDay[name] = () => `"${name}" + ${v}`; - if (v < 0) queryForDay[name] = () => `"${name}" - ${Math.abs(v)}`; - } else if (Array.isArray(v) && v.length > 0) { - // ユニークインクリメント - const tempColumnName = (uniqueTempColumnPrefix + - k.replaceAll(".", columnDot)) as keyof TempColumnsForUnique; - // TODO: item をSQLエスケープ - const itemsForHour = v - .filter((item) => !logHour[tempColumnName].includes(item)) - .map((item) => `"${item}"`); - const itemsForDay = v - .filter((item) => !logDay[tempColumnName].includes(item)) - .map((item) => `"${item}"`); - if (itemsForHour.length > 0) - queryForHour[tempColumnName] = () => - `array_cat("${tempColumnName}", '{${itemsForHour.join( - ",", - )}}'::varchar[])`; - if (itemsForDay.length > 0) - queryForDay[tempColumnName] = () => - `array_cat("${tempColumnName}", '{${itemsForDay.join( - ",", - )}}'::varchar[])`; - } - } - - // bake unique count - for (const [k, v] of Object.entries(finalDiffs)) { - if ( - this.schema[k].uniqueIncrement && - Array.isArray(v) && - v.length > 0 - ) { - const name = (columnPrefix + - k.replaceAll(".", columnDot)) as keyof Columns; - const tempColumnName = (uniqueTempColumnPrefix + - k.replaceAll(".", columnDot)) as keyof TempColumnsForUnique; - queryForHour[name] = new Set([ - ...(v as string[]), - ...logHour[tempColumnName], - ]).size; - queryForDay[name] = new Set([ - ...(v as string[]), - ...logDay[tempColumnName], - ]).size; - } - } - - // compute intersection - // TODO: intersectionに指定されたカラムがintersectionだった場合の対応 - for (const [k, v] of Object.entries(this.schema)) { - const intersection = v.intersection; - if (intersection) { - const name = (columnPrefix + - k.replaceAll(".", columnDot)) as keyof Columns; - const firstKey = intersection[0]; - const firstTempColumnName = (uniqueTempColumnPrefix + - firstKey.replaceAll( - ".", - columnDot, - )) as keyof TempColumnsForUnique; - const firstValues = finalDiffs[firstKey] as string[] | undefined; - const currentValuesForHour = new Set([ - ...(firstValues ?? []), - ...logHour[firstTempColumnName], - ]); - const currentValuesForDay = new Set([ - ...(firstValues ?? []), - ...logDay[firstTempColumnName], - ]); - for (let i = 1; i < intersection.length; i++) { - const targetKey = intersection[i]; - const targetTempColumnName = (uniqueTempColumnPrefix + - targetKey.replaceAll( - ".", - columnDot, - )) as keyof TempColumnsForUnique; - const targetValues = finalDiffs[targetKey] as string[] | undefined; - const targetValuesForHour = new Set([ - ...(targetValues ?? []), - ...logHour[targetTempColumnName], - ]); - const targetValuesForDay = new Set([ - ...(targetValues ?? []), - ...logDay[targetTempColumnName], - ]); - currentValuesForHour.forEach((v) => { - if (!targetValuesForHour.has(v)) currentValuesForHour.delete(v); - }); - currentValuesForDay.forEach((v) => { - if (!targetValuesForDay.has(v)) currentValuesForDay.delete(v); - }); - } - queryForHour[name] = currentValuesForHour.size; - queryForDay[name] = currentValuesForDay.size; - } - } - - // ログ更新 - await Promise.all([ - this.repositoryForHour - .createQueryBuilder() - .update() - .set(queryForHour as any) - .where("id = :id", { id: logHour.id }) - .execute(), - this.repositoryForDay - .createQueryBuilder() - .update() - .set(queryForDay as any) - .where("id = :id", { id: logDay.id }) - .execute(), - ]); - - logger.info( - `${this.name + (logHour.group ? `:${logHour.group}` : "")}: Updated`, - ); - - // TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする - this.buffer = this.buffer.filter( - (q) => q.group != null && q.group !== logHour.group, - ); - }; - - const startCount = this.buffer.length; - - const groups = removeDuplicates(this.buffer.map((log) => log.group)); - const groupCount = groups.length; - - // Limit the number of concurrent chart update queries executed on the database - // to 25 at a time, so as avoid excessive IO spinlocks like when 8k queries are - // sent out at once. - const limit = promiseLimit(25); - - const startTime = Date.now(); - await Promise.all( - groups.map((group) => - limit(() => - Promise.all([ - this.claimCurrentLog(group, "hour"), - this.claimCurrentLog(group, "day"), - ]).then(([logHour, logDay]) => update(logHour, logDay)), - ), - ), - ); - - const duration = Date.now() - startTime; - logger.info( - `Saved ${startCount} (${groupCount} unique) ${this.name} items in ${duration}ms (${this.buffer.length} remaining)`, - ); - } - - public async tick( - major: boolean, - group: string | null = null, - ): Promise { - const data = major - ? await this.tickMajor(group) - : await this.tickMinor(group); - - const columns = {} as Record, number>; - for (const [k, v] of Object.entries(data) as [ - keyof typeof data, - number, - ][]) { - const name = (columnPrefix + - (k as string).replaceAll(".", columnDot)) as keyof Columns; - columns[name] = v; - } - - if (Object.keys(columns).length === 0) { - return; - } - - const update = async ( - logHour: RawRecord, - logDay: RawRecord, - ): Promise => { - await Promise.all([ - this.repositoryForHour - .createQueryBuilder() - .update() - .set(columns) - .where("id = :id", { id: logHour.id }) - .execute(), - this.repositoryForDay - .createQueryBuilder() - .update() - .set(columns) - .where("id = :id", { id: logDay.id }) - .execute(), - ]); - }; - - return Promise.all([ - this.claimCurrentLog(group, "hour"), - this.claimCurrentLog(group, "day"), - ]).then(([logHour, logDay]) => update(logHour, logDay)); - } - - public resync(group: string | null = null): Promise { - return this.tick(true, group); - } - - public async clean(): Promise { - const current = dateUTC(Chart.getCurrentDate()); - - // 一日以上前かつ三日以内 - const gt = Chart.dateToTimestamp(current) - 60 * 60 * 24 * 3; - const lt = Chart.dateToTimestamp(current) - 60 * 60 * 24; - - const columns = {} as Record, []>; - for (const [k, v] of Object.entries(this.schema)) { - if (v.uniqueIncrement) { - const name = (uniqueTempColumnPrefix + - k.replaceAll(".", columnDot)) as keyof TempColumnsForUnique; - columns[name] = []; - } - } - - if (Object.keys(columns).length === 0) { - return; - } - - await Promise.all([ - this.repositoryForHour - .createQueryBuilder() - .update() - .set(columns) - .where("date > :gt", { gt }) - .andWhere("date < :lt", { lt }) - .execute(), - this.repositoryForDay - .createQueryBuilder() - .update() - .set(columns) - .where("date > :gt", { gt }) - .andWhere("date < :lt", { lt }) - .execute(), - ]); - } - - public async getChartRaw( - 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", - ) - : (new Error("not happen") as never); - - const repository = - span === "hour" - ? this.repositoryForHour - : span === "day" - ? this.repositoryForDay - : (new Error("not happen") as never); - - // ログ取得 - let logs = (await repository.find({ - where: { - date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt)), - ...(group ? { group: group } : {}), - }, - order: { - date: -1, - }, - })) as RawRecord[]; - - // 要求された範囲にログがひとつもなかったら - if (logs.length === 0) { - // もっとも新しいログを持ってくる - // (すくなくともひとつログが無いと隙間埋めできないため) - const recentLog = (await repository.findOne({ - where: group - ? { - group: group, - } - : {}, - order: { - date: -1, - }, - })) as RawRecord | undefined; - - if (recentLog) { - logs = [recentLog]; - } - - // 要求された範囲の最も古い箇所に位置するログが存在しなかったら - } else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) { - // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する - // (隙間埋めできないため) - const outdatedLog = (await repository.findOne({ - where: { - date: LessThan(Chart.dateToTimestamp(gt)), - ...(group ? { group: group } : {}), - }, - order: { - date: -1, - }, - })) as RawRecord | undefined; - - if (outdatedLog) { - logs.push(outdatedLog); - } - } - - const chart: KVs[] = []; - - for (let i = amount - 1; i >= 0; i--) { - const current = - span === "hour" - ? subtractTime(dateUTC([y, m, d, h]), i, "hour") - : span === "day" - ? subtractTime(dateUTC([y, m, d]), i, "day") - : (new Error("not happen") as never); - - const log = logs.find((l) => - isTimeSame(new Date(l.date * 1000), current), - ); - - if (log) { - chart.unshift(this.convertRawRecord(log)); - } else { - // 隙間埋め - const latest = logs.find((l) => - isTimeBefore(new Date(l.date * 1000), current), - ); - const data = latest ? this.convertRawRecord(latest) : null; - chart.unshift(this.getNewLog(data)); - } - } - - const res = {} as ChartResult; - - /** - * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] - * を - * { foo: [1, 2, 3], bar: [5, 6, 7] } - * にする - */ - for (const record of chart) { - for (const [k, v] of Object.entries(record) as [ - keyof typeof record, - number, - ][]) { - if (res[k]) { - res[k].push(v); - } else { - res[k] = [v]; - } - } - } - - return res; - } - - public async getChart( - span: "hour" | "day", - amount: number, - cursor: Date | null, - group: string | null = null, - ): Promise>> { - const result = await this.getChartRaw(span, amount, cursor, group); - const object = {}; - for (const [k, v] of Object.entries(result)) { - nestedProperty.set(object, k, v); - } - return object as Unflatten>; - } -} diff --git a/packages/backend/src/services/chart/entities.ts b/packages/backend/src/services/chart/entities.ts deleted file mode 100644 index e203dffdff..0000000000 --- a/packages/backend/src/services/chart/entities.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { entity as FederationChart } from "./charts/entities/federation.js"; -import { entity as NotesChart } from "./charts/entities/notes.js"; -import { entity as UsersChart } from "./charts/entities/users.js"; -import { entity as ActiveUsersChart } from "./charts/entities/active-users.js"; -import { entity as InstanceChart } from "./charts/entities/instance.js"; -import { entity as PerUserNotesChart } from "./charts/entities/per-user-notes.js"; -import { entity as DriveChart } from "./charts/entities/drive.js"; -import { entity as PerUserReactionsChart } from "./charts/entities/per-user-reactions.js"; -import { entity as HashtagChart } from "./charts/entities/hashtag.js"; -import { entity as PerUserFollowingChart } from "./charts/entities/per-user-following.js"; -import { entity as PerUserDriveChart } from "./charts/entities/per-user-drive.js"; -import { entity as ApRequestChart } from "./charts/entities/ap-request.js"; - -import { entity as TestChart } from "./charts/entities/test.js"; -import { entity as TestGroupedChart } from "./charts/entities/test-grouped.js"; -import { entity as TestUniqueChart } from "./charts/entities/test-unique.js"; -import { entity as TestIntersectionChart } from "./charts/entities/test-intersection.js"; - -export const entities = [ - FederationChart.hour, - FederationChart.day, - NotesChart.hour, - NotesChart.day, - UsersChart.hour, - UsersChart.day, - ActiveUsersChart.hour, - ActiveUsersChart.day, - InstanceChart.hour, - InstanceChart.day, - PerUserNotesChart.hour, - PerUserNotesChart.day, - DriveChart.hour, - DriveChart.day, - PerUserReactionsChart.hour, - PerUserReactionsChart.day, - HashtagChart.hour, - HashtagChart.day, - PerUserFollowingChart.hour, - PerUserFollowingChart.day, - PerUserDriveChart.hour, - PerUserDriveChart.day, - ApRequestChart.hour, - ApRequestChart.day, - - ...(process.env.NODE_ENV === "test" - ? [ - TestChart.hour, - TestChart.day, - TestGroupedChart.hour, - TestGroupedChart.day, - TestUniqueChart.hour, - TestUniqueChart.day, - TestIntersectionChart.hour, - TestIntersectionChart.day, - ] - : []), -]; diff --git a/packages/backend/src/services/chart/index.ts b/packages/backend/src/services/chart/index.ts deleted file mode 100644 index 969cdab6d7..0000000000 --- a/packages/backend/src/services/chart/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { beforeShutdown } from "@/misc/before-shutdown.js"; - -import FederationChart from "./charts/federation.js"; -import NotesChart from "./charts/notes.js"; -import UsersChart from "./charts/users.js"; -import ActiveUsersChart from "./charts/active-users.js"; -import InstanceChart from "./charts/instance.js"; -import PerUserNotesChart from "./charts/per-user-notes.js"; -import DriveChart from "./charts/drive.js"; -import PerUserReactionsChart from "./charts/per-user-reactions.js"; -import HashtagChart from "./charts/hashtag.js"; -import PerUserFollowingChart from "./charts/per-user-following.js"; -import PerUserDriveChart from "./charts/per-user-drive.js"; -import ApRequestChart from "./charts/ap-request.js"; - -export const federationChart = new FederationChart(); -export const notesChart = new NotesChart(); -export const usersChart = new UsersChart(); -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(); -export const apRequestChart = new ApRequestChart(); - -const charts = [ - federationChart, - notesChart, - usersChart, - activeUsersChart, - instanceChart, - perUserNotesChart, - driveChart, - perUserReactionsChart, - hashtagChart, - perUserFollowingChart, - perUserDriveChart, - apRequestChart, -]; - -// 20分おきにメモリ情報をDBに書き込み -setInterval(() => { - for (const chart of charts) { - chart.save(); - } -}, 1000 * 60 * 20); - -beforeShutdown(() => Promise.all(charts.map((chart) => chart.save()))); diff --git a/packages/backend/src/services/create-notification.ts b/packages/backend/src/services/create-notification.ts deleted file mode 100644 index f6545b131c..0000000000 --- a/packages/backend/src/services/create-notification.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import { pushNotification } from "@/services/push-notification.js"; -import { - Notifications, - Mutings, - NoteThreadMutings, - UserProfiles, - Users, -} from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { User } from "@/models/entities/user.js"; -import type { Notification } from "@/models/entities/notification.js"; -import { sendEmailNotification } from "./send-email-notification.js"; - -export async function createNotification( - notifieeId: User["id"], - type: Notification["type"], - data: Partial, -) { - if (data.notifierId && notifieeId === data.notifierId) { - return null; - } - - const profile = await UserProfiles.findOneBy({ userId: notifieeId }); - - const isMuted = profile?.mutingNotificationTypes.includes(type); - - if (data.note != null) { - const threadMute = await NoteThreadMutings.findOneBy({ - userId: notifieeId, - threadId: data.note.threadId || data.note.id, - }); - - if (threadMute) { - return null; - } - } - - // Create notification - const notification = await Notifications.insert({ - id: genId(), - createdAt: new Date(), - notifieeId: notifieeId, - type: type, - // 相手がこの通知をミュートしているようなら、既読を予めつけておく - isRead: isMuted, - ...data, - } as Partial).then((x) => - Notifications.findOneByOrFail(x.identifiers[0]), - ); - - const packed = await Notifications.pack(notification, {}); - - // Publish notification event - publishMainStream(notifieeId, "notification", packed); - - // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する - setTimeout(async () => { - const fresh = await Notifications.findOneBy({ id: notification.id }); - if (fresh == null) return; // 既に削除されているかもしれない - if (fresh.isRead) return; - - //#region ただしミュートしているユーザーからの通知なら無視 - const mutings = await Mutings.findBy({ - muterId: notifieeId, - }); - if ( - data.notifierId && - mutings.map((m) => m.muteeId).includes(data.notifierId) - ) { - return; - } - //#endregion - - publishMainStream(notifieeId, "unreadNotification", packed); - pushNotification(notifieeId, "notification", packed); - - if (type === "follow") - sendEmailNotification.follow( - notifieeId, - await Users.findOneByOrFail({ id: data.notifierId! }), - ); - if (type === "receiveFollowRequest") - sendEmailNotification.receiveFollowRequest( - notifieeId, - await Users.findOneByOrFail({ id: data.notifierId! }), - ); - }, 2000); - - return notification; -} diff --git a/packages/backend/src/services/create-system-user.ts b/packages/backend/src/services/create-system-user.ts deleted file mode 100644 index 24536090a9..0000000000 --- a/packages/backend/src/services/create-system-user.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { v4 as uuid } from "uuid"; -import generateNativeUserToken from "../server/api/common/generate-native-user-token.js"; -import { genRsaKeyPair } from "@/misc/gen-key-pair.js"; -import { User } from "@/models/entities/user.js"; -import { UserProfile } from "@/models/entities/user-profile.js"; -import { IsNull } from "typeorm"; -import { genId } from "@/misc/gen-id.js"; -import { UserKeypair } from "@/models/entities/user-keypair.js"; -import { UsedUsername } from "@/models/entities/used-username.js"; -import { db } from "@/db/postgre.js"; -import { hashPassword } from "@/misc/password.js"; - -export async function createSystemUser(username: string) { - const password = uuid(); - - // Generate hash of password - const hash = await hashPassword(password); - - // Generate secret - const secret = generateNativeUserToken(); - - const keyPair = await genRsaKeyPair(4096); - - let account!: User; - - // Start transaction - await db.transaction(async (transactionalEntityManager) => { - const exist = await transactionalEntityManager.findOneBy(User, { - usernameLower: username.toLowerCase(), - host: IsNull(), - }); - - 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.findOneByOrFail(User, x.identifiers[0]), - ); - - await transactionalEntityManager.insert(UserKeypair, { - publicKey: keyPair.publicKey, - privateKey: keyPair.privateKey, - userId: account.id, - }); - - await transactionalEntityManager.insert(UserProfile, { - userId: account.id, - autoAcceptFollowed: false, - password: hash, - }); - - await transactionalEntityManager.insert(UsedUsername, { - createdAt: new Date(), - username: username.toLowerCase(), - }); - }); - - return account; -} diff --git a/packages/backend/src/services/delete-account.ts b/packages/backend/src/services/delete-account.ts deleted file mode 100644 index 927776199a..0000000000 --- a/packages/backend/src/services/delete-account.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Users } from "@/models/index.js"; -import { createDeleteAccountJob } from "@/queue/index.js"; -import { publishUserEvent } from "./stream.js"; -import { doPostSuspend } from "./suspend-user.js"; - -export async function deleteAccount(user: { - id: string; - host: string | null; -}): Promise { - // 物理削除する前にDelete activityを送信する - await doPostSuspend(user).catch((e) => {}); - - createDeleteAccountJob(user, { - soft: false, - }); - - await Users.update(user.id, { - isDeleted: true, - }); - - // Terminate streaming - publishUserEvent(user.id, "terminate", {}); -} diff --git a/packages/backend/src/services/detect-sensitive.ts b/packages/backend/src/services/detect-sensitive.ts deleted file mode 100644 index df695e86da..0000000000 --- a/packages/backend/src/services/detect-sensitive.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as fs from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; -import * as nsfw from "nsfwjs"; -import si from "systeminformation"; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const REQUIRED_CPU_FLAGS = ["avx2", "fma"]; -let isSupportedCpu: undefined | boolean = undefined; - -let model: nsfw.NSFWJS; - -export async function detectSensitive( - path: string, -): Promise { - try { - if (isSupportedCpu === undefined) { - const cpuFlags = await getCpuFlags(); - isSupportedCpu = REQUIRED_CPU_FLAGS.every((required) => - cpuFlags.includes(required), - ); - } - - if (!isSupportedCpu) { - console.error("This CPU cannot use TensorFlow."); - return null; - } - - const tf = await import("@tensorflow/tfjs-node"); - - if (model == null) - model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { - size: 299, - }); - - const buffer = await fs.promises.readFile(path); - const image = (await tf.node.decodeImage(buffer, 3)) as any; - try { - const predictions = await model.classify(image); - return predictions; - } finally { - image.dispose(); - } - } catch (err) { - console.error(err); - return null; - } -} - -async function getCpuFlags(): Promise { - const str = await si.cpuFlags(); - return str.split(/\s+/); -} diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts deleted file mode 100644 index b25375b947..0000000000 --- a/packages/backend/src/services/drive/add-file.ts +++ /dev/null @@ -1,671 +0,0 @@ -import * as fs from "node:fs"; - -import { v4 as uuid } from "uuid"; - -import type S3 from "aws-sdk/clients/s3.js"; -import sharp from "sharp"; -import { IsNull } from "typeorm"; -import { publishMainStream, publishDriveStream } from "@/services/stream.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { contentDisposition } from "@/misc/content-disposition.js"; -import { getFileInfo } from "@/misc/get-file-info.js"; -import { - DriveFiles, - DriveFolders, - Users, - Instances, - UserProfiles, -} from "@/models/index.js"; -import { DriveFile } from "@/models/entities/drive-file.js"; -import type { IRemoteUser, User } from "@/models/entities/user.js"; -import { - driveChart, - perUserDriveChart, - instanceChart, -} from "@/services/chart/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; -import { FILE_TYPE_BROWSERSAFE } from "@/const.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import { getS3 } from "./s3.js"; -import { InternalStorage } from "./internal-storage.js"; -import type { IImage } from "./image-processor.js"; -import { convertSharpToWebp } from "./image-processor.js"; -import { driveLogger } from "./logger.js"; -import { GenerateVideoThumbnail } from "./generate-video-thumbnail.js"; -import { deleteFile } from "./delete-file.js"; - -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/avif") ext = ".avif"; - if (type === "image/vnd.mozilla.apng") ext = ".apng"; - } - - // Some cloud providers (notably upcloud) will infer the content-type based - // on extension, so we remove extensions from non-browser-safe types. - if (!FILE_TYPE_BROWSERSAFE.includes(type)) { - ext = ""; - } - - 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.webpublicType = alts.webpublic?.type ?? null; - file.name = name; - file.type = type; - file.md5 = hash; - file.size = size; - file.storedInternal = false; - - return await DriveFiles.insert(file).then((x) => - DriveFiles.findOneByOrFail(x.identifiers[0]), - ); - } 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.webpublicType = alts.webpublic?.type ?? null; - file.name = name; - file.type = type; - file.md5 = hash; - file.size = size; - - return await DriveFiles.insert(file).then((x) => - DriveFiles.findOneByOrFail(x.identifiers[0]), - ); - } -} - -/** - * 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 (err) { - logger.warn(`GenerateVideoThumbnail failed: ${err}`); - return { - webpublic: null, - thumbnail: null, - }; - } - } - - if ( - ![ - "image/jpeg", - "image/png", - "image/webp", - "image/svg+xml", - "image/avif", - ].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; - let satisfyWebpublic: boolean; - - 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, - }; - } - - satisfyWebpublic = !!( - type !== "image/svg+xml" && - type !== "image/webp" && - !( - metadata.exif || - metadata.iptc || - metadata.xmp || - metadata.tifftagPhotoshop - ) && - metadata.width && - metadata.width <= 2048 && - metadata.height && - metadata.height <= 2048 - ); - } catch (err) { - logger.warn(`sharp failed: ${err}`); - return { - webpublic: null, - thumbnail: null, - }; - } - - // #region webpublic - let webpublic: IImage | null = null; - - if (generateWeb && !satisfyWebpublic) { - logger.info("creating web image"); - - try { - if (["image/jpeg"].includes(type)) { - webpublic = await convertSharpToWebp(img, 2048, 2048); - } else if (["image/webp"].includes(type)) { - webpublic = await convertSharpToWebp(img, 2048, 2048); - } else if (["image/png"].includes(type)) { - webpublic = await convertSharpToWebp(img, 2048, 2048, 100); - } else if (["image/svg+xml"].includes(type)) { - webpublic = await convertSharpToWebp(img, 2048, 2048); - } else { - logger.debug("web image not created (not an required image)"); - } - } catch (err) { - logger.warn("web image not created (an error occured)", err as Error); - } - } else { - if (satisfyWebpublic) - logger.info("web image not created (original satisfies webpublic)"); - else logger.info("web image not created (from remote)"); - } - // #endregion webpublic - - // #region thumbnail - let thumbnail: IImage | null = null; - - try { - if ( - [ - "image/jpeg", - "image/webp", - "image/png", - "image/svg+xml", - "image/avif", - ].includes(type) - ) { - thumbnail = await convertSharpToWebp(img, 996, 560); - } else { - logger.debug("thumbnail not created (not an required file)"); - } - } catch (err) { - logger.warn("thumbnail not created (an error occured)", err as Error); - } - // #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"; - if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = "application/octet-stream"; - - 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); - } -} - -type AddFileArgs = { - /** User who wish to add file */ - user: { - id: User["id"]; - host: User["host"]; - driveCapacityOverrideMb: User["driveCapacityOverrideMb"]; - } | null; - /** File path */ - path: string; - /** Name */ - name?: string | null; - /** Comment */ - comment?: string | null; - /** Folder ID */ - folderId?: any; - /** If set to true, forcibly upload the file even if there is a file with the same hash. */ - force?: boolean; - /** Do not save file to local */ - isLink?: boolean; - /** URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL) */ - url?: string | null; - /** URL of source (リモートインスタンスのURLからアップロードされた場合の元URL) */ - uri?: string | null; - /** Mark file as sensitive */ - sensitive?: boolean | null; - - requestIp?: string | null; - requestHeaders?: Record | null; -}; - -/** - * Add file to drive - * - */ -export async function addFile({ - user, - path, - name = null, - comment = null, - folderId = null, - force = false, - isLink = false, - url = null, - uri = null, - sensitive = null, - requestIp = null, - requestHeaders = null, -}: AddFileArgs): Promise { - let skipNsfwCheck = false; - const instance = await fetchMeta(); - if (user == null) skipNsfwCheck = true; - if (instance.sensitiveMediaDetection === "none") skipNsfwCheck = true; - if ( - user && - instance.sensitiveMediaDetection === "local" && - Users.isRemoteUser(user) - ) - skipNsfwCheck = true; - if ( - user && - instance.sensitiveMediaDetection === "remote" && - Users.isLocalUser(user) - ) - skipNsfwCheck = true; - - const info = await getFileInfo(path, { - skipSensitiveDetection: skipNsfwCheck, - sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる - instance.sensitiveMediaDetectionSensitivity === "veryHigh" - ? 0.1 - : instance.sensitiveMediaDetectionSensitivity === "high" - ? 0.3 - : instance.sensitiveMediaDetectionSensitivity === "low" - ? 0.7 - : instance.sensitiveMediaDetectionSensitivity === "veryLow" - ? 0.9 - : 0.5, - sensitiveThresholdForPorn: 0.75, - enableSensitiveMediaDetectionForVideos: - instance.enableSensitiveMediaDetectionForVideos, - }); - logger.info(`${JSON.stringify(info)}`); - - // 現状 false positive が多すぎて実用に耐えない - //if (info.porn && instance.disallowUploadWhenPredictedAsPorn) { - // throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.'); - //} - - // 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.findOneBy({ - 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 u = await Users.findOneBy({ id: user.id }); - - const instance = await fetchMeta(); - let driveCapacity = - 1024 * - 1024 * - (Users.isLocalUser(user) - ? instance.localDriveCapacityMb - : instance.remoteDriveCapacityMb); - - if (Users.isLocalUser(user) && u?.driveCapacityOverrideMb != null) { - driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb; - logger.debug("drive capacity override applied"); - logger.debug( - `overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${ - usage + info.size - }bytes`, - ); - } - - logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); - - // If usage limit exceeded - if (usage + info.size > driveCapacity) { - if (Users.isLocalUser(user)) { - throw new IdentifiableError( - "c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6", - "No free space.", - ); - } else { - // (アバターまたはバナーを含まず)最も古いファイルを削除する - deleteOldFile( - (await Users.findOneByOrFail({ id: user.id })) as IRemoteUser, - ); - } - } - } - //#endregion - - const fetchFolder = async () => { - if (!folderId) { - return null; - } - - const driveFolder = await DriveFolders.findOneBy({ - id: folderId, - userId: user ? user.id : IsNull(), - }); - - if (driveFolder == null) throw new Error("folder-not-found"); - - return driveFolder; - }; - - const properties: { - width?: number; - height?: number; - orientation?: number; - } = {}; - - if (info.width) { - properties["width"] = info.width; - properties["height"] = info.height; - } - if (info.orientation != null) { - properties["orientation"] = info.orientation; - } - - const profile = user - ? await UserProfiles.findOneBy({ userId: 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.requestIp = requestIp; - file.requestHeaders = requestHeaders; - file.maybeSensitive = info.sensitive; - file.maybePorn = info.porn; - file.isSensitive = user - ? Users.isLocalUser(user) && profile!.alwaysMarkNsfw - ? true - : sensitive !== null && sensitive !== undefined - ? sensitive - : false - : false; - - if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; - if (info.sensitive && instance.setSensitiveFlagAutomatically) - file.isSensitive = true; - - 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.insert(file).then((x) => - DriveFiles.findOneByOrFail(x.identifiers[0]), - ); - } catch (err) { - // duplicate key error (when already registered) - if (isDuplicateKeyValueError(err)) { - logger.info(`already registered ${file.uri}`); - - file = (await DriveFiles.findOneBy({ - uri: file.uri!, - userId: user ? user.id : IsNull(), - })) as DriveFile; - } else { - logger.error(err as Error); - throw err; - } - } - } 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); - } - - return file; -} diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts deleted file mode 100644 index 215270df69..0000000000 --- a/packages/backend/src/services/drive/delete-file.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { InternalStorage } from "./internal-storage.js"; -import { DriveFiles, Instances } from "@/models/index.js"; -import { - driveChart, - perUserDriveChart, - instanceChart, -} from "@/services/chart/index.js"; -import { createDeleteObjectStorageFileJob } from "@/queue/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import { getS3 } from "./s3.js"; -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); - } -} - -export async function deleteObjectStorageFile(key: string) { - const meta = await fetchMeta(); - - const s3 = getS3(meta); - - await s3 - .deleteObject({ - Bucket: meta.objectStorageBucket!, - Key: key, - }) - .promise(); -} diff --git a/packages/backend/src/services/drive/generate-video-thumbnail.ts b/packages/backend/src/services/drive/generate-video-thumbnail.ts deleted file mode 100644 index 356623e79a..0000000000 --- a/packages/backend/src/services/drive/generate-video-thumbnail.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as fs from "node:fs"; -import { createTempDir } from "@/misc/create-temp.js"; -import type { IImage } from "./image-processor.js"; -import { convertToWebp } from "./image-processor.js"; -import FFmpeg from "fluent-ffmpeg"; - -export async function GenerateVideoThumbnail(source: string): Promise { - const [dir, cleanup] = await createTempDir(); - - try { - await new Promise((res, rej) => { - FFmpeg({ - source, - }) - .on("end", res) - .on("error", rej) - .screenshot({ - folder: dir, - filename: "out.png", // must have .png extension - count: 1, - timestamps: ["5%"], - }); - }); - - return await convertToWebp(`${dir}/out.png`, 996, 560); - } finally { - cleanup(); - } -} diff --git a/packages/backend/src/services/drive/image-processor.ts b/packages/backend/src/services/drive/image-processor.ts deleted file mode 100644 index 55869f478f..0000000000 --- a/packages/backend/src/services/drive/image-processor.ts +++ /dev/null @@ -1,44 +0,0 @@ -import sharp from "sharp"; - -export type IImage = { - data: Buffer; - ext: string | null; - type: string; -}; - -/** - * Convert to WebP - * with resize, remove metadata, resolve orientation, stop animation - */ -export async function convertToWebp( - path: string, - width: number, - height: number, - quality: number = 85, -): Promise { - return convertSharpToWebp(await sharp(path), width, height, quality); -} - -export async function convertSharpToWebp( - sharp: sharp.Sharp, - width: number, - height: number, - quality: number = 85, -): Promise { - const data = await sharp - .resize(width, height, { - fit: "inside", - withoutEnlargement: true, - }) - .rotate() - .webp({ - quality, - }) - .toBuffer(); - - return { - data, - ext: "webp", - type: "image/webp", - }; -} diff --git a/packages/backend/src/services/drive/internal-storage.ts b/packages/backend/src/services/drive/internal-storage.ts deleted file mode 100644 index bccb123be4..0000000000 --- a/packages/backend/src/services/drive/internal-storage.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as fs from "node:fs"; -import * as Path from "node:path"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; -import config from "@/config/index.js"; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -export class InternalStorage { - private static readonly path = Path.resolve(_dirname, "../../../../../files"); - - public static resolvePath = (key: string) => - Path.resolve(InternalStorage.path, key); - - public static read(key: string) { - return fs.createReadStream(InternalStorage.resolvePath(key)); - } - - public static saveFromPath(key: string, srcPath: string) { - fs.mkdirSync(InternalStorage.path, { recursive: true }); - fs.copyFileSync(srcPath, InternalStorage.resolvePath(key)); - return `${config.url}/files/${key}`; - } - - public static saveFromBuffer(key: string, data: Buffer) { - fs.mkdirSync(InternalStorage.path, { recursive: true }); - fs.writeFileSync(InternalStorage.resolvePath(key), data); - return `${config.url}/files/${key}`; - } - - public static del(key: string) { - fs.unlink(InternalStorage.resolvePath(key), () => {}); - } -} diff --git a/packages/backend/src/services/drive/logger.ts b/packages/backend/src/services/drive/logger.ts deleted file mode 100644 index ebde2d7058..0000000000 --- a/packages/backend/src/services/drive/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Logger from "../logger.js"; - -export const driveLogger = new Logger("drive", "blue"); diff --git a/packages/backend/src/services/drive/s3.ts b/packages/backend/src/services/drive/s3.ts deleted file mode 100644 index ca356e9124..0000000000 --- a/packages/backend/src/services/drive/s3.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { URL } from "node:url"; -import S3 from "aws-sdk/clients/s3.js"; -import type { Meta } from "@/models/entities/meta.js"; -import { getAgentByUrl } from "@/misc/fetch.js"; - -export function getS3(meta: Meta) { - const u = - meta.objectStorageEndpoint != null - ? `${meta.objectStorageUseSSL ? "https://" : "http://"}${ - meta.objectStorageEndpoint - }` - : `${meta.objectStorageUseSSL ? "https://" : "http://"}example.net`; - - return new S3({ - endpoint: meta.objectStorageEndpoint || undefined, - accessKeyId: meta.objectStorageAccessKey!, - secretAccessKey: meta.objectStorageSecretKey!, - region: meta.objectStorageRegion || undefined, - sslEnabled: meta.objectStorageUseSSL, - s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted - ? false - : meta.objectStorageS3ForcePathStyle, - httpOptions: { - agent: getAgentByUrl(new URL(u), !meta.objectStorageUseProxy), - }, - }); -} diff --git a/packages/backend/src/services/drive/upload-from-url.ts b/packages/backend/src/services/drive/upload-from-url.ts deleted file mode 100644 index 9d71757e35..0000000000 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { URL } from "node:url"; -import type { User } from "@/models/entities/user.js"; -import { createTemp } from "@/misc/create-temp.js"; -import { downloadUrl } from "@/misc/download-url.js"; -import type { DriveFolder } from "@/models/entities/drive-folder.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { DriveFiles } from "@/models/index.js"; -import { driveLogger } from "./logger.js"; -import { addFile } from "./add-file.js"; - -const logger = driveLogger.createSubLogger("downloader"); - -type Args = { - url: string; - user: { id: User["id"]; host: User["host"] } | null; - folderId?: DriveFolder["id"] | null; - uri?: string | null; - sensitive?: boolean; - force?: boolean; - isLink?: boolean; - comment?: string | null; - requestIp?: string | null; - requestHeaders?: Record | null; -}; - -export async function uploadFromUrl({ - url, - user, - folderId = null, - uri = null, - sensitive = false, - force = false, - isLink = false, - comment = null, - requestIp = null, - requestHeaders = null, -}: Args): 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(); - - try { - // write content at URL to temp file - await downloadUrl(url, path); - - const driveFile = await addFile({ - user, - path, - name, - comment, - folderId, - force, - isLink, - url, - uri, - sensitive, - requestIp, - requestHeaders, - }); - logger.succ(`Got: ${driveFile.id}`); - return driveFile!; - } catch (e) { - logger.error(`Failed to create drive file: ${e}`, { - url: url, - e: e, - }); - throw e; - } finally { - cleanup(); - } -} diff --git a/packages/backend/src/services/fetch-instance-metadata.ts b/packages/backend/src/services/fetch-instance-metadata.ts deleted file mode 100644 index 79354448f5..0000000000 --- a/packages/backend/src/services/fetch-instance-metadata.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { URL } from "node:url"; -import { JSDOM } from "jsdom"; -import fetch from "node-fetch"; -import tinycolor from "tinycolor2"; -import { getJson, getHtml, getAgentByUrl } from "@/misc/fetch.js"; -import type { Instance } from "@/models/entities/instance.js"; -import { Instances } from "@/models/index.js"; -import { getFetchInstanceMetadataLock } from "@/misc/app-lock.js"; -import Logger from "./logger.js"; -import type { DOMWindow } from "jsdom"; - -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.findOneBy({ host: instance.host }); - const now = Date.now(); - if ( - _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(info, 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 new Error("No nodeinfo provided"); - } else { - throw new Error(e.statusCode || e.message); - } - })) as Record; - - if (wellknown.links == null || !Array.isArray(wellknown.links)) { - throw new Error("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 new Error("No nodeinfo link provided"); - } - - const info = await getJson(link.href).catch((e) => { - throw new Error(e.statusCode || e.message); - }); - - logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`); - - return info as NodeInfo; - } catch (e) { - logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${e.message}`); - - 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)) as Record; - - return manifest; -} - -async function fetchFaviconUrl( - instance: Instance, - doc: DOMWindow["document"] | null, -): Promise { - const url = `https://${instance.host}`; - - if (doc) { - // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 - const href = Array.from(doc.getElementsByTagName("link")) - .reverse() - .find((link) => link.relList.contains("icon"))?.href; - - if (href) { - return new URL(href, url).href; - } - } - - const faviconUrl = `${url}/favicon.ico`; - - const favicon = await fetch(faviconUrl, { - // TODO - //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?.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}`; - - // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 - const links = Array.from(doc.getElementsByTagName("link")).reverse(); - // https://github.com/misskey-dev/misskey/pull/8220/files/0ec4eba22a914e31b86874f12448f88b3e58dd5a#r796487559 - const href = [ - links.find((link) => - link.relList.contains("apple-touch-icon-precomposed"), - )?.href, - links.find((link) => link.relList.contains("apple-touch-icon"))?.href, - links.find((link) => link.relList.contains("icon"))?.href, - ].find((href) => href); - - if (href) { - return new URL(href, url).href; - } - } - - return null; -} - -async function getThemeColor( - info: NodeInfo | null, - doc: DOMWindow["document"] | null, - manifest: Record | null, -): Promise { - const themeColor = - info?.metadata?.themeColor || - doc?.querySelector('meta[name="theme-color"]')?.getAttribute("content") || - manifest?.theme_color; - - if (themeColor) { - const color = new tinycolor(themeColor); - if (color.isValid()) return color.toHexString(); - } - - return null; -} - -async function getSiteName( - info: NodeInfo | null, - doc: DOMWindow["document"] | null, - manifest: Record | null, -): Promise { - if (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?.metadata) { - if (info.metadata.nodeDescription || info.metadata.description) { - return info.metadata.nodeDescription || info.metadata.description; - } - } - - if (doc) { - const meta = doc - .querySelector('meta[name="description"]') - ?.getAttribute("content"); - if (meta) { - return meta; - } - - const og = doc - .querySelector('meta[property="og:description"]') - ?.getAttribute("content"); - if (og) { - return og; - } - } - - if (manifest) { - return manifest.name || manifest.short_name; - } - - return null; -} diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts deleted file mode 100644 index 61a8c6b268..0000000000 --- a/packages/backend/src/services/following/create.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { publishMainStream, publishUserEvent } from "@/services/stream.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderFollow from "@/remote/activitypub/renderer/follow.js"; -import renderAccept from "@/remote/activitypub/renderer/accept.js"; -import renderReject from "@/remote/activitypub/renderer/reject.js"; -import { deliver } from "@/queue/index.js"; -import createFollowRequest from "./requests/create.js"; -import { registerOrFetchInstanceDoc } from "../register-or-fetch-instance-doc.js"; -import Logger from "../logger.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import type { User } from "@/models/entities/user.js"; -import { - Followings, - Users, - FollowRequests, - Blockings, - Instances, - UserProfiles, -} from "@/models/index.js"; -import { - instanceChart, - perUserFollowingChart, -} from "@/services/chart/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { createNotification } from "../create-notification.js"; -import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; -import type { Packed } from "@/misc/schema.js"; -import { getActiveWebhooks } from "@/misc/webhook-cache.js"; -import { webhookDeliver } from "@/queue/index.js"; - -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.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (req) { - await FollowRequests.delete({ - followeeId: followee.id, - followerId: follower.id, - }); - - // Create notification that request was accepted. - createNotification(follower.id, "followRequestAccepted", { - notifierId: followee.id, - }); - } - - if (alreadyFollowed) return; - - //#region Increment counts - await Promise.all([ - 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(async (packed) => { - publishUserEvent( - follower.id, - "follow", - packed as Packed<"UserDetailedNotMe">, - ); - publishMainStream( - follower.id, - "follow", - packed as Packed<"UserDetailedNotMe">, - ); - - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === follower.id && x.on.includes("follow"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "follow", { - user: packed, - }); - } - }); - } - - // Publish followed event - if (Users.isLocalUser(followee)) { - Users.pack(follower.id, followee).then(async (packed) => { - publishMainStream(followee.id, "followed", packed); - - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === followee.id && x.on.includes("followed"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "followed", { - user: 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.findOneByOrFail({ id: _follower.id }), - Users.findOneByOrFail({ id: _followee.id }), - ]); - - // check blocking - const [blocking, blocked] = await Promise.all([ - Blockings.findOneBy({ - blockerId: follower.id, - blockeeId: followee.id, - }), - Blockings.findOneBy({ - 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) - throw new IdentifiableError( - "710e8fb0-b8c3-4922-be49-d5d93d8e6a6e", - "blocking", - ); - if (blocked) - throw new IdentifiableError( - "3338392a-f764-498d-8855-db939dcf8c48", - "blocked", - ); - } - - const followeeProfile = await UserProfiles.findOneByOrFail({ - userId: 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.findOneBy({ - followerId: follower.id, - followeeId: followee.id, - }); - if (following) { - autoAccept = true; - } - - // フォローしているユーザーは自動承認オプション - if ( - !autoAccept && - Users.isLocalUser(followee) && - followeeProfile.autoAcceptFollowed - ) { - const followed = await Followings.findOneBy({ - followerId: followee.id, - followeeId: follower.id, - }); - - if (followed) autoAccept = true; - } - - if (!autoAccept) { - await createFollowRequest(follower, followee, requestId); - return; - } - } - - await insertFollowingDoc(followee, follower); - - if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { - const content = renderActivity( - renderAccept(renderFollow(follower, followee, requestId), followee), - ); - deliver(followee, content, follower.inbox); - } -} diff --git a/packages/backend/src/services/following/delete.ts b/packages/backend/src/services/following/delete.ts deleted file mode 100644 index fae4bd3cec..0000000000 --- a/packages/backend/src/services/following/delete.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { publishMainStream, publishUserEvent } from "@/services/stream.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderFollow from "@/remote/activitypub/renderer/follow.js"; -import renderUndo from "@/remote/activitypub/renderer/undo.js"; -import renderReject from "@/remote/activitypub/renderer/reject.js"; -import { deliver, webhookDeliver } from "@/queue/index.js"; -import Logger from "../logger.js"; -import { registerOrFetchInstanceDoc } from "../register-or-fetch-instance-doc.js"; -import type { User } from "@/models/entities/user.js"; -import { Followings, Users, Instances } from "@/models/index.js"; -import { - instanceChart, - perUserFollowingChart, -} from "@/services/chart/index.js"; -import { getActiveWebhooks } from "@/misc/webhook-cache.js"; - -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.findOneBy({ - 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(async (packed) => { - publishUserEvent(follower.id, "unfollow", packed); - publishMainStream(follower.id, "unfollow", packed); - - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === follower.id && x.on.includes("unfollow"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "unfollow", { - user: packed, - }); - } - }); - } - - if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - const content = renderActivity( - renderUndo(renderFollow(follower, followee), follower), - ); - deliver(follower, content, followee.inbox); - } - - if (Users.isLocalUser(followee) && Users.isRemoteUser(follower)) { - // local user has null host - const content = renderActivity( - renderReject(renderFollow(follower, followee), followee), - ); - deliver(followee, content, follower.inbox); - } -} - -export async function decrementFollowing( - follower: { id: User["id"]; host: User["host"] }, - followee: { id: User["id"]; host: User["host"] }, -) { - //#region Decrement following / followers counts - await Promise.all([ - Users.decrement({ id: follower.id }, "followingCount", 1), - Users.decrement({ id: followee.id }, "followersCount", 1), - ]); - //#endregion - - //#region Update instance stats - if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { - registerOrFetchInstanceDoc(follower.host).then((i) => { - Instances.decrement({ id: i.id }, "followingCount", 1); - instanceChart.updateFollowing(i.host, false); - }); - } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - registerOrFetchInstanceDoc(followee.host).then((i) => { - Instances.decrement({ id: i.id }, "followersCount", 1); - instanceChart.updateFollowers(i.host, false); - }); - } - //#endregion - - perUserFollowingChart.update(follower, followee, false); -} diff --git a/packages/backend/src/services/following/reject.ts b/packages/backend/src/services/following/reject.ts deleted file mode 100644 index 7464219bf6..0000000000 --- a/packages/backend/src/services/following/reject.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderFollow from "@/remote/activitypub/renderer/follow.js"; -import renderReject from "@/remote/activitypub/renderer/reject.js"; -import { deliver, webhookDeliver } from "@/queue/index.js"; -import { publishMainStream, publishUserEvent } from "@/services/stream.js"; -import type { ILocalUser, IRemoteUser } from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; -import { Users, FollowRequests, Followings } from "@/models/index.js"; -import { decrementFollowing } from "./delete.js"; -import { getActiveWebhooks } from "@/misc/webhook-cache.js"; - -type Local = - | ILocalUser - | { - id: ILocalUser["id"]; - host: ILocalUser["host"]; - uri: ILocalUser["uri"]; - }; -type Remote = - | IRemoteUser - | { - id: IRemoteUser["id"]; - host: IRemoteUser["host"]; - uri: IRemoteUser["uri"]; - inbox: IRemoteUser["inbox"]; - }; -type Both = Local | Remote; - -/** - * API following/request/reject - */ -export async function rejectFollowRequest(user: Local, follower: Both) { - if (Users.isRemoteUser(follower)) { - deliverReject(user, follower); - } - - await removeFollowRequest(user, follower); - - if (Users.isLocalUser(follower)) { - publishUnfollow(user, follower); - } -} - -/** - * API following/reject - */ -export async function rejectFollow(user: Local, follower: Both) { - if (Users.isRemoteUser(follower)) { - deliverReject(user, follower); - } - - await removeFollow(user, follower); - - if (Users.isLocalUser(follower)) { - publishUnfollow(user, follower); - } -} - -/** - * AP Reject/Follow - */ -export async function remoteReject(actor: Remote, follower: Local) { - await removeFollowRequest(actor, follower); - await removeFollow(actor, follower); - publishUnfollow(actor, follower); -} - -/** - * Remove follow request record - */ -async function removeFollowRequest(followee: Both, follower: Both) { - const request = await FollowRequests.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (!request) return; - - await FollowRequests.delete(request.id); -} - -/** - * Remove follow record - */ -async function removeFollow(followee: Both, follower: Both) { - const following = await Followings.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (!following) return; - - await Followings.delete(following.id); - decrementFollowing(follower, followee); -} - -/** - * Deliver Reject to remote - */ -async function deliverReject(followee: Local, follower: Remote) { - const request = await FollowRequests.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - const content = renderActivity( - renderReject( - renderFollow(follower, followee, request?.requestId || undefined), - followee, - ), - ); - deliver(followee, content, follower.inbox); -} - -/** - * Publish unfollow to local - */ -async function publishUnfollow(followee: Both, follower: Local) { - const packedFollowee = await Users.pack(followee.id, follower, { - detail: true, - }); - - publishUserEvent(follower.id, "unfollow", packedFollowee); - publishMainStream(follower.id, "unfollow", packedFollowee); - - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === follower.id && x.on.includes("unfollow"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "unfollow", { - user: packedFollowee, - }); - } -} diff --git a/packages/backend/src/services/following/requests/accept-all.ts b/packages/backend/src/services/following/requests/accept-all.ts deleted file mode 100644 index 2692433799..0000000000 --- a/packages/backend/src/services/following/requests/accept-all.ts +++ /dev/null @@ -1,24 +0,0 @@ -import accept from "./accept.js"; -import type { User } from "@/models/entities/user.js"; -import { FollowRequests, Users } from "@/models/index.js"; - -/** - * Approve all follow requests for the specified user - * @param user 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.findBy({ - followeeId: user.id, - }); - - for (const request of requests) { - const follower = await Users.findOneByOrFail({ id: request.followerId }); - accept(user, follower); - } -} diff --git a/packages/backend/src/services/following/requests/accept.ts b/packages/backend/src/services/following/requests/accept.ts deleted file mode 100644 index 6aa17b09ad..0000000000 --- a/packages/backend/src/services/following/requests/accept.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderFollow from "@/remote/activitypub/renderer/follow.js"; -import renderAccept from "@/remote/activitypub/renderer/accept.js"; -import { deliver } from "@/queue/index.js"; -import { publishMainStream } from "@/services/stream.js"; -import { insertFollowingDoc } from "../create.js"; -import type { User, CacheableUser } from "@/models/entities/user.js"; -import { ILocalUser } from "@/models/entities/user.js"; -import { FollowRequests, Users } from "@/models/index.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; - -export default async function ( - followee: { - id: User["id"]; - host: User["host"]; - uri: User["host"]; - inbox: User["inbox"]; - sharedInbox: User["sharedInbox"]; - }, - follower: CacheableUser, -) { - const request = await FollowRequests.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (request == null) { - throw new IdentifiableError( - "8884c2dd-5795-4ac9-b27e-6a01d38190f9", - "No follow request.", - ); - } - - await insertFollowingDoc(followee, follower); - - if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { - const content = renderActivity( - renderAccept( - renderFollow(follower, followee, request.requestId!), - followee, - ), - ); - deliver(followee, content, follower.inbox); - } - - Users.pack(followee.id, followee, { - detail: true, - }).then((packed) => publishMainStream(followee.id, "meUpdated", packed)); -} diff --git a/packages/backend/src/services/following/requests/cancel.ts b/packages/backend/src/services/following/requests/cancel.ts deleted file mode 100644 index 00daae380d..0000000000 --- a/packages/backend/src/services/following/requests/cancel.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderFollow from "@/remote/activitypub/renderer/follow.js"; -import renderUndo from "@/remote/activitypub/renderer/undo.js"; -import { deliver } from "@/queue/index.js"; -import { publishMainStream } from "@/services/stream.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import type { User } from "@/models/entities/user.js"; -import { ILocalUser } from "@/models/entities/user.js"; -import { Users, FollowRequests } from "@/models/index.js"; - -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.findOneBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (request == null) { - throw new IdentifiableError( - "17447091-ce07-46dd-b331-c1fd4f15b1e7", - "request not found", - ); - } - - await FollowRequests.delete({ - followeeId: followee.id, - followerId: follower.id, - }); - - Users.pack(followee.id, followee, { - detail: true, - }).then((packed) => publishMainStream(followee.id, "meUpdated", packed)); -} diff --git a/packages/backend/src/services/following/requests/create.ts b/packages/backend/src/services/following/requests/create.ts deleted file mode 100644 index 8b2e86ab5b..0000000000 --- a/packages/backend/src/services/following/requests/create.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderFollow from "@/remote/activitypub/renderer/follow.js"; -import { deliver } from "@/queue/index.js"; -import type { User } from "@/models/entities/user.js"; -import { Blockings, FollowRequests, Users } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { createNotification } from "../../create-notification.js"; - -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.findOneBy({ - blockerId: follower.id, - blockeeId: followee.id, - }), - Blockings.findOneBy({ - blockerId: followee.id, - blockeeId: follower.id, - }), - ]); - - if (blocking) throw new Error("blocking"); - if (blocked) throw new Error("blocked"); - - const followRequest = await FollowRequests.insert({ - 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, - }).then((x) => FollowRequests.findOneByOrFail(x.identifiers[0])); - - // Publish receiveRequest event - if (Users.isLocalUser(followee)) { - Users.pack(follower.id, followee).then((packed) => - publishMainStream(followee.id, "receiveFollowRequest", packed), - ); - - Users.pack(followee.id, followee, { - detail: true, - }).then((packed) => publishMainStream(followee.id, "meUpdated", packed)); - - // 通知を作成 - createNotification(followee.id, "receiveFollowRequest", { - notifierId: follower.id, - followRequestId: followRequest.id, - }); - } - - if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - const content = renderActivity(renderFollow(follower, followee)); - deliver(follower, content, followee.inbox); - } -} diff --git a/packages/backend/src/services/i/pin.ts b/packages/backend/src/services/i/pin.ts deleted file mode 100644 index 97045a9fa9..0000000000 --- a/packages/backend/src/services/i/pin.ts +++ /dev/null @@ -1,118 +0,0 @@ -import config from "@/config/index.js"; -import renderAdd from "@/remote/activitypub/renderer/add.js"; -import renderRemove from "@/remote/activitypub/renderer/remove.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import type { User } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -import { Notes, UserNotePinings, Users } from "@/models/index.js"; -import type { UserNotePining } from "@/models/entities/user-note-pining.js"; -import { genId } from "@/misc/gen-id.js"; -import { deliverToFollowers } from "@/remote/activitypub/deliver-manager.js"; -import { deliverToRelays } from "../relay.js"; - -/** - * 指定した投稿をピン留めします - * @param user - * @param noteId - */ -export async function addPinned( - user: { id: User["id"]; host: User["host"] }, - noteId: Note["id"], -) { - // Fetch pinee - const note = await Notes.findOneBy({ - id: noteId, - userId: user.id, - }); - - if (note == null) { - throw new IdentifiableError( - "70c4e51f-5bea-449c-a030-53bee3cce202", - "No such note.", - ); - } - - const pinings = await UserNotePinings.findBy({ 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.findOneBy({ - 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.findOneBy({ id: userId }); - if (user == null) throw new Error("user not found"); - - if (!Users.isLocalUser(user)) return; - - const target = `${config.url}/users/${user.id}/collections/featured`; - const item = `${config.url}/notes/${noteId}`; - const content = renderActivity( - isAddition - ? renderAdd(user, target, item) - : renderRemove(user, target, item), - ); - - deliverToFollowers(user, content); - deliverToRelays(user, content); -} diff --git a/packages/backend/src/services/i/update.ts b/packages/backend/src/services/i/update.ts deleted file mode 100644 index cc950ac85a..0000000000 --- a/packages/backend/src/services/i/update.ts +++ /dev/null @@ -1,21 +0,0 @@ -import renderUpdate from "@/remote/activitypub/renderer/update.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { Users } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import { renderPerson } from "@/remote/activitypub/renderer/person.js"; -import { deliverToFollowers } from "@/remote/activitypub/deliver-manager.js"; -import { deliverToRelays } from "../relay.js"; - -export async function publishToFollowers(userId: User["id"]) { - const user = await Users.findOneBy({ id: userId }); - if (user == null) throw new Error("user not found"); - - // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 - if (Users.isLocalUser(user)) { - const content = renderActivity( - renderUpdate(await renderPerson(user), user), - ); - deliverToFollowers(user, content); - deliverToRelays(user, content); - } -} diff --git a/packages/backend/src/services/insert-moderation-log.ts b/packages/backend/src/services/insert-moderation-log.ts deleted file mode 100644 index 8e2c5b78a1..0000000000 --- a/packages/backend/src/services/insert-moderation-log.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ModerationLogs } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { User } from "@/models/entities/user.js"; - -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/packages/backend/src/services/instance-actor.ts b/packages/backend/src/services/instance-actor.ts deleted file mode 100644 index 50ce227eba..0000000000 --- a/packages/backend/src/services/instance-actor.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createSystemUser } from "./create-system-user.js"; -import type { ILocalUser } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; -import { Cache } from "@/misc/cache.js"; -import { IsNull } from "typeorm"; - -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.findOneBy({ - host: IsNull(), - username: ACTOR_USERNAME, - })) as ILocalUser | undefined; - - if (user) { - cache.set(null, user); - return user; - } else { - const created = (await createSystemUser(ACTOR_USERNAME)) as ILocalUser; - cache.set(null, created); - return created; - } -} diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts deleted file mode 100644 index 86e1d10d32..0000000000 --- a/packages/backend/src/services/logger.ts +++ /dev/null @@ -1,199 +0,0 @@ -import cluster from "node:cluster"; -import chalk from "chalk"; -import { default as convertColor } from "color-convert"; -import { format as dateFormat } from "date-fns"; -import { envOption } from "../env.js"; -import config from "@/config/index.js"; - -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: "Calckey", - 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.isPrimary ? "*" : cluster.worker.id; - const l = - level === "error" - ? important - ? chalk.bgRed.white("ERR ") - : chalk.red("ERR ") - : level === "warning" - ? chalk.yellow("WARN") - : level === "success" - ? important - ? chalk.bgGreen.white("DONE") - : chalk.green("DONE") - : level === "debug" - ? chalk.gray("VERB") - : level === "info" - ? chalk.blue("INFO") - : null; - const domains = [this.domain] - .concat(subDomains) - .map((d) => - d.color - ? chalk.rgb(...convertColor.keyword.rgb(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/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts deleted file mode 100644 index 506f299966..0000000000 --- a/packages/backend/src/services/messages/create.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type { CacheableUser, User } from "@/models/entities/user.js"; -import type { UserGroup } from "@/models/entities/user-group.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import { - MessagingMessages, - UserGroupJoinings, - Mutings, - Users, -} from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { MessagingMessage } from "@/models/entities/messaging-message.js"; -import { - publishMessagingStream, - publishMessagingIndexStream, - publishMainStream, - publishGroupMessagingStream, -} from "@/services/stream.js"; -import { pushNotification } from "@/services/push-notification.js"; -import { Not } from "typeorm"; -import type { Note } from "@/models/entities/note.js"; -import renderNote from "@/remote/activitypub/renderer/note.js"; -import renderCreate from "@/remote/activitypub/renderer/create.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { deliver } from "@/queue/index.js"; - -export async function createMessage( - user: { id: User["id"]; host: User["host"] }, - recipientUser: CacheableUser | undefined, - recipientGroup: UserGroup | undefined, - text: string | null | 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.findBy({ - 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.findOneBy({ id: message.id }); - if (freshMessage == null) return; // メッセージが削除されている場合もある - - if (recipientUser && Users.isLocalUser(recipientUser)) { - if (freshMessage.isRead) return; // 既読 - - //#region ただしミュートされているなら発行しない - const mute = await Mutings.findBy({ - 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.findBy({ - userGroupId: recipientGroup.id, - userId: Not(user.id), - }); - for (const joining of joinings) { - if (freshMessage.reads.includes(joining.userId)) return; // 既読 - publishMainStream(joining.userId, "unreadMessagingMessage", messageObj); - pushNotification(joining.userId, "unreadMessagingMessage", messageObj); - } - } - }, 2000); - - if ( - recipientUser && - Users.isLocalUser(user) && - Users.isRemoteUser(recipientUser) - ) { - const note = { - id: message.id, - createdAt: message.createdAt, - fileIds: message.fileId ? [message.fileId] : [], - text: message.text, - userId: message.userId, - visibility: "specified", - mentions: [recipientUser].map((u) => u.id), - mentionedRemoteUsers: JSON.stringify( - [recipientUser].map((u) => ({ - uri: u.uri, - username: u.username, - host: u.host, - })), - ), - } as Note; - - const activity = renderActivity( - renderCreate(await renderNote(note, false, true), note), - ); - - deliver(user, activity, recipientUser.inbox); - } - return messageObj; -} diff --git a/packages/backend/src/services/messages/delete.ts b/packages/backend/src/services/messages/delete.ts deleted file mode 100644 index 77caba80ce..0000000000 --- a/packages/backend/src/services/messages/delete.ts +++ /dev/null @@ -1,50 +0,0 @@ -import config from "@/config/index.js"; -import { MessagingMessages, Users } from "@/models/index.js"; -import type { MessagingMessage } from "@/models/entities/messaging-message.js"; -import { - publishGroupMessagingStream, - publishMessagingStream, -} from "@/services/stream.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderDelete from "@/remote/activitypub/renderer/delete.js"; -import renderTombstone from "@/remote/activitypub/renderer/tombstone.js"; -import { deliver } from "@/queue/index.js"; - -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.findOneByOrFail({ id: message.userId }); - const recipient = await Users.findOneByOrFail({ id: message.recipientId }); - - if (Users.isLocalUser(user)) - publishMessagingStream( - message.userId, - message.recipientId, - "deleted", - message.id, - ); - if (Users.isLocalUser(recipient)) - publishMessagingStream( - message.recipientId, - message.userId, - "deleted", - message.id, - ); - - if (Users.isLocalUser(user) && Users.isRemoteUser(recipient)) { - const activity = renderActivity( - renderDelete( - renderTombstone(`${config.url}/notes/${message.id}`), - user, - ), - ); - deliver(user, activity, recipient.inbox); - } - } else if (message.groupId) { - publishGroupMessagingStream(message.groupId, "deleted", message.id); - } -} diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts deleted file mode 100644 index 5dd324d89a..0000000000 --- a/packages/backend/src/services/note/create.ts +++ /dev/null @@ -1,871 +0,0 @@ -import * as mfm from "mfm-js"; -import es from "../../db/elasticsearch.js"; -import sonic from "../../db/sonic.js"; -import { - publishMainStream, - publishNotesStream, - publishNoteStream, -} from "@/services/stream.js"; -import DeliverManager from "@/remote/activitypub/deliver-manager.js"; -import renderNote from "@/remote/activitypub/renderer/note.js"; -import renderCreate from "@/remote/activitypub/renderer/create.js"; -import renderAnnounce from "@/remote/activitypub/renderer/announce.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { resolveUser } from "@/remote/resolve-user.js"; -import config from "@/config/index.js"; -import { updateHashtags } from "../update-hashtag.js"; -import { concat } from "@/prelude/array.js"; -import { insertNoteUnread } from "@/services/note/unread.js"; -import { registerOrFetchInstanceDoc } from "../register-or-fetch-instance-doc.js"; -import { extractMentions } from "@/misc/extract-mentions.js"; -import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js"; -import { extractHashtags } from "@/misc/extract-hashtags.js"; -import type { IMentionedRemoteUsers } from "@/models/entities/note.js"; -import { Note } from "@/models/entities/note.js"; -import { - Mutings, - Users, - NoteWatchings, - Notes, - Instances, - UserProfiles, - Antennas, - Followings, - MutedNotes, - Channels, - ChannelFollowings, - Blockings, - NoteThreadMutings, -} from "@/models/index.js"; -import type { DriveFile } from "@/models/entities/drive-file.js"; -import type { App } from "@/models/entities/app.js"; -import { Not, In } from "typeorm"; -import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; -import { genId } from "@/misc/gen-id.js"; -import { - notesChart, - perUserNotesChart, - activeUsersChart, - instanceChart, -} from "@/services/chart/index.js"; -import type { IPoll } from "@/models/entities/poll.js"; -import { Poll } from "@/models/entities/poll.js"; -import { createNotification } from "../create-notification.js"; -import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; -import { checkHitAntenna } from "@/misc/check-hit-antenna.js"; -import { getWordMute } from "@/misc/check-word-mute.js"; -import { addNoteToAntenna } from "../add-note-to-antenna.js"; -import { countSameRenotes } from "@/misc/count-same-renotes.js"; -import { deliverToRelays } from "../relay.js"; -import type { Channel } from "@/models/entities/channel.js"; -import { normalizeForSearch } from "@/misc/normalize-for-search.js"; -import { getAntennas } from "@/misc/antenna-cache.js"; -import { endedPollNotificationQueue } from "@/queue/queues.js"; -import { webhookDeliver } from "@/queue/index.js"; -import { Cache } from "@/misc/cache.js"; -import type { UserProfile } from "@/models/entities/user-profile.js"; -import { db } from "@/db/postgre.js"; -import { getActiveWebhooks } from "@/misc/webhook-cache.js"; - -const mutedWordsCache = new Cache< - { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] ->(1000 * 60 * 5); - -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.findBy({ - 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, - note: this.note, - }); - } - } - } -} - -type MinimumUser = { - id: User["id"]; - host: User["host"]; - username: User["username"]; - uri: User["uri"]; -}; - -type Option = { - createdAt?: Date | null; - name?: string | null; - text?: string | null; - reply?: Note | null; - renote?: Note | null; - files?: DriveFile[] | null; - poll?: IPoll | null; - localOnly?: boolean | null; - cw?: string | null; - visibility?: string; - visibleUsers?: MinimumUser[] | null; - channel?: Channel | null; - apMentions?: MinimumUser[] | 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"]; - createdAt: User["createdAt"]; - }, - data: Option, - silent = false, -) => - new Promise(async (res, rej) => { - // If you reply outside the channel, match the scope of the target. - // TODO (I think it's a process that could be done on the client side, but it's server side for now.) - if ( - data.reply && - data.channel && - data.reply.channelId !== data.channel.id - ) { - if (data.reply.channelId) { - data.channel = await Channels.findOneBy({ id: data.reply.channelId }); - } else { - data.channel = null; - } - } - - // When you reply in a channel, match the scope of the target - // TODO (I think it's a process that could be done on the client side, but it's server side for now.) - if (data.reply && data.channel == null && data.reply.channelId) { - data.channel = await Channels.findOneBy({ id: data.reply.channelId }); - } - - if (data.createdAt == null) data.createdAt = new Date(); - if (data.visibility == null) data.visibility = "public"; - 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; - - // enforce silent clients on server - if ( - user.isSilenced && - data.visibility === "public" && - data.channel == null - ) { - data.visibility = "home"; - } - - // Reject if the target of the renote is a public range other than "Home or Entire". - 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"); - } - - // If the target of the renote is not public, make it home. - if ( - data.renote && - data.renote.visibility !== "public" && - data.visibility === "public" - ) { - data.visibility = "home"; - } - - // If the target of Renote is followers, make it followers. - if (data.renote && data.renote.visibility === "followers") { - data.visibility = "followers"; - } - - // If the reply target is not public, make it home. - if ( - data.reply && - data.reply.visibility !== "public" && - data.visibility === "public" - ) { - data.visibility = "home"; - } - - // Renote local only if you Renote local only. - if (data.renote?.localOnly && data.channel == null) { - data.localOnly = true; - } - - // If you reply to local only, make it local only. - if (data.reply?.localOnly && data.channel == null) { - data.localOnly = true; - } - - if (data.text) { - data.text = data.text.trim(); - } else { - data.text = null; - } - - let tags = data.apHashtags; - let emojis = data.apEmojis; - let mentionedUsers = data.apMentions; - - // Parse MFM if needed - if (!(tags && emojis && mentionedUsers)) { - const tokens = data.text ? mfm.parse(data.text)! : []; - const cwTokens = data.cw ? mfm.parse(data.cw)! : []; - const choiceTokens = data.poll?.choices - ? concat(data.poll.choices.map((choice) => mfm.parse(choice)!)) - : []; - - const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); - - tags = data.apHashtags || extractHashtags(combinedTokens); - - emojis = data.apEmojis || extractCustomEmojisFromMfm(combinedTokens); - - mentionedUsers = - data.apMentions || (await extractMentionedUsers(user, combinedTokens)); - } - - tags = tags - .filter((tag) => Array.from(tag || "").length <= 128) - .splice(0, 32); - - if ( - data.reply && - user.id !== data.reply.userId && - !mentionedUsers.some((u) => u.id === data.reply!.userId) - ) { - mentionedUsers.push( - await Users.findOneByOrFail({ id: 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.findOneByOrFail({ id: 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 - mutedWordsCache - .fetch(null, () => - UserProfiles.find({ - where: { - enableWordMute: true, - }, - select: ["userId", "mutedWords"], - }), - ) - .then((us) => { - for (const u of us) { - getWordMute(note, { id: u.userId }, u.mutedWords).then( - (shouldMute) => { - if (shouldMute.muted) { - MutedNotes.insert({ - id: genId(), - userId: u.userId, - noteId: note.id, - reason: "word", - }); - } - }, - ); - } - }); - - // Antenna - for (const antenna of await getAntennas()) { - checkHitAntenna(antenna, note, user).then((hit) => { - if (hit) { - addNoteToAntenna(antenna, note, user); - } - }); - } - - // Channel - if (note.channelId) { - ChannelFollowings.findBy({ 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 (data.poll?.expiresAt) { - const delay = data.poll.expiresAt.getTime() - Date.now(); - endedPollNotificationQueue.add( - { - noteId: note.id, - }, - { - delay, - removeOnComplete: true, - }, - ); - } - - if (!silent) { - if (Users.isLocalUser(user)) activeUsersChart.write(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, - }); - } - } - - publishNotesStream(note); - if (note.replyId != null) { - // Only provide the reply note id here as the recipient may not be authorized to see the note. - publishNoteStream(note.replyId, "replied", { - id: note.id, - }); - } - - const webhooks = await getActiveWebhooks().then((webhooks) => - webhooks.filter((x) => x.userId === user.id && x.on.includes("note")), - ); - - for (const webhook of webhooks) { - webhookDeliver(webhook, "note", { - note: await Notes.pack(note, user), - }); - } - - 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.findOneBy({ - userId: data.reply.userId, - threadId: data.reply.threadId || data.reply.id, - }); - - if (!threadMuted) { - nm.push(data.reply.userId, "reply"); - - const packedReply = await Notes.pack(note, { - id: data.reply.userId, - }); - publishMainStream(data.reply.userId, "reply", packedReply); - - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === data.reply!.userId && x.on.includes("reply"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "reply", { - note: packedReply, - }); - } - } - } - } - - // If it is renote - if (data.renote) { - const type = data.text ? "quote" : "renote"; - - // Notify - if (data.renote.userHost === null) { - const threadMuted = await NoteThreadMutings.findOneBy({ - userId: data.renote.userId, - threadId: data.renote.threadId || data.renote.id, - }); - - if (!threadMuted) { - 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) { - const packedRenote = await Notes.pack(note, { - id: data.renote.userId, - }); - publishMainStream(data.renote.userId, "renote", packedRenote); - - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === data.renote!.userId && x.on.includes("renote"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "renote", { - note: packedRenote, - }); - } - } - } - - 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.findOneBy({ id: data.reply.userId }); - if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); - } - - // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 - if (data.renote && data.renote.userHost !== null) { - const u = await Users.findOneBy({ id: 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(), - }); - - const count = await Notes.countBy({ - 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 - await 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: MinimumUser[], -) { - 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, - 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.findBy({ 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 db.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; - } -} - -export async function index(note: Note): Promise { - if (!note.text) return; - - if (config.elasticsearch && es) { - es.index({ - index: config.elasticsearch.index || "misskey_note", - id: note.id.toString(), - body: { - text: normalizeForSearch(note.text), - userId: note.userId, - userHost: note.userHost, - }, - }); - } - - if (sonic) { - await sonic.ingest.push( - sonic.collection, - sonic.bucket, - JSON.stringify({ - id: note.id, - userId: note.userId, - userHost: note.userHost, - channelId: note.channelId, - }), - note.text, - ); - } -} - -async function notifyToWatchersOfRenotee( - renote: Note, - user: { id: User["id"] }, - nm: NotificationManager, - type: NotificationType, -) { - const watchers = await NoteWatchings.findBy({ - 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.findBy({ - noteId: reply.id, - userId: Not(user.id), - }); - - for (const watcher of watchers) { - nm.push(watcher.userId, "reply"); - } -} - -async function createMentionedEvents( - mentionedUsers: MinimumUser[], - note: Note, - nm: NotificationManager, -) { - for (const u of mentionedUsers.filter((u) => Users.isLocalUser(u))) { - const threadMuted = await NoteThreadMutings.findOneBy({ - userId: u.id, - threadId: note.threadId || note.id, - }); - - if (threadMuted) { - continue; - } - - // note with "specified" visibility might not be visible to mentioned users - try { - const detailPackedNote = await Notes.pack(note, u, { - detail: true, - }); - - publishMainStream(u.id, "mention", detailPackedNote); - - const webhooks = (await getActiveWebhooks()).filter( - (x) => x.userId === u.id && x.on.includes("mention"), - ); - for (const webhook of webhooks) { - webhookDeliver(webhook, "mention", { - note: detailPackedNote, - }); - } - } catch (err) { - if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") continue; - throw err; - } - - // 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/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts deleted file mode 100644 index 392578e2f6..0000000000 --- a/packages/backend/src/services/note/delete.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { Brackets, In } from "typeorm"; -import { publishNoteStream } from "@/services/stream.js"; -import renderDelete from "@/remote/activitypub/renderer/delete.js"; -import renderAnnounce from "@/remote/activitypub/renderer/announce.js"; -import renderUndo from "@/remote/activitypub/renderer/undo.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderTombstone from "@/remote/activitypub/renderer/tombstone.js"; -import config from "@/config/index.js"; -import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; -import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; -import { Notes, Users, Instances } from "@/models/index.js"; -import { - notesChart, - perUserNotesChart, - instanceChart, -} from "@/services/chart/index.js"; -import { - deliverToFollowers, - deliverToUser, -} from "@/remote/activitypub/deliver-manager.js"; -import { countSameRenotes } from "@/misc/count-same-renotes.js"; -import { registerOrFetchInstanceDoc } from "../register-or-fetch-instance-doc.js"; -import { deliverToRelays } from "../relay.js"; - -/** - * 投稿を削除します。 - * @param user 投稿者 - * @param note 投稿 - */ -export default async function ( - user: { id: User["id"]; uri: User["uri"]; host: User["host"] }, - 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 (note.replyId) { - await Notes.decrement({ id: note.replyId }, "repliesCount", 1); - } - - if (!quiet) { - publishNoteStream(note.id, "deleted", { - deletedAt: deletedAt, - }); - - //#region ローカルの投稿なら削除アクティビティを配送 - if (Users.isLocalUser(user) && !note.localOnly) { - let renote: Note | null = null; - - // if deletd note is renote - if ( - note.renoteId && - note.text == null && - !note.hasPoll && - (note.fileIds == null || note.fileIds.length === 0) - ) { - renote = await Notes.findOneBy({ - 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: { id: ILocalUser["id"]; host: null }, - note: Note, - content: any, -) { - deliverToFollowers(user, content); - deliverToRelays(user, content); - const remoteUsers = await getMentionedRemoteUsers(note); - for (const remoteUser of remoteUsers) { - deliverToUser(user, content, remoteUser); - } -} diff --git a/packages/backend/src/services/note/polls/update.ts b/packages/backend/src/services/note/polls/update.ts deleted file mode 100644 index e02d48d055..0000000000 --- a/packages/backend/src/services/note/polls/update.ts +++ /dev/null @@ -1,23 +0,0 @@ -import renderUpdate from "@/remote/activitypub/renderer/update.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import renderNote from "@/remote/activitypub/renderer/note.js"; -import { Users, Notes } from "@/models/index.js"; -import type { Note } from "@/models/entities/note.js"; -import { deliverToFollowers } from "@/remote/activitypub/deliver-manager.js"; -import { deliverToRelays } from "../../relay.js"; - -export async function deliverQuestionUpdate(noteId: Note["id"]) { - const note = await Notes.findOneBy({ id: noteId }); - if (note == null) throw new Error("note not found"); - - const user = await Users.findOneBy({ id: note.userId }); - if (user == null) throw new Error("note not found"); - - if (Users.isLocalUser(user)) { - const content = renderActivity( - renderUpdate(await renderNote(note, false), user), - ); - deliverToFollowers(user, content); - deliverToRelays(user, content); - } -} diff --git a/packages/backend/src/services/note/polls/vote.ts b/packages/backend/src/services/note/polls/vote.ts deleted file mode 100644 index 582af0b17b..0000000000 --- a/packages/backend/src/services/note/polls/vote.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { publishNoteStream } from "@/services/stream.js"; -import type { CacheableUser } from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -import { PollVotes, NoteWatchings, Polls, Blockings } from "@/models/index.js"; -import { Not } from "typeorm"; -import { genId } from "@/misc/gen-id.js"; -import { createNotification } from "../../create-notification.js"; - -export default async function ( - user: CacheableUser, - note: Note, - choice: number, -) { - const poll = await Polls.findOneBy({ noteId: 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.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { - throw new Error("blocked"); - } - } - - // if already voted - const exist = await PollVotes.findBy({ - 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.findBy({ - noteId: note.id, - userId: Not(user.id), - }).then((watchers) => { - for (const watcher of watchers) { - createNotification(watcher.userId, "pollVote", { - notifierId: user.id, - noteId: note.id, - choice: choice, - }); - } - }); -} diff --git a/packages/backend/src/services/note/reaction/create.ts b/packages/backend/src/services/note/reaction/create.ts deleted file mode 100644 index 1a3c52eb51..0000000000 --- a/packages/backend/src/services/note/reaction/create.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { publishNoteStream } from "@/services/stream.js"; -import { renderLike } from "@/remote/activitypub/renderer/like.js"; -import DeliverManager from "@/remote/activitypub/deliver-manager.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { toDbReaction, decodeReaction } from "@/misc/reaction-lib.js"; -import type { User, IRemoteUser } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -import { - NoteReactions, - Users, - NoteWatchings, - Notes, - Emojis, - Blockings, -} from "@/models/index.js"; -import { IsNull, Not } from "typeorm"; -import { perUserReactionsChart } from "@/services/chart/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { createNotification } from "../../create-notification.js"; -import deleteReaction from "./delete.js"; -import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; -import type { NoteReaction } from "@/models/entities/note-reaction.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; - -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.findOneBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (block) { - throw new IdentifiableError("e70412a4-7197-4726-8e74-f3e0deb92aa7"); - } - } - - // check visibility - if (!(await Notes.isVisibleForMe(note, user.id))) { - throw new IdentifiableError( - "68e9d2d1-48bf-42c2-b90a-b20e09fd3d48", - "Note not accessible for you.", - ); - } - - // 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.findOneByOrFail({ - 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); - - const emoji = await Emojis.findOne({ - where: { - name: decodedReaction.name, - host: decodedReaction.host ?? IsNull(), - }, - select: ["name", "host", "originalUrl", "publicUrl"], - }); - - publishNoteStream(note.id, "reacted", { - reaction: decodedReaction.reaction, - emoji: - emoji != null - ? { - name: emoji.host - ? `${emoji.name}@${emoji.host}` - : `${emoji.name}@.`, - url: emoji.publicUrl || emoji.originalUrl, // || emoji.originalUrl してるのは後方互換性のため - } - : null, - userId: user.id, - }); - - // リアクションされたユーザーがローカルユーザーなら通知を作成 - if (note.userHost === null) { - createNotification(note.userId, "reaction", { - notifierId: user.id, - note: note, - noteId: note.id, - reaction: reaction, - }); - } - - // Fetch watchers - NoteWatchings.findBy({ - noteId: note.id, - userId: Not(user.id), - }).then((watchers) => { - for (const watcher of watchers) { - createNotification(watcher.userId, "reaction", { - notifierId: user.id, - note: note, - 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.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as IRemoteUser); - } - - if (["public", "home", "followers"].includes(note.visibility)) { - dm.addFollowersRecipe(); - } else if (note.visibility === "specified") { - const visibleUsers = await Promise.all( - note.visibleUserIds.map((id) => Users.findOneBy({ id })), - ); - for (const u of visibleUsers.filter((u) => u && Users.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); - } - } - - dm.execute(); - } - //#endregion -}; diff --git a/packages/backend/src/services/note/reaction/delete.ts b/packages/backend/src/services/note/reaction/delete.ts deleted file mode 100644 index 82648249e6..0000000000 --- a/packages/backend/src/services/note/reaction/delete.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { publishNoteStream } from "@/services/stream.js"; -import { renderLike } from "@/remote/activitypub/renderer/like.js"; -import renderUndo from "@/remote/activitypub/renderer/undo.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import DeliverManager from "@/remote/activitypub/deliver-manager.js"; -import { IdentifiableError } from "@/misc/identifiable-error.js"; -import type { User, IRemoteUser } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -import { NoteReactions, Users, Notes } from "@/models/index.js"; -import { decodeReaction } from "@/misc/reaction-lib.js"; - -export default async ( - user: { id: User["id"]; host: User["host"] }, - note: Note, -) => { - // if already unreacted - const exist = await NoteReactions.findOneBy({ - 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.findOneBy({ id: note.userId }); - dm.addDirectRecipe(reactee as IRemoteUser); - } - dm.addFollowersRecipe(); - dm.execute(); - } - //#endregion -}; diff --git a/packages/backend/src/services/note/read.ts b/packages/backend/src/services/note/read.ts deleted file mode 100644 index 53188f15f7..0000000000 --- a/packages/backend/src/services/note/read.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { publishMainStream } from "@/services/stream.js"; -import type { Note } from "@/models/entities/note.js"; -import type { User } from "@/models/entities/user.js"; -import { - NoteUnreads, - AntennaNotes, - Users, - Followings, - ChannelFollowings, -} from "@/models/index.js"; -import { Not, IsNull, In } from "typeorm"; -import type { Channel } from "@/models/entities/channel.js"; -import { checkHitAntenna } from "@/misc/check-hit-antenna.js"; -import { getAntennas } from "@/misc/antenna-cache.js"; -import { readNotificationByQuery } from "@/server/api/common/read-notification.js"; -import type { Packed } from "@/misc/schema.js"; - -/** - * 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?.includes(userId)) { - readMentions.push(note); - } else if (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, - 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.countBy({ - userId: userId, - isMentioned: true, - }).then((mentionsCount) => { - if (mentionsCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, "readAllUnreadMentions"); - } - }); - - NoteUnreads.countBy({ - userId: userId, - isSpecified: true, - }).then((specifiedCount) => { - if (specifiedCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, "readAllUnreadSpecifiedNotes"); - } - }); - - NoteUnreads.countBy({ - 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.countBy({ - antennaId: antenna.id, - read: false, - }); - - if (count === 0) { - publishMainStream(userId, "readAntenna", antenna); - } - } - - Users.getHasUnreadAntenna(userId).then((unread) => { - if (!unread) { - publishMainStream(userId, "readAllAntennas"); - } - }); - } -} diff --git a/packages/backend/src/services/note/unread.ts b/packages/backend/src/services/note/unread.ts deleted file mode 100644 index 275b230d47..0000000000 --- a/packages/backend/src/services/note/unread.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { Note } from "@/models/entities/note.js"; -import { publishMainStream } from "@/services/stream.js"; -import type { User } from "@/models/entities/user.js"; -import { Mutings, NoteThreadMutings, NoteUnreads } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; - -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.findBy({ - muterId: userId, - }); - if (mute.map((m) => m.muteeId).includes(note.userId)) return; - //#endregion - - // スレッドミュート - const threadMute = await NoteThreadMutings.findOneBy({ - 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.findOneBy({ id: unread.id }); - - if (exist == null) return; - - if (params.isMentioned) { - publishMainStream(userId, "unreadMention", note.id); - } - if (params.isSpecified) { - publishMainStream(userId, "unreadSpecifiedNote", note.id); - } - if (note.channelId) { - publishMainStream(userId, "unreadChannel", note.id); - } - }, 2000); -} diff --git a/packages/backend/src/services/note/unwatch.ts b/packages/backend/src/services/note/unwatch.ts deleted file mode 100644 index b4da5e86da..0000000000 --- a/packages/backend/src/services/note/unwatch.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import { NoteWatchings } from "@/models/index.js"; -import type { Note } from "@/models/entities/note.js"; - -export default async (me: User["id"], note: Note) => { - await NoteWatchings.delete({ - noteId: note.id, - userId: me, - }); -}; diff --git a/packages/backend/src/services/note/watch.ts b/packages/backend/src/services/note/watch.ts deleted file mode 100644 index 2a99dd6949..0000000000 --- a/packages/backend/src/services/note/watch.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -import { NoteWatchings } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { NoteWatching } from "@/models/entities/note-watching.js"; - -export default async (me: User["id"], note: Note) => { - // 自分の投稿はwatchできない - if (me === note.userId) { - return; - } - - await NoteWatchings.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: me, - noteUserId: note.userId, - } as NoteWatching); -}; diff --git a/packages/backend/src/services/push-notification.ts b/packages/backend/src/services/push-notification.ts deleted file mode 100644 index a207fae391..0000000000 --- a/packages/backend/src/services/push-notification.ts +++ /dev/null @@ -1,116 +0,0 @@ -import push from "web-push"; -import config from "@/config/index.js"; -import { SwSubscriptions } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import type { Packed } from "@/misc/schema.js"; -import { getNoteSummary } from "@/misc/get-note-summary.js"; - -// Defined also packages/sw/types.ts#L14-L21 -type pushNotificationsTypes = { - notification: Packed<"Notification">; - unreadMessagingMessage: Packed<"MessagingMessage">; - readNotifications: { notificationIds: string[] }; - readAllNotifications: undefined; - readAllMessagingMessages: undefined; - readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string }; -}; - -// プッシュメッセージサーバーには文字数制限があるため、内容を削減します -function truncateNotification(notification: Packed<"Notification">): any { - if (notification.note) { - return { - ...notification, - note: { - ...notification.note, - // textをgetNoteSummaryしたものに置き換える - text: getNoteSummary( - notification.type === "renote" - ? (notification.note.renote as Packed<"Note">) - : notification.note, - ), - - cw: undefined, - reply: undefined, - renote: undefined, - user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる - }, - }; - } - - return notification; -} - -export async function pushNotification( - userId: string, - type: T, - body: pushNotificationsTypes[T], -) { - 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.findBy({ - userId: userId, - }); - - for (const subscription of subscriptions) { - if ( - [ - "readNotifications", - "readAllNotifications", - "readAllMessagingMessages", - "readAllMessagingMessagesOfARoom", - ].includes(type) && - !subscription.sendReadMessage - ) - continue; - - const pushSubscription = { - endpoint: subscription.endpoint, - keys: { - auth: subscription.auth, - p256dh: subscription.publickey, - }, - }; - - push - .sendNotification( - pushSubscription, - JSON.stringify({ - type, - body: - type === "notification" - ? truncateNotification(body as Packed<"Notification">) - : body, - userId, - dateTime: new Date().getTime(), - }), - { - proxy: config.proxy, - }, - ) - .catch((err: any) => { - //swLogger.info(err.statusCode); - //swLogger.info(err.headers); - //swLogger.info(err.body); - - if (err.statusCode === 410) { - SwSubscriptions.delete({ - userId: userId, - endpoint: subscription.endpoint, - auth: subscription.auth, - publickey: subscription.publickey, - }); - } - }); - } -} diff --git a/packages/backend/src/services/register-or-fetch-instance-doc.ts b/packages/backend/src/services/register-or-fetch-instance-doc.ts deleted file mode 100644 index 4c3570e907..0000000000 --- a/packages/backend/src/services/register-or-fetch-instance-doc.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Instance } from "@/models/entities/instance.js"; -import { Instances } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { toPuny } from "@/misc/convert-host.js"; -import { Cache } from "@/misc/cache.js"; - -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.findOneBy({ host }); - - if (index == null) { - const i = await Instances.insert({ - id: genId(), - host, - caughtAt: new Date(), - lastCommunicatedAt: new Date(), - }).then((x) => Instances.findOneByOrFail(x.identifiers[0])); - - cache.set(host, i); - return i; - } else { - cache.set(host, index); - return index; - } -} diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts deleted file mode 100644 index 244e05c030..0000000000 --- a/packages/backend/src/services/relay.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { IsNull } from "typeorm"; -import { renderFollowRelay } from "@/remote/activitypub/renderer/follow-relay.js"; -import { - renderActivity, - attachLdSignature, -} from "@/remote/activitypub/renderer/index.js"; -import renderUndo from "@/remote/activitypub/renderer/undo.js"; -import { deliver } from "@/queue/index.js"; -import type { ILocalUser, User } from "@/models/entities/user.js"; -import { Users, Relays } from "@/models/index.js"; -import { genId } from "@/misc/gen-id.js"; -import { Cache } from "@/misc/cache.js"; -import type { Relay } from "@/models/entities/relay.js"; -import { createSystemUser } from "./create-system-user.js"; - -const ACTOR_USERNAME = "relay.actor" as const; - -const relaysCache = new Cache(1000 * 60 * 10); - -export async function getRelayActor(): Promise { - const user = await Users.findOneBy({ - host: IsNull(), - 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.insert({ - id: genId(), - inbox, - status: "requesting", - }).then((x) => Relays.findOneByOrFail(x.identifiers[0])); - - 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.findOneBy({ - inbox, - }); - - if (relay == null) { - throw new Error("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 relaysCache.fetch(null, () => - Relays.findBy({ - status: "accepted", - }), - ); - if (relays.length === 0) return; - - // TODO - //const copy = structuredClone(activity); - const copy = JSON.parse(JSON.stringify(activity)); - if (!copy.to) copy.to = ["https://www.w3.org/ns/activitystreams#Public"]; - - const signed = await attachLdSignature(copy, user); - - for (const relay of relays) { - deliver(user, signed, relay.inbox); - } -} diff --git a/packages/backend/src/services/send-email-notification.ts b/packages/backend/src/services/send-email-notification.ts deleted file mode 100644 index 14a9754fe5..0000000000 --- a/packages/backend/src/services/send-email-notification.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { UserProfiles } from "@/models/index.js"; -import type { User } from "@/models/entities/user.js"; -import { sendEmail } from "./send-email.js"; -import { I18n } from "@/misc/i18n.js"; -import * as Acct from "@/misc/acct.js"; -// TODO -//const locales = await import('../../../../locales/index.js'); - -// TODO: locale ファイルをクライアント用とサーバー用で分けたい - -async function follow(userId: User["id"], follower: User) { - /* - const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); - if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return; - const locale = locales[userProfile.lang || 'ja-JP']; - const i18n = new I18n(locale); - // TODO: render user information html - sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); - */ -} - -async function receiveFollowRequest(userId: User["id"], follower: User) { - /* - const userProfile = await UserProfiles.findOneByOrFail({ userId: userId }); - if (!userProfile.email || !userProfile.emailNotificationTypes.includes('receiveFollowRequest')) return; - const locale = locales[userProfile.lang || 'ja-JP']; - const i18n = new I18n(locale); - // TODO: render user information html - sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`); - */ -} - -export const sendEmailNotification = { - follow, - receiveFollowRequest, -}; diff --git a/packages/backend/src/services/send-email.ts b/packages/backend/src/services/send-email.ts deleted file mode 100644 index 4c442a168c..0000000000 --- a/packages/backend/src/services/send-email.ts +++ /dev/null @@ -1,132 +0,0 @@ -import * as nodemailer from "nodemailer"; -import { fetchMeta } from "@/misc/fetch-meta.js"; -import Logger from "./logger.js"; -import config from "@/config/index.js"; - -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} - - - -
-
- -

${meta.name}

-
-
-

${subject}

-
${html}
-
- -
- - -`, - }); - - logger.info(`Message sent: ${info.messageId}`); - } catch (err) { - logger.error(err as Error); - throw err; - } -} diff --git a/packages/backend/src/services/stream.ts b/packages/backend/src/services/stream.ts deleted file mode 100644 index f3846feaf1..0000000000 --- a/packages/backend/src/services/stream.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { redisClient } from "../db/redis.js"; -import type { User } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; -import type { UserList } from "@/models/entities/user-list.js"; -import type { UserGroup } from "@/models/entities/user-group.js"; -import config from "@/config/index.js"; -import type { Antenna } from "@/models/entities/antenna.js"; -import type { Channel } from "@/models/entities/channel.js"; -import type { - StreamChannels, - AdminStreamTypes, - AntennaStreamTypes, - BroadcastTypes, - ChannelStreamTypes, - DriveStreamTypes, - GroupMessagingStreamTypes, - InternalStreamTypes, - MainStreamTypes, - MessagingIndexStreamTypes, - MessagingStreamTypes, - NoteStreamTypes, - UserListStreamTypes, - UserStreamTypes, -} from "@/server/api/stream/types.js"; - -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 = < - K extends keyof GroupMessagingStreamTypes, - >( - groupId: UserGroup["id"], - type: K, - value?: GroupMessagingStreamTypes[K], - ): void => { - this.publish( - `messagingStream:${groupId}`, - type, - typeof value === "undefined" ? null : value, - ); - }; - - public publishMessagingIndexStream = < - K extends keyof MessagingIndexStreamTypes, - >( - userId: User["id"], - type: K, - value?: MessagingIndexStreamTypes[K], - ): void => { - this.publish( - `messagingIndexStream:${userId}`, - type, - typeof value === "undefined" ? null : value, - ); - }; - - public publishNotesStream = (note: 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 publishAdminStream = publisher.publishAdminStream; diff --git a/packages/backend/src/services/suspend-user.ts b/packages/backend/src/services/suspend-user.ts deleted file mode 100644 index f72b8ffcb1..0000000000 --- a/packages/backend/src/services/suspend-user.ts +++ /dev/null @@ -1,47 +0,0 @@ -import renderDelete from "@/remote/activitypub/renderer/delete.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { deliver } from "@/queue/index.js"; -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; -import { Users, Followings } from "@/models/index.js"; -import { Not, IsNull } from "typeorm"; -import { publishInternalEvent } from "@/services/stream.js"; - -export async function doPostSuspend(user: { - id: User["id"]; - host: User["host"]; -}) { - publishInternalEvent("userChangeSuspendedState", { - id: user.id, - isSuspended: true, - }); - - if (Users.isLocalUser(user)) { - // Send Delete to all known SharedInboxes - const content = renderActivity( - renderDelete(`${config.url}/users/${user.id}`, user), - ); - - const queue: string[] = []; - - const followings = await Followings.find({ - where: [ - { followerSharedInbox: Not(IsNull()) }, - { followeeSharedInbox: Not(IsNull()) }, - ], - select: ["followerSharedInbox", "followeeSharedInbox"], - }); - - const inboxes = followings.map( - (x) => x.followerSharedInbox || x.followeeSharedInbox, - ); - - for (const inbox of inboxes) { - if (inbox != null && !queue.includes(inbox)) queue.push(inbox); - } - - for (const inbox of queue) { - deliver(user, content, inbox); - } - } -} diff --git a/packages/backend/src/services/unsuspend-user.ts b/packages/backend/src/services/unsuspend-user.ts deleted file mode 100644 index 69447a4a26..0000000000 --- a/packages/backend/src/services/unsuspend-user.ts +++ /dev/null @@ -1,45 +0,0 @@ -import renderDelete from "@/remote/activitypub/renderer/delete.js"; -import renderUndo from "@/remote/activitypub/renderer/undo.js"; -import { renderActivity } from "@/remote/activitypub/renderer/index.js"; -import { deliver } from "@/queue/index.js"; -import config from "@/config/index.js"; -import type { User } from "@/models/entities/user.js"; -import { Users, Followings } from "@/models/index.js"; -import { Not, IsNull } from "typeorm"; -import { publishInternalEvent } from "@/services/stream.js"; - -export async function doPostUnsuspend(user: User) { - publishInternalEvent("userChangeSuspendedState", { - id: user.id, - isSuspended: false, - }); - - if (Users.isLocalUser(user)) { - // 知り得る全SharedInboxにUndo Delete配信 - const content = renderActivity( - renderUndo(renderDelete(`${config.url}/users/${user.id}`, user), user), - ); - - const queue: string[] = []; - - const followings = await Followings.find({ - where: [ - { followerSharedInbox: Not(IsNull()) }, - { followeeSharedInbox: Not(IsNull()) }, - ], - select: ["followerSharedInbox", "followeeSharedInbox"], - }); - - const inboxes = followings.map( - (x) => x.followerSharedInbox || x.followeeSharedInbox, - ); - - for (const inbox of inboxes) { - if (inbox != null && !queue.includes(inbox)) queue.push(inbox); - } - - for (const inbox of queue) { - deliver(user as any, content, inbox); - } - } -} diff --git a/packages/backend/src/services/update-hashtag.ts b/packages/backend/src/services/update-hashtag.ts deleted file mode 100644 index 0c65b08f0a..0000000000 --- a/packages/backend/src/services/update-hashtag.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { User } from "@/models/entities/user.js"; -import { Hashtags, Users } from "@/models/index.js"; -import { hashtagChart } from "@/services/chart/index.js"; -import { genId } from "@/misc/gen-id.js"; -import type { Hashtag } from "@/models/entities/hashtag.js"; -import { normalizeForSearch } from "@/misc/normalize-for-search.js"; - -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.findOneBy({ name: tag }); - - if (index == null && !inc) return; - - if (index != null) { - const q = Hashtags.createQueryBuilder("tag") - .update() - .where("name = :name", { name: tag }); - - const set = {} as any; - - if (isUserAttached) { - if (inc) { - // 自分が初めてこのタグを使ったなら - if (!index.attachedUserIds.some((id) => id === user.id)) { - set.attachedUserIds = () => - `array_append("attachedUserIds", '${user.id}')`; - set.attachedUsersCount = () => `"attachedUsersCount" + 1`; - } - // 自分が(ローカル内で)初めてこのタグを使ったなら - if ( - Users.isLocalUser(user) && - !index.attachedLocalUserIds.some((id) => id === user.id) - ) { - set.attachedLocalUserIds = () => - `array_append("attachedLocalUserIds", '${user.id}')`; - set.attachedLocalUsersCount = () => `"attachedLocalUsersCount" + 1`; - } - // 自分が(リモートで)初めてこのタグを使ったなら - if ( - Users.isRemoteUser(user) && - !index.attachedRemoteUserIds.some((id) => id === user.id) - ) { - set.attachedRemoteUserIds = () => - `array_append("attachedRemoteUserIds", '${user.id}')`; - set.attachedRemoteUsersCount = () => `"attachedRemoteUsersCount" + 1`; - } - } else { - set.attachedUserIds = () => - `array_remove("attachedUserIds", '${user.id}')`; - set.attachedUsersCount = () => `"attachedUsersCount" - 1`; - if (Users.isLocalUser(user)) { - set.attachedLocalUserIds = () => - `array_remove("attachedLocalUserIds", '${user.id}')`; - set.attachedLocalUsersCount = () => `"attachedLocalUsersCount" - 1`; - } else { - set.attachedRemoteUserIds = () => - `array_remove("attachedRemoteUserIds", '${user.id}')`; - set.attachedRemoteUsersCount = () => `"attachedRemoteUsersCount" - 1`; - } - } - } else { - // 自分が初めてこのタグを使ったなら - if (!index.mentionedUserIds.some((id) => id === user.id)) { - set.mentionedUserIds = () => - `array_append("mentionedUserIds", '${user.id}')`; - set.mentionedUsersCount = () => `"mentionedUsersCount" + 1`; - } - // 自分が(ローカル内で)初めてこのタグを使ったなら - if ( - Users.isLocalUser(user) && - !index.mentionedLocalUserIds.some((id) => id === user.id) - ) { - set.mentionedLocalUserIds = () => - `array_append("mentionedLocalUserIds", '${user.id}')`; - set.mentionedLocalUsersCount = () => `"mentionedLocalUsersCount" + 1`; - } - // 自分が(リモートで)初めてこのタグを使ったなら - if ( - Users.isRemoteUser(user) && - !index.mentionedRemoteUserIds.some((id) => id === user.id) - ) { - set.mentionedRemoteUserIds = () => - `array_append("mentionedRemoteUserIds", '${user.id}')`; - set.mentionedRemoteUsersCount = () => `"mentionedRemoteUsersCount" + 1`; - } - } - - if (Object.keys(set).length > 0) { - q.set(set); - q.execute(); - } - } else { - if (isUserAttached) { - Hashtags.insert({ - id: genId(), - name: tag, - mentionedUserIds: [], - mentionedUsersCount: 0, - mentionedLocalUserIds: [], - mentionedLocalUsersCount: 0, - mentionedRemoteUserIds: [], - mentionedRemoteUsersCount: 0, - attachedUserIds: [user.id], - attachedUsersCount: 1, - attachedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], - attachedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, - attachedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], - attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, - } as Hashtag); - } else { - Hashtags.insert({ - id: genId(), - name: tag, - mentionedUserIds: [user.id], - mentionedUsersCount: 1, - mentionedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], - mentionedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, - mentionedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], - mentionedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, - attachedUserIds: [], - attachedUsersCount: 0, - attachedLocalUserIds: [], - attachedLocalUsersCount: 0, - attachedRemoteUserIds: [], - attachedRemoteUsersCount: 0, - } as Hashtag); - } - } - - if (!isUserAttached) { - hashtagChart.update(tag, user); - } -} diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts deleted file mode 100644 index 9492448554..0000000000 --- a/packages/backend/src/services/user-cache.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { - CacheableLocalUser, - CacheableUser, - ILocalUser, -} from "@/models/entities/user.js"; -import { User } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; -import { Cache } from "@/misc/cache.js"; -import { subscriber } from "@/db/redis.js"; - -export const userByIdCache = new Cache(Infinity); -export const localUserByNativeTokenCache = new Cache( - Infinity, -); -export const localUserByIdCache = new Cache(Infinity); -export const uriPersonCache = new Cache(Infinity); - -subscriber.on("message", async (_, data) => { - const obj = JSON.parse(data); - - if (obj.channel === "internal") { - const { type, body } = obj.message; - switch (type) { - case "localUserUpdated": { - userByIdCache.delete(body.id); - localUserByIdCache.delete(body.id); - localUserByNativeTokenCache.cache.forEach((v, k) => { - if (v.value?.id === body.id) { - localUserByNativeTokenCache.delete(k); - } - }); - break; - } - case "userChangeSuspendedState": - case "userChangeSilencedState": - case "userChangeModeratorState": - case "remoteUserUpdated": { - const user = await Users.findOneByOrFail({ id: body.id }); - userByIdCache.set(user.id, user); - for (const [k, v] of uriPersonCache.cache.entries()) { - if (v.value?.id === user.id) { - uriPersonCache.set(k, user); - } - } - if (Users.isLocalUser(user)) { - localUserByNativeTokenCache.set(user.token, user); - localUserByIdCache.set(user.id, user); - } - break; - } - case "userTokenRegenerated": { - const user = (await Users.findOneByOrFail({ - id: body.id, - })) as ILocalUser; - localUserByNativeTokenCache.delete(body.oldToken); - localUserByNativeTokenCache.set(body.newToken, user); - break; - } - default: - break; - } - } -}); diff --git a/packages/backend/src/services/user-list/push.ts b/packages/backend/src/services/user-list/push.ts deleted file mode 100644 index 01bb066019..0000000000 --- a/packages/backend/src/services/user-list/push.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { publishUserListStream } from "@/services/stream.js"; -import type { User } from "@/models/entities/user.js"; -import type { UserList } from "@/models/entities/user-list.js"; -import { UserListJoinings, Users } from "@/models/index.js"; -import type { UserListJoining } from "@/models/entities/user-list-joining.js"; -import { genId } from "@/misc/gen-id.js"; -import { fetchProxyAccount } from "@/misc/fetch-proxy-account.js"; -import createFollowing from "../following/create.js"; - -export async function pushUserToUserList(target: User, list: UserList) { - await UserListJoinings.insert({ - id: genId(), - createdAt: new Date(), - userId: target.id, - userListId: list.id, - } as UserListJoining); - - publishUserListStream(list.id, "userAdded", await Users.pack(target)); - - // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする - if (Users.isRemoteUser(target)) { - const proxy = await fetchProxyAccount(); - if (proxy) { - createFollowing(proxy, target); - } - } -} diff --git a/packages/backend/src/services/validate-email-for-account.ts b/packages/backend/src/services/validate-email-for-account.ts deleted file mode 100644 index 2bb5e93e7e..0000000000 --- a/packages/backend/src/services/validate-email-for-account.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { validate as validateEmail } from "deep-email-validator"; -import { UserProfiles } from "@/models/index.js"; -import { fetchMeta } from "@/misc/fetch-meta.js"; - -export async function validateEmailForAccount(emailAddress: string): Promise<{ - available: boolean; - reason: null | "used" | "format" | "disposable" | "mx" | "smtp"; -}> { - const meta = await fetchMeta(); - - const exist = await UserProfiles.countBy({ - emailVerified: true, - email: emailAddress, - }); - - const validated = meta.enableActiveEmailValidation - ? await validateEmail({ - email: emailAddress, - validateRegex: true, - validateMx: true, - validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので - validateDisposable: true, // 捨てアドかどうかチェック - validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので - }) - : { valid: true }; - - const available = exist === 0 && validated.valid; - - return { - available, - reason: available - ? null - : exist !== 0 - ? "used" - : validated.reason === "regex" - ? "format" - : validated.reason === "disposable" - ? "disposable" - : validated.reason === "mx" - ? "mx" - : validated.reason === "smtp" - ? "smtp" - : null, - }; -} diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts deleted file mode 100644 index 9e53440a17..0000000000 --- a/packages/backend/src/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const notificationTypes = [ - "follow", - "mention", - "reply", - "renote", - "quote", - "reaction", - "pollVote", - "pollEnded", - "receiveFollowRequest", - "followRequestAccepted", - "groupInvited", - "app", -] as const; - -export const noteVisibilities = [ - "public", - "home", - "followers", - "specified", -] as const; - -export const mutedNoteReasons = ["word", "manual", "spam", "other"] as const; - -export const ffVisibility = ["public", "followers", "private"] as const; diff --git a/packages/backend/test/activitypub.ts b/packages/backend/test/activitypub.ts deleted file mode 100644 index 7b6f85f5a1..0000000000 --- a/packages/backend/test/activitypub.ts +++ /dev/null @@ -1,102 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import rndstr from "rndstr"; -import { initDb } from "../src/db/postgre.js"; -import { initTestDb } from "./utils.js"; - -describe("ActivityPub", () => { - before(async () => { - //await initTestDb(); - await initDb(); - }); - - describe("Parse minimum object", () => { - const host = "https://host1.test"; - const preferredUsername = `${rndstr("A-Z", 4)}${rndstr("a-z", 4)}`; - const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; - - const actor = { - "@context": "https://www.w3.org/ns/activitystreams", - id: actorId, - type: "Person", - preferredUsername, - inbox: `${actorId}/inbox`, - outbox: `${actorId}/outbox`, - }; - - const post = { - "@context": "https://www.w3.org/ns/activitystreams", - id: `${host}/users/${rndstr("0-9a-z", 8)}`, - type: "Note", - attributedTo: actor.id, - to: "https://www.w3.org/ns/activitystreams#Public", - content: "あ", - }; - - it("Minimum Actor", async () => { - const { MockResolver } = await import("./misc/mock-resolver.js"); - const { createPerson } = await import( - "../src/remote/activitypub/models/person.js" - ); - - const resolver = new MockResolver(); - resolver._register(actor.id, actor); - - const user = await createPerson(actor.id, resolver); - - assert.deepStrictEqual(user.uri, actor.id); - assert.deepStrictEqual(user.username, actor.preferredUsername); - assert.deepStrictEqual(user.inbox, actor.inbox); - }); - - it("Minimum Note", async () => { - const { MockResolver } = await import("./misc/mock-resolver.js"); - const { createNote } = await import( - "../src/remote/activitypub/models/note.js" - ); - - const resolver = new MockResolver(); - resolver._register(actor.id, actor); - resolver._register(post.id, post); - - const note = await createNote(post.id, resolver, true); - - assert.deepStrictEqual(note?.uri, post.id); - assert.deepStrictEqual(note.visibility, "public"); - assert.deepStrictEqual(note.text, post.content); - }); - }); - - describe("Truncate long name", () => { - const host = "https://host1.test"; - const preferredUsername = `${rndstr("A-Z", 4)}${rndstr("a-z", 4)}`; - const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; - - const name = rndstr("0-9a-z", 129); - - const actor = { - "@context": "https://www.w3.org/ns/activitystreams", - id: actorId, - type: "Person", - preferredUsername, - name, - inbox: `${actorId}/inbox`, - outbox: `${actorId}/outbox`, - }; - - it("Actor", async () => { - const { MockResolver } = await import("./misc/mock-resolver.js"); - const { createPerson } = await import( - "../src/remote/activitypub/models/person.js" - ); - - const resolver = new MockResolver(); - resolver._register(actor.id, actor); - - const user = await createPerson(actor.id, resolver); - - assert.deepStrictEqual(user.name, actor.name.substr(0, 128)); - }); - }); -}); diff --git a/packages/backend/test/ap-request.ts b/packages/backend/test/ap-request.ts deleted file mode 100644 index bf77a38532..0000000000 --- a/packages/backend/test/ap-request.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as assert from "assert"; -import httpSignature from "@peertube/http-signature"; -import { genRsaKeyPair } from "../src/misc/gen-key-pair.js"; -import { - createSignedPost, - createSignedGet, -} from "../src/remote/activitypub/ap-request.js"; - -export const buildParsedSignature = ( - signingString: string, - signature: string, - algorithm: string, -) => { - return { - scheme: "Signature", - params: { - keyId: "KeyID", // dummy, not used for verify - algorithm: algorithm, - headers: ["(request-target)", "date", "host", "digest"], // dummy, not used for verify - signature: signature, - }, - signingString: signingString, - algorithm: algorithm.toUpperCase(), - keyId: "KeyID", // dummy, not used for verify - }; -}; - -describe("ap-request", () => { - it("createSignedPost with verify", async () => { - const keypair = await genRsaKeyPair(); - const key = { keyId: "x", privateKeyPem: keypair.privateKey }; - const url = "https://example.com/inbox"; - const activity = { a: 1 }; - const body = JSON.stringify(activity); - const headers = { - "User-Agent": "UA", - }; - - const req = createSignedPost({ - key, - url, - body, - additionalHeaders: headers, - }); - - const parsed = buildParsedSignature( - req.signingString, - req.signature, - "rsa-sha256", - ); - - const result = httpSignature.verifySignature(parsed, keypair.publicKey); - assert.deepStrictEqual(result, true); - }); - - it("createSignedGet with verify", async () => { - const keypair = await genRsaKeyPair(); - const key = { keyId: "x", privateKeyPem: keypair.privateKey }; - const url = "https://example.com/outbox"; - const headers = { - "User-Agent": "UA", - }; - - const req = createSignedGet({ key, url, additionalHeaders: headers }); - - const parsed = buildParsedSignature( - req.signingString, - req.signature, - "rsa-sha256", - ); - - const result = httpSignature.verifySignature(parsed, keypair.publicKey); - assert.deepStrictEqual(result, true); - }); -}); diff --git a/packages/backend/test/api-visibility.ts b/packages/backend/test/api-visibility.ts deleted file mode 100644 index 0ee4a4d337..0000000000 --- a/packages/backend/test/api-visibility.ts +++ /dev/null @@ -1,535 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import { - async, - signup, - request, - post, - startServer, - shutdownServer, -} from "./utils.js"; - -describe("API visibility", () => { - let p: childProcess.ChildProcess; - - before(async () => { - p = await startServer(); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe("Note visibility", async () => { - //#region vars - /** ヒロイン */ - let alice: any; - /** フォロワー */ - let follower: any; - /** 非フォロワー */ - let other: any; - /** 非フォロワーでもリプライやメンションをされた人 */ - let target: any; - /** specified mentionでmentionを飛ばされる人 */ - let target2: any; - - /** public-post */ - let pub: any; - /** home-post */ - let home: any; - /** followers-post */ - let fol: any; - /** specified-post */ - let spe: any; - - /** public-reply to target's post */ - let pubR: any; - /** home-reply to target's post */ - let homeR: any; - /** followers-reply to target's post */ - let folR: any; - /** specified-reply to target's post */ - let speR: any; - - /** public-mention to target */ - let pubM: any; - /** home-mention to target */ - let homeM: any; - /** followers-mention to target */ - let folM: any; - /** specified-mention to target */ - let speM: any; - - /** reply target post */ - let tgt: any; - //#endregion - - const show = async (noteId: any, by: any) => { - return await request( - "/notes/show", - { - noteId, - }, - by, - ); - }; - - before(async () => { - //#region prepare - // signup - alice = await signup({ username: "alice" }); - follower = await signup({ username: "follower" }); - other = await signup({ username: "other" }); - target = await signup({ username: "target" }); - target2 = await signup({ username: "target2" }); - - // follow alice <= follower - await request("/following/create", { userId: alice.id }, follower); - - // normal posts - pub = await post(alice, { text: "x", visibility: "public" }); - home = await post(alice, { text: "x", visibility: "home" }); - fol = await post(alice, { text: "x", visibility: "followers" }); - spe = await post(alice, { - text: "x", - visibility: "specified", - visibleUserIds: [target.id], - }); - - // replies - tgt = await post(target, { text: "y", visibility: "public" }); - pubR = await post(alice, { - text: "x", - replyId: tgt.id, - visibility: "public", - }); - homeR = await post(alice, { - text: "x", - replyId: tgt.id, - visibility: "home", - }); - folR = await post(alice, { - text: "x", - replyId: tgt.id, - visibility: "followers", - }); - speR = await post(alice, { - text: "x", - replyId: tgt.id, - visibility: "specified", - }); - - // mentions - pubM = await post(alice, { - text: "@target x", - replyId: tgt.id, - visibility: "public", - }); - homeM = await post(alice, { - text: "@target x", - replyId: tgt.id, - visibility: "home", - }); - folM = await post(alice, { - text: "@target x", - replyId: tgt.id, - visibility: "followers", - }); - speM = await post(alice, { - text: "@target2 x", - replyId: tgt.id, - visibility: "specified", - }); - //#endregion - }); - - //#region show post - // public - it("[show] public-postを自分が見れる", async(async () => { - const res = await show(pub.id, alice); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] public-postをフォロワーが見れる", async(async () => { - const res = await show(pub.id, follower); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] public-postを非フォロワーが見れる", async(async () => { - const res = await show(pub.id, other); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] public-postを未認証が見れる", async(async () => { - const res = await show(pub.id, null); - assert.strictEqual(res.body.text, "x"); - })); - - // home - it("[show] home-postを自分が見れる", async(async () => { - const res = await show(home.id, alice); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] home-postをフォロワーが見れる", async(async () => { - const res = await show(home.id, follower); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] home-postを非フォロワーが見れる", async(async () => { - const res = await show(home.id, other); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] home-postを未認証が見れる", async(async () => { - const res = await show(home.id, null); - assert.strictEqual(res.body.text, "x"); - })); - - // followers - it("[show] followers-postを自分が見れる", async(async () => { - const res = await show(fol.id, alice); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] followers-postをフォロワーが見れる", async(async () => { - const res = await show(fol.id, follower); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] followers-postを非フォロワーが見れない", async(async () => { - const res = await show(fol.id, other); - assert.strictEqual(res.status, 404); - })); - - it("[show] followers-postを未認証が見れない", async(async () => { - const res = await show(fol.id, null); - assert.strictEqual(res.status, 404); - })); - - // specified - it("[show] specified-postを自分が見れる", async(async () => { - const res = await show(spe.id, alice); - assert.strictEqual(res.status, 404); - })); - - it("[show] specified-postを指定ユーザーが見れる", async(async () => { - const res = await show(spe.id, target); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] specified-postをフォロワーが見れない", async(async () => { - const res = await show(spe.id, follower); - assert.strictEqual(res.status, 404); - })); - - it("[show] specified-postを非フォロワーが見れない", async(async () => { - const res = await show(spe.id, other); - assert.strictEqual(res.status, 404); - })); - - it("[show] specified-postを未認証が見れない", async(async () => { - const res = await show(spe.id, null); - assert.strictEqual(res.status, 404); - })); - //#endregion - - //#region show reply - // public - it("[show] public-replyを自分が見れる", async(async () => { - const res = await show(pubR.id, alice); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] public-replyをされた人が見れる", async(async () => { - const res = await show(pubR.id, target); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] public-replyをフォロワーが見れる", async(async () => { - const res = await show(pubR.id, follower); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] public-replyを非フォロワーが見れる", async(async () => { - const res = await show(pubR.id, other); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] public-replyを未認証が見れる", async(async () => { - const res = await show(pubR.id, null); - assert.strictEqual(res.body.text, "x"); - })); - - // home - it("[show] home-replyを自分が見れる", async(async () => { - const res = await show(homeR.id, alice); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] home-replyをされた人が見れる", async(async () => { - const res = await show(homeR.id, target); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] home-replyをフォロワーが見れる", async(async () => { - const res = await show(homeR.id, follower); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] home-replyを非フォロワーが見れる", async(async () => { - const res = await show(homeR.id, other); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] home-replyを未認証が見れる", async(async () => { - const res = await show(homeR.id, null); - assert.strictEqual(res.body.text, "x"); - })); - - // followers - it("[show] followers-replyを自分が見れる", async(async () => { - const res = await show(folR.id, alice); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] followers-replyを非フォロワーでもリプライされていれば見れる", async(async () => { - const res = await show(folR.id, target); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] followers-replyをフォロワーが見れる", async(async () => { - const res = await show(folR.id, follower); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] followers-replyを非フォロワーが見れない", async(async () => { - const res = await show(folR.id, other); - assert.strictEqual(res.status, 404); - })); - - it("[show] followers-replyを未認証が見れない", async(async () => { - const res = await show(folR.id, null); - assert.strictEqual(res.status, 404); - })); - - // specified - it("[show] specified-replyを自分が見れる", async(async () => { - const res = await show(speR.id, alice); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] specified-replyを指定ユーザーが見れる", async(async () => { - const res = await show(speR.id, target); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] specified-replyをされた人が指定されてなくても見れる", async(async () => { - const res = await show(speR.id, target); - assert.strictEqual(res.body.text, "x"); - })); - - it("[show] specified-replyをフォロワーが見れない", async(async () => { - const res = await show(speR.id, follower); - assert.strictEqual(res.status, 404); - })); - - it("[show] specified-replyを非フォロワーが見れない", async(async () => { - const res = await show(speR.id, other); - assert.strictEqual(res.status, 404); - })); - - it("[show] specified-replyを未認証が見れない", async(async () => { - const res = await show(speR.id, null); - assert.strictEqual(res.status, 404); - })); - //#endregion - - //#region show mention - // public - it("[show] public-mentionを自分が見れる", async(async () => { - const res = await show(pubM.id, alice); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] public-mentionをされた人が見れる", async(async () => { - const res = await show(pubM.id, target); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] public-mentionをフォロワーが見れる", async(async () => { - const res = await show(pubM.id, follower); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] public-mentionを非フォロワーが見れる", async(async () => { - const res = await show(pubM.id, other); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] public-mentionを未認証が見れる", async(async () => { - const res = await show(pubM.id, null); - assert.strictEqual(res.body.text, "@target x"); - })); - - // home - it("[show] home-mentionを自分が見れる", async(async () => { - const res = await show(homeM.id, alice); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] home-mentionをされた人が見れる", async(async () => { - const res = await show(homeM.id, target); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] home-mentionをフォロワーが見れる", async(async () => { - const res = await show(homeM.id, follower); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] home-mentionを非フォロワーが見れる", async(async () => { - const res = await show(homeM.id, other); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] home-mentionを未認証が見れる", async(async () => { - const res = await show(homeM.id, null); - assert.strictEqual(res.body.text, "@target x"); - })); - - // followers - it("[show] followers-mentionを自分が見れる", async(async () => { - const res = await show(folM.id, alice); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] followers-mentionをメンションされていれば非フォロワーでも見れる", async(async () => { - const res = await show(folM.id, target); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] followers-mentionをフォロワーが見れる", async(async () => { - const res = await show(folM.id, follower); - assert.strictEqual(res.body.text, "@target x"); - })); - - it("[show] followers-mentionを非フォロワーが見れない", async(async () => { - const res = await show(folM.id, other); - assert.strictEqual(res.status, 404); - })); - - it("[show] followers-mentionを未認証が見れない", async(async () => { - const res = await show(folM.id, null); - assert.strictEqual(res.status, 404); - })); - - // specified - it("[show] specified-mentionを自分が見れる", async(async () => { - const res = await show(speM.id, alice); - assert.strictEqual(res.body.text, "@target2 x"); - })); - - it("[show] specified-mentionを指定ユーザーが見れる", async(async () => { - const res = await show(speM.id, target); - assert.strictEqual(res.body.text, "@target2 x"); - })); - - it("[show] specified-mentionをされた人が指定されてなかったら見れない", async(async () => { - const res = await show(speM.id, target2); - assert.strictEqual(res.status, 404); - })); - - it("[show] specified-mentionをフォロワーが見れない", async(async () => { - const res = await show(speM.id, follower); - assert.strictEqual(res.status, 404); - })); - - it("[show] specified-mentionを非フォロワーが見れない", async(async () => { - const res = await show(speM.id, other); - assert.strictEqual(res.status, 404); - })); - - it("[show] specified-mentionを未認証が見れない", async(async () => { - const res = await show(speM.id, null); - assert.strictEqual(res.status, 404); - })); - //#endregion - - //#region HTL - it("[HTL] public-post が 自分が見れる", async(async () => { - const res = await request("/notes/timeline", { limit: 100 }, alice); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == pub.id); - assert.strictEqual(notes[0].text, "x"); - })); - - it("[HTL] public-post が 非フォロワーから見れない", async(async () => { - const res = await request("/notes/timeline", { limit: 100 }, other); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == pub.id); - assert.strictEqual(notes.length, 0); - })); - - it("[HTL] followers-post が フォロワーから見れる", async(async () => { - const res = await request("/notes/timeline", { limit: 100 }, follower); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == fol.id); - assert.strictEqual(notes[0].text, "x"); - })); - //#endregion - - //#region RTL - it("[replies] followers-reply が フォロワーから見れる", async(async () => { - const res = await request( - "/notes/replies", - { noteId: tgt.id, limit: 100 }, - follower, - ); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); - assert.strictEqual(notes[0].text, "x"); - })); - - it("[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない", async(async () => { - const res = await request( - "/notes/replies", - { noteId: tgt.id, limit: 100 }, - other, - ); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); - assert.strictEqual(notes.length, 0); - })); - - it("[replies] followers-reply が 非フォロワー (リプライ先である) から見れる", async(async () => { - const res = await request( - "/notes/replies", - { noteId: tgt.id, limit: 100 }, - target, - ); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); - assert.strictEqual(notes[0].text, "x"); - })); - //#endregion - - //#region MTL - it("[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる", async(async () => { - const res = await request("/notes/mentions", { limit: 100 }, target); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folR.id); - assert.strictEqual(notes[0].text, "x"); - })); - - it("[mentions] followers-mention が 非フォロワー (メンション先である) から見れる", async(async () => { - const res = await request("/notes/mentions", { limit: 100 }, target); - assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id == folM.id); - assert.strictEqual(notes[0].text, "@target x"); - })); - //#endregion - }); -}); diff --git a/packages/backend/test/api.ts b/packages/backend/test/api.ts deleted file mode 100644 index 19a754552c..0000000000 --- a/packages/backend/test/api.ts +++ /dev/null @@ -1,92 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import { - async, - signup, - request, - post, - react, - uploadFile, - startServer, - shutdownServer, -} from "./utils.js"; - -describe("API", () => { - let p: childProcess.ChildProcess; - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: "alice" }); - bob = await signup({ username: "bob" }); - carol = await signup({ username: "carol" }); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe("General validation", () => { - it("wrong type", async(async () => { - const res = await request("/test", { - required: true, - string: 42, - }); - assert.strictEqual(res.status, 400); - })); - - it("missing require param", async(async () => { - const res = await request("/test", { - string: "a", - }); - assert.strictEqual(res.status, 400); - })); - - it("invalid misskey:id (empty string)", async(async () => { - const res = await request("/test", { - required: true, - id: "", - }); - assert.strictEqual(res.status, 400); - })); - - it("valid misskey:id", async(async () => { - const res = await request("/test", { - required: true, - id: "8wvhjghbxu", - }); - assert.strictEqual(res.status, 200); - })); - - it("default value", async(async () => { - const res = await request("/test", { - required: true, - string: "a", - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.default, "hello"); - })); - - it("can set null even if it has default value", async(async () => { - const res = await request("/test", { - required: true, - nullableDefault: null, - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.nullableDefault, null); - })); - - it("cannot set undefined if it has default value", async(async () => { - const res = await request("/test", { - required: true, - nullableDefault: undefined, - }); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.nullableDefault, "hello"); - })); - }); -}); diff --git a/packages/backend/test/block.ts b/packages/backend/test/block.ts deleted file mode 100644 index 08192e4869..0000000000 --- a/packages/backend/test/block.ts +++ /dev/null @@ -1,129 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import { - async, - signup, - request, - post, - startServer, - shutdownServer, -} from "./utils.js"; - -describe("Block", () => { - let p: childProcess.ChildProcess; - - // alice blocks bob - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: "alice" }); - bob = await signup({ username: "bob" }); - carol = await signup({ username: "carol" }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it("Block作成", async(async () => { - const res = await request( - "/blocking/create", - { - userId: bob.id, - }, - alice, - ); - - assert.strictEqual(res.status, 200); - })); - - it("ブロックされているユーザーをフォローできない", async(async () => { - const res = await request("/following/create", { userId: alice.id }, bob); - - assert.strictEqual(res.status, 400); - assert.strictEqual( - res.body.error.id, - "c4ab57cc-4e41-45e9-bfd9-584f61e35ce0", - ); - })); - - it("ブロックされているユーザーにリアクションできない", async(async () => { - const note = await post(alice, { text: "hello" }); - - const res = await request( - "/notes/reactions/create", - { noteId: note.id, reaction: "👍" }, - bob, - ); - - assert.strictEqual(res.status, 400); - assert.strictEqual( - res.body.error.id, - "20ef5475-9f38-4e4c-bd33-de6d979498ec", - ); - })); - - it("ブロックされているユーザーに返信できない", async(async () => { - const note = await post(alice, { text: "hello" }); - - const res = await request( - "/notes/create", - { replyId: note.id, text: "yo" }, - bob, - ); - - assert.strictEqual(res.status, 400); - assert.strictEqual( - res.body.error.id, - "b390d7e1-8a5e-46ed-b625-06271cafd3d3", - ); - })); - - it("ブロックされているユーザーのノートをRenoteできない", async(async () => { - const note = await post(alice, { text: "hello" }); - - const res = await request( - "/notes/create", - { renoteId: note.id, text: "yo" }, - bob, - ); - - assert.strictEqual(res.status, 400); - assert.strictEqual( - res.body.error.id, - "b390d7e1-8a5e-46ed-b625-06271cafd3d3", - ); - })); - - // TODO: ユーザーリストに入れられないテスト - - // TODO: ユーザーリストから除外されるテスト - - it("タイムライン(LTL)にブロックされているユーザーの投稿が含まれない", async(async () => { - const aliceNote = await post(alice); - const bobNote = await post(bob); - const carolNote = await post(carol); - - const res = await request("/notes/local-timeline", {}, bob); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual( - res.body.some((note: any) => note.id === aliceNote.id), - false, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === bobNote.id), - true, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === carolNote.id), - true, - ); - })); -}); diff --git a/packages/backend/test/chart.ts b/packages/backend/test/chart.ts deleted file mode 100644 index e194c6c195..0000000000 --- a/packages/backend/test/chart.ts +++ /dev/null @@ -1,575 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as lolex from "@sinonjs/fake-timers"; -import TestChart from "../src/services/chart/charts/test.js"; -import TestGroupedChart from "../src/services/chart/charts/test-grouped.js"; -import TestUniqueChart from "../src/services/chart/charts/test-unique.js"; -import TestIntersectionChart from "../src/services/chart/charts/test-intersection.js"; -import { initDb } from "../src/db/postgre.js"; - -describe("Chart", () => { - let testChart: TestChart; - let testGroupedChart: TestGroupedChart; - let testUniqueChart: TestUniqueChart; - let testIntersectionChart: TestIntersectionChart; - let clock: lolex.InstalledClock; - - beforeEach(async () => { - await initDb(true); - - testChart = new TestChart(); - testGroupedChart = new TestGroupedChart(); - testUniqueChart = new TestUniqueChart(); - testIntersectionChart = new TestIntersectionChart(); - - clock = lolex.install({ - now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), - shouldClearNativeTimers: true, - }); - }); - - afterEach(() => { - clock.uninstall(); - }); - - it("Can updates", async () => { - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - }); - - it("Can updates (dec)", async () => { - await testChart.decrement(); - await testChart.save(); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [1, 0, 0], - inc: [0, 0, 0], - total: [-1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [1, 0, 0], - inc: [0, 0, 0], - total: [-1, 0, 0], - }, - }); - }); - - it("Empty chart", async () => { - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [0, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [0, 0, 0], - }, - }); - }); - - it("Can updates at multiple times at same time", async () => { - await testChart.increment(); - await testChart.increment(); - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [3, 0, 0], - total: [3, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [3, 0, 0], - total: [3, 0, 0], - }, - }); - }); - - it("複数回saveされてもデータの更新は一度だけ", async () => { - await testChart.increment(); - await testChart.save(); - await testChart.save(); - await testChart.save(); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - }); - - it("Can updates at different times", async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick("01:00:00"); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 1, 0], - total: [2, 1, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - // 仕様上はこうなってほしいけど、実装は難しそうなのでskip - /* - it('Can updates at different times without save', async () => { - await testChart.increment(); - - clock.tick('01:00:00'); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart('hour', 3, null); - const chartDays = await testChart.getChart('day', 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 1, 0], - total: [2, 1, 0] - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0] - }, - }); - }); - */ - - it("Can padding", async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick("02:00:00"); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 1], - total: [2, 1, 1], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - // 要求された範囲にログがひとつもない場合でもパディングできる - it("Can padding from past range", async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick("05:00:00"); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [1, 1, 1], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - }); - - // 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる - // Issue #3190 - it("Can padding from past range 2", async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick("05:00:00"); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [2, 1, 1], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - it("Can specify offset", async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick("01:00:00"); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart( - "hour", - 3, - new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), - ); - const chartDays = await testChart.getChart( - "day", - 3, - new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), - ); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - it("Can specify offset (floor time)", async () => { - clock.tick("00:30:00"); - - await testChart.increment(); - await testChart.save(); - - clock.tick("01:30:00"); - - await testChart.increment(); - await testChart.save(); - - const chartHours = await testChart.getChart( - "hour", - 3, - new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), - ); - const chartDays = await testChart.getChart( - "day", - 3, - new Date(Date.UTC(2000, 0, 1, 0, 0, 0)), - ); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [2, 0, 0], - total: [2, 0, 0], - }, - }); - }); - - describe("Grouped", () => { - it("Can updates", async () => { - await testGroupedChart.increment("alice"); - await testGroupedChart.save(); - - const aliceChartHours = await testGroupedChart.getChart( - "hour", - 3, - null, - "alice", - ); - const aliceChartDays = await testGroupedChart.getChart( - "day", - 3, - null, - "alice", - ); - const bobChartHours = await testGroupedChart.getChart( - "hour", - 3, - null, - "bob", - ); - const bobChartDays = await testGroupedChart.getChart( - "day", - 3, - null, - "bob", - ); - - assert.deepStrictEqual(aliceChartHours, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(aliceChartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(bobChartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [0, 0, 0], - }, - }); - - assert.deepStrictEqual(bobChartDays, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [0, 0, 0], - }, - }); - }); - }); - - describe("Unique increment", () => { - it("Can updates", async () => { - await testUniqueChart.uniqueIncrement("alice"); - await testUniqueChart.uniqueIncrement("alice"); - await testUniqueChart.uniqueIncrement("bob"); - await testUniqueChart.save(); - - const chartHours = await testUniqueChart.getChart("hour", 3, null); - const chartDays = await testUniqueChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: [2, 0, 0], - }); - - assert.deepStrictEqual(chartDays, { - foo: [2, 0, 0], - }); - }); - - describe("Intersection", () => { - it("条件が満たされていない場合はカウントされない", async () => { - await testIntersectionChart.addA("alice"); - await testIntersectionChart.addA("bob"); - await testIntersectionChart.addB("carol"); - await testIntersectionChart.save(); - - const chartHours = await testIntersectionChart.getChart( - "hour", - 3, - null, - ); - const chartDays = await testIntersectionChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - a: [2, 0, 0], - b: [1, 0, 0], - aAndB: [0, 0, 0], - }); - - assert.deepStrictEqual(chartDays, { - a: [2, 0, 0], - b: [1, 0, 0], - aAndB: [0, 0, 0], - }); - }); - - it("条件が満たされている場合にカウントされる", async () => { - await testIntersectionChart.addA("alice"); - await testIntersectionChart.addA("bob"); - await testIntersectionChart.addB("carol"); - await testIntersectionChart.addB("alice"); - await testIntersectionChart.save(); - - const chartHours = await testIntersectionChart.getChart( - "hour", - 3, - null, - ); - const chartDays = await testIntersectionChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - a: [2, 0, 0], - b: [2, 0, 0], - aAndB: [1, 0, 0], - }); - - assert.deepStrictEqual(chartDays, { - a: [2, 0, 0], - b: [2, 0, 0], - aAndB: [1, 0, 0], - }); - }); - }); - }); - - describe("Resync", () => { - it("Can resync", async () => { - testChart.total = 1; - - await testChart.resync(); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [1, 0, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [0, 0, 0], - total: [1, 0, 0], - }, - }); - }); - - it("Can resync (2)", async () => { - await testChart.increment(); - await testChart.save(); - - clock.tick("01:00:00"); - - testChart.total = 100; - - await testChart.resync(); - - const chartHours = await testChart.getChart("hour", 3, null); - const chartDays = await testChart.getChart("day", 3, null); - - assert.deepStrictEqual(chartHours, { - foo: { - dec: [0, 0, 0], - inc: [0, 1, 0], - total: [100, 1, 0], - }, - }); - - assert.deepStrictEqual(chartDays, { - foo: { - dec: [0, 0, 0], - inc: [1, 0, 0], - total: [100, 0, 0], - }, - }); - }); - }); -}); diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/docker-compose.yml deleted file mode 100644 index 5f95bec4c0..0000000000 --- a/packages/backend/test/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: "3" - -services: - redistest: - image: redis:6 - ports: - - "127.0.0.1:56312:6379" - - dbtest: - image: postgres:13 - ports: - - "127.0.0.1:54312:5432" - environment: - POSTGRES_DB: "test-misskey" - POSTGRES_HOST_AUTH_METHOD: trust diff --git a/packages/backend/test/endpoints.ts b/packages/backend/test/endpoints.ts deleted file mode 100644 index 2aedc25f2c..0000000000 --- a/packages/backend/test/endpoints.ts +++ /dev/null @@ -1,865 +0,0 @@ -/* -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js'; - -describe('API: Endpoints', () => { - let p: childProcess.ChildProcess; - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe('signup', () => { - it('不正なユーザー名でアカウントが作成できない', async(async () => { - const res = await request('/signup', { - username: 'test.', - password: 'test' - }); - assert.strictEqual(res.status, 400); - })); - - it('空のパスワードでアカウントが作成できない', async(async () => { - const res = await request('/signup', { - username: 'test', - password: '' - }); - assert.strictEqual(res.status, 400); - })); - - it('正しくアカウントが作成できる', async(async () => { - const me = { - username: 'test1', - password: 'test1' - }; - - const res = await request('/signup', me); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.username, me.username); - })); - - it('同じユーザー名のアカウントは作成できない', async(async () => { - await signup({ - username: 'test2' - }); - - const res = await request('/signup', { - username: 'test2', - password: 'test2' - }); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('signin', () => { - it('間違ったパスワードでサインインできない', async(async () => { - await signup({ - username: 'test3', - password: 'foo' - }); - - const res = await request('/signin', { - username: 'test3', - password: 'bar' - }); - - assert.strictEqual(res.status, 403); - })); - - it('クエリをインジェクションできない', async(async () => { - await signup({ - username: 'test4' - }); - - const res = await request('/signin', { - username: 'test4', - password: { - $gt: '' - } - }); - - assert.strictEqual(res.status, 400); - })); - - it('正しい情報でサインインできる', async(async () => { - await signup({ - username: 'test5', - password: 'foo' - }); - - const res = await request('/signin', { - username: 'test5', - password: 'foo' - }); - - assert.strictEqual(res.status, 200); - })); - }); - - describe('i/update', () => { - it('アカウント設定を更新できる', async(async () => { - const myName = '大室櫻子'; - const myLocation = '七森中'; - const myBirthday = '2000-09-07'; - - const res = await request('/i/update', { - name: myName, - location: myLocation, - birthday: myBirthday - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, myName); - assert.strictEqual(res.body.location, myLocation); - assert.strictEqual(res.body.birthday, myBirthday); - })); - - it('名前を空白にできない', async(async () => { - const res = await request('/i/update', { - name: ' ' - }, alice); - assert.strictEqual(res.status, 400); - })); - - it('誕生日の設定を削除できる', async(async () => { - await request('/i/update', { - birthday: '2000-09-07' - }, alice); - - const res = await request('/i/update', { - birthday: null - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.birthday, null); - })); - - it('不正な誕生日の形式で怒られる', async(async () => { - const res = await request('/i/update', { - birthday: '2000/09/07' - }, alice); - assert.strictEqual(res.status, 400); - })); - }); - - describe('users/show', () => { - it('ユーザーが取得できる', async(async () => { - const res = await request('/users/show', { - userId: alice.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.id, alice.id); - })); - - it('ユーザーが存在しなかったら怒る', async(async () => { - const res = await request('/users/show', { - userId: '000000000000000000000000' - }); - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/users/show', { - userId: 'kyoppie' - }); - assert.strictEqual(res.status, 400); - })); - }); - - describe('notes/show', () => { - it('投稿が取得できる', async(async () => { - const myPost = await post(alice, { - text: 'test' - }); - - const res = await request('/notes/show', { - noteId: myPost.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.id, myPost.id); - assert.strictEqual(res.body.text, myPost.text); - })); - - it('投稿が存在しなかったら怒る', async(async () => { - const res = await request('/notes/show', { - noteId: '000000000000000000000000' - }); - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/notes/show', { - noteId: 'kyoppie' - }); - assert.strictEqual(res.status, 400); - })); - }); - - describe('notes/reactions/create', () => { - it('リアクションできる', async(async () => { - const bobPost = await post(bob); - - const alice = await signup({ username: 'alice' }); - const res = await request('/notes/reactions/create', { - noteId: bobPost.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 204); - - const resNote = await request('/notes/show', { - noteId: bobPost.id, - }, alice); - - assert.strictEqual(resNote.status, 200); - assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]); - })); - - it('自分の投稿にもリアクションできる', async(async () => { - const myPost = await post(alice); - - const res = await request('/notes/reactions/create', { - noteId: myPost.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 204); - })); - - it('二重にリアクションできない', async(async () => { - const bobPost = await post(bob); - - await react(alice, bobPost, 'like'); - - const res = await request('/notes/reactions/create', { - noteId: bobPost.id, - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しない投稿にはリアクションできない', async(async () => { - const res = await request('/notes/reactions/create', { - noteId: '000000000000000000000000', - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('空のパラメータで怒られる', async(async () => { - const res = await request('/notes/reactions/create', {}, alice); - - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/notes/reactions/create', { - noteId: 'kyoppie', - reaction: '🚀', - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('following/create', () => { - it('フォローできる', async(async () => { - const res = await request('/following/create', { - userId: alice.id - }, bob); - - assert.strictEqual(res.status, 200); - })); - - it('既にフォローしている場合は怒る', async(async () => { - const res = await request('/following/create', { - userId: alice.id - }, bob); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないユーザーはフォローできない', async(async () => { - const res = await request('/following/create', { - userId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('自分自身はフォローできない', async(async () => { - const res = await request('/following/create', { - userId: alice.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('空のパラメータで怒られる', async(async () => { - const res = await request('/following/create', {}, alice); - - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/following/create', { - userId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('following/delete', () => { - it('フォロー解除できる', async(async () => { - await request('/following/create', { - userId: alice.id - }, bob); - - const res = await request('/following/delete', { - userId: alice.id - }, bob); - - assert.strictEqual(res.status, 200); - })); - - it('フォローしていない場合は怒る', async(async () => { - const res = await request('/following/delete', { - userId: alice.id - }, bob); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないユーザーはフォロー解除できない', async(async () => { - const res = await request('/following/delete', { - userId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('自分自身はフォロー解除できない', async(async () => { - const res = await request('/following/delete', { - userId: alice.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('空のパラメータで怒られる', async(async () => { - const res = await request('/following/delete', {}, alice); - - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/following/delete', { - userId: 'kyoppie' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('drive', () => { - it('ドライブ情報を取得できる', async(async () => { - await uploadFile({ - userId: alice.id, - size: 256 - }); - await uploadFile({ - userId: alice.id, - size: 512 - }); - await uploadFile({ - userId: alice.id, - size: 1024 - }); - const res = await request('/drive', {}, alice); - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - expect(res.body).have.property('usage').eql(1792); - })); - }); - - describe('drive/files/create', () => { - it('ファイルを作成できる', async(async () => { - const res = await uploadFile(alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'Lenna.png'); - })); - - it('ファイルに名前を付けられる', async(async () => { - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', alice.token) - .field('name', 'Belmond.png') - .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('Belmond.png'); - })); - - it('ファイル無しで怒られる', async(async () => { - const res = await request('/drive/files/create', {}, alice); - - assert.strictEqual(res.status, 400); - })); - - it('SVGファイルを作成できる', async(async () => { - const res = await uploadFile(alice, __dirname + '/resources/image.svg'); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'image.svg'); - assert.strictEqual(res.body.type, 'image/svg+xml'); - })); - }); - - describe('drive/files/update', () => { - it('名前を更新できる', async(async () => { - const file = await uploadFile(alice); - const newName = 'いちごパスタ.png'; - - const res = await request('/drive/files/update', { - fileId: file.id, - name: newName - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, newName); - })); - - it('他人のファイルは更新できない', async(async () => { - const file = await uploadFile(bob); - - const res = await request('/drive/files/update', { - fileId: file.id, - name: 'いちごパスタ.png' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('親フォルダを更新できる', async(async () => { - const file = await uploadFile(alice); - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: folder.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.folderId, folder.id); - })); - - it('親フォルダを無しにできる', async(async () => { - const file = await uploadFile(alice); - - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - await request('/drive/files/update', { - fileId: file.id, - folderId: folder.id - }, alice); - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: null - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.folderId, null); - })); - - it('他人のフォルダには入れられない', async(async () => { - const file = await uploadFile(alice); - const folder = (await request('/drive/folders/create', { - name: 'test' - }, bob)).body; - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: folder.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないフォルダで怒られる', async(async () => { - const file = await uploadFile(alice); - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('不正なフォルダIDで怒られる', async(async () => { - const file = await uploadFile(alice); - - const res = await request('/drive/files/update', { - fileId: file.id, - folderId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('ファイルが存在しなかったら怒る', async(async () => { - const res = await request('/drive/files/update', { - fileId: '000000000000000000000000', - name: 'いちごパスタ.png' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('間違ったIDで怒られる', async(async () => { - const res = await request('/drive/files/update', { - fileId: 'kyoppie', - name: 'いちごパスタ.png' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('drive/folders/create', () => { - it('フォルダを作成できる', async(async () => { - const res = await request('/drive/folders/create', { - name: 'test' - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'test'); - })); - }); - - describe('drive/folders/update', () => { - it('名前を更新できる', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - name: 'new name' - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'new name'); - })); - - it('他人のフォルダを更新できない', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, bob)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - name: 'new name' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('親フォルダを更新できる', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await request('/drive/folders/create', { - name: 'parent' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.parentId, parentFolder.id); - })); - - it('親フォルダを無しに更新できる', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await request('/drive/folders/create', { - name: 'parent' - }, alice)).body; - await request('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: null - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.parentId, null); - })); - - it('他人のフォルダを親フォルダに設定できない', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await request('/drive/folders/create', { - name: 'parent' - }, bob)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('フォルダが循環するような構造にできない', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const parentFolder = (await request('/drive/folders/create', { - name: 'parent' - }, alice)).body; - await request('/drive/folders/update', { - folderId: parentFolder.id, - parentId: folder.id - }, alice); - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: parentFolder.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('フォルダが循環するような構造にできない(再帰的)', async(async () => { - const folderA = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const folderB = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - const folderC = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - await request('/drive/folders/update', { - folderId: folderB.id, - parentId: folderA.id - }, alice); - await request('/drive/folders/update', { - folderId: folderC.id, - parentId: folderB.id - }, alice); - - const res = await request('/drive/folders/update', { - folderId: folderA.id, - parentId: folderC.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('フォルダが循環するような構造にできない(自身)', async(async () => { - const folderA = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folderA.id, - parentId: folderA.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しない親フォルダを設定できない', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('不正な親フォルダIDで怒られる', async(async () => { - const folder = (await request('/drive/folders/create', { - name: 'test' - }, alice)).body; - - const res = await request('/drive/folders/update', { - folderId: folder.id, - parentId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないフォルダを更新できない', async(async () => { - const res = await request('/drive/folders/update', { - folderId: '000000000000000000000000' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('不正なフォルダIDで怒られる', async(async () => { - const res = await request('/drive/folders/update', { - folderId: 'foo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('messaging/messages/create', () => { - it('メッセージを送信できる', async(async () => { - const res = await request('/messaging/messages/create', { - userId: bob.id, - text: 'test' - }, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.text, 'test'); - })); - - it('自分自身にはメッセージを送信できない', async(async () => { - const res = await request('/messaging/messages/create', { - userId: alice.id, - text: 'Yo' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('存在しないユーザーにはメッセージを送信できない', async(async () => { - const res = await request('/messaging/messages/create', { - userId: '000000000000000000000000', - text: 'test' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('不正なユーザーIDで怒られる', async(async () => { - const res = await request('/messaging/messages/create', { - userId: 'foo', - text: 'test' - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('テキストが無くて怒られる', async(async () => { - const res = await request('/messaging/messages/create', { - userId: bob.id - }, alice); - - assert.strictEqual(res.status, 400); - })); - - it('文字数オーバーで怒られる', async(async () => { - const res = await request('/messaging/messages/create', { - userId: bob.id, - text: '!'.repeat(1001) - }, alice); - - assert.strictEqual(res.status, 400); - })); - }); - - describe('notes/replies', () => { - it('自分に閲覧権限のない投稿は含まれない', async(async () => { - const alicePost = await post(alice, { - text: 'foo' - }); - - await post(bob, { - replyId: alicePost.id, - text: 'bar', - visibility: 'specified', - visibleUserIds: [alice.id] - }); - - const res = await request('/notes/replies', { - noteId: alicePost.id - }, carol); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 0); - })); - }); - - describe('notes/timeline', () => { - it('フォロワー限定投稿が含まれる', async(async () => { - await request('/following/create', { - userId: alice.id - }, bob); - - const alicePost = await post(alice, { - text: 'foo', - visibility: 'followers' - }); - - const res = await request('/notes/timeline', {}, bob); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 1); - assert.strictEqual(res.body[0].id, alicePost.id); - })); - }); -}); -*/ diff --git a/packages/backend/test/extract-mentions.ts b/packages/backend/test/extract-mentions.ts deleted file mode 100644 index f400e1e634..0000000000 --- a/packages/backend/test/extract-mentions.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as assert from "assert"; - -import { parse } from "mfm-js"; -import { extractMentions } from "../src/misc/extract-mentions.js"; - -describe("Extract mentions", () => { - it("simple", () => { - const ast = parse("@foo @bar @baz")!; - const mentions = extractMentions(ast); - assert.deepStrictEqual(mentions, [ - { - username: "foo", - acct: "@foo", - host: null, - }, - { - username: "bar", - acct: "@bar", - host: null, - }, - { - username: "baz", - acct: "@baz", - host: null, - }, - ]); - }); - - it("nested", () => { - const ast = parse("@foo **@bar** @baz")!; - const mentions = extractMentions(ast); - assert.deepStrictEqual(mentions, [ - { - username: "foo", - acct: "@foo", - host: null, - }, - { - username: "bar", - acct: "@bar", - host: null, - }, - { - username: "baz", - acct: "@baz", - host: null, - }, - ]); - }); -}); diff --git a/packages/backend/test/fetch-resource.ts b/packages/backend/test/fetch-resource.ts deleted file mode 100644 index da3116f0e8..0000000000 --- a/packages/backend/test/fetch-resource.ts +++ /dev/null @@ -1,213 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import * as openapi from "@redocly/openapi-core"; -import { - async, - startServer, - signup, - post, - request, - simpleGet, - port, - shutdownServer, -} from "./utils.js"; - -// Request Accept -const ONLY_AP = "application/activity+json"; -const PREFER_AP = "application/activity+json, */*"; -const PREFER_HTML = "text/html, */*"; -const UNSPECIFIED = "*/*"; - -// Response Contet-Type -const AP = "application/activity+json; charset=utf-8"; -const JSON = "application/json; charset=utf-8"; -const HTML = "text/html; charset=utf-8"; - -describe("Fetch resource", () => { - let p: childProcess.ChildProcess; - - let alice: any; - let alicesPost: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: "alice" }); - alicesPost = await post(alice, { - text: "test", - }); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe("Common", () => { - it("meta", async(async () => { - const res = await request("/meta", {}); - - assert.strictEqual(res.status, 200); - })); - - it("GET root", async(async () => { - const res = await simpleGet("/"); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it("GET docs", async(async () => { - const res = await simpleGet("/docs/ja-JP/about"); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it("GET api-doc", async(async () => { - const res = await simpleGet("/api-doc"); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it("GET api.json", async(async () => { - const res = await simpleGet("/api.json"); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, JSON); - })); - - it("Validate api.json", async(async () => { - const config = await openapi.loadConfig(); - const result = await openapi.bundle({ - config, - ref: `http://localhost:${port}/api.json`, - }); - - for (const problem of result.problems) { - console.log(`${problem.message} - ${problem.location[0]?.pointer}`); - } - - assert.strictEqual(result.problems.length, 0); - })); - - it("GET favicon.ico", async(async () => { - const res = await simpleGet("/favicon.ico"); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, "image/x-icon"); - })); - - it("GET apple-touch-icon.png", async(async () => { - const res = await simpleGet("/apple-touch-icon.png"); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, "image/png"); - })); - - it("GET twemoji svg", async(async () => { - const res = await simpleGet("/twemoji/2764.svg"); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, "image/svg+xml"); - })); - - it("GET twemoji svg with hyphen", async(async () => { - const res = await simpleGet("/twemoji/2764-fe0f-200d-1f525.svg"); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, "image/svg+xml"); - })); - }); - - describe("/@:username", () => { - it("Only AP => AP", async(async () => { - const res = await simpleGet(`/@${alice.username}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it("Prefer AP => AP", async(async () => { - const res = await simpleGet(`/@${alice.username}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it("Prefer HTML => HTML", async(async () => { - const res = await simpleGet(`/@${alice.username}`, PREFER_HTML); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it("Unspecified => HTML", async(async () => { - const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - }); - - describe("/users/:id", () => { - it("Only AP => AP", async(async () => { - const res = await simpleGet(`/users/${alice.id}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it("Prefer AP => AP", async(async () => { - const res = await simpleGet(`/users/${alice.id}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it("Prefer HTML => Redirect to /@:username", async(async () => { - const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML); - assert.strictEqual(res.status, 302); - assert.strictEqual(res.location, `/@${alice.username}`); - })); - - it("Undecided => HTML", async(async () => { - const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED); - assert.strictEqual(res.status, 302); - assert.strictEqual(res.location, `/@${alice.username}`); - })); - }); - - describe("/notes/:id", () => { - it("Only AP => AP", async(async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it("Prefer AP => AP", async(async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, AP); - })); - - it("Prefer HTML => HTML", async(async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - - it("Unspecified => HTML", async(async () => { - const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, HTML); - })); - }); - - describe("Feeds", () => { - it("RSS", async(async () => { - const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, "application/rss+xml; charset=utf-8"); - })); - - it("ATOM", async(async () => { - const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, "application/atom+xml; charset=utf-8"); - })); - - it("JSON", async(async () => { - const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED); - assert.strictEqual(res.status, 200); - assert.strictEqual(res.type, "application/json; charset=utf-8"); - })); - }); -}); diff --git a/packages/backend/test/ff-visibility.ts b/packages/backend/test/ff-visibility.ts deleted file mode 100644 index f898926d99..0000000000 --- a/packages/backend/test/ff-visibility.ts +++ /dev/null @@ -1,283 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import { - async, - signup, - request, - post, - react, - connectStream, - startServer, - shutdownServer, - simpleGet, -} from "./utils.js"; - -describe("FF visibility", () => { - let p: childProcess.ChildProcess; - - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: "alice" }); - bob = await signup({ username: "bob" }); - carol = await signup({ username: "carol" }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it("ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる", async(async () => { - await request( - "/i/update", - { - ffVisibility: "public", - }, - alice, - ); - - const followingRes = await request( - "/users/following", - { - userId: alice.id, - }, - bob, - ); - const followersRes = await request( - "/users/followers", - { - userId: alice.id, - }, - bob, - ); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it("ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる", async(async () => { - await request( - "/i/update", - { - ffVisibility: "followers", - }, - alice, - ); - - const followingRes = await request( - "/users/following", - { - userId: alice.id, - }, - alice, - ); - const followersRes = await request( - "/users/followers", - { - userId: alice.id, - }, - alice, - ); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it("ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない", async(async () => { - await request( - "/i/update", - { - ffVisibility: "followers", - }, - alice, - ); - - const followingRes = await request( - "/users/following", - { - userId: alice.id, - }, - bob, - ); - const followersRes = await request( - "/users/followers", - { - userId: alice.id, - }, - bob, - ); - - assert.strictEqual(followingRes.status, 400); - assert.strictEqual(followersRes.status, 400); - })); - - it("ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる", async(async () => { - await request( - "/i/update", - { - ffVisibility: "followers", - }, - alice, - ); - - await request( - "/following/create", - { - userId: alice.id, - }, - bob, - ); - - const followingRes = await request( - "/users/following", - { - userId: alice.id, - }, - bob, - ); - const followersRes = await request( - "/users/followers", - { - userId: alice.id, - }, - bob, - ); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it("ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる", async(async () => { - await request( - "/i/update", - { - ffVisibility: "private", - }, - alice, - ); - - const followingRes = await request( - "/users/following", - { - userId: alice.id, - }, - alice, - ); - const followersRes = await request( - "/users/followers", - { - userId: alice.id, - }, - alice, - ); - - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(Array.isArray(followingRes.body), true); - assert.strictEqual(followersRes.status, 200); - assert.strictEqual(Array.isArray(followersRes.body), true); - })); - - it("ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない", async(async () => { - await request( - "/i/update", - { - ffVisibility: "private", - }, - alice, - ); - - const followingRes = await request( - "/users/following", - { - userId: alice.id, - }, - bob, - ); - const followersRes = await request( - "/users/followers", - { - userId: alice.id, - }, - bob, - ); - - assert.strictEqual(followingRes.status, 400); - assert.strictEqual(followersRes.status, 400); - })); - - describe("AP", () => { - it("ffVisibility が public 以外ならばAPからは取得できない", async(async () => { - { - await request( - "/i/update", - { - ffVisibility: "public", - }, - alice, - ); - - const followingRes = await simpleGet( - `/users/${alice.id}/following`, - "application/activity+json", - ); - const followersRes = await simpleGet( - `/users/${alice.id}/followers`, - "application/activity+json", - ); - assert.strictEqual(followingRes.status, 200); - assert.strictEqual(followersRes.status, 200); - } - { - await request( - "/i/update", - { - ffVisibility: "followers", - }, - alice, - ); - - const followingRes = await simpleGet( - `/users/${alice.id}/following`, - "application/activity+json", - ).catch((res) => ({ status: res.statusCode })); - const followersRes = await simpleGet( - `/users/${alice.id}/followers`, - "application/activity+json", - ).catch((res) => ({ status: res.statusCode })); - assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); - } - { - await request( - "/i/update", - { - ffVisibility: "private", - }, - alice, - ); - - const followingRes = await simpleGet( - `/users/${alice.id}/following`, - "application/activity+json", - ).catch((res) => ({ status: res.statusCode })); - const followersRes = await simpleGet( - `/users/${alice.id}/followers`, - "application/activity+json", - ).catch((res) => ({ status: res.statusCode })); - assert.strictEqual(followingRes.status, 403); - assert.strictEqual(followersRes.status, 403); - } - })); - }); -}); diff --git a/packages/backend/test/get-file-info.ts b/packages/backend/test/get-file-info.ts deleted file mode 100644 index 22dc28c8e0..0000000000 --- a/packages/backend/test/get-file-info.ts +++ /dev/null @@ -1,209 +0,0 @@ -import * as assert from "assert"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; -import { getFileInfo } from "../src/misc/get-file-info.js"; -import { async } from "./utils.js"; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -describe("Get file info", () => { - it("Empty file", async(async () => { - const path = `${_dirname}/resources/emptyfile`; - const info = (await getFileInfo(path, { - skipSensitiveDetection: true, - })) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 0, - md5: "d41d8cd98f00b204e9800998ecf8427e", - type: { - mime: "application/octet-stream", - ext: null, - }, - width: undefined, - height: undefined, - orientation: undefined, - }); - })); - - it("Generic JPEG", async(async () => { - const path = `${_dirname}/resources/Lenna.jpg`; - const info = (await getFileInfo(path, { - skipSensitiveDetection: true, - })) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 25360, - md5: "091b3f259662aa31e2ffef4519951168", - type: { - mime: "image/jpeg", - ext: "jpg", - }, - width: 512, - height: 512, - orientation: undefined, - }); - })); - - it("Generic APNG", async(async () => { - const path = `${_dirname}/resources/anime.png`; - const info = (await getFileInfo(path, { - skipSensitiveDetection: true, - })) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 1868, - md5: "08189c607bea3b952704676bb3c979e0", - type: { - mime: "image/apng", - ext: "apng", - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it("Generic AGIF", async(async () => { - const path = `${_dirname}/resources/anime.gif`; - const info = (await getFileInfo(path, { - skipSensitiveDetection: true, - })) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 2248, - md5: "32c47a11555675d9267aee1a86571e7e", - type: { - mime: "image/gif", - ext: "gif", - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it("PNG with alpha", async(async () => { - const path = `${_dirname}/resources/with-alpha.png`; - const info = (await getFileInfo(path, { - skipSensitiveDetection: true, - })) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 3772, - md5: "f73535c3e1e27508885b69b10cf6e991", - type: { - mime: "image/png", - ext: "png", - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it("Generic SVG", async(async () => { - const path = `${_dirname}/resources/image.svg`; - const info = (await getFileInfo(path, { - skipSensitiveDetection: true, - })) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 505, - md5: "b6f52b4b021e7b92cdd04509c7267965", - type: { - mime: "image/svg+xml", - ext: "svg", - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it("SVG with XML definition", async(async () => { - // https://github.com/misskey-dev/misskey/issues/4413 - const path = `${_dirname}/resources/with-xml-def.svg`; - const info = (await getFileInfo(path, { - skipSensitiveDetection: true, - })) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 544, - md5: "4b7a346cde9ccbeb267e812567e33397", - type: { - mime: "image/svg+xml", - ext: "svg", - }, - width: 256, - height: 256, - orientation: undefined, - }); - })); - - it("Dimension limit", async(async () => { - const path = `${_dirname}/resources/25000x25000.png`; - const info = (await getFileInfo(path, { - skipSensitiveDetection: true, - })) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 75933, - md5: "268c5dde99e17cf8fe09f1ab3f97df56", - type: { - mime: "application/octet-stream", // do not treat as image - ext: null, - }, - width: 25000, - height: 25000, - orientation: undefined, - }); - })); - - it("Rotate JPEG", async(async () => { - const path = `${_dirname}/resources/rotate.jpg`; - const info = (await getFileInfo(path, { - skipSensitiveDetection: true, - })) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; - assert.deepStrictEqual(info, { - size: 12624, - md5: "68d5b2d8d1d1acbbce99203e3ec3857e", - type: { - mime: "image/jpeg", - ext: "jpg", - }, - width: 512, - height: 256, - orientation: 8, - }); - })); -}); diff --git a/packages/backend/test/loader.js b/packages/backend/test/loader.js deleted file mode 100644 index 7e1bf379dc..0000000000 --- a/packages/backend/test/loader.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * ts-node/esmローダーに投げる前にpath mappingを解決する - * 参考 - * - https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115 - * - https://nodejs.org/api/esm.html#loaders - * ※ https://github.com/TypeStrong/ts-node/pull/1585 が取り込まれたらこのカスタムローダーは必要なくなる - */ - -import { resolve as resolveTs, load } from "ts-node/esm"; -import { loadConfig, createMatchPath } from "tsconfig-paths"; -import { pathToFileURL } from "url"; - -const tsconfig = loadConfig(); -const matchPath = createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths); - -export function resolve(specifier, ctx, defaultResolve) { - let resolvedSpecifier; - if (specifier.endsWith(".js")) { - // maybe transpiled - const specifierWithoutExtension = specifier.substring( - 0, - specifier.length - ".js".length, - ); - const matchedSpecifier = matchPath(specifierWithoutExtension); - if (matchedSpecifier) { - resolvedSpecifier = pathToFileURL(`${matchedSpecifier}.js`).href; - } - } else { - const matchedSpecifier = matchPath(specifier); - if (matchedSpecifier) { - resolvedSpecifier = pathToFileURL(matchedSpecifier).href; - } - } - return resolveTs(resolvedSpecifier ?? specifier, ctx, defaultResolve); -} - -export { load }; diff --git a/packages/backend/test/mfm.ts b/packages/backend/test/mfm.ts deleted file mode 100644 index 605daa7107..0000000000 --- a/packages/backend/test/mfm.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as assert from "assert"; -import * as mfm from "mfm-js"; - -import { toHtml } from "../src/mfm/to-html.js"; -import { fromHtml } from "../src/mfm/from-html.js"; - -describe("toHtml", () => { - it("br", () => { - const input = "foo\nbar\nbaz"; - const output = "

foo
bar
baz

"; - assert.equal(toHtml(mfm.parse(input)), output); - }); - - it("br alt", () => { - const input = "foo\r\nbar\rbaz"; - const output = "

foo
bar
baz

"; - assert.equal(toHtml(mfm.parse(input)), output); - }); -}); - -describe("fromHtml", () => { - it("p", () => { - assert.deepStrictEqual(fromHtml("

a

b

"), "a\n\nb"); - }); - - it("block element", () => { - assert.deepStrictEqual(fromHtml("
a
b
"), "a\nb"); - }); - - it("inline element", () => { - assert.deepStrictEqual(fromHtml("
  • a
  • b
"), "a\nb"); - }); - - it("block code", () => { - assert.deepStrictEqual( - fromHtml("
a\nb
"), - "```\na\nb\n```", - ); - }); - - it("inline code", () => { - assert.deepStrictEqual(fromHtml("a"), "`a`"); - }); - - it("quote", () => { - assert.deepStrictEqual( - fromHtml("
a\nb
"), - "> a\n> b", - ); - }); - - it("br", () => { - assert.deepStrictEqual(fromHtml("

abc

d

"), "abc\n\nd"); - }); - - it("link with different text", () => { - assert.deepStrictEqual( - fromHtml('

a c d

'), - "a [c](https://example.com/b) d", - ); - }); - - it("link with different text, but not encoded", () => { - assert.deepStrictEqual( - fromHtml('

a c d

'), - "a [c]() d", - ); - }); - - it("link with same text", () => { - assert.deepStrictEqual( - fromHtml( - '

a https://example.com/b d

', - ), - "a https://example.com/b d", - ); - }); - - it("link with same text, but not encoded", () => { - assert.deepStrictEqual( - fromHtml( - '

a https://example.com/ä d

', - ), - "a d", - ); - }); - - it("link with no url", () => { - assert.deepStrictEqual( - fromHtml('

a c d

'), - "a [c](b) d", - ); - }); - - it("link without href", () => { - assert.deepStrictEqual(fromHtml("

a c d

"), "a c d"); - }); - - it("link without text", () => { - assert.deepStrictEqual( - fromHtml('

a d

'), - "a https://example.com/b d", - ); - }); - - it("link without both", () => { - assert.deepStrictEqual(fromHtml("

a d

"), "a d"); - }); - - it("mention", () => { - assert.deepStrictEqual( - fromHtml( - '

a @user d

', - ), - "a @user@example.com d", - ); - }); - - it("hashtag", () => { - assert.deepStrictEqual( - fromHtml('

a #a d

', [ - "#a", - ]), - "a #a d", - ); - }); -}); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts deleted file mode 100644 index 74c67e3d3f..0000000000 --- a/packages/backend/test/misc/mock-resolver.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Resolver from "../../src/remote/activitypub/resolver.js"; -import { IObject } from "../../src/remote/activitypub/type.js"; - -type MockResponse = { - type: string; - content: string; -}; - -export class MockResolver extends Resolver { - private _rs = new Map(); - public async _register( - uri: string, - content: string | Record, - type = "application/activity+json", - ) { - this._rs.set(uri, { - type, - content: typeof content === "string" ? content : JSON.stringify(content), - }); - } - - public async resolve(value: string | IObject): Promise { - if (typeof value !== "string") return value; - - const r = this._rs.get(value); - - if (!r) { - throw { - name: "StatusError", - statusCode: 404, - message: "Not registed for mock", - }; - } - - const object = JSON.parse(r.content); - - return object; - } -} diff --git a/packages/backend/test/mute.ts b/packages/backend/test/mute.ts deleted file mode 100644 index c511628342..0000000000 --- a/packages/backend/test/mute.ts +++ /dev/null @@ -1,176 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import { - async, - signup, - request, - post, - react, - startServer, - shutdownServer, - waitFire, -} from "./utils.js"; - -describe("Mute", () => { - let p: childProcess.ChildProcess; - - // alice mutes carol - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: "alice" }); - bob = await signup({ username: "bob" }); - carol = await signup({ username: "carol" }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it("ミュート作成", async(async () => { - const res = await request( - "/mute/create", - { - userId: carol.id, - }, - alice, - ); - - assert.strictEqual(res.status, 204); - })); - - it("「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない", async(async () => { - const bobNote = await post(bob, { text: "@alice hi" }); - const carolNote = await post(carol, { text: "@alice hi" }); - - const res = await request("/notes/mentions", {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual( - res.body.some((note: any) => note.id === bobNote.id), - true, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === carolNote.id), - false, - ); - })); - - it("ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない", async(async () => { - // 状態リセット - await request("/i/read-all-unread-notes", {}, alice); - - await post(carol, { text: "@alice hi" }); - - const res = await request("/i", {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.hasUnreadMentions, false); - })); - - it("ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない", async () => { - // 状態リセット - await request("/i/read-all-unread-notes", {}, alice); - - const fired = await waitFire( - alice, - "main", - () => post(carol, { text: "@alice hi" }), - (msg) => msg.type === "unreadMention", - ); - - assert.strictEqual(fired, false); - }); - - it("ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない", async () => { - // 状態リセット - await request("/i/read-all-unread-notes", {}, alice); - await request("/notifications/mark-all-as-read", {}, alice); - - const fired = await waitFire( - alice, - "main", - () => post(carol, { text: "@alice hi" }), - (msg) => msg.type === "unreadNotification", - ); - - assert.strictEqual(fired, false); - }); - - describe("Timeline", () => { - it("タイムラインにミュートしているユーザーの投稿が含まれない", async(async () => { - const aliceNote = await post(alice); - const bobNote = await post(bob); - const carolNote = await post(carol); - - const res = await request("/notes/local-timeline", {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual( - res.body.some((note: any) => note.id === aliceNote.id), - true, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === bobNote.id), - true, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === carolNote.id), - false, - ); - })); - - it("タイムラインにミュートしているユーザーの投稿のRenoteが含まれない", async(async () => { - const aliceNote = await post(alice); - const carolNote = await post(carol); - const bobNote = await post(bob, { - renoteId: carolNote.id, - }); - - const res = await request("/notes/local-timeline", {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual( - res.body.some((note: any) => note.id === aliceNote.id), - true, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === bobNote.id), - false, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === carolNote.id), - false, - ); - })); - }); - - describe("Notification", () => { - it("通知にミュートしているユーザーの通知が含まれない(リアクション)", async(async () => { - const aliceNote = await post(alice); - await react(bob, aliceNote, "like"); - await react(carol, aliceNote, "like"); - - const res = await request("/i/notifications", {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual( - res.body.some((notification: any) => notification.userId === bob.id), - true, - ); - assert.strictEqual( - res.body.some((notification: any) => notification.userId === carol.id), - false, - ); - })); - }); -}); diff --git a/packages/backend/test/note.ts b/packages/backend/test/note.ts deleted file mode 100644 index 3af4b88d87..0000000000 --- a/packages/backend/test/note.ts +++ /dev/null @@ -1,517 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import { Note } from "../src/models/entities/note.js"; -import { - async, - signup, - request, - post, - uploadUrl, - startServer, - shutdownServer, - initTestDb, - api, -} from "./utils.js"; - -describe("Note", () => { - let p: childProcess.ChildProcess; - let Notes: any; - - let alice: any; - let bob: any; - - before(async () => { - p = await startServer(); - const connection = await initTestDb(true); - Notes = connection.getRepository(Note); - alice = await signup({ username: "alice" }); - bob = await signup({ username: "bob" }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it("投稿できる", async(async () => { - const post = { - text: "test", - }; - - const res = await request("/notes/create", post, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.strictEqual(res.body.createdNote.text, post.text); - })); - - it("ファイルを添付できる", async(async () => { - const file = await uploadUrl( - alice, - "https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg", - ); - - const res = await request( - "/notes/create", - { - fileIds: [file.id], - }, - alice, - ); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]); - })); - - it("他人のファイルは無視", async(async () => { - const file = await uploadUrl( - bob, - "https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg", - ); - - const res = await request( - "/notes/create", - { - text: "test", - fileIds: [file.id], - }, - alice, - ); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); - })); - - it("存在しないファイルは無視", async(async () => { - const res = await request( - "/notes/create", - { - text: "test", - fileIds: ["000000000000000000000000"], - }, - alice, - ); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); - })); - - it("不正なファイルIDは無視", async(async () => { - const res = await request( - "/notes/create", - { - fileIds: ["kyoppie"], - }, - alice, - ); - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.deepStrictEqual(res.body.createdNote.fileIds, []); - })); - - it("返信できる", async(async () => { - const bobPost = await post(bob, { - text: "foo", - }); - - const alicePost = { - text: "bar", - replyId: bobPost.id, - }; - - const res = await request("/notes/create", alicePost, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.strictEqual(res.body.createdNote.text, alicePost.text); - assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); - assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); - })); - - it("renoteできる", async(async () => { - const bobPost = await post(bob, { - text: "test", - }); - - const alicePost = { - renoteId: bobPost.id, - }; - - const res = await request("/notes/create", alicePost, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); - })); - - it("引用renoteできる", async(async () => { - const bobPost = await post(bob, { - text: "test", - }); - - const alicePost = { - text: "test", - renoteId: bobPost.id, - }; - - const res = await request("/notes/create", alicePost, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.strictEqual(res.body.createdNote.text, alicePost.text); - assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); - assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); - })); - - it("文字数ぎりぎりで怒られない", async(async () => { - const post = { - text: "!".repeat(3000), - }; - const res = await request("/notes/create", post, alice); - assert.strictEqual(res.status, 200); - })); - - it("文字数オーバーで怒られる", async(async () => { - const post = { - text: "!".repeat(3001), - }; - const res = await request("/notes/create", post, alice); - assert.strictEqual(res.status, 400); - })); - - it("存在しないリプライ先で怒られる", async(async () => { - const post = { - text: "test", - replyId: "000000000000000000000000", - }; - const res = await request("/notes/create", post, alice); - assert.strictEqual(res.status, 400); - })); - - it("存在しないrenote対象で怒られる", async(async () => { - const post = { - renoteId: "000000000000000000000000", - }; - const res = await request("/notes/create", post, alice); - assert.strictEqual(res.status, 400); - })); - - it("不正なリプライ先IDで怒られる", async(async () => { - const post = { - text: "test", - replyId: "foo", - }; - const res = await request("/notes/create", post, alice); - assert.strictEqual(res.status, 400); - })); - - it("不正なrenote対象IDで怒られる", async(async () => { - const post = { - renoteId: "foo", - }; - const res = await request("/notes/create", post, alice); - assert.strictEqual(res.status, 400); - })); - - it("存在しないユーザーにメンションできる", async(async () => { - const post = { - text: "@ghost yo", - }; - - const res = await request("/notes/create", post, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.strictEqual(res.body.createdNote.text, post.text); - })); - - it("同じユーザーに複数メンションしても内部的にまとめられる", async(async () => { - const post = { - text: "@bob @bob @bob yo", - }; - - const res = await request("/notes/create", post, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.strictEqual(res.body.createdNote.text, post.text); - - const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id }); - assert.deepStrictEqual(noteDoc.mentions, [bob.id]); - })); - - describe("notes/create", () => { - it("投票を添付できる", async(async () => { - const res = await request( - "/notes/create", - { - text: "test", - poll: { - choices: ["foo", "bar"], - }, - }, - alice, - ); - - assert.strictEqual(res.status, 200); - assert.strictEqual( - typeof res.body === "object" && !Array.isArray(res.body), - true, - ); - assert.strictEqual(res.body.createdNote.poll != null, true); - })); - - it("投票の選択肢が無くて怒られる", async(async () => { - const res = await request( - "/notes/create", - { - poll: {}, - }, - alice, - ); - assert.strictEqual(res.status, 400); - })); - - it("投票の選択肢が無くて怒られる (空の配列)", async(async () => { - const res = await request( - "/notes/create", - { - poll: { - choices: [], - }, - }, - alice, - ); - assert.strictEqual(res.status, 400); - })); - - it("投票の選択肢が1つで怒られる", async(async () => { - const res = await request( - "/notes/create", - { - poll: { - choices: ["Strawberry Pasta"], - }, - }, - alice, - ); - assert.strictEqual(res.status, 400); - })); - - it("投票できる", async(async () => { - const { body } = await request( - "/notes/create", - { - text: "test", - poll: { - choices: ["sakura", "izumi", "ako"], - }, - }, - alice, - ); - - const res = await request( - "/notes/polls/vote", - { - noteId: body.createdNote.id, - choice: 1, - }, - alice, - ); - - assert.strictEqual(res.status, 204); - })); - - it("複数投票できない", async(async () => { - const { body } = await request( - "/notes/create", - { - text: "test", - poll: { - choices: ["sakura", "izumi", "ako"], - }, - }, - alice, - ); - - await request( - "/notes/polls/vote", - { - noteId: body.createdNote.id, - choice: 0, - }, - alice, - ); - - const res = await request( - "/notes/polls/vote", - { - noteId: body.createdNote.id, - choice: 2, - }, - alice, - ); - - assert.strictEqual(res.status, 400); - })); - - it("許可されている場合は複数投票できる", async(async () => { - const { body } = await request( - "/notes/create", - { - text: "test", - poll: { - choices: ["sakura", "izumi", "ako"], - multiple: true, - }, - }, - alice, - ); - - await request( - "/notes/polls/vote", - { - noteId: body.createdNote.id, - choice: 0, - }, - alice, - ); - - await request( - "/notes/polls/vote", - { - noteId: body.createdNote.id, - choice: 1, - }, - alice, - ); - - const res = await request( - "/notes/polls/vote", - { - noteId: body.createdNote.id, - choice: 2, - }, - alice, - ); - - assert.strictEqual(res.status, 204); - })); - - it("締め切られている場合は投票できない", async(async () => { - const { body } = await request( - "/notes/create", - { - text: "test", - poll: { - choices: ["sakura", "izumi", "ako"], - expiredAfter: 1, - }, - }, - alice, - ); - - await new Promise((x) => setTimeout(x, 2)); - - const res = await request( - "/notes/polls/vote", - { - noteId: body.createdNote.id, - choice: 1, - }, - alice, - ); - - assert.strictEqual(res.status, 400); - })); - }); - - describe("notes/delete", () => { - it("delete a reply", async(async () => { - const mainNoteRes = await api( - "notes/create", - { - text: "main post", - }, - alice, - ); - const replyOneRes = await api( - "notes/create", - { - text: "reply one", - replyId: mainNoteRes.body.createdNote.id, - }, - alice, - ); - const replyTwoRes = await api( - "notes/create", - { - text: "reply two", - replyId: mainNoteRes.body.createdNote.id, - }, - alice, - ); - - const deleteOneRes = await api( - "notes/delete", - { - noteId: replyOneRes.body.createdNote.id, - }, - alice, - ); - - assert.strictEqual(deleteOneRes.status, 204); - let mainNote = await Notes.findOneBy({ - id: mainNoteRes.body.createdNote.id, - }); - assert.strictEqual(mainNote.repliesCount, 1); - - const deleteTwoRes = await api( - "notes/delete", - { - noteId: replyTwoRes.body.createdNote.id, - }, - alice, - ); - - assert.strictEqual(deleteTwoRes.status, 204); - mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); - assert.strictEqual(mainNote.repliesCount, 0); - })); - }); -}); diff --git a/packages/backend/test/prelude/maybe.ts b/packages/backend/test/prelude/maybe.ts deleted file mode 100644 index df589981c3..0000000000 --- a/packages/backend/test/prelude/maybe.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as assert from "assert"; -import { just, nothing } from "../../src/prelude/maybe.js"; - -describe("just", () => { - it("has a value", () => { - assert.deepStrictEqual(just(3).isJust(), true); - }); - - it("has the inverse called get", () => { - assert.deepStrictEqual(just(3).get(), 3); - }); -}); - -describe("nothing", () => { - it("has no value", () => { - assert.deepStrictEqual(nothing().isJust(), false); - }); -}); diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.ts deleted file mode 100644 index 5d08ff8924..0000000000 --- a/packages/backend/test/prelude/url.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as assert from "assert"; -import { query } from "../../src/prelude/url.js"; - -describe("url", () => { - it("query", () => { - const s = query({ - foo: "ふぅ", - bar: "b a r", - baz: undefined, - }); - assert.deepStrictEqual(s, "foo=%E3%81%B5%E3%81%85&bar=b%20a%20r"); - }); -}); diff --git a/packages/backend/test/reaction-lib.ts b/packages/backend/test/reaction-lib.ts deleted file mode 100644 index 7c61dc76c2..0000000000 --- a/packages/backend/test/reaction-lib.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* -import * as assert from 'assert'; - -import { toDbReaction } from '../src/misc/reaction-lib.js'; - -describe('toDbReaction', async () => { - it('既存の文字列リアクションはそのまま', async () => { - assert.strictEqual(await toDbReaction('like'), 'like'); - }); - - it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => { - assert.strictEqual(await toDbReaction('🍮'), '🍮'); - }); - - it('プリン以外の既存のリアクションは文字列化する like', async () => { - assert.strictEqual(await toDbReaction('👍'), 'like'); - }); - - it('プリン以外の既存のリアクションは文字列化する love', async () => { - assert.strictEqual(await toDbReaction('❤️'), 'love'); - }); - - it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => { - assert.strictEqual(await toDbReaction('❤'), 'love'); - }); - - it('プリン以外の既存のリアクションは文字列化する laugh', async () => { - assert.strictEqual(await toDbReaction('😆'), 'laugh'); - }); - - it('プリン以外の既存のリアクションは文字列化する hmm', async () => { - assert.strictEqual(await toDbReaction('🤔'), 'hmm'); - }); - - it('プリン以外の既存のリアクションは文字列化する surprise', async () => { - assert.strictEqual(await toDbReaction('😮'), 'surprise'); - }); - - it('プリン以外の既存のリアクションは文字列化する congrats', async () => { - assert.strictEqual(await toDbReaction('🎉'), 'congrats'); - }); - - it('プリン以外の既存のリアクションは文字列化する angry', async () => { - assert.strictEqual(await toDbReaction('💢'), 'angry'); - }); - - it('プリン以外の既存のリアクションは文字列化する confused', async () => { - assert.strictEqual(await toDbReaction('😥'), 'confused'); - }); - - it('プリン以外の既存のリアクションは文字列化する rip', async () => { - assert.strictEqual(await toDbReaction('😇'), 'rip'); - }); - - it('それ以外はUnicodeのまま', async () => { - assert.strictEqual(await toDbReaction('🍅'), '🍅'); - }); - - it('異体字セレクタ除去', async () => { - assert.strictEqual(await toDbReaction('㊗️'), '㊗'); - }); - - it('異体字セレクタ除去 必要なし', async () => { - assert.strictEqual(await toDbReaction('㊗'), '㊗'); - }); - - it('fallback - undefined', async () => { - assert.strictEqual(await toDbReaction(undefined), 'like'); - }); - - it('fallback - null', async () => { - assert.strictEqual(await toDbReaction(null), 'like'); - }); - - it('fallback - empty', async () => { - assert.strictEqual(await toDbReaction(''), 'like'); - }); - - it('fallback - unknown', async () => { - assert.strictEqual(await toDbReaction('unknown'), 'like'); - }); -}); -*/ diff --git a/packages/backend/test/resources/25000x25000.png b/packages/backend/test/resources/25000x25000.png deleted file mode 100644 index 0ed4666925f715eca6fdd9831206ec478b304047..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75933 zcmeI&y-HhQ0LS6?cp^w3Q3pW{loTAi19gy^56yo);7$+;8+ za|eD*p>uJ_kV$%)H+S*EbKrb%&T{_G{QK5EJFb*B%OQkH>!kT5g#K+jvwZeu@?r1m z?3TArJBLM4{CKF1Lbly)H5;AvjlaLMZ1!<*^Y7=|(!%V?L4W`O0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PCN8FnAf}F$M_r;_0Z}br2vxfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009E23-p((rPu-t-g~u1>SO#r0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAke+u-(86-`!F#XPNPdk01PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfWY^X~Kc)qni7KGgsK diff --git a/packages/backend/test/resources/Lenna.jpg b/packages/backend/test/resources/Lenna.jpg deleted file mode 100644 index 6b5b32281c1ffd5893d23392169cc368d72e0823..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25360 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 z 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

Q*ie`|Cj&2i?YTA`Sqeun>bF zfdE3P9SfOT1XIdxC%{Q02neSl`-zL;WLzdI>F%4i|Kk7p_x{_z_swxhYp)!@M%21H zPzKIOnI(8vvcoMq^4KQ^AOJ%(%Z) z2;kWoVQ#sXA<^@v`6T|t8q5E6uj|NJyZ;Ox+y{ zx&;pbp1N_kH-KXiL|Eyk&Q8J>Ipp}011OpVkiS;&On$3Bw!Ir5u76^ zN`@Yu6KM-hM5Z<|t>_abWZ=LP*%cwNoq{HJL?S?sWuZm@WMSAAwPbB;O*X|^dtJae zmdBGvAXCYun@Q%B+?xv#4{RPJ;fW=P*G-CeMMTAY;@ZX`x(amVl3RB+_YRR=O06>`<>~SD zo3#mY*Z%C?(s;(ws12U^C+G zo63^dY=bFL66%{Ga96i&pK!Rim|?BHp&X89eEIVB?$mM_YVArHK}8h6uP)Z6{-{oS%KF=U(uKULe>j6eF_o2zR{ z`1-3a&g)5Q01g0=bP$I9`yagc^bZK$kWn`_X^+@gID5r+44^$>EdFKECEt{q1j$ukqp4c8sAUI$ZDP?|nEvzZnX|2C$ns znZiVXt%*hWN`z#!^_3zWp!AFmIna~4N^FZWafe;9QEg(5aCIyJIpU;qY zt_B0;RD#_w36F_SRvUttHPa5gQ5r)MfdP6%L2TrYZWcfl;`Vnx`xpPmKmF5xe1ERP zJeDU{7xSKX!xMO1DPTzH<~=Ki8`N+K-?YpnvEeZLva+N>NQ{RxOao(ojF`=q1WZCC zsF;`l2^j`T5dl1nK$7>Dm}e-!1x1MVd*|`~UVrvq|LNcTJ73)Q5zq)TT2I8DJ(t8( z!aEb26SxytPT~oA19K38ZXUHoB+`Z)+JXWUEdp2|Vi+;7j7TQl8H)Danp4-d)Pb2a zCqjz${>}LG=IU^ed6$KS!@$EV+z2ozJfb849)t#>Op*w;8i^b-gHr;xMCb;4h;y=l zk;s4y9UP#$I3Y)5h%G45m>|%#LkVGxfB;C`0?8wRm{0~ZLG7|b5VF;Jb}sGkT7WE zA^7B$BLchOoY35p1av`&-;;)0CpJI`1Lw)x=49c?A%mHF*EDNyr*5{??%}<+0GEsn z*c}-mPbo5_j+}_ZqYF|Hm^d(!GEaNZ)jKhOB~0Pi!NmiSlSnuf@#dh40gMo>Lyk5v zRr4T7k%-k)f(WsYxHcH(kzpIf5z#bkqp}0nMv71(h=BBr(~~y6xN!cEFZQAjbbq_8 z&2PSwuM^+@a{DGf8mOuVje^r`6%YK)1D01dK55_H>%-dLygj|E7BTox!uN-vg}1(= zR;52EjxZFJ!pg9oYNWNBzj@P!t0yz$U132$%(3yjw?+()md9-|i?bmFPhvT;Qgf(Y z4F{oo^L!=^b6%{_;*dfcY@S1={cfI{4^w&a?6~$f?~ZT2Ia)ohh_%Y=caLwsy{pP) z+ySCsf)=?(D!CoEwfAyy=%;E;QQfl(maUz~DW$1gK79cpuU`MQHf-3TB!Py3^SB2* zz4+`83*El^@+&#Kxc&7n-oAS1t|Fra9G*SHp3Hl>%pd=i|6qFW{rb(XU;XMg>#-l+ z|LF3&pTK^^)iUnT^gY{ZOa$Y?Hx&CFahJcFa} z1Q|saiV_tud(S&Cg$7FD2sjX6bIQ@qfE19405OHvek4I~$~YwHPEvpz69B4LKm-s3 z8_+3Gg*y;f0J0GEK!CO=!`bA`&;IG(|3Cb*fBjoJA3;JdpFX>~*zYe7JkKQQ?(W`A zudc7Jo@P&1eSL_vb*nT84~PD6?5?>u73AF^>*@{;!L`f0qXg)ck{}oI#{i(b=b;0* z(40nr6gU$S`+zQTcYFVffADwyUw`lQ+hcNAF^`AKYuz?Xl$l0GjY^afA-d))OhTb< zly-~`0RaYX7SNMqvrfn^ggq*D>B)uiuJrX(WDM#^ELpTcM;hmg8f`x(Y%z@a`O|sc zrzbBC6p`~pzI7RmAvzIOoJH!e;QoN&-0bRK;q7hJZ z1acO#9Bu?`XsItQiDHWYtU%c?2A~2+28uM0Z*BuwMcksYgJD3JxiEz}_8bl(-B-#J zghi6%$wdGS6)|C0XJJ(Ya^(U|frz*uXt)Q4G9oed#{b19-vcuva#PemNj-oW4G37k zfx>`E0|1cRC{56|kQ9nxVeI|N@#GCK2_;bSfRb3xX&3+zg(wd!PO!uXkP=4&$dG}= zu_Kxh7&J!gu7;4w(K$gX;3I$`>?uy}=CQNo9-KN2ws>20J8c{0da_=9sgN`JvhsdF zV6o;#G!AxdIo!09xVK?BDf5FsGIkpd!75V5=0&V{k6 z1EF_skp_{JI$>hU&;ltdp?A==@X&)&^gyW7g~S?(A8MNKj=PUEfAS~nxAO4Sy3X|E zqx6LN@z?bgEj-8L7;%O?h!?py!~Epavs_S z*96<<+B*xJ(lB8^cei%7wbk|I+d4HJvLsa6 zx;2AgN@Bxq8pNS*JRX#{9+%114g8&4o!&g&KRn9aSx;}~%dyOR-TGbV>jhuEemFI| zr;O#0hdG!KIw;MVr)fWwi^>{#wlLomvk?u}t3RqyOas0D-5-qm=f}t2y?t|Q8>%Up z1cLB>x8HHWC(nNT`rH5C^!%@k7y0e4ezWR2r5y>mnrls18rI9F&p!O)pFDj2%f~NX zZWVV=-@ADKMpA$Go39?fzQ6tE-P>wk{U{&3 z_u2RU`X3$U<9A-ngkPWk#c#j8Psh_HS(1#?2QS7Sd^o-TV%XULXNmwK0EE^N#BGZZ zLc(fc;Zl+=ZX3$n#Trf`BN+FyAMbyp!HR^VH`@1jy=|BDXe}DhgOLgOklORT{*Edfy z$B-J}PPcwMpHh{ zcE~4`UF0O@P=KaA3<8Iwr`KQp*+2hB|J6VL$N%W=?a56W0V5c!k3lX5B-9NOnSl@k zN&z)?=;2J^ogfi4w(x138Y%A*8n;tJ+?prCf{5WYBefPhW~2y@t{dRoxyE_JPQmPc zy1u@+x!mukx$IyhNjbpXqkuYCMBg+I1sji&8H0=DW}PWVAQ~qC2&^bOhy`(QCqU=U z?k3=92QLZ^EbJ2Kgs93BRYeAfWKcXTU=O~aflK0k(g?=v(LiPrLnNh4vW8BODF>0{ z@C~s&0LckF~5djbsP?1Z7q8L@fG%{?F#J3KF%;+#c@06VX<_~^?OeV=WhzJx3 z$XzK7(2XL%$il@aK?{RH2o#fd4Ro||*vD3fl$o3eD5X@2qyQEnpm_vh;=q!KGqC`B zP;$>mjVuwWLBW(Fjjl?>hzW8>V1N^%5O#M8Z`KDe@m2|`o8We<%{NTj>1+((?4-Ry zP9Ym$?quX-44ucMZb*Pg5k&wzBe8%vbp!KU5XmEB$z+BZN9?Nus{(~a8L$Oo075TW zh=mwY0x1-k8Dj8g>cj?WD2c72LoH=kkTZmnkH z-X%Hyy1#nd_MZ_gacXe4DBQyCS?R=Z+Ye9ejmeYamY&|e+#c_nyX1x& z2F$tSp&5`y&`IFx=f~|X2D-qqzc?h{7EM!-bM=7DiPVTzEIcawO_OBS3(Q+iDg{3_fXW<{ls}8KZ)* zb>sx(Lyi>#`<7rxoD%5jG+|)(irtiEva`e9iC8<80}q9C(+cQGgoxd}2Tjh2*t3CS zLu3I6uM`QelZe@)-Jh+;`r9|J{`BWx|MYZUb>!h{*g=GW5pr06adZV{CakAa7&<;$Vc592@X8<^jSa z?`UG4axeYy&F#W%f{a0AN)T8O zG^8Y!Ea78-5L7^8YaI|I%~*!4+Ocy4X_u1O4MU@YGUKna-8&>>W;8jA)~R3cmeh5$!&Xn+Is=AvMd zWNncIgB+Tt0VIgRQIW`@bKN-a5ti@~oBKE;8ESOs-~ylt1<}Zv!VpkF0{=ZiAe$jP zWDG>0jRXTn(ol4^K;D^cC?TV|OA%kD!K31erU06K5lDxZP>P0TBqL|2EaD#uY+4o zgwc}{0B~4$Ph5~yjq#Nfp!Z|01l1{Va5eh7F9#_a7RT-0M><( z-9m7}h;d*5#{lh8GP5B8R`5v#6^)HMGJ10WuQ*)lyWOX|Ty~M4$w;*R+8|) z)kAUIU!>t$j-e%M1K7M=d2LN@s2z#J;GhoKbtqyiK#3rc^W~V}a-z;jVwo<)&A`xm zHGFq}d%j;EkIUO*zdh+a!2OD+lWt307vm@UX_#~Xt9pLdibFkh{Ud+uL(hq+o9!e)?CgK6#G!k6--a&u&jEI1xZ3;xr;z8sy1~r_(sk6aVhZ zUtV8*clr8v?~YB~Tzkqh0*<@=ZkkG>=kI-ofYzp4Ti+btYO5Aep1dd}ef6u~0E|F$ zzqW_k)Y2&L|Kv~J`(J!&k8u9{XTSP)zcd)<9e?!RcYpGq{mHW@pf7*^<)44O)m`(R zVcqYa>_7T&|AY5-SNkH5$OGX34Awd5 zDI$mA+S(N(K}6vcrw-=6B>lIi+R`;+Ryalkn*8S17%sP3mUq0Q?~Pc z_lt4tG>$OmA-l^A%?T)ws2YG6Dlt$>;gwcU_&AXr&b`1weIaHA%LYM(lf=*}{ z-U1Rv7a1Ldtzn{Yv$PXmz&r?vIW(AoJb(!>x;i>Jcc7j~+&#p?8-{}$fQ`GgZ01h6WUs3*l{8&Fndb4D>+#z!KY#woAHDqQ z=bNrLP?13jUhVR;7nhPoYwP3odVO5qef#Eo|7hNs^7Z@A`EuXy&)@##Z#L`dvA@Jm z{)4|UUuS!}{+s{j-&s$3ndyU%KKagPfB61~ZTjXfj$gie`^{;U-36U@7whyW494qeH+bxi~)8T4$X?gewVf|!iRn4?v}-jUsV zL><~&agf7exEds}g2im~Nz#3LE+GB{Q?^4R9V`9Xt zOZ3`XhjAbC)x~ge!J6N`K5nP`a(!Kw3S^La9&TR$`ltKnSMzR!KVR+^cODu1 z_wq0g7o)oiU}QsgqMCB%g3$xPqiaCN5O6XH?>3cU2)3;<9QwLO=en$>ttrZw)}>~% z7D^7*J5h|jq^N=In2TvHRvAVm>SVYmhzaB0V(i^ zpdAn?C|^Ww&PGX`85=`v?ubZ`R)b6eZsCZ~8L%@mum?v3K!@-K6a)ehi6X+p-Gsr+ z!2ui`ctC6cVG%(g0No=adw2$M0u8zhuiy&E0f_|3NYjY6fud6;fPfNV5Dq*eh(rfd zhfLKWLFmT+>kq%zYKP25K!RYXX##IDO>UJ_0x+x=90W!*fLx6>)M)%ujIQb$cf&^yVPsTrX{o7@qD!J#QS5TSP!k6`uyP9P}3z#?EB zP)Q1d0TPgrBya=*Z;W|=UTP9+Dmf*H-i@75!6(;bKy00_c0@u6s^|MtyIphb zcJaZ}1E0}Jy+@x`bOL1us#|x|`+ZY8WzC9Ww2%Zm6A0nWiC^c^Jp5 zQ+wDpTGtcR$T(a+f08FO?Wg7bbX;nQ^X+lf2U6=xctCh27t@|(dLl``{pQxTX6+4HA=@&Ulj!_R*Ci+}rXKl+9jK|d#A71H>=+S%zKzIVFlU;B@A6Q3{Tja}K7$w(+#X$NRWnwz`;0KW)X|miKP*;eF}d8Qo0+ zY|{oNpa|o`!|TfzGY&H;LGNA@klOi7UDI%o0@JV~!;x0zDTTr$oD!6bInjRLi;4FI zOJ46@{`w#N^zZ-Qe*Nd4)5E5S2?@(#^tFycr-$`G(V+{M6o3e9!N@sN00A3wS8aXp? zm* zK$yCbloBDpF=Yl14{~Qj?{01-Qw#$-tGHka1_ll^0jSE4%?`CrpS&Y}51)U;@Q%*+ zy?I+}vc#7T>6&!IaGnHnpsF8VwdMTqySMu%RDSeX{`#S}fNGefl*EyNMJ+Vax>fC{ zD4t3lOJX9B8{IK^Y zPez7|o#Zl4eaZFAw1d+-)S9Mgki@#+Fq*gH-Fj+gmOSR+>LL##YVY-^clYnM-rB>t zxqE}{9PU<|4)gBm)8~XbjWWz2yAPI!FTVcrB?=YF-Bl<_DU3u;SI^%|Bke+fg~d| zZ(^DuC&^ijnV8bx+MGl~03twXIpeZ;=fIrIhFvNkB*7l+7|pXSR#}@uT{g_o)5D|n z+c&%S_ZQDTVjLfCU%9n%xM7#2KF+%jZ1HTT-Ssn7a3-)thAEcduIcisP!!q#1X+so z*rmKL!~;*n`)7G@nyvS{HsYSASl>+09DsqcH6d&%v6|( zDJ4!!0we-~C>%YIBZ+kHK{>`4A+*&5Qb83R0$hr>-qxCv2~UJ-N-3!fJ03$r#^LF@ zo`PA@cr_FbPY#DGJUqK9g5#Jm`l1363|kFUCn=DJDGx#!pa{A&Y zhqbzzx6bO-gj|`}&Ao(jgbx$@ibJ6QBzGLRuh4y%DM)}QOg$qTC}&33xLZG1rfp2FsCFEeF@A4L9W3WITTF17&^B`;pP-XP(4&bM14^2VRBk` zGrT?g_)mDe;m4nPzwC})TNEB>qdntRV$hNp;#?o-^p|$`&wu`SgztQOb@l94x4hjy zs(XrNoB*?-4KC*56JyV?v2%o@1rAp^v!weFNL zxJWMEJ7Wsw0rZmK!tO|9FFLQs4H^vlCH3Y0>AZA3Z&EII1>y3^&Eax% zQm2E`>3rUZ@Vqr-@D>5YEEzEXt}b?Ldh_8knMazYuYdY)Zr>bD6?TbYD1|V>HY4tr zH}9=)UrCYUc0ZPj^?ZLmcLr09Db0kiMX#+=?=dEBcwUZ8eeJs2@2@`meroOU%g594 zouN(pi|_x>{?qZ*0Qg+20)1$M1Qg7xaV-#I;;c2^}`%yy~}kw~D7 zkV9Y{h9M0pC6vHnro0~{WgJ8<_i4w#b-O>k{^@Ui`rrTbpZ$~D-#tbL5*oBySSymv z0umM$TAXqWC9x!Ey~xOHXm8As;%Zs%$~b~yVYI=8%Ee^+%YB*TVv4*IK!nnd3J~a zaV0O{J+MFu14NAOg+W09a|g>9N_Dk)av|&s#f-L5>OhwwkC1i{0E7@kcyx4#FbZHN zcZCEI4gm^A1k}QFL_>sF42fJSYKr1W1th>NazoFE7U7&m3^N44nXm#SM}!KHPzc@0 z7)xMtWhybcAdnOPZ+`Sw4KYCnWI`e6Nut7>Y%>fr3e1MafR5}Dm`k{0sOQAqfY2?4 zAZZ@~fG`pzA_3+?z$lrCCSqqq%$W>HI-t8uVh?0HlFI7t5R{o6&>>wwRK$r*2o_Q| zoT07;nT5!6gg^*OKx0N^v^*zYvI3)9&{)KQ zSU`XqQ$Pd*5{iRvR&gBb@ySmp-TW9I-$JhFiQ0w%v5qC5ZG!yhu}3#r-r}pD{_gXa zR-O#gryq>x+ScuSroaGT?>!PQ*iu2Z)8peAlVsN`#-hCm4dTXozw{L3o z09lpp?$`720dd{}B@dV5FvzgKx!Bs74^Knn_KV-x25mDYf^a}$CbS+#6s~>S?+*JH z@8!!OG`{)yzdPNp>KHvh`))U+JYHO0=iL~+FUxYee=E3iFx@&6OUU&HAMB<*ZjXuf z9^k|bGNfH^A(AlloA*B0J-I%9`DO3(ww}89`yYSr*^j@E%ej8@yUUx)?|yP|^YpRb z&Bs)h&)=Tky?OHSlll3R>BEm@KRkVJ9-)ezBPRq=MW+EQfD@3ohdG672#1R$iCBm= zFh}h~thkdDT$@uOVsgN=QD;}jb|0-o5-vng37e$ZBoKwklb@|MvKRQh;6kcTnXBOZSmcca=6k3kFUSHe8T&We@vNE z0Qh;_Uj`7M0r?)=cy(ntQFpgf9xnln+{=?IAek6T?ztSwp>v+$LRi)w=i~2w_Lu+g z-~6k8cKc8NZN0s>4Ko=)W@1Lb0CaK|7SjMs;K1y}B}F7agr;VpzJ>xTA$Ew2gkTn_ zBy?2k06}TA+PXWs3mP$jS#%|G%A;gPCS(o>+qSK$Oqoe+i~Z=ki>vv18b{3IM1f#y zL~2MqAP8yP&AF!i%*i?kfGHsuI3oobh9dxKPmoS- z6e!^2X{R727c(3RNzSOAg;IhtrPw4oB*wBQW+cSie3;M?&^>kXJ+Otb zCBfbaHXw1_s8sYz?hipjaKnvE*fxz&Fwt6DRa@&ha%~>gs*m26U`CPzMrFuk#7twd z%@`?l6v*Hm5ekWO3M3STZf=BKllM*uQE1&f5AML+BZ+rM)mk8H%Mm(F!NG}}IyeMU zXA+0%K^zDm-W^4P04NoKFa>~|fVo;t2{WMA&0CK=gLXUH^6lBoKkhWW+*$>lt~2kR1EpMLt?-II^jm+#K) zbdjG`J-z?g_jbFCE1q9{v%j7{x;Z`k?Ct65!}b0_?-kl|^YPPX-~Zml$4{i5cbAt8 z@1hLk?xuzr>VmNbBw+A=&(x7|2@3=Q1H=I~4cb{!88|kyU_gw*=!V@^RdNee&wG{# zBC$rsOc=mY2DUq=YxIpID`0X*nzSxh8w8Je-jU5Hf}51t-9-Qj6T3-48eA7Q;-+r? zd$Fxt8*b~`qqi-%mc#qD>D}u$PdUm#U*wSh1ZL7y~U(fFz9-dqr%39hA2VF+hZU}sGtWLiUzt8p_c%P=wuRu6B05(L=0g_UXhFff~YwrWFqZBD_A!l zL2AUvv;YklC1gX&giO>h5|D6oYy#B5ldAx+S{_;3Hg_Xv7}EC}F+#K|GpecP}k30JpLYoi3-0vXUSD9Yg0&4R=<2C}FD zIRQ8kN0v0q$psT^AcaC`#1IvRNFgnI3DWK{iavUpII2S;*8m{Mz@1bmswZM442|m0 zH&p}{s1?*2puuoF)vc|Tvag#-3bUe$T@!nE5CZM3V2YiX8jQgMh9el{k*YbmI1_M! zu{bDGM(r?6ATFk?=%(bq2Lucrghk1%CiNCvG&6xQXCMy7Xox~Nf|=2XCW;WlT*8~0 zp(Hf)Ad9HcQbt$cvaK?0b$lWDJ3r|gQANscL#~T`+2q>SLn;rz#SqV5)wf^YUhQK4 zS3luj-)`pjZH&6cICxXBb>Yz;!?sgf46^Jcmrg@kZaodKw8v`w z)Tt`A7-~i6Ia3sD;Q*XV&r|k`tJ=>;$NSsm&E4a1;Wv+WRpr<=>9!b7GJ(drtPSD8V7(1Bm5Kc zC2kAkDNq=Vuo3Q~3F#>Yi7bZbEpu{I55h18K*BCMl`I#9r?iNwrIG7~g;BB*3k4D< zX_65sn6m0hBLGg=I-D1W(ds$c))6=w2Hr1t{^rYa!{zEb_U`!ZS6^bDUcG#cwEyPY zSEbl)z6zu84a|Wgc_&T-LdGrcQU>>Owzpp%zj*!f-@W|j|MKo+7g#GZfhXoX?RRX-$N+%Dkjz^IiE+8&6|@~2 zL@|K~NvypI*qHN*JdSB?(Hb&@<+PjTGR{RG5999Q*`W+RiBv)88&x|yA#~_14_6my z<$0eAa6ytOzz{n14kZz^kObQTa=_l%3{b)eu>fj9azfxh8qh07j;Q3>2`CIYLU$^H z(ac3X1UKjGFhLjL9z3u$NAy;S(E%wC{OoB@w8ihCSpY;JZ3c<4hd)vWk;TFG+YY|B}J`exW%!k~=A#AY$D7|SeB0kDZ9 z8VNE8SW{vGZ48Xwuo*ELVuWW44j;%GhKM*rP(s513jz#6s1XvOVmf4DhXFAZX~f(- zP;ft12sIOta~P-+BM^n!NW3i_CypSp2tW#2P@X;O`|o{*JL<#JyaLi=U!|b+v}Hf} z{sV0K`0ATh%gwIMSAP1YdfgD*c=ldZMnuDa74&#S@FUaQ5O>;wB;br(Iuz$@s`dC7 zRUvJGb&c`K#mY{dqec zjne-5x{SltZLMu&nnx)_4{z>U;d#H~vBYMc(wo=64Np<3p4%{^onk)Z+HFgiI->Sr z#(B76Q9stlw{K~G*&oiQ#|6aaJcYJ=$S*$rk8i*D*T;u7Me?jgfP~1o>Pf9_?Ex-_ zo=PobIBv{@BZRna~_b$?;oo7!;=?y_U%}$ ztEG$G_uo%t?#BmP&QJf^#~*$_;oVx_RvL$C7l0?$q;Q5RZiaQU?B<2ZJpft@F*0>? z*w$E-V}%$7Uo6CwA!oG4`WWbLdn_HBKuO^az*wf(wnzcNjsiv;svr# zF%2UDL0}|LKv_@{go^;u1h$|DO9o&9KyZ>dMF2Q$mOw!akJUA-u8*k+9e{RU-uL4d z-{N>Yy#J$mcU(QkjOKcYx-v-k!cX-M_m3 z=5@S#*S~ppJ|4ltB1RC0ZkC8rHUn&dS%_RkIFm?5>|qK4loDg8s`S=cH|79E0))*i zsO01T*h2>Ax>nca)WZN15phN==pC@Q5CaH!Zx)Qj*3AqVu@HuB^Y2aNFvi8j;2txj zj>(YC0b)Yl_G4pwkOp21HrWDJI=kaunk1-vUeLnw{{ z*b&u&h0VJ_SQIrA0t<7*jN*<22*Ncu00*GXks=XL!C0MGATUxV0>%J_kVt{rDHQ}o zU@VBbW86?UIyka;kRSm-5UFwmO9gjRj-a689OyIu7eD@SFa}J(kr=EwVMk)?#%O>n z(m~x>q8mv@7x2x!OJXAwwD2k#Of>ZF^CSaFDv1MAq(K~Agar+`g9Q(V5I{^p0fa@9 zfs1X>!I`OC5Uv&iL|nHZ2lD{+K7{s&WQ<_u8@8=#_bnWPTcrx#&yK~Zlu*wl;UIv9 z!Q3}VkWy9)0#rA2%mZ=lNaR{`+5ve)lnCpJiCReQK>!HhLJ$-JBqH6Kwt+a%Ga*J3 z$Kc$dLn!6}-H~xPtKlY+yX46$LqRd{@PzC^iIk8#HX$?!O#%JcO*;JOyF6qm=&{4? zAwYu*CWC&#`CVJD`0LMQ{~pHLA8kAJ2b;xv5()LwSf7HZhr0@D=x>ynI-h4=J}_wDU*^RpzLbItC+j2(M_Tpo0* zCq?o^n8%&*!@Ijym({n-Lnhj4-0K-p%1{X8e0mo!Ea%%`rX=0NnljqCoxG{bKu$<) zH(xVTN1M`UWzG31Z|mcByf|FX`&s4y93PHfzWnBE@_>}EnT3KHq6acV1P3_ixo*qC zE1jcuSK#6C{_%Y7ZMCKkUCMr#-oAM`?k^&$woQufZa!#hd;R9~`RT>}(~rg(!&*DO zn-24b-#N_7?c<$3;;`kAO9_S3LkY zjswII*0aM96(R-om6C)^;tN<}uaO~~oroocpB)D9Zk2>0yO!ayls1%u1DJ~417u9eV~Z_lX|OI^MY6uF)S|WI;OVPdy?gt`r$2Zy z-27pE^*c}oYUObKaB951J=|QB-KFa}g0l$NlIIzcCefbb;Z@wfc{o3w&iP_l^xe^y z*2&45A|xP!LAWrrurMLArc@oEj|QAMunDu7fEb661doGIM5}~IDKn?7t&TvQ4HPhu zhk;N)Gz@m$jqWZknAx3&A!$={M;?)Yr`ict&whfA29D|x!r@lCW0J7o1j)_OJgFl@4UVV? z-N?1G0QuH{9YYABr;CkELg)iJ$b5EKlwD#g1FL9pw19)VY=de_Ck3b z3|}9cj_-~7?eX|pH#}HBQ+dj%>2MHRN#*pooV##IRCXm4hpBKXZHqJp5}IjB>taM1 ztb!4h-E@%oVjK&jvm8%ac=d5<nPdw2V|UG6091h+H^YdyJgTPu()iNdrzzdCPg z+gf+5x`vwJ*nQQFHTof6TQs}O-ptkosnBM1scx|Llli!XOJve^*kAA{z!_`Ps_W@E z?_QL7?DhWU#b@ZQ+j4jN`uyhJ!jnv62~$YyuI|R<8s^{<=75n)-bkTtroBI|JsekU zUV#b>vd{bVyrj04ESE2yb?e4C)_C>pmo)6>_jY>w3fA)$P!1P8&DTCB`ASPlqnD~A$3AV+yI2E83lSWUkLzO z!ra3vHVHwwt#vTQ<-yj6$Kmo)9Nyi_j+kt`=LzOxh}vpqdc4_&zyj!n)Zo zxrLc2L@w!&GB6&5_s@41v+u8-h_6Xfm||Tm%oL#6FpxkWN{(U5x~3xWduj~OK|oYO z0M6Yxf#L6YMP_#dIaiz6%^(s3Fa}GQ18d+I=wReypikh6!qJoqhC2&l7(@%sL5>#0 z7Q&nq8X^_shEW2Q0|F9ZU|T8gAU5h1rUF}nEM^c;5oX}ZQP2ax5h4spkigXvMNjBX zJBBlN=~c~OaPnq9r@-x ztZ%^3T5QHvw-&f9o3FH4sCki~;4%!Po~D^bfsQl|Nt<<74o1%2(vZQtGY#1{$cO=s zJu?KbIpry|J5w|>CSZh4XwiYuf(RifggLr<1WIPJ;KmRD2w><*=IEvai@49%aISE5 z<=#^gBSAzrZzy)`E|?QOeG=Dy^-oe5_SZyPloEYP`nk7@oEFQ6csLBFiWlwi&F-vg z+i2v(T)|AV_u*<=Y<+0WdtuDuRgR@_*gQl70PS^aTWtmsgoi`XMyQ8f87|62iewvX zr=BzNj{4e@JudQY^T*@;`2LewEvTKh2qd)pcsk#`eM_q|%6dE{nNx=9)sd9&R5z{G ztYYGFIi%sLzI+QH31eF}#ej%;cfD;bVjJ@lPeCH%NSVj|xYO3&-M>px=IiHE$}&C2 zxJwsT*4F#w5wqlS*==>TFmnl@0_XuLGIL6#4r4JhS3^#HTVoulq~05pF#r%V!A9kWWuDFBoHAWGL=@%m(#fgth(GEU!Sz4xdmI_f`CZCsC#D&!i?c491w*Emc{m$(i>9lii8ebfdG+# zkcNF&Cnhwa#I;rR@Xg&U7m2_QoSeG#1i>r>l$#TwwrGI?Ooo9nP9sW4lwnBwX_%jm zbCERU1nX*;7Hu&cwwmI&luQ@11Lo-<r<(KO;tH3$+YWJm zd~-ZKn#OWowl-W{s*Mdo`^nZ-n-Sv3yVL2RZy}UW>NMY!DILFkOT*>%_!c~;RK{Hy z#{;`5P#HOzf*Z1yd`P?L;qKk>?$jk-eEfYG+PI&Ixt`8;?%&?N!gYh~gaQo2A^;4{FppuvTrVy^c=7D=kmTxScYQVE{kK}1ae?xDJKr9j zfBO87J{vwfFs{3+i<=M9VLV+<`9j)M7=}YyTBNF82mz;w5=Rc?0?~mWdWU6mnni98{phf32a*41=MyZQ34zo4yX!nQ`t z$cSV}dyfZ9BU^+ZQ2+)>acKmF7%|9!(Hjm*4CwA^;DBCT$seoss%xx|b+yQosOIDS z`p_3j`Mk;9Lp_~ee)#cs)`t^LLtAU$w7h$Fbveu75B!ajM^y%JU8%0WyS!+ z?xBJbu7nYq5+SsXg$cV017t)3hX4-_G;=N>W)+CHuo>M06K#MQ2zP*tQ4N7YEHQ#v z@8L+I;6NM!9fAOrGJ!iVgLhR;k%LS0ih;}!d0r!<3nv)UdOZu8shb zHTKZg3^ti~cT){C@Ub_SaEMh29acujK;1f{#{L=us3b#Tdk6+7V{i`&Z(z*95knvV z85t&|q>;l65&$JougH)G6c1V<7mPt_bIAY;=0r#djR2uSXqTqOP8jM2=3&IuLd3xx zogAT|w5{6~=L(&*ww6+lsGg$w&?=#Vf;>F*PKFK^HjIinl?lhvnFbCAH$-6QFp)O5 zpv=-7Fe4a*G$nu*NSF%f+G9X+8FBz3a(G7wR1>De-0=506--IEZox(oC~sj%pbU0Q z0zMZUwl2HAx|~P+`qkYx=kwi3o8z|f-O+vCe0U_0`z80+2Zr%_NW(NijG_WvyDa8) z!~0-@iWl=XinnPuY?pJMn4zz9T7BfL*^ zERe+d649gg4(_&vEvBJcLul*+(vnbbo*dr) zD^Pb3Ldp=%z=#GqqE=WNLJg`>fE0aQossg44WTUr0hvTfK8!L1X-Mt1-CtZ^5YO5k z2kDH%@gd#aeSP`jGL=D9_tQn$Up$^y;MBZAnX321fEv_oXjL)~R?oTARd0{IR`9yA zR@Eh?5)jdi5Hnj^5wtU))kU|JYx3FwOsoYHQeXhK6-oP}TDQaXV7BJrvY+n#u3}r@ zypb#EnM7*eWFkn6J+|JGXY?QxG9>`FG!EGVOXsq8#$hfgjY{XGmy~_;#DJjPAnhr+ z^qMpCP`G#+#JwTb@a#3H2b7H4LV>QG5Q5Ov2{mBGfxy*CIgN4Jz|Yoq2!ok2QxFiX zzO9hZTLcPh%`^~SMXXLKA^--4Ed@xNs87rtk|Aju3e;}68L0X|;7DWU2GNrUk(dw> zAQ9q_6R?GQ0u&HroT+3Q5qb}UR8ofm+c97XBuI_ACPI2f%AgTrYB*O;z%gP1L~InC5@a7HFn4r8%yU9Y zk~8M6X|zB`w>e|Zxv8Cv)LI%{ST_J99|g!rGOIydGh*oCYZr7!z{T@w4hiasgSumN zWJ2s!%smf@2?a`1R7%DK<|=I669?syB2wlJ-C%7EMH1-DNMR@ly*n6oM_xl-P~U($ zr{^g-?mA#r>9@Z;p1y7Cve=><44#Ct3JW>CL0UAWK@Pa?XBA&NaKpK3KR?#v+4%7^ z5gZ6EcuhN?Ven?SJXXpLNRYXV1NC>MoIL5n+heV+Z9AS%FJGSS9v&WTS?c|L{o>W~ zhC>c7VE5Egt;?yNm;TrvPj7p8SkIY^lCGQf(9?a5KJTtkfMvOO{-P0P^uSiGUJQcZ zd4sm1YG%j9e4$xMqCeJ$+po+Da?m`DmwO+_j0Av;fmw)idv{MbQs&6$d7zNG99ugZ zPJOGZ8x_%0R0R??wCQkhd42fcVi<4AlkY#ryT4q&dGm0;vfISC+sn=K=Z6ul$9i@D zDiK^2zszkIgLVff6XKp+aC967&JFT_!%h+g3~p{wJex^kw^&ZilB-6VNV~TTa1snq z_YlvHLyj8S)<|dx`op7YMBRE@tYe&=+nT4{i_xA8mW6oEP>3*LgeNqb9myG8b_mVE zz^glWcmyJt^3FIQNwE<-Mi9gmnlT(rAb9VpG7}#5#I!3i3bylBf}(GS6bO+*Q{uX8 zU;OeLP2i-%KGlBAs6V=eTZ16dMq@`O>I=}y(ZYi$z$voO(t*f( zLryF;(lrf}?t0E&AgIS0K4qm(WPKvm(yMkxJUa?m_HV}bL6&40hVU$rsPzaD&Cf5!I#B7iy z0&>8n*bGDhYBvcWJU6DCD8;fl6foyBB6HF}ctD&CcV1@-MIWGT(Zmx3<$yrymUaN0 zQJ}kfQ0t-QoG?ya4NN2LA_9dXMA|}Nz^SuGVoo9fK&01oLzl9Vo&$4c`JhabW zonQOIMs3+Tv2%hX9tFj=)qpZid#~2IVP6c)Fg5Mgt4Ah~sEemko=m%Zeej2btdZMN zqpe7@J}!QK)1+L~nozfVSnAfn&!^Mvuin0UynFS{tKWV3?yJ}5cY2b17;X**X6?a1 zwQc(UrwC7*W=YQUK<|DQcaMn7e3!G;0u+D((CCHT&Ed$5)C~WJhSG=8Kb6_o%s1H> zIif_W8$@HPd9AUo}=Ys@vSfA!U8uRnh^dyEf1?2q)=o3wU3l-t+y zi(h^A`7gex8M7fQ$+g`z-!Cyh)LAI1@4jc2n5vkntU^~BEB9yGSBh)BUd#Sy!@P%c zXPsz#j7WFJW%Z}b%{+l}lhK2a>3Na7KD(3Sko#triMR{#u^w-4r5u%U2x8zuc}@oT z&fv<)%A|}oQWr#$3xSf;4N1hfNSmax&q4VYp3QSp+d?oyn5hzh3&FZfN3U92Nvhc~ zF3%2PA5BIT-D3LgUw)82Bw2NkZR?QryWi__h<(^~f4Xq05M>$9wJcpkK0Q7^ul)jI z=TK}m)q-#?T7*jzArwW9pPRv3Ef8=T zz7z-rFW9pNNgBfyNfO)^U^kp*#r)b zD_A3fIG9FqV-B_F1Qd(NLSry7(Zq4Z=Y9wfpso!It){W=$tbPQJ(At{c^ zaE7K#zAI5dq*)TAWld=|xCo-q+BrPCRVS)dVWQNN1u@MsaVStrnQKl2pR^^Y00W`i zyGKtY7tLr%b>RaxBg&eskXD^rSem3Piw;p98atR#4>X?P6L}|glb`bWlkk%_zm6~L zom~FghYL@a2y>Z=3x@c`<5OQr(=mZFWR{7#%W1`n4#dUj>^!dZMyAt?n;Hu}5NO*d zV)sU?93XX7`nb3KVdRH*9nOcw$D6b2_aC3WUAI4cSpWQ^{kY{L=?A3G)!MxE?eh5D z^SgKZ!?Q)$7MW;Mksv0BRuwU3ZZnrK#&mx(y*TchgG>M6iFa({I+vUEx}^$H%N*4g zsbxOSSCI=aU;pa%?j9}{vIUYTMr>CT%6XoNebyS4A$+-- zZ622$RJ~L!`uev1iE&D@q$DNg(BfG_a-(T>)XZk6J;lX6b?g$|b09=yP^W5xug52H6{oDmgA)pN$nJ2`Nn7Pa>oyXuX4l15h7^R|tSh71| zS5Bd1yC8$dps5ja6=va-JDv^Yfa8+6a0aRQ*c)eeEVvw}CauzCbIrn8Rr-c^-+g%e z@WCS|F6;SOS)bls%Ix#)%+raNS#R#PC^=P{#P;dOPx*Y!r|zv3JuZVN10$b5UX>O!(97*ne~7+p&pl)H#94^nyuh&7GYaAl2V!SLM;Mgu;|=$ zkQavdmYUfuJ-V5Pr-+i&VxCd#64gfNjLHJX#7h;|AHO)wCSppodzo|w8cV+jH$5!DRI5mxIE4MRAG z1@n>u$dojbn@3CEofm?+9XKYAOcEp^EE4cNDPb_R41*IwlSXEOJgKDYBpSgHoLV#{ zqK;CVtSI0rzzf7K|KV5v2BBQ*6eA-WDB+nJwk03oJ9VOpPK@bDmj~F+laO;-PfWJ2 za57~CDZnC3Lr~}WSTtyUCA?AynowkFE$G&6iyFYmD@BRIkRGn!XN&`UM^$77rS#oo z!r;hrH?UtZ!bf+DvBh=8MQUEJqr;pB284+U##*X+IFn!tIo23EwHc8JP%UG#LT*Ir zkjPGskz-i25@zZnqp2c2xD@X@bAUB=_~@%jsY|Epi<=&`JlnTx(oFRl}NJ z*3R4f(qx*(Cslb4efsv#zyId)eD!O}$Y|x*NqS!E@p!y_?UxUa56{lF_wZl`)zO_w z6x32ib{^9EjtJs@m=9$hB8PtIem=WkJ{?(|%WTpgK0J?|h+S$N4nJvs0vFFTQ;7*Pp-no6nnkdj9s|X^)#%FFdEa`#!%2k0g^oaL$nj zWQwxu#I=A!h?s+57}fL9hsJKH=FdZ7L#-$i$hnHOBRE9vibldB9l?i)c-`0})Q%hzArF2~Pu z{m6pp^=GtRq!tN#H2v{UfBOB8zKSyyn#h^B&VISTY>&}I#ujAWrdfhAF)t7(*NOYe zQikt7Op3sSIm#ezEiczFu2c1OjTD1-AAC@_kXVkfZ&q8IrVJxS_5ltIbJHsIpsEw& zH0j;pc%1lj+vbI*J}B#k;!XOKQ&TD_rL;CJ(vt$x!%o<*xF%@>CB;Bl5J^TrRUYY< zRzV#)K?i{|hsTt*F%HxQ?3i{@hPXKjkHH9BQ)&mDXn%$%N^lDNLUCZ)S@%H8NM;i9 zP{2oMN`OlOoIbnvtP{bQ88jygQ@~abnH*VFkZ=zpPO;pCQ`2hSodAnKbiX3GW=v_( z(>PQ9r?3AW0ge$Y9;3?wGSXv?=UiS@f7()MI^<;+8{(EEI>^&hb;n=;s&J|UUR+8! z-OXB9$%~FGt??Gi3#KdALbMZR!k{vfcc_Df1!Mt6X5pAfjJ%}nlv6UHOXf;hi6_T! z=ja{5MzD-Eu?=kQo3rbj*AqTPOYd5kTqvM6qG(4-Pmg4-1k-d% zcGJ*|V73h?;%qoAvcCM=oA%|eI8DZTl!H^rI16*U|MBsE`|kbwytoue5lPOd2$^A5 z*J?(hv^!UGkn*0sKa(XuDspVs(^NFP^u8O|JpHM!tG63jCfIfTXcy;t5ZAi7tXYSj zS6lvcehk8O)R8E?m(zV_KOJ7&AMe+v@8P;$SBHlvxhqVhxP!+>Uj0DU&Olq`1ZR!S4f?7{_ys9+K$t4ndW6S>AM~8 zzgU(Ruio6>zy6w3X!!c+(%mB&O|AO|P^pO}SZr)}clE#jo3DTI_dogiCpy`Ok3ZV` zcYVBnJl@Fto6kS{%~vme^JbB6<9&aB(fyJGMQ}OYoepg$WhaZikxa1w(uk{SAy(Fv zLE#d8rMT`-!f?*fahD~9xwu4GBofUho;Z3ebE*=Iy!&vm4!*XellEJdLqstHDJG(5 z0ElZUr3kAdgK|$7ND%^;0E;Iv3uTai(lb-407=>)#@tApLdiUZK|{1e4ob_hp(DFn zX7ug6+um`BF|zC~PuuxI)y-z%-Na&39=ESv#=b?5%XO=SFFyZ#UNoi~xvVz_(#l#J z`}p+X{Nvl_?;fA1XJV}99(#;!FO}h_+ALjIq}23Ku@KH3l`>=KQTjIO>6XphI;m6B z4ENEAOHyZdC*D6j=S&#vdk;cBAW?;FQHf=K(=xc6Ea z(G|)J0-2444ATp4$UbQ7wao__!-oiZBr~W-+K`DI(#GyhlZ%S(MRW898_eb@Eat8r zv{Y)s%cN!)2@>A7l%!HgDyOBBvxFNb3uVFL}#XqSw=E;sw;8Q08^v2)6nRzvr&GR-_W+-ogs#%(!XE*Bbo z6f|vG-1^Kk*2}Xl^P8XiGM=A*c>7&YOl2wa($Ci(`{9=EfAO`Ae*Nyx&yVl=`f0ko znO{^{+Hv{(vVGr&yKPh@x{X(%=@cEb0mPWD0(@L?3ZZPBH#HyTodKF59nr>c|)4v_Nnip$0% z(1B6~#HEr8+_Dv-5L5yaYf=v?z(!O7qc(9Oxd`nyfY)UIjQ`!1{ea5=lHK|OJQz;+`98XfD*dTWX;^xKl;)^d-ItsM{F5IT`-tpnj z+xLI^{l|;#ID{oMLK`2d?ZLJijES@s8=JJ6VVOqEkv?3V83p_E9#o5>PA5x?&8UC~ z5k9UPg{(9p=unR%*TPjIVz*jL?t5tm_=eiX2$`2bF)y=nt7OaV;qHjjtGl~npq1KK z={)ut&>XvnM`_1aS)>Y}Oj4qcoS0lZcSs8|DzlF?4(xz;=o~vi5xR(y4@nxF8Wd3? zU04LXkwa7hPMI`nFoJerK@t!~CADXCrUG0uYR(xQ5{7h2&qm@S#s+GD02!5tt{e-+ zM#_{Bb|;WI;*R;8(p*9$BT6C|Ml^*D5{@l`3Tr}q3{jB$hoAnE#>mzvi8;WnuEWWa zM7$46EyP)omd;9pIEcjr5|eEvm3Vcc;*_Omf$%ad#fb!^6tZkZ>M>j43~CF@N_NGe z=2VayNXQ1E=h(7M5SKljiG>LYB-=H!L?n$ZNE{$XZZX_1sr?c=HT!Tq?|VR~2)ei9 zO;!UGY$~-hA|$qr0!&E~Ta-e;9(lOoan-`8q83R`$rxERisbH8-7Fnt)(9dYpd>ob zA~?DUz>@GvJP@f|GCheUsfl#YMM@!(q7*)Y;Eq&o92}lZ*sm_f0|$cI?X(`geyPV3 z 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ڵ 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 W2~9|w)0}XH4ipRvSICP30028Jj!=I9 diff --git a/packages/backend/test/resources/anime.png b/packages/backend/test/resources/anime.png deleted file mode 100644 index f13600f7a49951d0bd67cd0c8c34c06379b6a4cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1868 zcmbtVYgAKL8a?-t1Plby0#i{W6e~DGL5d2DJQ4&3k*5$!DHwx1Mz9dZ0Kq6BC}6P` zgz#!@2@)_2?-4ZU0srXfKnEi@<$kY3ItAd_7Qzg2C={{w`!ha-psAnN9eVE}aU{k%QNnX|8- z_-8zEzmNX8Tu3~1IymdV21^_Q*j+^cwm%Uq ze$6+p^L6yt6lFVf$h`WL24){dR_z$(Vl9E{g$RWQxD6Xvk6ozT0EpnMhWxAG&?pyW z3GTlXL3oh7!3TQm9_2xRA}y+SAQtGF%xw!*L^Se?N2zR$5^7seE<#gOB%E2G9z(43 z3g9#Qd-bDn&`po4+>Z- z=zQQdLc~$iL8oI)ylFKcQP0$ApV%`EG05mmoK@nU@Eo|Fj=Uwn;1#R4woLOO0`p{WM7S{9+$X97Rqn`gBse z=WIIkWo2-<+L&EwW;#IBP30c*Q_|lVVzU$I@wT{;|+u{G^ zZyr_2<(%%>1N-TP7T6LdR8s=Yc_w*kF}-w>*HINEMgl$DO0>+LnX+Z*J|w-TaFw6g zvfoPS@)PdSMsKmY!bRbV`X&)OLK-$7$V|zSa_fBKv))%5Dbr-O%Sd=Nu){MsZ{a?wE z${|&4W=>V(^8b_+b*{%J8Vc8p$d^nBC=I^n-6JM>w>|#MY7!L^mLD(D6i-!C{)n^S z%LIr0S;8EyMU?E^R$?P=+N(7<)%IHOAIEeS)HjSvs?B~-IxHQK9W&l4)71XF$~~$O zL>7OndbIn4<6H6aVx!T5l;fP&vKHS;%0ZEtH(_T_D6xMqkM_vue&{vU@`78Pw#lT1 zyE`&LeyYq$&Us)p$9T7TJcgO^D7J!f$I)#-H+Vf}+(^GUJML&R@hgwaVn-sU?xw*^ zTmSn34-zM6Ez78-@Kw6ugYm@g>lXa4K0YZQTV+;H6L>|euzgV%#_O=I@*naJp7bF$ zwKpy5>p%9Xb^fqvqCGOZqR-0RKqZKSB$xG&!9Dd2bP@uH^04F zdzIxWJ~7~VWs0Md-%erJ`4uo$jtnKq54(&SO;$0JP2@jEJBU&z8e8{fg?z;M$hkYl z|8n6Nz5ZDT!RyLW#ibe2TYdYE$l^$JI8Nn4hR zKV9nhy*PHY;#o%Vd*@%BbZSXvrcX0w(`}v@_AGGU?6muFQ<?e^tmoclp68d>s~$q!dw|;$oSo;3E%L|q-#r2&031^xFBbxUXrPj zZbF3_=lNFu=9D65dv}`8XK7)ygfvFXxp?4-yp)!nbb5U9=`Z5XSvuM~_bopD5|IHv MpCIp+qtr{k0MoP{oB#j- diff --git a/packages/backend/test/resources/emptyfile b/packages/backend/test/resources/emptyfile deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/backend/test/resources/image.svg b/packages/backend/test/resources/image.svg deleted file mode 100644 index 1e2bf5b5bb..0000000000 --- a/packages/backend/test/resources/image.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/backend/test/resources/rotate.jpg b/packages/backend/test/resources/rotate.jpg deleted file mode 100644 index 477c2baf5bbd0039dc08fa36e3d4b3cd5d39ecb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12624 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 diff --git a/packages/backend/test/resources/with-alpha.png b/packages/backend/test/resources/with-alpha.png deleted file mode 100644 index adc8d01805dbcf1bf38f506096b99bc157f25de5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3772 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 Gh5idy2NOjA diff --git a/packages/backend/test/resources/with-xml-def.svg b/packages/backend/test/resources/with-xml-def.svg deleted file mode 100644 index 90971215a6..0000000000 --- a/packages/backend/test/resources/with-xml-def.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.ts deleted file mode 100644 index 3292c66e17..0000000000 --- a/packages/backend/test/streaming.ts +++ /dev/null @@ -1,766 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import { Following } from "../src/models/entities/following.js"; -import { - connectStream, - signup, - api, - post, - startServer, - shutdownServer, - initTestDb, - waitFire, -} from "./utils.js"; - -describe("Streaming", () => { - let p: childProcess.ChildProcess; - let Followings: any; - - const follow = async (follower: any, followee: any) => { - await Followings.save({ - id: "a", - createdAt: new Date(), - followerId: follower.id, - followeeId: followee.id, - followerHost: follower.host, - followerInbox: null, - followerSharedInbox: null, - followeeHost: followee.host, - followeeInbox: null, - followeeSharedInbox: null, - }); - }; - - describe("Streaming", () => { - // Local users - let ayano: any; - let kyoko: any; - let chitose: any; - - // Remote users - let akari: any; - let chinatsu: any; - - let kyokoNote: any; - let list: any; - - before(async () => { - p = await startServer(); - const connection = await initTestDb(true); - Followings = connection.getRepository(Following); - - ayano = await signup({ username: "ayano" }); - kyoko = await signup({ username: "kyoko" }); - chitose = await signup({ username: "chitose" }); - - akari = await signup({ username: "akari", host: "example.com" }); - chinatsu = await signup({ username: "chinatsu", host: "example.com" }); - - kyokoNote = await post(kyoko, { text: "foo" }); - - // Follow: ayano => kyoko - await api("following/create", { userId: kyoko.id }, ayano); - - // Follow: ayano => akari - await follow(ayano, akari); - - // List: chitose => ayano, kyoko - list = await api( - "users/lists/create", - { - name: "my list", - }, - chitose, - ).then((x) => x.body); - - await api( - "users/lists/push", - { - listId: list.id, - userId: ayano.id, - }, - chitose, - ); - - await api( - "users/lists/push", - { - listId: list.id, - userId: kyoko.id, - }, - chitose, - ); - }); - - after(async () => { - await shutdownServer(p); - }); - - describe("Events", () => { - it("mention event", async () => { - const fired = await waitFire( - kyoko, - "main", // kyoko:main - () => post(ayano, { text: "foo @kyoko bar" }), // ayano mention => kyoko - (msg) => msg.type === "mention" && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, true); - }); - - it("renote event", async () => { - const fired = await waitFire( - kyoko, - "main", // kyoko:main - () => post(ayano, { renoteId: kyokoNote.id }), // ayano renote - (msg) => msg.type === "renote" && msg.body.renoteId === kyokoNote.id, // wait renote - ); - - assert.strictEqual(fired, true); - }); - }); - - describe("Home Timeline", () => { - it("自分の投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "homeTimeline", // ayano:Home - () => api("notes/create", { text: "foo" }, ayano), // ayano posts - (msg) => msg.type === "note" && msg.body.text === "foo", - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしているユーザーの投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "homeTimeline", // ayano:home - () => api("notes/create", { text: "foo" }, kyoko), // kyoko posts - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしていないユーザーの投稿は流れない", async () => { - const fired = await waitFire( - kyoko, - "homeTimeline", // kyoko:home - () => api("notes/create", { text: "foo" }, ayano), // ayano posts - (msg) => msg.type === "note" && msg.body.userId === ayano.id, // wait ayano - ); - - assert.strictEqual(fired, false); - }); - - it("フォローしているユーザーのダイレクト投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "homeTimeline", // ayano:home - () => - api( - "notes/create", - { - text: "foo", - visibility: "specified", - visibleUserIds: [ayano.id], - }, - kyoko, - ), // kyoko dm => ayano - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "homeTimeline", // ayano:home - () => - api( - "notes/create", - { - text: "foo", - visibility: "specified", - visibleUserIds: [chitose.id], - }, - kyoko, - ), // kyoko dm => chitose - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - }); // Home - - describe("Local Timeline", () => { - it("自分の投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "localTimeline", // ayano:Local - () => api("notes/create", { text: "foo" }, ayano), // ayano posts - (msg) => msg.type === "note" && msg.body.text === "foo", - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしていないローカルユーザーの投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "localTimeline", // ayano:Local - () => api("notes/create", { text: "foo" }, chitose), // chitose posts - (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - it("リモートユーザーの投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "localTimeline", // ayano:Local - () => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts - (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu - ); - - assert.strictEqual(fired, false); - }); - - it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "localTimeline", // ayano:Local - () => api("notes/create", { text: "foo" }, akari), // akari posts - (msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari - ); - - assert.strictEqual(fired, false); - }); - - it("ホーム指定の投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "localTimeline", // ayano:Local - () => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "localTimeline", // ayano:Local - () => - api( - "notes/create", - { - text: "foo", - visibility: "specified", - visibleUserIds: [ayano.id], - }, - kyoko, - ), // kyoko DM => ayano - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "localTimeline", // ayano:Local - () => - api( - "notes/create", - { text: "foo", visibility: "followers" }, - chitose, - ), - (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, false); - }); - }); - - describe("Recommended Timeline", () => { - it("自分の投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "recommendedTimeline", // ayano:Local - () => api("notes/create", { text: "foo" }, ayano), // ayano posts - (msg) => msg.type === "note" && msg.body.text === "foo", - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしていないローカルユーザーの投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "recommendedTimeline", // ayano:Local - () => api("notes/create", { text: "foo" }, chitose), // chitose posts - (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - it("リモートユーザーの投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "recommendedTimeline", // ayano:Local - () => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts - (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu - ); - - assert.strictEqual(fired, false); - }); - - it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "recommendedTimeline", // ayano:Local - () => api("notes/create", { text: "foo" }, akari), // akari posts - (msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari - ); - - assert.strictEqual(fired, false); - }); - - it("ホーム指定の投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "recommendedTimeline", // ayano:Local - () => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "recommendedTimeline", // ayano:Local - () => - api( - "notes/create", - { - text: "foo", - visibility: "specified", - visibleUserIds: [ayano.id], - }, - kyoko, - ), // kyoko DM => ayano - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - - it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "recommendedTimeline", // ayano:Local - () => - api( - "notes/create", - { text: "foo", visibility: "followers" }, - chitose, - ), - (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, false); - }); - }); - - describe("Hybrid Timeline", () => { - it("自分の投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "hybridTimeline", // ayano:Hybrid - () => api("notes/create", { text: "foo" }, ayano), // ayano posts - (msg) => msg.type === "note" && msg.body.text === "foo", - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしていないローカルユーザーの投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "hybridTimeline", // ayano:Hybrid - () => api("notes/create", { text: "foo" }, chitose), // chitose posts - (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしているリモートユーザーの投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "hybridTimeline", // ayano:Hybrid - () => api("notes/create", { text: "foo" }, akari), // akari posts - (msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしていないリモートユーザーの投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "hybridTimeline", // ayano:Hybrid - () => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts - (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu - ); - - assert.strictEqual(fired, false); - }); - - it("フォローしているユーザーのダイレクト投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "hybridTimeline", // ayano:Hybrid - () => - api( - "notes/create", - { - text: "foo", - visibility: "specified", - visibleUserIds: [ayano.id], - }, - kyoko, - ), - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしているユーザーのホーム投稿が流れる", async () => { - const fired = await waitFire( - ayano, - "hybridTimeline", // ayano:Hybrid - () => api("notes/create", { text: "foo", visibility: "home" }, kyoko), - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしていないローカルユーザーのホーム投稿は流れない", async () => { - const fired = await waitFire( - ayano, - "hybridTimeline", // ayano:Hybrid - () => - api("notes/create", { text: "foo", visibility: "home" }, chitose), - (msg) => msg.type === "note" && msg.body.userId === chitose.id, - ); - - assert.strictEqual(fired, false); - }); - - it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", () => - async () => { - const fired = await waitFire( - ayano, - "hybridTimeline", // ayano:Hybrid - () => - api( - "notes/create", - { text: "foo", visibility: "followers" }, - chitose, - ), - (msg) => msg.type === "note" && msg.body.userId === chitose.id, - ); - - assert.strictEqual(fired, false); - }); - }); - - describe("Global Timeline", () => { - it("フォローしていないローカルユーザーの投稿が流れる", () => async () => { - const fired = await waitFire( - ayano, - "globalTimeline", // ayano:Global - () => api("notes/create", { text: "foo" }, chitose), // chitose posts - (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose - ); - - assert.strictEqual(fired, true); - }); - - it("フォローしていないリモートユーザーの投稿が流れる", () => async () => { - const fired = await waitFire( - ayano, - "globalTimeline", // ayano:Global - () => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts - (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu - ); - - assert.strictEqual(fired, true); - }); - - it("ホーム投稿は流れない", () => async () => { - const fired = await waitFire( - ayano, - "globalTimeline", // ayano:Global - () => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko posts - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko - ); - - assert.strictEqual(fired, false); - }); - }); - - describe("UserList Timeline", () => { - it("リストに入れているユーザーの投稿が流れる", () => async () => { - const fired = await waitFire( - chitose, - "userList", - () => api("notes/create", { text: "foo" }, ayano), - (msg) => msg.type === "note" && msg.body.userId === ayano.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, true); - }); - - it("リストに入れていないユーザーの投稿は流れない", () => async () => { - const fired = await waitFire( - chitose, - "userList", - () => api("notes/create", { text: "foo" }, chinatsu), - (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - - // #4471 - it("リストに入れているユーザーのダイレクト投稿が流れる", () => - async () => { - const fired = await waitFire( - chitose, - "userList", - () => - api( - "notes/create", - { - text: "foo", - visibility: "specified", - visibleUserIds: [chitose.id], - }, - ayano, - ), - (msg) => msg.type === "note" && msg.body.userId === ayano.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, true); - }); - - // #4335 - it("リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない", () => - async () => { - const fired = await waitFire( - chitose, - "userList", - () => - api( - "notes/create", - { text: "foo", visibility: "followers" }, - kyoko, - ), - (msg) => msg.type === "note" && msg.body.userId === kyoko.id, - { listId: list.id }, - ); - - assert.strictEqual(fired, false); - }); - }); - - describe("Hashtag Timeline", () => { - it("指定したハッシュタグの投稿が流れる", () => - new Promise(async (done) => { - const ws = await connectStream( - chitose, - "hashtag", - ({ type, body }) => { - if (type == "note") { - assert.deepStrictEqual(body.text, "#foo"); - ws.close(); - done(); - } - }, - { - q: [["foo"]], - }, - ); - - post(chitose, { - text: "#foo", - }); - })); - - it("指定したハッシュタグの投稿が流れる (AND)", () => - new Promise(async (done) => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - - const ws = await connectStream( - chitose, - "hashtag", - ({ type, body }) => { - if (type == "note") { - if (body.text === "#foo") fooCount++; - if (body.text === "#bar") barCount++; - if (body.text === "#foo #bar") fooBarCount++; - } - }, - { - q: [["foo", "bar"]], - }, - ); - - post(chitose, { - text: "#foo", - }); - - post(chitose, { - text: "#bar", - }); - - post(chitose, { - text: "#foo #bar", - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 0); - assert.strictEqual(barCount, 0); - assert.strictEqual(fooBarCount, 1); - ws.close(); - done(); - }, 3000); - })); - - it("指定したハッシュタグの投稿が流れる (OR)", () => - new Promise(async (done) => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - let piyoCount = 0; - - const ws = await connectStream( - chitose, - "hashtag", - ({ type, body }) => { - if (type == "note") { - if (body.text === "#foo") fooCount++; - if (body.text === "#bar") barCount++; - if (body.text === "#foo #bar") fooBarCount++; - if (body.text === "#piyo") piyoCount++; - } - }, - { - q: [["foo"], ["bar"]], - }, - ); - - post(chitose, { - text: "#foo", - }); - - post(chitose, { - text: "#bar", - }); - - post(chitose, { - text: "#foo #bar", - }); - - post(chitose, { - text: "#piyo", - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 1); - assert.strictEqual(barCount, 1); - assert.strictEqual(fooBarCount, 1); - assert.strictEqual(piyoCount, 0); - ws.close(); - done(); - }, 3000); - })); - - it("指定したハッシュタグの投稿が流れる (AND + OR)", () => - new Promise(async (done) => { - let fooCount = 0; - let barCount = 0; - let fooBarCount = 0; - let piyoCount = 0; - let waaaCount = 0; - - const ws = await connectStream( - chitose, - "hashtag", - ({ type, body }) => { - if (type == "note") { - if (body.text === "#foo") fooCount++; - if (body.text === "#bar") barCount++; - if (body.text === "#foo #bar") fooBarCount++; - if (body.text === "#piyo") piyoCount++; - if (body.text === "#waaa") waaaCount++; - } - }, - { - q: [["foo", "bar"], ["piyo"]], - }, - ); - - post(chitose, { - text: "#foo", - }); - - post(chitose, { - text: "#bar", - }); - - post(chitose, { - text: "#foo #bar", - }); - - post(chitose, { - text: "#piyo", - }); - - post(chitose, { - text: "#waaa", - }); - - setTimeout(() => { - assert.strictEqual(fooCount, 0); - assert.strictEqual(barCount, 0); - assert.strictEqual(fooBarCount, 1); - assert.strictEqual(piyoCount, 1); - assert.strictEqual(waaaCount, 0); - ws.close(); - done(); - }, 3000); - })); - }); - }); -}); diff --git a/packages/backend/test/thread-mute.ts b/packages/backend/test/thread-mute.ts deleted file mode 100644 index 9b3bb8dfe4..0000000000 --- a/packages/backend/test/thread-mute.ts +++ /dev/null @@ -1,161 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import { - async, - signup, - request, - post, - react, - connectStream, - startServer, - shutdownServer, -} from "./utils.js"; - -describe("Note thread mute", () => { - let p: childProcess.ChildProcess; - - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: "alice" }); - bob = await signup({ username: "bob" }); - carol = await signup({ username: "carol" }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it("notes/mentions にミュートしているスレッドの投稿が含まれない", async(async () => { - const bobNote = await post(bob, { text: "@alice @carol root note" }); - const aliceReply = await post(alice, { - replyId: bobNote.id, - text: "@bob @carol child note", - }); - - await request("/notes/thread-muting/create", { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { - replyId: bobNote.id, - text: "@bob @alice child note", - }); - const carolReplyWithoutMention = await post(carol, { - replyId: aliceReply.id, - text: "child note", - }); - - const res = await request("/notes/mentions", {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual( - res.body.some((note: any) => note.id === bobNote.id), - false, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === carolReply.id), - false, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === carolReplyWithoutMention.id), - false, - ); - })); - - it("ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない", async(async () => { - // 状態リセット - await request("/i/read-all-unread-notes", {}, alice); - - const bobNote = await post(bob, { text: "@alice @carol root note" }); - - await request("/notes/thread-muting/create", { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { - replyId: bobNote.id, - text: "@bob @alice child note", - }); - - const res = await request("/i", {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.hasUnreadMentions, false); - })); - - it("ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない", () => - new Promise(async (done) => { - // 状態リセット - await request("/i/read-all-unread-notes", {}, alice); - - const bobNote = await post(bob, { text: "@alice @carol root note" }); - - await request( - "/notes/thread-muting/create", - { noteId: bobNote.id }, - alice, - ); - - let fired = false; - - const ws = await connectStream(alice, "main", async ({ type, body }) => { - if (type === "unreadMention") { - if (body === bobNote.id) return; - fired = true; - } - }); - - const carolReply = await post(carol, { - replyId: bobNote.id, - text: "@bob @alice child note", - }); - - setTimeout(() => { - assert.strictEqual(fired, false); - ws.close(); - done(); - }, 5000); - })); - - it("i/notifications にミュートしているスレッドの通知が含まれない", async(async () => { - const bobNote = await post(bob, { text: "@alice @carol root note" }); - const aliceReply = await post(alice, { - replyId: bobNote.id, - text: "@bob @carol child note", - }); - - await request("/notes/thread-muting/create", { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { - replyId: bobNote.id, - text: "@bob @alice child note", - }); - const carolReplyWithoutMention = await post(carol, { - replyId: aliceReply.id, - text: "child note", - }); - - const res = await request("/i/notifications", {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual( - res.body.some( - (notification: any) => notification.note.id === carolReply.id, - ), - false, - ); - assert.strictEqual( - res.body.some( - (notification: any) => - notification.note.id === carolReplyWithoutMention.id, - ), - false, - ); - - // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい - })); -}); diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json deleted file mode 100644 index bc7a9968b5..0000000000 --- a/packages/backend/test/tsconfig.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "noEmitOnError": false, - "noImplicitAny": true, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": true, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": true, - "target": "es2021", - "module": "es2020", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": true, - "strictPropertyInitialization": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "isolatedModules": true, - "baseUrl": "./", - "paths": { - "@/*": ["../src/*"] - }, - "typeRoots": [ - "../node_modules/@types", - "../src/@types" - ], - "lib": [ - "esnext" - ] - }, - "compileOnSave": false, - "include": [ - "./**/*.ts" - ] -} diff --git a/packages/backend/test/user-notes.ts b/packages/backend/test/user-notes.ts deleted file mode 100644 index 86a541c101..0000000000 --- a/packages/backend/test/user-notes.ts +++ /dev/null @@ -1,98 +0,0 @@ -process.env.NODE_ENV = "test"; - -import * as assert from "assert"; -import * as childProcess from "child_process"; -import { - async, - signup, - request, - post, - uploadUrl, - startServer, - shutdownServer, -} from "./utils.js"; - -describe("users/notes", () => { - let p: childProcess.ChildProcess; - - let alice: any; - let jpgNote: any; - let pngNote: any; - let jpgPngNote: any; - - before(async () => { - p = await startServer(); - alice = await signup({ username: "alice" }); - const jpg = await uploadUrl( - alice, - "https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg", - ); - const png = await uploadUrl( - alice, - "https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png", - ); - jpgNote = await post(alice, { - fileIds: [jpg.id], - }); - pngNote = await post(alice, { - fileIds: [png.id], - }); - jpgPngNote = await post(alice, { - fileIds: [jpg.id, png.id], - }); - }); - - after(async () => { - await shutdownServer(p); - }); - - it("ファイルタイプ指定 (jpg)", async(async () => { - const res = await request( - "/users/notes", - { - userId: alice.id, - fileType: ["image/jpeg"], - }, - alice, - ); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 2); - assert.strictEqual( - res.body.some((note: any) => note.id === jpgNote.id), - true, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === jpgPngNote.id), - true, - ); - })); - - it("ファイルタイプ指定 (jpg or png)", async(async () => { - const res = await request( - "/users/notes", - { - userId: alice.id, - fileType: ["image/jpeg", "image/png"], - }, - alice, - ); - - assert.strictEqual(res.status, 200); - assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.length, 3); - assert.strictEqual( - res.body.some((note: any) => note.id === jpgNote.id), - true, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === pngNote.id), - true, - ); - assert.strictEqual( - res.body.some((note: any) => note.id === jpgPngNote.id), - true, - ); - })); -}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts deleted file mode 100644 index f3f68b2609..0000000000 --- a/packages/backend/test/utils.ts +++ /dev/null @@ -1,403 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; -import * as childProcess from "child_process"; -import * as http from "node:http"; -import { SIGKILL } from "constants"; -import WebSocket from "ws"; -import * as misskey from "calckey-js"; -import fetch from "node-fetch"; -import FormData from "form-data"; -import { DataSource } from "typeorm"; -import loadConfig from "../src/config/load.js"; -import { entities } from "../src/db/postgre.js"; -import got from "got"; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const config = loadConfig(); -export const port = config.port; - -export const async = (fn: Function) => (done: Function) => { - fn().then( - () => { - done(); - }, - (err: Error) => { - done(err); - }, - ); -}; - -export const api = async (endpoint: string, params: any, me?: any) => { - endpoint = endpoint.replace(/^\//, ""); - - const auth = me - ? { - i: me.token, - } - : {}; - - const res = await got(`http://localhost:${port}/api/${endpoint}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(Object.assign(auth, params)), - retry: { - limit: 0, - }, - hooks: { - beforeError: [ - (error) => { - const { response } = error; - if (response && response.body) console.warn(response.body); - return error; - }, - ], - }, - }); - - const status = res.statusCode; - const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; - - return { - status, - body, - }; -}; - -export const request = async ( - endpoint: string, - params: any, - me?: any, -): Promise<{ body: any; status: number }> => { - const auth = me - ? { - i: me.token, - } - : {}; - - const res = await fetch(`http://localhost:${port}/api${endpoint}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(Object.assign(auth, params)), - }); - - const status = res.status; - const body = res.status !== 204 ? await res.json().catch() : null; - - return { - body, - status, - }; -}; - -export const signup = async (params?: any): Promise => { - const q = Object.assign( - { - username: "test", - password: "test", - }, - params, - ); - - const res = await api("signup", q); - - return res.body; -}; - -export const post = async ( - user: any, - params?: misskey.Endpoints["notes/create"]["req"], -): Promise => { - const q = Object.assign( - { - text: "test", - }, - params, - ); - - const res = await api("notes/create", q, user); - - return res.body ? res.body.createdNote : null; -}; - -export const react = async ( - user: any, - note: any, - reaction: string, -): Promise => { - await api( - "notes/reactions/create", - { - noteId: note.id, - reaction: reaction, - }, - user, - ); -}; - -/** - * Upload file - * @param user User - * @param _path Optional, absolute path or relative from ./resources/ - */ -export const uploadFile = async (user: any, _path?: string): Promise => { - const absPath = - _path == null - ? `${_dirname}/resources/Lenna.jpg` - : path.isAbsolute(_path) - ? _path - : `${_dirname}/resources/${_path}`; - - const formData = new FormData() as any; - formData.append("i", user.token); - formData.append("file", fs.createReadStream(absPath)); - formData.append("force", "true"); - - const res = await got( - `http://localhost:${port}/api/drive/files/create`, - { - method: "POST", - body: formData, - retry: { - limit: 0, - }, - }, - ); - - const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null; - - return body; -}; - -export const uploadUrl = async (user: any, url: string) => { - let file: any; - - const ws = await connectStream(user, "main", (msg) => { - if (msg.type === "driveFileCreated") { - file = msg.body; - } - }); - - await api( - "drive/files/upload-from-url", - { - url, - force: true, - }, - user, - ); - - await sleep(5000); - ws.close(); - - return file; -}; - -export function connectStream( - user: any, - channel: string, - listener: (message: Record) => any, - params?: any, -): Promise { - return new Promise((res, rej) => { - const ws = new WebSocket( - `ws://localhost:${port}/streaming?i=${user.token}`, - ); - - ws.on("open", () => { - ws.on("message", (data) => { - const msg = JSON.parse(data.toString()); - if (msg.type === "channel" && msg.body.id === "a") { - listener(msg.body); - } else if (msg.type === "connected" && msg.body.id === "a") { - res(ws); - } - }); - - ws.send( - JSON.stringify({ - type: "connect", - body: { - channel: channel, - id: "a", - pong: true, - params: params, - }, - }), - ); - }); - }); -} - -export const waitFire = async ( - user: any, - channel: string, - trgr: () => any, - cond: (msg: Record) => boolean, - params?: any, -) => { - return new Promise(async (res, rej) => { - let timer: NodeJS.Timeout; - - let ws: WebSocket; - try { - ws = await connectStream( - user, - channel, - (msg) => { - if (cond(msg)) { - ws.close(); - if (timer) clearTimeout(timer); - res(true); - } - }, - params, - ); - } catch (e) { - rej(e); - } - - if (!ws!) return; - - timer = setTimeout(() => { - ws.close(); - res(false); - }, 3000); - - try { - await trgr(); - } catch (e) { - ws.close(); - if (timer) clearTimeout(timer); - rej(e); - } - }); -}; - -export const simpleGet = async ( - path: string, - accept = "*/*", -): Promise<{ status?: number; type?: string; location?: string }> => { - // node-fetchだと3xxを取れない - return await new Promise((resolve, reject) => { - const req = http.request( - `http://localhost:${port}${path}`, - { - headers: { - Accept: accept, - }, - }, - (res) => { - if (res.statusCode! >= 400) { - reject(res); - } else { - resolve({ - status: res.statusCode, - type: res.headers["content-type"], - location: res.headers.location, - }); - } - }, - ); - - req.end(); - }); -}; - -export function launchServer( - callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, - moreProcess: () => Promise = async () => {}, -) { - return (done: (err?: Error) => any) => { - const p = childProcess.spawn("node", [_dirname + "/../index.js"], { - stdio: ["inherit", "inherit", "inherit", "ipc"], - env: { NODE_ENV: "test", PATH: process.env.PATH }, - }); - callbackSpawnedProcess(p); - p.on("message", (message) => { - if (message === "ok") - moreProcess() - .then(() => done()) - .catch((e) => done(e)); - }); - }; -} - -export async function initTestDb(justBorrow = false, initEntities?: any[]) { - if (process.env.NODE_ENV !== "test") throw "NODE_ENV is not a test"; - - const db = new DataSource({ - type: "postgres", - host: config.db.host, - port: config.db.port, - username: config.db.user, - password: config.db.pass, - database: config.db.db, - synchronize: true && !justBorrow, - dropSchema: true && !justBorrow, - entities: initEntities || entities, - }); - - await db.initialize(); - - return db; -} - -export function startServer( - timeout = 60 * 1000, -): Promise { - return new Promise((res, rej) => { - const t = setTimeout(() => { - p.kill(SIGKILL); - rej("timeout to start"); - }, timeout); - - const p = childProcess.spawn("node", [_dirname + "/../built/index.js"], { - stdio: ["inherit", "inherit", "inherit", "ipc"], - env: { NODE_ENV: "test", PATH: process.env.PATH }, - }); - - p.on("error", (e) => rej(e)); - - p.on("message", (message) => { - if (message === "ok") { - clearTimeout(t); - res(p); - } - }); - }); -} - -export function shutdownServer( - p: childProcess.ChildProcess, - timeout = 20 * 1000, -) { - return new Promise((res, rej) => { - const t = setTimeout(() => { - p.kill(SIGKILL); - res("force exit"); - }, timeout); - - p.once("exit", () => { - clearTimeout(t); - res("exited"); - }); - - p.kill(); - }); -} - -export function sleep(msec: number) { - return new Promise((res) => { - setTimeout(() => { - res(); - }, msec); - }); -} diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json deleted file mode 100644 index 692d7b95b7..0000000000 --- a/packages/backend/tsconfig.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "noEmitOnError": false, - "noImplicitAny": true, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": false, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": false, - "target": "es2021", - "module": "es2020", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": true, - "strictPropertyInitialization": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "isolatedModules": false, - "rootDir": "./src", - "baseUrl": "./", - "paths": { - "@/*": [ - "./src/*" - ] - }, - "outDir": "./built", - "types": [ - "node" - ], - "typeRoots": [ - "./node_modules/@types", - "./src/@types" - ], - "lib": [ - "esnext" - ] - }, - "compileOnSave": false, - "include": [ - "./src/**/*.ts" - ], -}