From 28c1cf1e21103ec821ebe08de8adaa6557efec65 Mon Sep 17 00:00:00 2001 From: Michcio Date: Mon, 16 Jan 2023 20:28:38 +0100 Subject: [PATCH] having the time of my life --- .gitignore | 1 + gulpfile.js | 14 +- locales/en-US.json | 1314 ++++ package.json | 1 + packages/backend/.eslintignore | 4 - packages/backend/.eslintrc.cjs | 36 - packages/backend/.mocharc.json | 10 - packages/backend/.vscode/settings.json | 10 - packages/backend/jsconfig.json | 13 - .../backend/migration/1000000000000-Init.js | 482 -- .../backend/migration/1556348509290-Pages.js | 28 - .../migration/1556746559567-UserProfile.js | 13 - .../migration/1557476068003-PinnedUsers.js | 10 - .../migration/1557761316509-AddSomeUrls.js | 14 - .../1557932705754-ObjectStorageSetting.js | 28 - .../migration/1558072954435-PageLike.js | 20 - .../migration/1558103093633-UserGroup.js | 38 - .../1558257926829-UserGroupInvite.js | 22 - .../1558266512381-UserListJoining.js | 10 - .../migration/1561706992953-webauthn.js | 26 - .../migration/1561873850023-ChartIndexes.js | 198 - .../1562422242907-PasswordLessLogin.js | 10 - .../migration/1562444565093-PinnedPage.js | 14 - .../1562448332510-PageTitleHideOption.js | 10 - .../migration/1562869971568-ModerationLog.js | 14 - .../migration/1563757595828-UsedUsername.js | 10 - .../backend/migration/1565634203341-room.js | 10 - .../1571220798684-CustomEmojiCategory.js | 10 - .../migration/1572760203493-nodeinfo.js | 26 - .../1576269851876-TalkFederationId.js | 13 - .../1576869585998-ProxyRemoteFiles.js | 13 - .../backend/migration/1579267006611-v12.js | 33 - .../backend/migration/1579270193251-v12-2.js | 13 - .../backend/migration/1579282808087-v12-3.js | 13 - .../backend/migration/1579544426412-v12-4.js | 15 - .../backend/migration/1579977526288-v12-5.js | 53 - .../backend/migration/1579993013959-v12-6.js | 17 - .../backend/migration/1580069531114-v12-7.js | 23 - .../backend/migration/1580148575182-v12-8.js | 15 - .../backend/migration/1580154400017-v12-9.js | 13 - .../backend/migration/1580276619901-v12-10.js | 18 - .../backend/migration/1580331224276-v12-11.js | 17 - .../backend/migration/1580508795118-v12-12.js | 45 - .../backend/migration/1580543501339-v12-13.js | 13 - .../backend/migration/1580864313253-v12-14.js | 19 - .../1581526429287-user-group-invitation.js | 37 - .../1581695816408-user-group-antenna.js | 27 - ...581708415836-drive-user-folder-id-index.js | 13 - .../backend/migration/1581979837262-promo.js | 27 - .../1582019042083-featured-injecttion.js | 13 - .../1582210532752-antenna-exclude.js | 13 - .../1582875306439-note-reaction-length.js | 13 - .../backend/migration/1585361548360-miauth.js | 35 - .../1585385921215-custom-notification.js | 47 - .../backend/migration/1585772678853-ap-url.js | 13 - .../1586624197029-AddObjectStorageUseProxy.js | 13 - .../1586641139527-remote-reaction.js | 13 - .../migration/1586708940386-pageAiScript.js | 13 - .../migration/1588044505511-hCaptcha.js | 17 - .../migration/1589023282116-pubRelay.js | 17 - .../migration/1595075960584-blurhash.js | 13 - ...595077605646-blurhash-for-avatar-banner.js | 19 - .../1595676934834-instance-icon-url.js | 13 - .../migration/1595771249699-word-mute.js | 29 - .../migration/1595782306083-word-mute2.js | 17 - .../migration/1596548170836-channel.js | 57 - .../migration/1596786425167-channel2.js | 13 - ...597230137744-objectStorageSetPublicRead.js | 13 - ...597236229720-IncludingNotificationTypes.js | 15 - .../1597385880794-add-sensitive-index.js | 13 - .../migration/1597459042300-channel-unread.js | 26 - .../1597893996136-ChannelNoteIdDescIndex.js | 15 - .../1600353287890-mutingNotificationTypes.js | 19 - .../1603094348345-refine-abuse-user-report.js | 31 - ...1603095701770-refine-abuse-user-report2.js | 19 - .../1603776877564-instance-theme-color.js | 13 - .../1603781553011-instance-favicon.js | 13 - .../1604821689616-delete-auto-watch.js | 13 - .../1605408848373-clip-description.js | 13 - .../migration/1605408971051-comments.js | 433 -- .../1605585339718-instance-pinned-pages.js | 13 - .../1605965516823-instance-images.js | 15 - .../migration/1606191203881-no-crawle.js | 15 - .../1607151207216-instance-pinned-clip.js | 13 - .../migration/1607353487793-isExplorable.js | 17 - .../migration/1610277136869-registry.js | 21 - .../migration/1610277585759-registry2.js | 15 - .../migration/1610283021566-registry3.js | 13 - .../migration/1611354329133-followersUri.js | 15 - .../migration/1611397665007-gallery.js | 39 - ...547387175-objectStorageS3ForcePathStyle.js | 13 - .../1612619156584-announcement-email.js | 13 - .../1613155914446-emailNotificationTypes.js | 13 - .../migration/1613181457597-user-lang.js | 13 - ...1613503367223-use-bigint-for-driveUsage.js | 14 - .../migration/1615965918224-chart-v2.js | 216 - .../migration/1615966519402-chart-v2-2.js | 21 - .../1618637372000-user-last-active-date.js | 15 - .../1618639857000-user-hide-online-status.js | 13 - .../migration/1619942102890-password-reset.js | 19 - .../backend/migration/1620019354680-ad.js | 17 - .../backend/migration/1620364649428-ad2.js | 13 - .../1621479946000-add-note-indexes.js | 15 - ...9304522-user-profile-description-length.js | 13 - .../1622681548499-log-message-length.js | 13 - .../1626509500668-fix-remote-file-proxy.js | 22 - .../migration/1629004542760-chart-reindex.js | 181 - .../1629024377804-deepl-integration.js | 13 - .../1629288472000-fix-channel-userId.js | 13 - .../1629512953000-user-is-deleted.js | 14 - .../1629778475000-deepl-integration2.js | 13 - .../1629833361000-AddShowTLReplies.js | 14 - .../1629968054000_userInstanceBlocks.js | 14 - .../1631880003000-user-block-federation.js | 12 - ...1633068642000-email-required-for-signup.js | 13 - .../migration/1633071909016-user-pending.js | 15 - .../1634486652000-user-public-reactions.js | 13 - .../migration/1634902659689-delete-log.js | 12 - .../1635500777168-note-thread-mute.js | 25 - .../migration/1636197624383-ff-visibility.js | 15 - .../1636697408073-remove-via-mobile.js | 13 - .../1637320813000-forwarded-report.js | 13 - .../migration/1639325650583-chart-v3.js | 189 - .../migration/1642611822809-emoji-url.js | 15 - ...1642613870898-drive-file-webpublic-type.js | 13 - .../migration/1642698156335-reported-urls.js | 11 - .../migration/1643963705770-chart-v4.js | 63 - .../migration/1643966656277-chart-v5.js | 27 - .../migration/1643967331284-chart-v6.js | 343 - .../1644010796173-convert-hard-mutes.js | 65 - .../migration/1644058404077-chart-v7.js | 501 -- .../migration/1644059847460-chart-v8.js | 25 - .../migration/1644060125705-chart-v9.js | 25 - .../migration/1644073149413-chart-v10.js | 35 - .../migration/1644095659741-chart-v11.js | 91 - .../migration/1644328606241-chart-v12.js | 27 - .../migration/1644331238153-chart-v13.js | 19 - .../migration/1644344266289-chart-v14.js | 47 - .../1644395759931-instance-theme-color.js | 13 - .../migration/1644481657998-chart-v15.js | 31 - .../1644551208096-following-indexes.js | 15 - ...45340161439-remove-max-note-text-length.js | 13 - .../1645599900873-federation-chart-pubsub.js | 15 - .../1646143552768-instance-default-theme.js | 13 - .../1646387162108-mute-expires-at.js | 13 - .../1646549089451-poll-ended-notification.js | 18 - .../1646633030285-chart-federation-active.js | 13 - ...655454495-remove-instance-drive-columns.js | 13 - ...2390560-chart-federation-active-sub-pub.js | 21 - .../migration/1648548247382-webhook.js | 19 - .../migration/1648816172177-webhook-2.js | 14 - .../migration/1651224615271-foreign-key.js | 89 - .../1652859567549-uniform-themecolor.js | 36 - .../1654124623992-remove-unused-image-urls.js | 14 - ...1655793461890-thread-mute-notifications.js | 13 - .../migration/1657570176749-remove-ads.js | 12 - .../1658146000392-remove-repo-url.js | 13 - .../1658656633972-note-replies-function.js | 52 - .../1659335999000-pages-to-plaintext.js | 82 - ...659516638000-drive-file-user-constraint.js | 13 - .../1660251834642-remove-promo-entities.js | 25 - ...00-remove-mentioned-remote-users-column.js | 12 - .../migration/1662489803045-remove-rooms.js | 11 - ...1662943835603-larger-follow-request-ids.js | 12 - .../1662999442223-update-pinned-pages.js | 11 - ...663399074403-resize-comments-drive-file.js | 14 - .../1665091090561-add-renote-muting.js | 20 - .../backend/migration/1667503570994-sync.js | 44 - .../1667653936442-token-permissions.js | 33 - .../backend/migration/1667738304733-pkce.js | 12 - .../1668374092227-forceEnablePush.js | 24 - .../1668661888188-add-libretranslate.js | 19 - .../migration/1669545702493-detectDeeplPro.js | 12 - .../1670359028055-removeIntegrations.js | 29 - .../migration/1672607891750-remove-reversi.js | 21 - .../1672991292018-remove-user-group-invite.js | 23 - packages/backend/ormconfig.js | 15 - packages/backend/package.json | 178 - packages/backend/src/@types/hcaptcha.d.ts | 11 - .../backend/src/@types/http-signature.d.ts | 77 - .../backend/src/@types/koa-json-body.d.ts | 15 - packages/backend/src/@types/koa-slow.d.ts | 14 - packages/backend/src/@types/os-utils.d.ts | 30 - packages/backend/src/@types/package.json.d.ts | 10 - .../backend/src/@types/probe-image-size.d.ts | 27 - packages/backend/src/boot/index.ts | 81 - packages/backend/src/boot/master.ts | 171 - packages/backend/src/boot/worker.ts | 30 - packages/backend/src/config/index.ts | 3 - packages/backend/src/config/load.ts | 89 - packages/backend/src/config/redis.ts | 34 - packages/backend/src/config/types.ts | 95 - packages/backend/src/const.ts | 56 - packages/backend/src/daemons/queue-stats.ts | 61 - packages/backend/src/daemons/server-stats.ts | 82 - packages/backend/src/db/elasticsearch.ts | 56 - packages/backend/src/db/logger.ts | 3 - packages/backend/src/db/postgre.ts | 245 - packages/backend/src/db/redis.ts | 12 - packages/backend/src/env.ts | 20 - packages/backend/src/global.d.ts | 1 - packages/backend/src/index.ts | 13 - packages/backend/src/mfm/from-html.ts | 231 - packages/backend/src/mfm/to-html.ts | 179 - packages/backend/src/misc/acct.ts | 17 - packages/backend/src/misc/antenna-cache.ts | 36 - packages/backend/src/misc/api-permissions.ts | 35 - packages/backend/src/misc/app-lock.ts | 29 - packages/backend/src/misc/before-shutdown.ts | 94 - packages/backend/src/misc/cache.ts | 56 - packages/backend/src/misc/captcha.ts | 57 - .../backend/src/misc/check-hit-antenna.ts | 103 - packages/backend/src/misc/check-word-mute.ts | 47 - .../backend/src/misc/content-disposition.ts | 6 - packages/backend/src/misc/convert-host.ts | 26 - .../backend/src/misc/count-same-renotes.ts | 15 - 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 | 90 - packages/backend/src/misc/emoji-regex.ts | 4 - .../misc/extract-custom-emojis-from-mfm.ts | 10 - packages/backend/src/misc/extract-hashtags.ts | 9 - packages/backend/src/misc/extract-mentions.ts | 11 - packages/backend/src/misc/fetch-meta.ts | 61 - .../backend/src/misc/fetch-proxy-account.ts | 9 - packages/backend/src/misc/fetch.ts | 147 - packages/backend/src/misc/gen-id.ts | 21 - packages/backend/src/misc/gen-identicon.ts | 91 - packages/backend/src/misc/gen-key-pair.ts | 36 - packages/backend/src/misc/get-file-info.ts | 204 - packages/backend/src/misc/get-ip-hash.ts | 9 - packages/backend/src/misc/get-note-summary.ts | 50 - .../backend/src/misc/get-reaction-emoji.ts | 16 - packages/backend/src/misc/hard-limits.ts | 14 - packages/backend/src/misc/i18n.ts | 28 - .../backend/src/misc/identifiable-error.ts | 13 - .../src/misc/is-duplicate-key-value-error.ts | 3 - .../backend/src/misc/is-instance-muted.ts | 15 - packages/backend/src/misc/is-mime-image.ts | 8 - packages/backend/src/misc/is-user-related.ts | 7 - packages/backend/src/misc/keypair-store.ts | 13 - packages/backend/src/misc/langmap.ts | 666 -- .../backend/src/misc/normalize-for-search.ts | 6 - packages/backend/src/misc/nyaize.ts | 15 - packages/backend/src/misc/password.ts | 17 - packages/backend/src/misc/populate-emojis.ts | 144 - packages/backend/src/misc/reaction-lib.ts | 131 - packages/backend/src/misc/renote.ts | 11 - packages/backend/src/misc/safe-for-sql.ts | 3 - packages/backend/src/misc/schema.ts | 174 - packages/backend/src/misc/secure-rndstr.ts | 25 - .../backend/src/misc/should-block-instance.ts | 15 - .../backend/src/misc/show-machine-info.ts | 13 - .../backend/src/misc/skipped-instances.ts | 53 - packages/backend/src/misc/truncate.ts | 11 - packages/backend/src/misc/webhook-cache.ts | 49 - .../src/models/entities/abuse-user-report.ts | 84 - .../src/models/entities/access-token.ts | 90 - .../src/models/entities/announcement-read.ts | 36 - .../src/models/entities/announcement.ts | 35 - .../src/models/entities/antenna-note.ts | 43 - .../backend/src/models/entities/antenna.ts | 99 - packages/backend/src/models/entities/app.ts | 60 - .../models/entities/attestation-challenge.ts | 38 - .../src/models/entities/auth-session.ts | 49 - .../backend/src/models/entities/blocking.ts | 42 - .../src/models/entities/channel-following.ts | 43 - .../models/entities/channel-note-pining.ts | 35 - .../backend/src/models/entities/channel.ts | 75 - .../backend/src/models/entities/clip-note.ts | 37 - packages/backend/src/models/entities/clip.ts | 44 - .../backend/src/models/entities/drive-file.ts | 169 - .../src/models/entities/drive-folder.ts | 49 - packages/backend/src/models/entities/emoji.ts | 58 - .../src/models/entities/follow-request.ts | 85 - .../backend/src/models/entities/following.ts | 82 - .../src/models/entities/gallery-like.ts | 33 - .../src/models/entities/gallery-post.ts | 79 - .../backend/src/models/entities/hashtag.ts | 87 - .../backend/src/models/entities/instance.ts | 164 - .../src/models/entities/messaging-message.ts | 89 - packages/backend/src/models/entities/meta.ts | 362 -- .../src/models/entities/moderation-log.ts | 32 - .../backend/src/models/entities/muted-note.ts | 48 - .../backend/src/models/entities/muting.ts | 48 - .../src/models/entities/note-favorite.ts | 35 - .../src/models/entities/note-reaction.ts | 44 - .../src/models/entities/note-thread-muting.ts | 40 - .../src/models/entities/note-unread.ts | 63 - .../src/models/entities/note-watching.ts | 52 - packages/backend/src/models/entities/note.ts | 230 - .../src/models/entities/notification.ts | 173 - .../backend/src/models/entities/page-like.ts | 33 - packages/backend/src/models/entities/page.ts | 110 - .../models/entities/password-reset-request.ts | 30 - .../backend/src/models/entities/poll-vote.ts | 40 - packages/backend/src/models/entities/poll.ts | 72 - .../models/entities/registration-tickets.ts | 17 - .../src/models/entities/registry-item.ts | 58 - packages/backend/src/models/entities/relay.ts | 19 - .../src/models/entities/renote-muting.ts | 42 - .../backend/src/models/entities/signin.ts | 35 - .../src/models/entities/sw-subscription.ts | 37 - .../src/models/entities/used-username.ts | 20 - .../models/entities/user-group-invitation.ts | 42 - .../src/models/entities/user-group-joining.ts | 42 - .../backend/src/models/entities/user-group.ts | 46 - .../src/models/entities/user-keypair.ts | 33 - .../src/models/entities/user-list-joining.ts | 42 - .../backend/src/models/entities/user-list.ts | 33 - .../src/models/entities/user-note-pining.ts | 35 - .../src/models/entities/user-pending.ts | 32 - .../src/models/entities/user-profile.ts | 210 - .../src/models/entities/user-publickey.ts | 34 - .../src/models/entities/user-security-key.ts | 48 - packages/backend/src/models/entities/user.ts | 256 - .../backend/src/models/entities/webhook.ts | 73 - packages/backend/src/models/id.ts | 4 - packages/backend/src/models/index.ts | 125 - .../models/repositories/abuse-user-report.ts | 39 - .../src/models/repositories/antenna.ts | 32 - .../backend/src/models/repositories/app.ts | 40 - .../src/models/repositories/auth-session.ts | 20 - .../src/models/repositories/blocking.ts | 31 - .../src/models/repositories/channel.ts | 41 - .../backend/src/models/repositories/clip.ts | 30 - .../src/models/repositories/drive-file.ts | 139 - .../src/models/repositories/drive-folder.ts | 42 - .../backend/src/models/repositories/emoji.ts | 27 - .../src/models/repositories/follow-request.ts | 19 - .../src/models/repositories/following.ts | 83 - .../src/models/repositories/gallery-like.ts | 24 - .../src/models/repositories/gallery-post.ts | 39 - .../src/models/repositories/hashtag.ts | 25 - .../src/models/repositories/instance.ts | 42 - .../models/repositories/messaging-message.ts | 39 - .../models/repositories/moderation-logs.ts | 29 - .../backend/src/models/repositories/muting.ts | 32 - .../src/models/repositories/note-favorite.ts | 29 - .../src/models/repositories/note-reaction.ts | 46 - .../backend/src/models/repositories/note.ts | 275 - .../src/models/repositories/notification.ts | 83 - .../src/models/repositories/page-like.ts | 25 - .../backend/src/models/repositories/page.ts | 42 - .../backend/src/models/repositories/relay.ts | 5 - .../src/models/repositories/renote-muting.ts | 31 - .../backend/src/models/repositories/signin.ts | 10 - .../repositories/user-group-invitation.ts | 22 - .../src/models/repositories/user-group.ts | 24 - .../src/models/repositories/user-list.ts | 23 - .../backend/src/models/repositories/user.ts | 421 -- packages/backend/src/models/schema/antenna.ts | 89 - packages/backend/src/models/schema/app.ts | 33 - .../backend/src/models/schema/blocking.ts | 26 - packages/backend/src/models/schema/channel.ts | 51 - packages/backend/src/models/schema/clip.ts | 38 - .../backend/src/models/schema/drive-file.ts | 107 - .../backend/src/models/schema/drive-folder.ts | 39 - packages/backend/src/models/schema/emoji.ts | 37 - .../src/models/schema/federation-instance.ts | 110 - .../backend/src/models/schema/following.ts | 36 - .../backend/src/models/schema/gallery-post.ts | 69 - packages/backend/src/models/schema/hashtag.ts | 34 - .../src/models/schema/messaging-message.ts | 73 - packages/backend/src/models/schema/muting.ts | 31 - .../src/models/schema/note-favorite.ts | 26 - .../src/models/schema/note-reaction.ts | 25 - packages/backend/src/models/schema/note.ts | 179 - .../backend/src/models/schema/notification.ts | 66 - packages/backend/src/models/schema/page.ts | 47 - packages/backend/src/models/schema/queue.ts | 25 - .../src/models/schema/renote-muting.ts | 26 - .../backend/src/models/schema/user-group.ts | 34 - .../backend/src/models/schema/user-list.ts | 29 - packages/backend/src/models/schema/user.ts | 468 -- packages/backend/src/prelude/README.md | 3 - packages/backend/src/prelude/array.ts | 138 - packages/backend/src/prelude/await-all.ts | 21 - packages/backend/src/prelude/relation.ts | 5 - packages/backend/src/prelude/symbol.ts | 1 - packages/backend/src/prelude/time.ts | 25 - packages/backend/src/prelude/url.ts | 13 - packages/backend/src/prelude/xml.ts | 41 - packages/backend/src/queue/get-job-info.ts | 15 - packages/backend/src/queue/index.ts | 338 - packages/backend/src/queue/initialize.ts | 30 - packages/backend/src/queue/logger.ts | 3 - .../src/queue/processors/db/delete-account.ts | 92 - .../queue/processors/db/delete-drive-files.ts | 55 - .../queue/processors/db/export-blocking.ts | 93 - .../processors/db/export-custom-emojis.ts | 112 - .../queue/processors/db/export-following.ts | 95 - .../src/queue/processors/db/export-mute.ts | 94 - .../src/queue/processors/db/export-notes.ts | 118 - .../queue/processors/db/export-user-lists.ts | 70 - .../queue/processors/db/import-blocking.ts | 75 - .../processors/db/import-custom-emojis.ts | 81 - .../queue/processors/db/import-following.ts | 74 - .../src/queue/processors/db/import-muting.ts | 84 - .../queue/processors/db/import-user-lists.ts | 80 - .../backend/src/queue/processors/db/index.ts | 37 - .../backend/src/queue/processors/deliver.ts | 77 - .../processors/ended-poll-notification.ts | 29 - .../backend/src/queue/processors/inbox.ts | 142 - .../object-storage/clean-remote-files.ts | 50 - .../processors/object-storage/delete-file.ts | 11 - .../queue/processors/object-storage/index.ts | 15 - .../queue/processors/system/check-expired.ts | 58 - .../queue/processors/system/clean-charts.ts | 28 - .../src/queue/processors/system/index.ts | 18 - .../queue/processors/system/resync-charts.ts | 21 - .../queue/processors/system/tick-charts.ts | 28 - .../src/queue/processors/webhook-deliver.ts | 58 - packages/backend/src/queue/queues.ts | 21 - packages/backend/src/queue/types.ts | 63 - .../src/remote/activitypub/ap-request.ts | 104 - .../src/remote/activitypub/audience.ts | 92 - .../src/remote/activitypub/db-resolver.ts | 101 - .../src/remote/activitypub/deliver-manager.ts | 185 - .../activitypub/kernel/accept/follow.ts | 29 - .../remote/activitypub/kernel/accept/index.ts | 20 - .../remote/activitypub/kernel/add/index.ts | 24 - .../activitypub/kernel/announce/index.ts | 15 - .../activitypub/kernel/announce/note.ts | 66 - .../remote/activitypub/kernel/block/index.ts | 23 - .../remote/activitypub/kernel/create/index.ts | 39 - .../remote/activitypub/kernel/create/note.ts | 44 - .../remote/activitypub/kernel/delete/actor.ts | 25 - .../remote/activitypub/kernel/delete/index.ts | 49 - .../remote/activitypub/kernel/delete/note.ts | 38 - .../remote/activitypub/kernel/flag/index.ts | 31 - .../src/remote/activitypub/kernel/follow.ts | 20 - .../src/remote/activitypub/kernel/index.ts | 79 - .../src/remote/activitypub/kernel/like.ts | 21 - .../src/remote/activitypub/kernel/read.ts | 27 - .../activitypub/kernel/reject/follow.ts | 30 - .../remote/activitypub/kernel/reject/index.ts | 20 - .../remote/activitypub/kernel/remove/index.ts | 24 - .../remote/activitypub/kernel/undo/accept.ts | 26 - .../activitypub/kernel/undo/announce.ts | 18 - .../remote/activitypub/kernel/undo/block.ts | 21 - .../remote/activitypub/kernel/undo/follow.ts | 40 - .../remote/activitypub/kernel/undo/index.ts | 32 - .../remote/activitypub/kernel/undo/like.ts | 21 - .../remote/activitypub/kernel/update/index.ts | 36 - .../backend/src/remote/activitypub/logger.ts | 3 - .../src/remote/activitypub/misc/auth-user.ts | 50 - .../src/remote/activitypub/misc/contexts.ts | 526 -- .../remote/activitypub/misc/ld-signature.ts | 135 - .../src/remote/activitypub/models/icon.ts | 5 - .../remote/activitypub/models/identifier.ts | 5 - .../src/remote/activitypub/models/image.ts | 66 - .../src/remote/activitypub/models/mention.ts | 22 - .../src/remote/activitypub/models/note.ts | 359 -- .../src/remote/activitypub/models/person.ts | 450 -- .../src/remote/activitypub/models/question.ts | 81 - .../src/remote/activitypub/models/tag.ts | 18 - .../backend/src/remote/activitypub/perform.ts | 19 - .../src/remote/activitypub/renderer/accept.ts | 8 - .../src/remote/activitypub/renderer/add.ts | 9 - .../remote/activitypub/renderer/announce.ts | 29 - .../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 | 14 - .../src/remote/activitypub/renderer/flag.ts | 12 - .../activitypub/renderer/follow-relay.ts | 21 - .../activitypub/renderer/follow-user.ts | 12 - .../src/remote/activitypub/renderer/follow.ts | 34 - .../remote/activitypub/renderer/hashtag.ts | 7 - .../src/remote/activitypub/renderer/image.ts | 9 - .../src/remote/activitypub/renderer/index.ts | 59 - .../src/remote/activitypub/renderer/key.ts | 14 - .../src/remote/activitypub/renderer/like.ts | 31 - .../remote/activitypub/renderer/mention.ts | 9 - .../src/remote/activitypub/renderer/note.ts | 168 - .../renderer/ordered-collection-page.ts | 23 - .../renderer/ordered-collection.ts | 28 - .../src/remote/activitypub/renderer/person.ts | 88 - .../remote/activitypub/renderer/question.ts | 23 - .../src/remote/activitypub/renderer/read.ts | 9 - .../src/remote/activitypub/renderer/reject.ts | 8 - .../src/remote/activitypub/renderer/remove.ts | 9 - .../remote/activitypub/renderer/tombstone.ts | 4 - .../src/remote/activitypub/renderer/undo.ts | 15 - .../src/remote/activitypub/renderer/update.ts | 15 - .../src/remote/activitypub/renderer/vote.ts | 23 - .../backend/src/remote/activitypub/request.ts | 73 - .../src/remote/activitypub/resolver.ts | 148 - .../backend/src/remote/activitypub/type.ts | 295 - packages/backend/src/remote/logger.ts | 3 - packages/backend/src/remote/resolve-user.ts | 117 - packages/backend/src/remote/webfinger.ts | 34 - packages/backend/src/server/activitypub.ts | 265 - .../src/server/activitypub/featured.ts | 41 - .../src/server/activitypub/followers.ts | 95 - .../src/server/activitypub/following.ts | 95 - .../backend/src/server/activitypub/outbox.ts | 109 - packages/backend/src/server/api/2fa.ts | 422 -- .../backend/src/server/api/api-handler.ts | 51 - .../backend/src/server/api/authenticate.ts | 69 - packages/backend/src/server/api/call.ts | 111 - .../src/server/api/common/compare-url.ts | 42 - .../server/api/common/generate-block-query.ts | 42 - .../api/common/generate-channel-query.ts | 24 - .../api/common/generate-muted-note-query.ts | 13 - .../generate-muted-note-thread-query.ts | 17 - .../api/common/generate-muted-user-query.ts | 57 - .../api/common/generate-native-user-token.ts | 3 - .../api/common/generate-replies-query.ts | 27 - .../api/common/generate-visibility-query.ts | 42 - .../common/generated-muted-renote-query.ts | 22 - .../backend/src/server/api/common/getters.ts | 64 - .../src/server/api/common/inject-featured.ts | 57 - .../src/server/api/common/is-native-token.ts | 1 - .../api/common/make-pagination-query.ts | 28 - .../backend/src/server/api/common/oauth.ts | 129 - .../api/common/read-messaging-message.ts | 150 - .../server/api/common/read-notification.ts | 50 - .../backend/src/server/api/common/signin.ts | 44 - .../backend/src/server/api/common/signup.ts | 113 - .../src/server/api/common/translator.ts | 12 - packages/backend/src/server/api/define.ts | 49 - packages/backend/src/server/api/endpoints.ts | 739 --- .../api/endpoints/admin/abuse-user-reports.ts | 113 - .../api/endpoints/admin/accounts/create.ts | 62 - .../api/endpoints/admin/accounts/delete.ts | 39 - .../endpoints/admin/announcements/create.ts | 69 - .../endpoints/admin/announcements/delete.ts | 29 - .../api/endpoints/admin/announcements/list.ts | 89 - .../endpoints/admin/announcements/update.ts | 37 - .../admin/delete-all-files-of-a-user.ts | 29 - .../admin/drive/clean-remote-files.ts | 20 - .../server/api/endpoints/admin/drive/files.ts | 70 - .../api/endpoints/admin/drive/show-file.ts | 180 - .../endpoints/admin/emoji/add-aliases-bulk.ts | 40 - .../server/api/endpoints/admin/emoji/add.ts | 59 - .../server/api/endpoints/admin/emoji/copy.ts | 74 - .../api/endpoints/admin/emoji/delete-bulk.ts | 37 - .../api/endpoints/admin/emoji/delete.ts | 35 - .../api/endpoints/admin/emoji/import-zip.ts | 21 - .../api/endpoints/admin/emoji/list-remote.ts | 91 - .../server/api/endpoints/admin/emoji/list.ts | 89 - .../admin/emoji/remove-aliases-bulk.ts | 40 - .../endpoints/admin/emoji/set-aliases-bulk.ts | 36 - .../admin/emoji/set-category-bulk.ts | 38 - .../api/endpoints/admin/emoji/update.ts | 46 - .../admin/federation/delete-all-files.ts | 29 - .../refresh-remote-instance-metadata.ts | 33 - .../admin/federation/remove-all-following.ts | 34 - .../admin/federation/update-instance.ts | 35 - .../api/endpoints/admin/get-index-stats.ts | 28 - .../api/endpoints/admin/get-table-stats.ts | 49 - .../src/server/api/endpoints/admin/invite.ts | 47 - .../src/server/api/endpoints/admin/meta.ts | 329 - .../api/endpoints/admin/moderators/add.ts | 42 - .../api/endpoints/admin/moderators/remove.ts | 36 - .../server/api/endpoints/admin/queue/clear.ts | 23 - .../endpoints/admin/queue/deliver-delayed.ts | 59 - .../endpoints/admin/queue/inbox-delayed.ts | 59 - .../server/api/endpoints/admin/queue/stats.ts | 53 - .../server/api/endpoints/admin/relays/add.ts | 60 - .../server/api/endpoints/admin/relays/list.ts | 51 - .../api/endpoints/admin/relays/remove.ts | 22 - .../api/endpoints/admin/reset-password.ts | 58 - .../admin/resolve-abuse-user-report.ts | 40 - .../server/api/endpoints/admin/send-email.ts | 24 - .../server/api/endpoints/admin/server-info.ts | 127 - .../endpoints/admin/show-moderation-logs.ts | 68 - .../server/api/endpoints/admin/show-user.ts | 70 - .../server/api/endpoints/admin/show-users.ts | 82 - .../api/endpoints/admin/silence-user.ts | 45 - .../api/endpoints/admin/suspend-user.ts | 84 - .../api/endpoints/admin/unsilence-user.ts | 41 - .../api/endpoints/admin/unsuspend-user.ts | 41 - .../server/api/endpoints/admin/update-meta.ts | 343 - .../src/server/api/endpoints/admin/vacuum.ts | 36 - .../src/server/api/endpoints/announcements.ts | 86 - .../server/api/endpoints/antennas/create.ts | 92 - .../server/api/endpoints/antennas/delete.ts | 36 - .../src/server/api/endpoints/antennas/list.ts | 35 - .../server/api/endpoints/antennas/notes.ts | 81 - .../src/server/api/endpoints/antennas/show.ts | 40 - .../server/api/endpoints/antennas/update.ts | 97 - .../src/server/api/endpoints/ap/get.ts | 34 - .../src/server/api/endpoints/ap/show.ts | 143 - .../src/server/api/endpoints/app/create.ts | 58 - .../src/server/api/endpoints/app/show.ts | 38 - .../src/server/api/endpoints/auth/accept.ts | 80 - .../src/server/api/endpoints/auth/deny.ts | 38 - .../api/endpoints/auth/session/generate.ts | 109 - .../api/endpoints/auth/session/oauth.ts | 5 - .../server/api/endpoints/auth/session/show.ts | 52 - .../api/endpoints/auth/session/userkey.ts | 72 - .../server/api/endpoints/blocking/create.ts | 70 - .../server/api/endpoints/blocking/delete.ts | 66 - .../src/server/api/endpoints/blocking/list.ts | 43 - .../server/api/endpoints/channels/create.ts | 56 - .../server/api/endpoints/channels/featured.ts | 35 - .../server/api/endpoints/channels/follow.ts | 41 - .../server/api/endpoints/channels/followed.ts | 43 - .../server/api/endpoints/channels/owned.ts | 43 - .../src/server/api/endpoints/channels/show.ts | 36 - .../server/api/endpoints/channels/timeline.ts | 68 - .../server/api/endpoints/channels/unfollow.ts | 38 - .../server/api/endpoints/channels/update.ts | 62 - .../api/endpoints/charts/active-users.ts | 27 - .../server/api/endpoints/charts/ap-request.ts | 27 - .../src/server/api/endpoints/charts/drive.ts | 27 - .../server/api/endpoints/charts/federation.ts | 27 - .../server/api/endpoints/charts/hashtag.ts | 28 - .../server/api/endpoints/charts/instance.ts | 28 - .../src/server/api/endpoints/charts/notes.ts | 27 - .../server/api/endpoints/charts/user/drive.ts | 28 - .../api/endpoints/charts/user/following.ts | 28 - .../server/api/endpoints/charts/user/notes.ts | 28 - .../api/endpoints/charts/user/reactions.ts | 28 - .../src/server/api/endpoints/charts/users.ts | 27 - .../server/api/endpoints/clips/add-note.ts | 52 - .../src/server/api/endpoints/clips/create.ts | 41 - .../src/server/api/endpoints/clips/delete.ts | 33 - .../src/server/api/endpoints/clips/list.ts | 35 - .../src/server/api/endpoints/clips/notes.ts | 78 - .../server/api/endpoints/clips/remove-note.ts | 45 - .../src/server/api/endpoints/clips/show.ts | 43 - .../src/server/api/endpoints/clips/update.ts | 49 - .../backend/src/server/api/endpoints/drive.ts | 44 - .../src/server/api/endpoints/drive/files.ts | 57 - .../endpoints/drive/files/attached-notes.ts | 52 - .../endpoints/drive/files/check-existence.ts | 33 - .../api/endpoints/drive/files/create.ts | 75 - .../api/endpoints/drive/files/delete.ts | 42 - .../api/endpoints/drive/files/find-by-hash.ts | 40 - .../server/api/endpoints/drive/files/find.ts | 43 - .../server/api/endpoints/drive/files/show.ts | 71 - .../api/endpoints/drive/files/update.ts | 83 - .../endpoints/drive/files/upload-from-url.ts | 45 - .../src/server/api/endpoints/drive/folders.ts | 48 - .../api/endpoints/drive/folders/create.ts | 61 - .../api/endpoints/drive/folders/delete.ts | 47 - .../api/endpoints/drive/folders/find.ts | 41 - .../api/endpoints/drive/folders/show.ts | 42 - .../api/endpoints/drive/folders/update.ts | 96 - .../src/server/api/endpoints/drive/stream.ts | 50 - .../api/endpoints/email-address/available.ts | 36 - .../src/server/api/endpoints/endpoint.ts | 28 - .../src/server/api/endpoints/endpoints.ts | 34 - .../api/endpoints/export-custom-emojis.ts | 31 - .../api/endpoints/federation/followers.ts | 43 - .../api/endpoints/federation/following.ts | 43 - .../api/endpoints/federation/instances.ts | 117 - .../api/endpoints/federation/show-instance.ts | 34 - .../server/api/endpoints/federation/stats.ts | 50 - .../federation/update-remote-user.ts | 24 - .../server/api/endpoints/federation/users.ts | 42 - .../src/server/api/endpoints/fetch-rss.ts | 39 - .../server/api/endpoints/following/create.ts | 70 - .../server/api/endpoints/following/delete.ts | 61 - .../api/endpoints/following/invalidate.ts | 61 - .../endpoints/following/requests/accept.ts | 38 - .../endpoints/following/requests/cancel.ts | 50 - .../api/endpoints/following/requests/list.ts | 51 - .../endpoints/following/requests/reject.ts | 35 - .../server/api/endpoints/gallery/featured.ts | 37 - .../server/api/endpoints/gallery/popular.ts | 35 - .../src/server/api/endpoints/gallery/posts.ts | 37 - .../api/endpoints/gallery/posts/create.ts | 72 - .../api/endpoints/gallery/posts/delete.ts | 33 - .../api/endpoints/gallery/posts/like.ts | 46 - .../api/endpoints/gallery/posts/show.ts | 36 - .../api/endpoints/gallery/posts/unlike.ts | 39 - .../api/endpoints/gallery/posts/update.ts | 75 - .../api/endpoints/get-online-users-count.ts | 27 - .../src/server/api/endpoints/hashtags/list.ts | 68 - .../server/api/endpoints/hashtags/search.ts | 40 - .../src/server/api/endpoints/hashtags/show.ts | 34 - .../server/api/endpoints/hashtags/trend.ts | 153 - .../server/api/endpoints/hashtags/users.ts | 63 - .../backend/src/server/api/endpoints/i.ts | 31 - .../src/server/api/endpoints/i/2fa/done.ts | 46 - .../server/api/endpoints/i/2fa/key-done.ts | 144 - .../api/endpoints/i/2fa/password-less.ts | 23 - .../api/endpoints/i/2fa/register-key.ts | 61 - .../server/api/endpoints/i/2fa/register.ts | 58 - .../server/api/endpoints/i/2fa/remove-key.ts | 45 - .../server/api/endpoints/i/2fa/unregister.ts | 34 - .../src/server/api/endpoints/i/apps.ts | 40 - .../server/api/endpoints/i/authorized-apps.ts | 37 - .../server/api/endpoints/i/change-password.ts | 34 - .../server/api/endpoints/i/delete-account.ts | 39 - .../server/api/endpoints/i/export-blocking.ts | 23 - .../api/endpoints/i/export-following.ts | 26 - .../src/server/api/endpoints/i/export-mute.ts | 23 - .../server/api/endpoints/i/export-notes.ts | 23 - .../api/endpoints/i/export-user-lists.ts | 23 - .../src/server/api/endpoints/i/favorites.ts | 44 - .../server/api/endpoints/i/gallery/likes.ts | 55 - .../server/api/endpoints/i/gallery/posts.ts | 43 - .../endpoints/i/get-word-muted-notes-count.ts | 37 - .../server/api/endpoints/i/import-blocking.ts | 36 - .../api/endpoints/i/import-following.ts | 35 - .../server/api/endpoints/i/import-muting.ts | 36 - .../api/endpoints/i/import-user-lists.ts | 35 - .../server/api/endpoints/i/notifications.ts | 144 - .../src/server/api/endpoints/i/page-likes.ts | 54 - .../src/server/api/endpoints/i/pages.ts | 43 - .../backend/src/server/api/endpoints/i/pin.ts | 42 - .../i/read-all-messaging-messages.ts | 41 - .../api/endpoints/i/read-all-unread-notes.ts | 29 - .../api/endpoints/i/read-announcement.ts | 51 - .../api/endpoints/i/regenerate-token.ts | 49 - .../api/endpoints/i/registry/get-all.ts | 36 - .../api/endpoints/i/registry/get-detail.ts | 40 - .../server/api/endpoints/i/registry/get.ts | 37 - .../endpoints/i/registry/keys-with-type.ts | 44 - .../server/api/endpoints/i/registry/keys.ts | 31 - .../server/api/endpoints/i/registry/remove.ts | 37 - .../server/api/endpoints/i/registry/scopes.ts | 33 - .../server/api/endpoints/i/registry/set.ts | 58 - .../server/api/endpoints/i/revoke-token.ts | 32 - .../server/api/endpoints/i/signin-history.ts | 29 - .../src/server/api/endpoints/i/unpin.ts | 40 - .../server/api/endpoints/i/update-email.ts | 78 - .../src/server/api/endpoints/i/update.ts | 219 - .../api/endpoints/i/user-group-invites.ts | 55 - .../server/api/endpoints/i/webhooks/create.ts | 43 - .../server/api/endpoints/i/webhooks/delete.ts | 36 - .../server/api/endpoints/i/webhooks/list.ts | 25 - .../server/api/endpoints/i/webhooks/show.ts | 33 - .../server/api/endpoints/i/webhooks/update.ts | 50 - .../server/api/endpoints/messaging/history.ts | 91 - .../api/endpoints/messaging/messages.ts | 119 - .../endpoints/messaging/messages/create.ts | 140 - .../endpoints/messaging/messages/delete.ts | 41 - .../api/endpoints/messaging/messages/read.ts | 41 - .../backend/src/server/api/endpoints/meta.ts | 332 - .../server/api/endpoints/miauth/gen-token.ts | 64 - .../src/server/api/endpoints/mute/create.ts | 72 - .../src/server/api/endpoints/mute/delete.ts | 52 - .../src/server/api/endpoints/mute/list.ts | 43 - .../src/server/api/endpoints/my/apps.ts | 44 - .../backend/src/server/api/endpoints/notes.ts | 79 - .../server/api/endpoints/notes/children.ts | 70 - .../src/server/api/endpoints/notes/clips.ts | 55 - .../api/endpoints/notes/conversation.ts | 77 - .../src/server/api/endpoints/notes/create.ts | 247 - .../src/server/api/endpoints/notes/delete.ts | 50 - .../api/endpoints/notes/favorites/create.ts | 48 - .../api/endpoints/notes/favorites/delete.ts | 42 - .../server/api/endpoints/notes/featured.ts | 72 - .../api/endpoints/notes/global-timeline.ts | 94 - .../api/endpoints/notes/hybrid-timeline.ts | 135 - .../api/endpoints/notes/local-timeline.ts | 115 - .../server/api/endpoints/notes/mentions.ts | 85 - .../endpoints/notes/polls/recommendation.ts | 83 - .../server/api/endpoints/notes/polls/vote.ts | 141 - .../server/api/endpoints/notes/reactions.ts | 82 - .../api/endpoints/notes/reactions/create.ts | 37 - .../api/endpoints/notes/reactions/delete.ts | 41 - .../src/server/api/endpoints/notes/renotes.ts | 72 - .../src/server/api/endpoints/notes/replies.ts | 72 - .../api/endpoints/notes/search-by-tag.ts | 140 - .../src/server/api/endpoints/notes/search.ts | 141 - .../src/server/api/endpoints/notes/show.ts | 47 - .../src/server/api/endpoints/notes/state.ts | 82 - .../endpoints/notes/thread-muting/create.ts | 72 - .../endpoints/notes/thread-muting/delete.ts | 35 - .../server/api/endpoints/notes/timeline.ts | 133 - .../server/api/endpoints/notes/translate.ts | 222 - .../server/api/endpoints/notes/unrenote.ts | 52 - .../api/endpoints/notes/user-list-timeline.ts | 115 - .../api/endpoints/notes/watching/create.ts | 32 - .../api/endpoints/notes/watching/delete.ts | 32 - .../api/endpoints/notifications/create.ts | 30 - .../notifications/mark-all-as-read.ts | 33 - .../api/endpoints/notifications/read.ts | 44 - .../src/server/api/endpoints/page-push.ts | 37 - .../src/server/api/endpoints/pages/create.ts | 82 - .../src/server/api/endpoints/pages/delete.ts | 32 - .../server/api/endpoints/pages/featured.ts | 36 - .../src/server/api/endpoints/pages/like.ts | 46 - .../src/server/api/endpoints/pages/show.ts | 62 - .../src/server/api/endpoints/pages/unlike.ts | 39 - .../src/server/api/endpoints/pages/update.ts | 79 - .../backend/src/server/api/endpoints/ping.ts | 31 - .../src/server/api/endpoints/pinned-users.ts | 40 - .../api/endpoints/renote-mute/create.ts | 57 - .../api/endpoints/renote-mute/delete.ts | 52 - .../server/api/endpoints/renote-mute/list.ts | 43 - .../api/endpoints/request-reset-password.ts | 70 - .../src/server/api/endpoints/reset-db.ts | 26 - .../server/api/endpoints/reset-password.ts | 51 - .../src/server/api/endpoints/server-info.ts | 36 - .../backend/src/server/api/endpoints/stats.ts | 83 - .../src/server/api/endpoints/sw/register.ts | 72 - .../src/server/api/endpoints/sw/unregister.ts | 26 - .../api/endpoints/username/available.ts | 43 - .../backend/src/server/api/endpoints/users.ts | 81 - .../src/server/api/endpoints/users/clips.ts | 43 - .../server/api/endpoints/users/followers.ts | 93 - .../server/api/endpoints/users/following.ts | 93 - .../api/endpoints/users/gallery/posts.ts | 42 - .../api/endpoints/users/groups/create.ts | 49 - .../api/endpoints/users/groups/delete.ts | 35 - .../users/groups/invitations/accept.ts | 46 - .../users/groups/invitations/reject.ts | 36 - .../api/endpoints/users/groups/invite.ts | 72 - .../api/endpoints/users/groups/joined.ts | 45 - .../api/endpoints/users/groups/leave.ts | 37 - .../api/endpoints/users/groups/owned.ts | 37 - .../server/api/endpoints/users/groups/pull.ts | 47 - .../server/api/endpoints/users/groups/show.ts | 50 - .../api/endpoints/users/groups/transfer.ts | 61 - .../api/endpoints/users/groups/update.ts | 47 - .../api/endpoints/users/lists/create.ts | 40 - .../api/endpoints/users/lists/delete.ts | 35 - .../server/api/endpoints/users/lists/list.ts | 37 - .../server/api/endpoints/users/lists/pull.ts | 48 - .../server/api/endpoints/users/lists/push.ts | 62 - .../server/api/endpoints/users/lists/show.ts | 42 - .../api/endpoints/users/lists/update.ts | 47 - .../src/server/api/endpoints/users/notes.ts | 116 - .../src/server/api/endpoints/users/pages.ts | 43 - .../server/api/endpoints/users/reactions.ts | 60 - .../api/endpoints/users/recommendation.ts | 62 - .../server/api/endpoints/users/relation.ts | 129 - .../api/endpoints/users/report-abuse.ts | 87 - .../users/search-by-username-and-host.ts | 116 - .../src/server/api/endpoints/users/search.ts | 125 - .../src/server/api/endpoints/users/show.ts | 117 - .../src/server/api/endpoints/users/stats.ts | 187 - packages/backend/src/server/api/error.ts | 387 -- packages/backend/src/server/api/index.ts | 147 - packages/backend/src/server/api/limiter.ts | 76 - packages/backend/src/server/api/logger.ts | 3 - .../src/server/api/openapi/gen-spec.ts | 226 - .../src/server/api/openapi/http-codes.ts | 67 - .../backend/src/server/api/openapi/schemas.ts | 63 - .../backend/src/server/api/private/signin.ts | 223 - .../src/server/api/private/signup-pending.ts | 35 - .../backend/src/server/api/private/signup.ts | 108 - .../backend/src/server/api/stream/channel.ts | 94 - .../src/server/api/stream/channels/admin.ts | 14 - .../src/server/api/stream/channels/antenna.ts | 56 - .../src/server/api/stream/channels/channel.ts | 80 - .../src/server/api/stream/channels/drive.ts | 14 - .../api/stream/channels/global-timeline.ts | 66 - .../src/server/api/stream/channels/hashtag.ts | 48 - .../api/stream/channels/home-timeline.ts | 64 - .../api/stream/channels/hybrid-timeline.ts | 72 - .../src/server/api/stream/channels/index.ts | 33 - .../api/stream/channels/local-timeline.ts | 63 - .../src/server/api/stream/channels/main.ts | 31 - .../api/stream/channels/messaging-index.ts | 14 - .../server/api/stream/channels/messaging.ts | 107 - .../server/api/stream/channels/queue-stats.ts | 42 - .../api/stream/channels/server-stats.ts | 42 - .../server/api/stream/channels/user-list.ts | 72 - .../backend/src/server/api/stream/index.ts | 420 -- .../backend/src/server/api/stream/types.ts | 264 - packages/backend/src/server/api/streaming.ts | 73 - .../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 | 40 - .../src/server/file/send-drive-file.ts | 126 - packages/backend/src/server/index.ts | 168 - packages/backend/src/server/nodeinfo.ts | 129 - packages/backend/src/server/oauth.ts | 16 - packages/backend/src/server/proxy/index.ts | 26 - .../backend/src/server/proxy/proxy-media.ts | 98 - packages/backend/src/server/web/boot.js | 4 +- packages/backend/src/server/web/error.css | 98 - packages/backend/src/server/web/feed.ts | 58 - packages/backend/src/server/web/index.ts | 515 -- packages/backend/src/server/web/manifest.json | 28 - packages/backend/src/server/web/manifest.ts | 16 - .../backend/src/server/web/url-preview.ts | 65 - .../backend/src/server/web/views/base.pug | 90 - .../backend/src/server/web/views/channel.pug | 18 - .../backend/src/server/web/views/clip.pug | 31 - .../backend/src/server/web/views/flush.pug | 53 - .../src/server/web/views/gallery-post.pug | 33 - .../src/server/web/views/info-card.pug | 50 - .../backend/src/server/web/views/note.pug | 42 - .../backend/src/server/web/views/page.pug | 31 - .../backend/src/server/web/views/user.pug | 39 - packages/backend/src/server/well-known.ts | 163 - .../src/services/add-note-to-antenna.ts | 55 - .../backend/src/services/blocking/create.ts | 143 - .../backend/src/services/blocking/delete.ts | 34 - .../src/services/chart/charts/active-users.ts | 40 - .../src/services/chart/charts/ap-request.ts | 38 - .../src/services/chart/charts/drive.ts | 36 - .../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 | 104 - .../src/services/chart/charts/hashtag.ts | 29 - .../src/services/chart/charts/instance.ts | 103 - .../src/services/chart/charts/notes.ts | 45 - .../services/chart/charts/per-user-drive.ts | 42 - .../chart/charts/per-user-following.ts | 56 - .../services/chart/charts/per-user-notes.ts | 41 - .../chart/charts/per-user-reactions.ts | 30 - .../src/services/chart/charts/test-grouped.ts | 35 - .../chart/charts/test-intersection.ts | 32 - .../src/services/chart/charts/test-unique.ts | 26 - .../backend/src/services/chart/charts/test.ts | 42 - .../src/services/chart/charts/users.ts | 41 - packages/backend/src/services/chart/core.ts | 675 -- .../backend/src/services/chart/entities.ts | 39 - packages/backend/src/services/chart/index.ts | 52 - .../src/services/create-notification.ts | 58 - .../src/services/create-system-user.ts | 64 - .../backend/src/services/delete-account.ts | 27 - .../backend/src/services/drive/add-file.ts | 504 -- .../backend/src/services/drive/delete-file.ts | 101 - .../drive/generate-video-thumbnail.ts | 28 - .../src/services/drive/image-processor.ts | 87 - .../src/services/drive/internal-storage.ts | 36 - packages/backend/src/services/drive/logger.ts | 3 - packages/backend/src/services/drive/s3.ts | 24 - .../src/services/drive/upload-from-url.ts | 71 - .../src/services/fetch-instance-metadata.ts | 271 - .../backend/src/services/following/create.ts | 200 - .../backend/src/services/following/delete.ts | 88 - .../backend/src/services/following/reject.ts | 122 - .../services/following/requests/accept-all.ts | 18 - .../src/services/following/requests/accept.ts | 36 - .../src/services/following/requests/cancel.ts | 41 - .../src/services/following/requests/create.ts | 69 - packages/backend/src/services/i/pin.ts | 98 - packages/backend/src/services/i/update.ts | 23 - .../src/services/insert-moderation-log.ts | 13 - .../backend/src/services/instance-actor.ts | 19 - packages/backend/src/services/logger.ts | 172 - .../backend/src/services/messages/create.ts | 103 - .../backend/src/services/messages/delete.ts | 30 - packages/backend/src/services/note/create.ts | 688 -- packages/backend/src/services/note/delete.ts | 165 - .../backend/src/services/note/polls/update.ts | 21 - .../backend/src/services/note/polls/vote.ts | 89 - .../src/services/note/reaction/create.ts | 151 - .../src/services/note/reaction/delete.ts | 58 - packages/backend/src/services/note/read.ts | 132 - packages/backend/src/services/note/unread.ts | 56 - packages/backend/src/services/note/unwatch.ts | 10 - packages/backend/src/services/note/watch.ts | 20 - .../backend/src/services/push-notification.ts | 77 - .../register-or-fetch-instance-doc.ts | 33 - packages/backend/src/services/relay.ts | 107 - packages/backend/src/services/send-email.ts | 122 - packages/backend/src/services/stream.ts | 115 - packages/backend/src/services/suspend-user.ts | 20 - .../backend/src/services/unsuspend-user.ts | 38 - .../backend/src/services/update-hashtag.ts | 128 - packages/backend/src/services/user-cache.ts | 52 - .../backend/src/services/user-list/push.ts | 27 - .../services/validate-email-for-account.ts | 34 - packages/backend/test/.eslintrc.cjs | 11 - packages/backend/test/activitypub.ts | 113 - packages/backend/test/ap-request.ts | 55 - packages/backend/test/api-visibility.ts | 476 -- packages/backend/test/api.ts | 83 - packages/backend/test/block.ts | 85 - packages/backend/test/chart.ts | 531 -- packages/backend/test/docker-compose.yml | 15 - packages/backend/test/endpoints.ts | 865 --- packages/backend/test/extract-mentions.ts | 42 - packages/backend/test/fetch-resource.ts | 205 - packages/backend/test/ff-visibility.ts | 167 - packages/backend/test/get-file-info.ts | 173 - packages/backend/test/loader.js | 34 - packages/backend/test/mfm.ts | 88 - packages/backend/test/misc/mock-resolver.ts | 35 - packages/backend/test/mute.ts | 123 - packages/backend/test/note.ts | 370 -- 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 | Bin 505 -> 0 bytes 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 | Bin 544 -> 0 bytes packages/backend/test/services/blocking.ts | 58 - packages/backend/test/streaming.ts | 545 -- packages/backend/test/thread-mute.ts | 103 - packages/backend/test/tsconfig.json | 41 - packages/backend/test/user-notes.ts | 61 - packages/backend/test/utils.ts | 324 - packages/backend/tsconfig.json | 49 - packages/backend/watch.mjs | 23 - packages/client/index.html | 12 + packages/client/package.json | 2 +- .../client/src/components/form/textarea.vue | 2 +- packages/client/src/config.ts | 11 +- packages/client/src/init.ts | 40 +- packages/client/src/os.ts | 261 +- packages/client/src/scripts/twemoji-base.ts | 2 +- packages/client/vite.config.ts | 7 - packages/foundkey-js/src/streaming.ts | 20 +- scripts/dev.mjs | 16 +- yarn.lock | 5609 +---------------- 1027 files changed, 1683 insertions(+), 68154 deletions(-) create mode 100644 locales/en-US.json delete mode 100644 packages/backend/.eslintignore delete mode 100644 packages/backend/.eslintrc.cjs delete mode 100644 packages/backend/.mocharc.json delete mode 100644 packages/backend/.vscode/settings.json delete mode 100644 packages/backend/jsconfig.json delete mode 100644 packages/backend/migration/1000000000000-Init.js delete mode 100644 packages/backend/migration/1556348509290-Pages.js delete mode 100644 packages/backend/migration/1556746559567-UserProfile.js delete mode 100644 packages/backend/migration/1557476068003-PinnedUsers.js delete mode 100644 packages/backend/migration/1557761316509-AddSomeUrls.js delete mode 100644 packages/backend/migration/1557932705754-ObjectStorageSetting.js delete mode 100644 packages/backend/migration/1558072954435-PageLike.js delete mode 100644 packages/backend/migration/1558103093633-UserGroup.js delete mode 100644 packages/backend/migration/1558257926829-UserGroupInvite.js delete mode 100644 packages/backend/migration/1558266512381-UserListJoining.js delete mode 100644 packages/backend/migration/1561706992953-webauthn.js delete mode 100644 packages/backend/migration/1561873850023-ChartIndexes.js delete mode 100644 packages/backend/migration/1562422242907-PasswordLessLogin.js delete mode 100644 packages/backend/migration/1562444565093-PinnedPage.js delete mode 100644 packages/backend/migration/1562448332510-PageTitleHideOption.js delete mode 100644 packages/backend/migration/1562869971568-ModerationLog.js delete mode 100644 packages/backend/migration/1563757595828-UsedUsername.js delete mode 100644 packages/backend/migration/1565634203341-room.js delete mode 100644 packages/backend/migration/1571220798684-CustomEmojiCategory.js delete mode 100644 packages/backend/migration/1572760203493-nodeinfo.js delete mode 100644 packages/backend/migration/1576269851876-TalkFederationId.js delete mode 100644 packages/backend/migration/1576869585998-ProxyRemoteFiles.js delete mode 100644 packages/backend/migration/1579267006611-v12.js delete mode 100644 packages/backend/migration/1579270193251-v12-2.js delete mode 100644 packages/backend/migration/1579282808087-v12-3.js delete mode 100644 packages/backend/migration/1579544426412-v12-4.js delete mode 100644 packages/backend/migration/1579977526288-v12-5.js delete mode 100644 packages/backend/migration/1579993013959-v12-6.js delete mode 100644 packages/backend/migration/1580069531114-v12-7.js delete mode 100644 packages/backend/migration/1580148575182-v12-8.js delete mode 100644 packages/backend/migration/1580154400017-v12-9.js delete mode 100644 packages/backend/migration/1580276619901-v12-10.js delete mode 100644 packages/backend/migration/1580331224276-v12-11.js delete mode 100644 packages/backend/migration/1580508795118-v12-12.js delete mode 100644 packages/backend/migration/1580543501339-v12-13.js delete mode 100644 packages/backend/migration/1580864313253-v12-14.js delete mode 100644 packages/backend/migration/1581526429287-user-group-invitation.js delete mode 100644 packages/backend/migration/1581695816408-user-group-antenna.js delete mode 100644 packages/backend/migration/1581708415836-drive-user-folder-id-index.js delete mode 100644 packages/backend/migration/1581979837262-promo.js delete mode 100644 packages/backend/migration/1582019042083-featured-injecttion.js delete mode 100644 packages/backend/migration/1582210532752-antenna-exclude.js delete mode 100644 packages/backend/migration/1582875306439-note-reaction-length.js delete mode 100644 packages/backend/migration/1585361548360-miauth.js delete mode 100644 packages/backend/migration/1585385921215-custom-notification.js delete mode 100644 packages/backend/migration/1585772678853-ap-url.js delete mode 100644 packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js delete mode 100644 packages/backend/migration/1586641139527-remote-reaction.js delete mode 100644 packages/backend/migration/1586708940386-pageAiScript.js delete mode 100644 packages/backend/migration/1588044505511-hCaptcha.js delete mode 100644 packages/backend/migration/1589023282116-pubRelay.js delete mode 100644 packages/backend/migration/1595075960584-blurhash.js delete mode 100644 packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js delete mode 100644 packages/backend/migration/1595676934834-instance-icon-url.js delete mode 100644 packages/backend/migration/1595771249699-word-mute.js delete mode 100644 packages/backend/migration/1595782306083-word-mute2.js delete mode 100644 packages/backend/migration/1596548170836-channel.js delete mode 100644 packages/backend/migration/1596786425167-channel2.js delete mode 100644 packages/backend/migration/1597230137744-objectStorageSetPublicRead.js delete mode 100644 packages/backend/migration/1597236229720-IncludingNotificationTypes.js delete mode 100644 packages/backend/migration/1597385880794-add-sensitive-index.js delete mode 100644 packages/backend/migration/1597459042300-channel-unread.js delete mode 100644 packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js delete mode 100644 packages/backend/migration/1600353287890-mutingNotificationTypes.js delete mode 100644 packages/backend/migration/1603094348345-refine-abuse-user-report.js delete mode 100644 packages/backend/migration/1603095701770-refine-abuse-user-report2.js delete mode 100644 packages/backend/migration/1603776877564-instance-theme-color.js delete mode 100644 packages/backend/migration/1603781553011-instance-favicon.js delete mode 100644 packages/backend/migration/1604821689616-delete-auto-watch.js delete mode 100644 packages/backend/migration/1605408848373-clip-description.js delete mode 100644 packages/backend/migration/1605408971051-comments.js delete mode 100644 packages/backend/migration/1605585339718-instance-pinned-pages.js delete mode 100644 packages/backend/migration/1605965516823-instance-images.js delete mode 100644 packages/backend/migration/1606191203881-no-crawle.js delete mode 100644 packages/backend/migration/1607151207216-instance-pinned-clip.js delete mode 100644 packages/backend/migration/1607353487793-isExplorable.js delete mode 100644 packages/backend/migration/1610277136869-registry.js delete mode 100644 packages/backend/migration/1610277585759-registry2.js delete mode 100644 packages/backend/migration/1610283021566-registry3.js delete mode 100644 packages/backend/migration/1611354329133-followersUri.js delete mode 100644 packages/backend/migration/1611397665007-gallery.js delete mode 100644 packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js delete mode 100644 packages/backend/migration/1612619156584-announcement-email.js delete mode 100644 packages/backend/migration/1613155914446-emailNotificationTypes.js delete mode 100644 packages/backend/migration/1613181457597-user-lang.js delete mode 100644 packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js delete mode 100644 packages/backend/migration/1615965918224-chart-v2.js delete mode 100644 packages/backend/migration/1615966519402-chart-v2-2.js delete mode 100644 packages/backend/migration/1618637372000-user-last-active-date.js delete mode 100644 packages/backend/migration/1618639857000-user-hide-online-status.js delete mode 100644 packages/backend/migration/1619942102890-password-reset.js delete mode 100644 packages/backend/migration/1620019354680-ad.js delete mode 100644 packages/backend/migration/1620364649428-ad2.js delete mode 100644 packages/backend/migration/1621479946000-add-note-indexes.js delete mode 100644 packages/backend/migration/1622679304522-user-profile-description-length.js delete mode 100644 packages/backend/migration/1622681548499-log-message-length.js delete mode 100644 packages/backend/migration/1626509500668-fix-remote-file-proxy.js delete mode 100644 packages/backend/migration/1629004542760-chart-reindex.js delete mode 100644 packages/backend/migration/1629024377804-deepl-integration.js delete mode 100644 packages/backend/migration/1629288472000-fix-channel-userId.js delete mode 100644 packages/backend/migration/1629512953000-user-is-deleted.js delete mode 100644 packages/backend/migration/1629778475000-deepl-integration2.js delete mode 100644 packages/backend/migration/1629833361000-AddShowTLReplies.js delete mode 100644 packages/backend/migration/1629968054000_userInstanceBlocks.js delete mode 100644 packages/backend/migration/1631880003000-user-block-federation.js delete mode 100644 packages/backend/migration/1633068642000-email-required-for-signup.js delete mode 100644 packages/backend/migration/1633071909016-user-pending.js delete mode 100644 packages/backend/migration/1634486652000-user-public-reactions.js delete mode 100644 packages/backend/migration/1634902659689-delete-log.js delete mode 100644 packages/backend/migration/1635500777168-note-thread-mute.js delete mode 100644 packages/backend/migration/1636197624383-ff-visibility.js delete mode 100644 packages/backend/migration/1636697408073-remove-via-mobile.js delete mode 100644 packages/backend/migration/1637320813000-forwarded-report.js delete mode 100644 packages/backend/migration/1639325650583-chart-v3.js delete mode 100644 packages/backend/migration/1642611822809-emoji-url.js delete mode 100644 packages/backend/migration/1642613870898-drive-file-webpublic-type.js delete mode 100644 packages/backend/migration/1642698156335-reported-urls.js delete mode 100644 packages/backend/migration/1643963705770-chart-v4.js delete mode 100644 packages/backend/migration/1643966656277-chart-v5.js delete mode 100644 packages/backend/migration/1643967331284-chart-v6.js delete mode 100644 packages/backend/migration/1644010796173-convert-hard-mutes.js delete mode 100644 packages/backend/migration/1644058404077-chart-v7.js delete mode 100644 packages/backend/migration/1644059847460-chart-v8.js delete mode 100644 packages/backend/migration/1644060125705-chart-v9.js delete mode 100644 packages/backend/migration/1644073149413-chart-v10.js delete mode 100644 packages/backend/migration/1644095659741-chart-v11.js delete mode 100644 packages/backend/migration/1644328606241-chart-v12.js delete mode 100644 packages/backend/migration/1644331238153-chart-v13.js delete mode 100644 packages/backend/migration/1644344266289-chart-v14.js delete mode 100644 packages/backend/migration/1644395759931-instance-theme-color.js delete mode 100644 packages/backend/migration/1644481657998-chart-v15.js delete mode 100644 packages/backend/migration/1644551208096-following-indexes.js delete mode 100644 packages/backend/migration/1645340161439-remove-max-note-text-length.js delete mode 100644 packages/backend/migration/1645599900873-federation-chart-pubsub.js delete mode 100644 packages/backend/migration/1646143552768-instance-default-theme.js delete mode 100644 packages/backend/migration/1646387162108-mute-expires-at.js delete mode 100644 packages/backend/migration/1646549089451-poll-ended-notification.js delete mode 100644 packages/backend/migration/1646633030285-chart-federation-active.js delete mode 100644 packages/backend/migration/1646655454495-remove-instance-drive-columns.js delete mode 100644 packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js delete mode 100644 packages/backend/migration/1648548247382-webhook.js delete mode 100644 packages/backend/migration/1648816172177-webhook-2.js delete mode 100644 packages/backend/migration/1651224615271-foreign-key.js delete mode 100644 packages/backend/migration/1652859567549-uniform-themecolor.js delete mode 100644 packages/backend/migration/1654124623992-remove-unused-image-urls.js delete mode 100644 packages/backend/migration/1655793461890-thread-mute-notifications.js delete mode 100644 packages/backend/migration/1657570176749-remove-ads.js delete mode 100644 packages/backend/migration/1658146000392-remove-repo-url.js delete mode 100644 packages/backend/migration/1658656633972-note-replies-function.js delete mode 100644 packages/backend/migration/1659335999000-pages-to-plaintext.js delete mode 100644 packages/backend/migration/1659516638000-drive-file-user-constraint.js delete mode 100644 packages/backend/migration/1660251834642-remove-promo-entities.js delete mode 100644 packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js delete mode 100644 packages/backend/migration/1662489803045-remove-rooms.js delete mode 100644 packages/backend/migration/1662943835603-larger-follow-request-ids.js delete mode 100644 packages/backend/migration/1662999442223-update-pinned-pages.js delete mode 100644 packages/backend/migration/1663399074403-resize-comments-drive-file.js delete mode 100644 packages/backend/migration/1665091090561-add-renote-muting.js delete mode 100644 packages/backend/migration/1667503570994-sync.js delete mode 100644 packages/backend/migration/1667653936442-token-permissions.js delete mode 100644 packages/backend/migration/1667738304733-pkce.js delete mode 100644 packages/backend/migration/1668374092227-forceEnablePush.js delete mode 100644 packages/backend/migration/1668661888188-add-libretranslate.js delete mode 100644 packages/backend/migration/1669545702493-detectDeeplPro.js delete mode 100644 packages/backend/migration/1670359028055-removeIntegrations.js delete mode 100644 packages/backend/migration/1672607891750-remove-reversi.js delete mode 100644 packages/backend/migration/1672991292018-remove-user-group-invite.js delete mode 100644 packages/backend/ormconfig.js delete mode 100644 packages/backend/package.json 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-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 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/redis.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/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/env.ts delete mode 100644 packages/backend/src/global.d.ts delete mode 100644 packages/backend/src/index.ts 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/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/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-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/reaction-lib.ts delete mode 100644 packages/backend/src/misc/renote.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/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/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-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/relation.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/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-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.ts delete mode 100644 packages/backend/src/queue/processors/system/clean-charts.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/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/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/auth-user.ts delete mode 100644 packages/backend/src/remote/activitypub/misc/contexts.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/compare-url.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/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/oauth.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/common/translator.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/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-all-files-of-a-user.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/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/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/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/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/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/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/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/deny.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/oauth.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/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/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/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/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-user-lists.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.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/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/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/ping.ts delete mode 100644 packages/backend/src/server/api/endpoints/pinned-users.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/unregister.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/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.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/openapi/gen-spec.ts delete mode 100644 packages/backend/src/server/api/openapi/http-codes.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/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/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/oauth.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/error.css 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/url-preview.ts delete mode 100644 packages/backend/src/server/web/views/base.pug delete mode 100644 packages/backend/src/server/web/views/channel.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/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.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/test/.eslintrc.cjs 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/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/services/blocking.ts 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 delete mode 100644 packages/backend/watch.mjs create mode 100644 packages/client/index.html diff --git a/.gitignore b/.gitignore index ddfa0195c..1ecf96860 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ cypress/videos !/.config/docker_example.env # misskey +/packages/client/dist /build built /data diff --git a/gulpfile.js b/gulpfile.js index 898869b06..5b5486084 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,20 +16,24 @@ gulp.task('copy:backend:views', () => ); gulp.task('copy:client:fonts', () => - gulp.src('./node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/_client_dist_/fonts/')) + gulp.src('./node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./packages/client/dist/fonts/')) ); gulp.task('copy:client:fontawesome', () => - gulp.src('./node_modules/@fortawesome/fontawesome-free/**/*').pipe(gulp.dest('./built/_client_dist_/fontawesome/')) + gulp.src('./node_modules/@fortawesome/fontawesome-free/**/*').pipe(gulp.dest('./packages/client/dist/fontawesome/')) +); + +gulp.task('copy:client:twemoji', () => + gulp.src('./node_modules/@discordapp/twemoji/dist/svg/**/*').pipe(gulp.dest('./packages/client/dist/twemoji/')) ); gulp.task('copy:client:locales', cb => { - fs.mkdirSync('./built/_client_dist_/locales', { recursive: true }); + fs.mkdirSync('./packages/client/dist/locales', { recursive: true }); const v = { '_version_': meta.version }; for (const [lang, locale] of Object.entries(locales)) { - fs.writeFileSync(`./built/_client_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8'); + fs.writeFileSync(`./packages/client/dist/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8'); } cb(); @@ -53,7 +57,7 @@ gulp.task('build:backend:style', () => { }); gulp.task('build', gulp.parallel( - 'copy:client:locales', 'copy:backend:views', 'build:backend:script', 'build:backend:style', 'copy:client:fonts', 'copy:client:fontawesome' + 'copy:client:locales', 'copy:backend:views', 'build:backend:script', 'build:backend:style', 'copy:client:fonts', 'copy:client:fontawesome', 'copy:client:twemoji' )); gulp.task('default', gulp.task('build')); diff --git a/locales/en-US.json b/locales/en-US.json new file mode 100644 index 000000000..ff2e62e73 --- /dev/null +++ b/locales/en-US.json @@ -0,0 +1,1314 @@ +{ + "_lang_": "English", + "headlineMisskey": "A network connected by notes", + "introMisskey": "Welcome! FoundKey is an open source, decentralized microblogging service.\nCreate \"notes\" to share your thoughts with everyone around you. 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes. 👍\nLet's explore a new world! 🚀", + "monthAndDay": "{month}/{day}", + "search": "Search", + "notifications": "Notifications", + "username": "Username", + "password": "Password", + "forgotPassword": "Forgot password", + "fetchingAsApObject": "Fetching from the Fediverse...", + "ok": "OK", + "gotIt": "Got it!", + "cancel": "Cancel", + "renotedBy": "Renoted by {user}", + "noNotes": "No notes", + "noNotifications": "No notifications", + "instance": "Instance", + "settings": "Settings", + "basicSettings": "Basic Settings", + "otherSettings": "Other Settings", + "openInWindow": "Open in window", + "profile": "Profile", + "timeline": "Timeline", + "noAccountDescription": "This user has not written their bio yet.", + "login": "Sign In", + "loggingIn": "Signing In", + "logout": "Sign Out", + "signup": "Sign Up", + "save": "Save", + "users": "Users", + "addUser": "Add a user", + "favorite": "Add to favorites", + "favorites": "Favorites", + "unfavorite": "Remove from favorites", + "pin": "Pin to profile", + "unpin": "Unpin from profile", + "copyContent": "Copy contents", + "copyLink": "Copy link", + "delete": "Delete", + "deleteAndEdit": "Delete and edit", + "deleteAndEditConfirm": "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it.", + "addToList": "Add to list", + "sendMessage": "Send a message", + "copyUsername": "Copy username", + "reply": "Reply", + "loadMore": "Load more", + "showMore": "Show more", + "showLess": "Show less", + "youGotNewFollower": "followed you", + "receiveFollowRequest": "Follow request received", + "followRequestAccepted": "Follow request accepted", + "mention": "Mention", + "mentions": "Mentions", + "directNotes": "Direct notes", + "importAndExport": "Import / Export", + "import": "Import", + "export": "Export", + "exportAll": "Export all", + "exportSelected": "Export selected", + "files": "Files", + "download": "Download", + "driveFileDeleteConfirm": "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted.", + "unfollowConfirm": "Are you sure that you want to unfollow {name}?", + "exportRequested": "You've requested an export. This may take a while. It will be added to your Drive once completed.", + "importRequested": "You've requested an import. This may take a while.", + "lists": "Lists", + "note": "Note", + "notes": "Notes", + "following": "Following", + "followers": "Followers", + "followsYou": "Follows you", + "createList": "Create list", + "manageLists": "Manage lists", + "error": "Error", + "somethingHappened": "An error has occurred", + "retry": "Retry", + "pageLoadError": "An error occurred loading the page.", + "pageLoadErrorDescription": "This is normally caused by network errors or the browser's cache. Try clearing the cache and then try again after waiting a little while.", + "serverIsDead": "This server is not responding. Please wait for a while and try again.", + "youShouldUpgradeClient": "To view this page, please refresh to update your client.", + "enterListName": "Enter a name for the list", + "privacy": "Privacy", + "makeFollowManuallyApprove": "Follow requests require approval", + "defaultNoteVisibility": "Default visibility", + "follow": "Follow", + "followRequest": "Send follow request", + "followRequests": "Follow requests", + "unfollow": "Unfollow", + "followRequestPending": "Follow request pending", + "renote": "Renote", + "unrenote": "Take back renote", + "unrenoteAll": "Take back all renotes", + "unrenoteAllConfirm": "Are you sure that you want to take back all renotes of this note?", + "quote": "Quote", + "pinnedNote": "Pinned note", + "you": "You", + "clickToShow": "Click to show", + "sensitive": "NSFW", + "add": "Add", + "reaction": "Reactions", + "reactionSettingDescription2": "Drag to reorder, click to delete, press \"+\" to add.", + "attachCancel": "Remove attachment", + "markAsSensitive": "Mark as NSFW", + "unmarkAsSensitive": "Unmark as NSFW", + "enterFileName": "Enter filename", + "mute": "Mute", + "unmute": "Unmute", + "renoteMute": "Hide renotes", + "renoteUnmute": "Show renotes", + "block": "Block", + "unblock": "Unblock", + "suspend": "Suspend", + "unsuspend": "Unsuspend", + "blockConfirm": "Are you sure that you want to block this account?", + "unblockConfirm": "Are you sure that you want to unblock this account?", + "suspendConfirm": "Are you sure that you want to suspend this account?", + "unsuspendConfirm": "Are you sure that you want to unsuspend this account?", + "selectList": "Select a list", + "selectAntenna": "Select an antenna", + "selectWidget": "Select a widget", + "editWidgets": "Edit widgets", + "editWidgetsExit": "Done", + "customEmojis": "Custom Emoji", + "emoji": "Emoji", + "emojis": "Emoji", + "addEmoji": "Add an emoji", + "cacheRemoteFiles": "Cache remote files", + "cacheRemoteFilesDescription": "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated.", + "flagAsBot": "Mark this account as a bot", + "flagAsBotDescription": "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust FoundKey's internal systems to treat this account as a bot.", + "flagAsCat": "Mark this account as a cat", + "flagAsCatDescription": "Enable this option to mark this account as a cat.", + "flagShowTimelineReplies": "Show replies in timeline", + "flagShowTimelineRepliesDescription": "Shows replies of users to notes of other users in the timeline if turned on.", + "autoAcceptFollowed": "Automatically approve follow requests from users you're following", + "addAccount": "Add account", + "loginFailed": "Failed to sign in", + "showOnRemote": "View on remote instance", + "general": "General", + "setWallpaper": "Set wallpaper", + "removeWallpaper": "Remove wallpaper", + "youHaveNoLists": "You don't have any lists", + "followConfirm": "Are you sure that you want to follow {name}?", + "proxyAccount": "Proxy account", + "proxyAccountDescription": "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead.", + "host": "Host", + "selectUser": "Select a user", + "recipient": "Recipient", + "annotation": "Comments", + "federation": "Federation", + "registeredAt": "Registered at", + "latestRequestSentAt": "Last request sent", + "latestRequestReceivedAt": "Last request received", + "latestStatus": "Latest status", + "charts": "Charts", + "perHour": "Per Hour", + "perDay": "Per Day", + "stopActivityDelivery": "Stop sending activities", + "stopActivityDeliveryDescription": "Local activities will not be sent to this instance. Receiving activities works as before.", + "blockThisInstance": "Block this instance", + "blockThisInstanceDescription": "Local activites will not be sent to this instance. Activites from this instance will be discarded.", + "software": "Software", + "version": "Version", + "withNFiles": "{n} file(s)", + "jobQueue": "Job Queue", + "instanceInfo": "Instance Information", + "statistics": "Statistics", + "clearQueue": "Clear queue", + "clearQueueConfirmTitle": "Are you sure that you want to clear the queue?", + "clearQueueConfirmText": "Any undelivered notes remaining in the queue will not be federated. Usually this operation is not needed.", + "clearCachedFiles": "Clear cache", + "clearCachedFilesConfirm": "Are you sure that you want to delete all cached remote files?", + "blockedInstances": "Blocked Instances", + "blockedInstancesDescription": "List the hostnames of the instances that you want to block. Listed instances will no longer be able to communicate with this instance. Non-ASCII domain names must be encoded in punycode. Subdomains of the listed instances will also be blocked.", + "muteAndBlock": "Mutes and Blocks", + "mutedUsers": "Muted users", + "blockedUsers": "Blocked users", + "noUsers": "There are no users", + "editProfile": "Edit profile", + "noteDeleteConfirm": "Are you sure you want to delete this note?", + "pinLimitExceeded": "You cannot pin any more notes.", + "intro": "Installation of FoundKey has been finished! Please create an admin user.", + "done": "Done", + "processing": "Processing...", + "preview": "Preview", + "default": "Default", + "noCustomEmojis": "There are no emoji", + "noJobs": "There are no jobs", + "federating": "Federating", + "blocked": "Blocked", + "suspended": "Suspended", + "all": "All", + "subscribing": "Subscribing", + "publishing": "Publishing", + "notResponding": "Not responding", + "changePassword": "Change password", + "security": "Security", + "retypedNotMatch": "The inputs do not match.", + "currentPassword": "Current password", + "newPassword": "New password", + "newPasswordRetype": "Retype new password", + "attachFile": "Attach files", + "more": "More!", + "featured": "Featured", + "usernameOrUserId": "Username or user id", + "noSuchUser": "User not found", + "lookup": "Lookup", + "announcements": "Announcements", + "imageUrl": "Image URL", + "remove": "Delete", + "removeAreYouSure": "Are you sure that you want to remove \"{x}\"?", + "deleteAreYouSure": "Are you sure that you want to delete \"{x}\"?", + "resetAreYouSure": "Really reset?", + "saved": "Saved", + "messaging": "Chat", + "upload": "Upload", + "keepOriginalUploading": "Keep original image", + "keepOriginalUploadingDescription": "Saves the originally uploaded image as-is. If turned off, a version to display on the web will be generated on upload.", + "fromDrive": "From Drive", + "fromUrl": "From URL", + "uploadFromUrl": "Upload from a URL", + "uploadFromUrlDescription": "URL of the file you want to upload", + "uploadFromUrlRequested": "Upload requested", + "uploadFromUrlMayTakeTime": "It may take some time until the upload is complete.", + "explore": "Explore", + "messageRead": "Read", + "noMoreHistory": "There is no further history", + "startMessaging": "Start a new chat", + "nUsersRead": "read by {n}", + "agreeTo": "I agree to {0}", + "tos": "Terms of Service", + "start": "Begin", + "home": "Home", + "remoteUserCaution": "As this user is from a remote instance, the shown information may be incomplete.", + "activity": "Activity", + "images": "Images", + "birthday": "Birthday", + "yearsOld": "{age} years old", + "registeredDate": "Joined on", + "location": "Location", + "theme": "Themes", + "themeForLightMode": "Theme to use in Light Mode", + "themeForDarkMode": "Theme to use in Dark Mode", + "light": "Light", + "dark": "Dark", + "lightThemes": "Light themes", + "darkThemes": "Dark themes", + "syncDeviceDarkMode": "Sync Dark Mode with your device settings", + "drive": "Drive", + "selectFile": "Select a file", + "selectFiles": "Select files", + "selectFolder": "Select a folder", + "selectFolders": "Select folders", + "renameFile": "Rename file", + "folderName": "Folder name", + "createFolder": "Create a folder", + "renameFolder": "Rename this folder", + "deleteFolder": "Delete this folder", + "addFile": "Add a file", + "emptyDrive": "Your Drive is empty", + "emptyFolder": "This folder is empty", + "unableToDelete": "Unable to delete", + "inputNewFileName": "Enter a new filename", + "inputNewDescription": "Enter new caption", + "inputNewFolderName": "Enter a new folder name", + "circularReferenceFolder": "The destination folder is a subfolder of the folder you wish to move.", + "hasChildFilesOrFolders": "Since this folder is not empty, it can not be deleted.", + "copyUrl": "Copy URL", + "rename": "Rename", + "avatar": "Avatar", + "banner": "Banner", + "nsfw": "NSFW", + "whenServerDisconnected": "When losing connection to the server", + "disconnectedFromServer": "Connection to server has been lost", + "reload": "Refresh", + "doNothing": "Ignore", + "reloadConfirm": "Would you like to refresh the timeline?", + "watch": "Watch", + "unwatch": "Stop watching", + "accept": "Accept", + "reject": "Reject", + "normal": "Normal", + "instanceName": "Instance name", + "instanceDescription": "Instance description", + "maintainerName": "Maintainer", + "maintainerEmail": "Maintainer email", + "tosUrl": "Terms of Service URL", + "thisYear": "Year", + "thisMonth": "Month", + "today": "Today", + "dayX": "{day}", + "monthX": "{month}", + "yearX": "{year}", + "pages": "Pages", + "enableLocalTimeline": "Enable local timeline", + "enableGlobalTimeline": "Enable global timeline", + "disablingTimelinesInfo": "Adminstrators and Moderators will always have access to all timelines, even if they are not enabled.", + "enableRegistration": "Enable new user registration", + "invite": "Invite", + "driveCapacityPerLocalAccount": "Drive capacity per local user", + "driveCapacityPerRemoteAccount": "Drive capacity per remote user", + "inMb": "In megabytes", + "iconUrl": "Icon URL", + "bannerUrl": "Banner image URL", + "backgroundImageUrl": "Background image URL", + "pinnedUsers": "Pinned users", + "pinnedUsersDescription": "List usernames separated by line breaks to be pinned in the \"Explore\" tab.", + "hcaptchaSiteKey": "Site key", + "hcaptchaSecretKey": "Secret key", + "recaptchaSiteKey": "Site key", + "recaptchaSecretKey": "Secret key", + "antennas": "Antennas", + "manageAntennas": "Manage Antennas", + "name": "Name", + "antennaSource": "Antenna source", + "antennaKeywords": "Keywords to listen to", + "antennaExcludeKeywords": "Keywords to exclude", + "antennaKeywordsDescription": "Separate with spaces for an AND condition or with line breaks for an OR condition.", + "notifyAntenna": "Notify about new notes", + "withFileAntenna": "Only notes with files", + "antennaUsersDescription": "List one username per line", + "caseSensitive": "Case sensitive", + "withReplies": "Include replies", + "connectedTo": "Following account(s) are connected", + "notesAndReplies": "Notes and replies", + "withFiles": "Including files", + "silence": "Silence", + "silenceConfirm": "Are you sure that you want to silence this user?", + "unsilence": "Undo silencing", + "unsilenceConfirm": "Are you sure that you want to undo the silencing of this user?", + "popularUsers": "Popular users", + "recentlyUpdatedUsers": "Recently active users", + "recentlyRegisteredUsers": "Newly joined users", + "recentlyDiscoveredUsers": "Newly discovered users", + "popularTags": "Popular tags", + "userList": "Lists", + "aboutMisskey": "About FoundKey", + "administrator": "Administrator", + "token": "Token", + "twoStepAuthentication": "Two-factor authentication", + "moderator": "Moderator", + "nUsersMentioned": "Mentioned by {n} users", + "securityKey": "Security key", + "securityKeyName": "Key name", + "registerSecurityKey": "Register a security key", + "lastUsed": "Last used", + "unregister": "Unregister", + "passwordLessLogin": "Password-less login", + "resetPassword": "Reset password", + "newPasswordIs": "The new password is \"{password}\"", + "reduceUiAnimation": "Reduce UI animations", + "share": "Share", + "notFound": "Not found", + "notFoundDescription": "No page corresponding to this URL could be found.", + "uploadFolder": "Default folder for uploads", + "markAsReadAllNotifications": "Mark all notifications as read", + "markAsReadAllUnreadNotes": "Mark all notes as read", + "markAsReadAllTalkMessages": "Mark all messages as read", + "help": "Help", + "inputMessageHere": "Enter message here", + "close": "Close", + "group": "Group", + "groups": "Groups", + "createGroup": "Create a group", + "ownedGroups": "Owned Groups", + "joinedGroups": "Joined groups", + "invites": "Invites", + "groupName": "Group name", + "members": "Members", + "transfer": "Transfer", + "messagingWithUser": "Private chat", + "messagingWithGroup": "Group chat", + "title": "Title", + "text": "Text", + "enable": "Enable", + "next": "Next", + "retype": "Enter again", + "noteOf": "Note by {user}", + "inviteToGroup": "Invite to group", + "quoteAttached": "Quote", + "quoteQuestion": "Append as quote?", + "noMessagesYet": "No messages yet", + "newMessageExists": "There are new messages", + "onlyOneFileCanBeAttached": "You can only attach one file to a message", + "signinRequired": "Please sign in", + "invitationCode": "Invitation code", + "checking": "Checking...", + "available": "Available", + "unavailable": "Not available", + "usernameInvalidFormat": "You can use upper- and lowercase letters, numbers, and underscores.", + "tooShort": "Too short", + "tooLong": "Too long", + "weakPassword": "Weak password", + "normalPassword": "Average password", + "strongPassword": "Strong password", + "passwordMatched": "Matches", + "passwordNotMatched": "Does not match", + "signinFailed": "Unable to sign in. The entered username or password is incorrect.", + "tapSecurityKey": "Tap your security key", + "or": "Or", + "language": "Language", + "uiLanguage": "User interface language", + "groupInvited": "You've been invited to a group", + "useOsNativeEmojis": "Use OS native Emoji", + "disableDrawer": "Don't use drawer-style menus", + "youHaveNoGroups": "You have no groups", + "joinOrCreateGroup": "Get invited to a group or create your own.", + "noHistory": "No history available", + "signinHistory": "Login history", + "signinHistoryExpires": "Data about past login attempts is automatically deleted after 60 days to comply with privacy regulations.", + "disableAnimatedMfm": "Disable MFM with animation", + "category": "Category", + "tags": "Tags", + "createAccount": "Create account", + "existingAccount": "Existing account", + "fontSize": "Font size", + "noFollowRequests": "You don't have any pending follow requests", + "openImageInNewTab": "Open images in new tab", + "dashboard": "Dashboard", + "local": "Local", + "remote": "Remote", + "dayOverDayChanges": "Changes to yesterday", + "appearance": "Appearance", + "clientSettings": "Client Settings", + "showFeaturedNotesInTimeline": "Show featured notes in timelines", + "objectStorage": "Object Storage", + "useObjectStorage": "Use object storage", + "objectStorageBaseUrl": "Base URL", + "objectStorageBaseUrlDesc": "The URL used as reference. Specify the URL of your CDN or Proxy if you are using either.\nFor S3 use 'https://.s3.amazonaws.com' and for GCS or equivalent services use 'https://storage.googleapis.com/', etc.", + "objectStorageBucket": "Bucket", + "objectStorageBucketDesc": "Please specify the bucket name used at your provider.", + "objectStoragePrefix": "Prefix", + "objectStoragePrefixDesc": "Files will be stored under directories with this prefix.", + "objectStorageEndpoint": "Endpoint", + "objectStorageEndpointDesc": "Leave this empty if you are using AWS S3, otherwise specify the endpoint as '' or ':', depending on the service you are using.", + "objectStorageRegion": "Region", + "objectStorageRegionDesc": "Specify a region like 'xx-east-1'. If your service does not distinguish between regions, leave this blank or enter 'us-east-1'.", + "objectStorageUseSSL": "Use SSL", + "objectStorageUseSSLDesc": "Turn this off if you are not going to use HTTPS for API connections", + "objectStorageUseProxy": "Connect over Proxy", + "objectStorageUseProxyDesc": "Turn this off if you are not going to use a Proxy for API connections", + "objectStorageSetPublicRead": "Set \"public-read\" on upload", + "showFixedPostForm": "Display the posting form at the top of the timeline", + "newNoteRecived": "There are new notes", + "sounds": "Sounds", + "listen": "Listen", + "none": "None", + "showInPage": "Show in page", + "popout": "Pop-out", + "volume": "Volume", + "masterVolume": "Master volume", + "details": "Details", + "unableToProcess": "The operation could not be completed", + "recentUsed": "Recently used", + "install": "Install", + "uninstall": "Uninstall", + "installedApps": "Authorized Applications", + "nothing": "There's nothing to see here", + "installedDate": "Authorized at", + "lastUsedDate": "Last used at", + "state": "State", + "sort": "Sort", + "ascendingOrder": "Ascending", + "descendingOrder": "Descending", + "scratchpad": "Scratchpad", + "scratchpadDescription": "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with FoundKey in it.", + "output": "Output", + "updateRemoteUser": "Update remote user information", + "deleteAllFiles": "Delete all files", + "deleteAllFilesConfirm": "Are you sure that you want to delete all files?", + "removeAllFollowing": "Unfollow all followed users", + "removeAllFollowingDescription": "Executing this unfollows all accounts from {host}. Please run this if the instance e.g. no longer exists.", + "userSuspended": "This user has been suspended.", + "userSilenced": "This user is being silenced.", + "yourAccountSuspendedTitle": "This account is suspended", + "yourAccountSuspendedDescription": "This account has been suspended due to breaking the server's terms of services or similar. Contact the administrator if you would like to know a more detailed reason. Please do not create a new account.", + "menu": "Menu", + "divider": "Divider", + "addItem": "Add Item", + "relays": "Relays", + "addRelay": "Add Relay", + "inboxUrl": "Inbox URL", + "deletedNote": "Deleted note", + "enableInfiniteScroll": "Automatically load more", + "visibility": "Visiblility", + "poll": "Poll", + "useCw": "Hide content", + "enablePlayer": "Open video player", + "disablePlayer": "Close video player", + "themeEditor": "Theme editor", + "description": "Description", + "describeFile": "Add caption", + "author": "Author", + "leaveConfirm": "There are unsaved changes. Do you want to discard them?", + "manage": "Management", + "plugins": "Plugins", + "deck": "Deck", + "useBlurEffectForModal": "Use blur effect for modals", + "width": "Width", + "height": "Height", + "large": "Big", + "medium": "Medium", + "small": "Small", + "generateAccessToken": "Generate access token", + "permission": "Permissions", + "enableAll": "Enable all", + "disableAll": "Disable all", + "tokenRequested": "Grant access to account", + "pluginTokenRequestedDescription": "This plugin will be able to use the permissions set here.", + "edit": "Edit", + "useStarForReactionFallback": "Use ★ as fallback if the reaction emoji is unknown", + "emailServer": "Email server", + "enableEmail": "Enable email distribution", + "emailConfigInfo": "Used to confirm your email during sign-up or if you forget your password", + "email": "Email", + "emailAddress": "Email address", + "smtpConfig": "SMTP Server Configuration", + "smtpHost": "Host", + "smtpPort": "Port", + "smtpUser": "Username", + "smtpPass": "Password", + "emptyToDisableSmtpAuth": "Leave username and password empty to disable SMTP verification", + "smtpSecure": "Use implicit SSL/TLS for SMTP connections", + "smtpSecureInfo": "Turn this off when using STARTTLS.", + "testEmail": "Test email delivery", + "wordMute": "Word mute", + "regexpError": "Regular Expression error", + "regexpErrorDescription": "An error occurred in the regular expression on line {line} of your {tab} word mutes:", + "instanceMute": "Instance Mutes", + "userSaysSomething": "{name} said something", + "makeActive": "Activate", + "display": "Display", + "copy": "Copy", + "overview": "Overview", + "database": "Database", + "channel": "Channels", + "create": "Create", + "notificationSetting": "Notification settings", + "notificationSettingDesc": "Select the types of notification to display.", + "useGlobalSetting": "Use global settings", + "useGlobalSettingDesc": "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made.", + "other": "Other", + "regenerateLoginToken": "Regenerate login token", + "regenerateLoginTokenDescription": "Regenerates the token used internally during login. Normally this action is not necessary. If regenerated, all devices will be logged out.", + "setMultipleBySeparatingWithSpace": "Separate multiple entries with spaces.", + "fileIdOrUrl": "File ID or URL", + "behavior": "Behavior", + "abuseReports": "Reports", + "reportAbuse": "Report", + "reportAbuseOf": "Report {name}", + "fillAbuseReportDescription": "Please fill in details regarding this report.", + "abuseReported": "Your report has been sent. Thank you very much.", + "reporter": "Reporter", + "reporteeOrigin": "Reportee Origin", + "reporterOrigin": "Reporter Origin", + "forwardReport": "Forward report to remote instance", + "forwardReportIsAnonymous": "Instead of your account, an anonymous system account will be displayed as reporter at the remote instance.", + "send": "Send", + "abuseMarkAsResolved": "Mark report as resolved", + "openInNewTab": "Open in new tab", + "defaultNavigationBehaviour": "Default navigation behavior", + "instanceTicker": "Instance information of notes", + "system": "System", + "switchUi": "Switch UI", + "desktop": "Desktop", + "clip": "Clip", + "createNew": "Create new", + "optional": "Optional", + "createNewClip": "Create new clip", + "public": "Public", + "i18nInfo": "FoundKey is being translated into various languages by volunteers. You can help at {link}.", + "manageAccessTokens": "Manage access tokens", + "accountInfo": "Account Info", + "notesCount": "Number of notes", + "repliesCount": "Number of replies sent", + "renotesCount": "Number of renotes sent", + "repliedCount": "Number of replies received", + "renotedCount": "Number of renotes received", + "followingCount": "Number of followed accounts", + "followersCount": "Number of followers", + "sentReactionsCount": "Number of sent reactions", + "receivedReactionsCount": "Number of received reactions", + "pollVotesCount": "Number of sent poll votes", + "pollVotedCount": "Number of received poll votes", + "yes": "Yes", + "no": "No", + "driveFilesCount": "Number of Drive files", + "driveUsage": "Drive space usage", + "noCrawle": "Reject crawler indexing", + "noCrawleDescription": "Ask search engines to not index your profile page, notes, Pages, etc.", + "lockedAccountInfo": "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved.", + "alwaysMarkSensitive": "Mark as NSFW by default", + "loadRawImages": "Load original images instead of showing thumbnails", + "disableShowingAnimatedImages": "Don't play animated images", + "verificationEmailSent": "A verification email has been sent. Please follow the included link to complete verification.", + "emailVerified": "Email has been verified", + "noteFavoritesCount": "Number of favorite notes", + "pageLikesCount": "Number of liked Pages", + "pageLikedCount": "Number of received Page likes", + "contact": "Contact", + "useSystemFont": "Use the system's default font", + "clips": "Clips", + "makeExplorable": "Make account visible in \"Explore\"", + "makeExplorableDescription": "If you turn this off, your account will not show up in the \"Explore\" section.", + "showGapBetweenNotesInTimeline": "Show a gap between posts on the timeline", + "duplicate": "Duplicate", + "left": "Left", + "center": "Center", + "wide": "Wide", + "narrow": "Narrow", + "reloadToApplySetting": "This setting will only apply after a page reload. Reload now?", + "needReloadToApply": "A reload is required for this to be reflected.", + "clearCache": "Clear cache", + "onlineUsersCount": "{n} users are online", + "backgroundColor": "Background color", + "accentColor": "Accent color", + "textColor": "Text color", + "saveAs": "Save as...", + "createdAt": "Created at", + "updatedAt": "Updated at", + "deleteConfirm": "Really delete?", + "closeAccount": "Close account", + "newVersionOfClientAvailable": "There is a newer version of your client available.", + "usageAmount": "Usage", + "capacity": "Capacity", + "inUse": "Used", + "editCode": "Edit code", + "apply": "Apply", + "receiveAnnouncementFromInstance": "Receive notifications from this instance", + "emailNotification": "Email notifications", + "publish": "Publish", + "useReactionPickerForContextMenu": "Open reaction picker on right-click", + "typingUsers": "{users} is/are typing...", + "jumpToSpecifiedDate": "Jump to specific date", + "clear": "Return", + "markAllAsRead": "Mark all as read", + "goBack": "Back", + "unlikeConfirm": "Really remove your like?", + "fullView": "Full view", + "quitFullView": "Exit full view", + "addDescription": "Add description", + "userPagePinTip": "You can display notes here by selecting \"Pin to profile\" from the menu of individual notes.", + "notSpecifiedMentionWarning": "This note contains mentions of users not included as recipients", + "info": "About", + "userInfo": "User information", + "unknown": "Unknown", + "hideOnlineStatus": "Hide online status", + "hideOnlineStatusDescription": "Hiding your online status reduces the convenience of some features such as the search.", + "federateBlocks": "Federate blocks", + "federateBlocksDescription": "If disabled, block activities won't be sent.", + "online": "Online", + "active": "Active", + "offline": "Offline", + "notRecommended": "Not recommended", + "botProtection": "Bot Protection", + "instanceBlocking": "Blocked Instances", + "selectAccount": "Select account", + "switchAccount": "Switch account", + "enabled": "Enabled", + "disabled": "Disabled", + "quickAction": "Quick actions", + "user": "User", + "administration": "Management", + "accounts": "Accounts", + "switch": "Switch", + "noMaintainerInformationWarning": "Maintainer information is not configured.", + "noBotProtectionWarning": "Bot protection is not configured.", + "configure": "Configure", + "postToGallery": "Create new gallery post", + "attachmentRequired": "At least 1 attachment is required.", + "gallery": "Gallery", + "recentPosts": "Recent posts", + "popularPosts": "Popular posts", + "shareWithNote": "Share with note", + "emailNotConfiguredWarning": "Email address not set.", + "ratio": "Ratio", + "previewNoteText": "Show preview", + "customCss": "Custom CSS", + "customCssWarn": "This setting should only be used if you know what it does. Entering improper values may cause the client to stop functioning normally.", + "squareAvatars": "Display squared avatars", + "searchResult": "Search results", + "hashtags": "Hashtags", + "troubleshooting": "Troubleshooting", + "useBlurEffect": "Use blur effects in the UI", + "learnMore": "Learn more", + "misskeyUpdated": "FoundKey has been updated!", + "whatIsNew": "Show changes", + "translate": "Translate", + "translatedFrom": "Translated from {x}", + "translationSettings": "Translation Settings", + "translationService": "Translation Service", + "accountDeletionInProgress": "Account deletion is currently in progress.", + "usernameInfo": "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later.", + "keepCw": "Keep content warnings", + "pubSub": "Pub/Sub Accounts", + "lastCommunication": "Last communication", + "resolved": "Resolved", + "unresolved": "Unresolved", + "breakFollow": "Remove follower", + "itsOn": "Enabled", + "itsOff": "Disabled", + "emailRequiredForSignup": "Require email address for sign-up", + "unread": "Unread", + "filter": "Filter", + "controlPanel": "Control Panel", + "manageAccounts": "Manage Accounts", + "makeReactionsPublic": "Set reaction history to public", + "makeReactionsPublicDescription": "This will make the list of all your past reactions publicly visible.", + "classic": "Classic", + "muteThread": "Mute thread", + "unmuteThread": "Unmute thread", + "threadMuteNotificationsDesc": "Select the notifications you wish to view from this thread. Global notification settings also apply. Disabling takes precedence.", + "ffVisibility": "Follows/Followers Visibility", + "ffVisibilityDescription": "Allows you to configure who can see who you follow and who follows you.", + "continueThread": "View thread continuation", + "deleteAccountConfirm": "This will irreversibly delete the account {handle}. Proceed?", + "incorrectPassword": "Incorrect password.", + "voteConfirm": "Confirm your vote for \"{choice}\"?", + "hide": "Hide", + "leaveGroup": "Leave group", + "leaveGroupConfirm": "Are you sure you want to leave \"{name}\"?", + "useDrawerReactionPickerForMobile": "Display reaction picker as drawer on mobile", + "clickToFinishEmailVerification": "Please click [{ok}] to complete email verification.", + "overridedDeviceKind": "Device type", + "smartphone": "Smartphone", + "tablet": "Tablet", + "auto": "Auto", + "themeColor": "Instance Ticker Color", + "size": "Size", + "numberOfColumn": "Number of columns", + "instanceDefaultLightTheme": "Instance-wide default light theme", + "instanceDefaultDarkTheme": "Instance-wide default dark theme", + "instanceDefaultThemeDescription": "Enter the theme code in object format.", + "mutePeriod": "Mute duration", + "indefinitely": "Permanently", + "tenMinutes": "10 minutes", + "oneHour": "One hour", + "oneDay": "One day", + "oneWeek": "One week", + "reflectMayTakeTime": "It may take some time for this to be reflected.", + "failedToFetchAccountInformation": "Could not fetch account information", + "rateLimitExceeded": "Rate limit exceeded", + "cropImage": "Crop image", + "cropImageAsk": "Do you want to crop this image?", + "recentNHours": "Last {n} hours", + "recentNDays": "Last {n} days", + "isSystemAccount": "An account created and automatically operated by the system.", + "typeToConfirm": "Please enter {x} to confirm", + "deleteAccount": "Delete account", + "numberOfPageCache": "Number of cached pages", + "numberOfPageCacheDescription": "Increasing this number will improve convenience for users but cause more server load as well as more memory to be used.", + "documentation": "Documentation", + "file": "File", + "unclip": "Unclip", + "confirmToUnclipAlreadyClippedNote": "This note is already part of the \"{name}\" clip. Do you want to remove it from this clip instead?", + "noEmailServerWarning": "Email server not configured.", + "thereIsUnresolvedAbuseReportWarning": "There are unsolved reports.", + "recommended": "Recommended", + "check": "Check", + "unlimited": "Unlimited", + "selectMode": "Select multiple", + "selectAll": "Select all", + "setCategory": "Set category", + "setTag": "Set tag", + "addTag": "Add tag", + "removeTag": "Remove tag", + "externalCssSnippets": "Some CSS snippets for your inspiration (not managed by FoundKey)", + "oauthErrorGoBack": "An error happened while trying to authenticate a 3rd party app. Please go back and try again.", + "appAuthorization": "App authorization", + "noPermissionsRequested": "(No permissions requested.)", + "_emailUnavailable": { + "used": "This email address is already being used", + "format": "The format of this email address is invalid", + "disposable": "Disposable email addresses may not be used", + "mx": "This email server is invalid", + "smtp": "This email server is not responding" + }, + "_ffVisibility": { + "public": "Public", + "followers": "Visible to followers only", + "private": "Private" + }, + "_signup": { + "almostThere": "Almost there", + "emailAddressInfo": "Please enter your email address. It will not be made public.", + "emailSent": "A confirmation email has been sent to your email address ({email}). Please click the included link to complete account creation." + }, + "_accountDelete": { + "accountDelete": "Delete account", + "mayTakeTime": "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded.", + "sendEmail": "Once account deletion has been completed, an email will be sent to the email address registered to this account.", + "requestAccountDelete": "Request account deletion", + "started": "Deletion has been started.", + "inProgress": "Deletion is currently in progress" + }, + "_forgotPassword": { + "enterEmail": "Enter the email address you used to register. A link with which you can reset your password will then be sent to it.", + "ifNoEmail": "If you did not use an email during registration, please contact the instance administrator instead.", + "contactAdmin": "This instance does not support using email addresses, please contact the instance administrator to reset your password instead." + }, + "_gallery": { + "my": "My Gallery", + "liked": "Liked Posts", + "like": "Like", + "unlike": "Remove like" + }, + "_email": { + "_follow": { + "title": "You've got a new follower" + }, + "_receiveFollowRequest": { + "title": "You've received a follow request" + } + }, + "_plugin": { + "install": "Install plugins", + "installWarn": "Please do not install untrustworthy plugins." + }, + "_registry": { + "scope": "Scope", + "key": "Key", + "keys": "Keys", + "domain": "Domain", + "createKey": "Create key" + }, + "_aboutMisskey": { + "about": "FoundKey is a fork of Misskey developed since July 2022.", + "allContributors": "All contributors", + "source": "Source code" + }, + "_nsfw": { + "respect": "Hide NSFW media", + "ignore": "Don't hide NSFW media", + "force": "Hide all media" + }, + "_mfm": { + "cheatSheet": "MFM Cheatsheet", + "intro": "MFM is a FoundKey-exclusive markup language that can be used in many places. Here you can view a list of all available MFM syntax.", + "dummy": "FoundKey expands the world of the Fediverse", + "mention": "Mention", + "mentionDescription": "You can specify a user by using an At-Symbol and a username.", + "hashtag": "Hashtag", + "hashtagDescription": "You can specify a hashtag using a number sign and text.", + "url": "URL", + "urlDescription": "URLs can be displayed.", + "link": "Link", + "linkDescription": "Specific parts of text can be displayed as a URL.", + "bold": "Bold", + "boldDescription": "Highlights letters by making them thicker.", + "small": "Small", + "smallDescription": "Displays content small and thin.", + "center": "Center", + "centerDescription": "Displays content centered.", + "inlineCode": "Code (Inline)", + "inlineCodeDescription": "Displays inline syntax highlighting for (program) code.", + "blockCode": "Code (Block)", + "blockCodeDescription": "Displays syntax highlighting for multi-line (program) code in a block.", + "inlineMath": "Math (Inline)", + "inlineMathDescription": "Display math formulas (KaTeX) in-line.", + "blockMath": "Math (Block)", + "blockMathDescription": "Display multi-line math formulas (KaTeX) in a block.", + "quote": "Quote", + "quoteDescription": "Displays content as a quote.", + "emoji": "Custom Emoji", + "emojiDescription": "By surrounding a custom emoji name with colons, custom emoji can be displayed.", + "search": "Search", + "searchDescription": "Displays a search box with pre-entered text.", + "flip": "Flip", + "flipDescription": "Flips content horizontally or vertically.", + "jelly": "Animation (Jelly)", + "jellyDescription": "Gives content a jelly-like animation.", + "tada": "Animation (Tada)", + "tadaDescription": "Gives content a \"Tada!\"-like animation.", + "jump": "Animation (Jump)", + "jumpDescription": "Gives content a jumping animation.", + "bounce": "Animation (Bounce)", + "bounceDescription": "Gives content a bouncy animation.", + "shake": "Animation (Shake)", + "shakeDescription": "Gives content a shaking animation.", + "twitch": "Animation (Twitch)", + "twitchDescription": "Gives content a strongly twitching animation.", + "spin": "Animation (Spin)", + "spinDescription": "Gives content a spinning animation.", + "x2": "Big", + "x2Description": "Displays content bigger.", + "x3": "Very big", + "x3Description": "Displays content even bigger.", + "x4": "Unbelievably big", + "x4Description": "Displays content even bigger than bigger than big.", + "blur": "Blur", + "blurDescription": "Blurs content. It will be displayed clearly when hovered over.", + "font": "Font", + "fontDescription": "Sets the font to display content in.", + "rainbow": "Rainbow", + "rainbowDescription": "Makes the content appear in rainbow colors.", + "sparkle": "Sparkle", + "sparkleDescription": "Gives content a sparkling particle effect.", + "rotate": "Rotate", + "rotateDescription": "Turns content by a specified angle." + }, + "_instanceTicker": { + "none": "Never show", + "remote": "Show for remote users", + "always": "Always show" + }, + "_serverDisconnectedBehavior": { + "reload": "Automatically reload", + "dialog": "Show warning dialog", + "quiet": "Show unobtrusive warning" + }, + "maxCustomEmojiPicker": "Maximum suggested custom emoji in picker", + "maxUnicodeEmojiPicker": "Maximum suggested unicode emoji in picker", + "_channel": { + "create": "Create channel", + "edit": "Edit channel", + "setBanner": "Set banner", + "removeBanner": "Remove banner", + "featured": "Trending", + "owned": "Owned", + "following": "Followed", + "usersCount": "{n} Participants", + "notesCount": "{n} Notes" + }, + "_menuDisplay": { + "sideFull": "Side", + "sideIcon": "Side (Icons)", + "top": "Top", + "hide": "Hide" + }, + "_wordMute": { + "muteWords": "Muted words", + "muteWordsDescription": "Separate with spaces for an AND condition or with line breaks for an OR condition.", + "muteWordsDescription2": "Surround keywords with slashes to use regular expressions.", + "softDescription": "Hide notes that fulfil the set conditions from the timeline.", + "hardDescription": "Prevents notes fulfilling the set conditions from being added to the timeline. In addition, these notes will not be added to the timeline even if the conditions are changed.", + "soft": "Soft", + "hard": "Hard", + "mutedNotes": "Muted notes" + }, + "_instanceMute": { + "instanceMuteDescription": "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance.", + "instanceMuteDescription2": "Separate with newlines", + "title": "Hides notes from listed instances.", + "heading": "List of instances to be muted" + }, + "_theme": { + "explore": "Explore Themes", + "install": "Install a theme", + "manage": "Manage themes", + "code": "Theme code", + "description": "Description", + "installed": "{name} has been installed", + "installedThemes": "Installed themes", + "builtinThemes": "Built-in themes", + "alreadyInstalled": "This theme is already installed", + "invalid": "The format of this theme is invalid", + "make": "Make a theme" + }, + "_sfx": { + "note": "New note", + "noteMy": "Own note", + "notification": "Notifications", + "chat": "Chat", + "chatBg": "Chat (Background)", + "antenna": "Antennas", + "channel": "Channel notifications" + }, + "_ago": { + "future": "Future", + "justNow": "Just now", + "secondsAgo": "{n} second(s) ago", + "minutesAgo": "{n} minute(s) ago", + "hoursAgo": "{n} hour(s) ago", + "daysAgo": "{n} day(s) ago", + "weeksAgo": "{n} week(s) ago", + "monthsAgo": "{n} month(s) ago", + "yearsAgo": "{n} year(s) ago" + }, + "_time": { + "second": "Second(s)", + "minute": "Minute(s)", + "hour": "Hour(s)", + "day": "Day(s)" + }, + "_tutorial": { + "title": "How to use FoundKey", + "step1_1": "Welcome!", + "step1_2": "This page is called the \"timeline\". It shows chronologically ordered \"notes\" of people who you \"follow\".", + "step1_3": "Your timeline is currently empty, since you have not posted any notes or followed anyone yet.", + "step2_1": "Let's finish setting up your profile before writing a note or following anyone.", + "step2_2": "Providing some information about who you are will make it easier for others to tell if they want to see your notes or follow you.", + "step3_1": "Finished setting up your profile?", + "step3_2": "Then let's try posting a note next. You can do so by pressing the button with a pencil icon on the screen.", + "step3_3": "Fill in the modal and press the button on the top right to post.", + "step3_4": "Have nothing to say? Try \"just setting up my msky\"!", + "step4_1": "Finished posting your first note?", + "step4_2": "Hurray! Now your first note should be displayed on your timeline.", + "step5_1": "Now, let's try making your timeline more lively by following other people.", + "step5_2": "{featured} will show you popular notes in this instance. {explore} will let you find popular users. Try finding people you'd like to follow there!", + "step5_3": "To follow other users, click on their icon and press the \"Follow\" button on their profile.", + "step5_4": "If the other user has a lock icon next to their name, it may take some time for that user to manually approve your follow request.", + "step6_1": "You should be able to see other users' notes on your timeline now.", + "step6_2": "You can also put \"reactions\" on other people's notes to quickly respond to them.", + "step6_3": "To attach a \"reaction\", press the \"+\" mark on another user's note and choose an emoji you'd like to react with.", + "step7_1": "Congratulations! You have now finished FoundKey's basic tutorial.", + "step7_2": "If you would like to learn more about FoundKey, try the {help} section.", + "step7_3": "Now then, good luck and have fun with FoundKey! 🚀" + }, + "_2fa": { + "alreadyRegistered": "You have already registered a 2-factor authentication device.", + "registerDevice": "Register a new device", + "registerKey": "Register a security key", + "step1": "First, install an authentication app (such as {a} or {b}) on your device.", + "step2": "Then, scan the QR code displayed on this screen.", + "step2Url": "You can also enter this URL if you're using a desktop program:", + "step3": "Enter the token provided by your app to finish setup.", + "step4": "From now on, any future login attempts will ask for such a login token.", + "securityKeyInfo": "Besides fingerprint or PIN authentication, you can also setup authentication via hardware security keys that support FIDO2 to further secure your account." + }, + "_permissions": { + "read:account": "Read account information", + "write:account": "Edit account information", + "read:blocks": "Read which users are blocked", + "write:blocks": "Block and unblock users", + "read:drive": "List files and folders in the drive", + "write:drive": "Create, change and delete files in the drive", + "read:favorites": "List favourited notes", + "write:favorites": "Favorite and unfavorite notes", + "read:following": "List followed and following users", + "write:following": "Follow and unfollow other users", + "read:messaging": "View chat messages and history", + "write:messaging": "Create and delete chat messages", + "read:mutes": "List users which are muted or whose renotes are muted", + "write:mutes": "Mute and unmute users or their renotes", + "write:notes": "Create and delete notes", + "read:notifications": "Read notifications", + "write:notifications": "Mark notifications as read and create custom notifications", + "read:reactions": "View reactions", + "write:reactions": "Create and delete reactions", + "write:votes": "Vote in polls", + "read:pages": "List and read pages", + "write:pages": "Create, change and delete pages", + "read:page-likes": "List and read page likes", + "write:page-likes": "Like and unlike pages", + "read:user-groups": "List and view joined, owned and invited to groups", + "write:user-groups": "Create, modify, delete, transfer, join and leave groups. Invite and ban others from groups. Accept and reject group invitations.", + "read:channels": "List and read followed and joined channels", + "write:channels": "Create, modify, follow and unfollow channels", + "read:gallery": "List and read gallery posts", + "write:gallery": "Create, modify and delete gallery posts", + "read:gallery-likes": "List and read gallery post likes", + "write:gallery-likes": "Like and unlike gallery posts" + }, + "_auth": { + "shareAccess": "Would you like to authorize \"{name}\" to access this account?", + "shareAccessAsk": "Are you sure you want to authorize this application to access your account?", + "permissionAsk": "This application requests the following permissions", + "pleaseGoBack": "Please go back to the application", + "callback": "Returning to the application", + "denied": "Access denied" + }, + "_antennaSources": { + "all": "All notes", + "homeTimeline": "Notes from followed users", + "users": "Notes from specific users", + "userList": "Notes from a specified list of users", + "userGroup": "Notes from users in a specified group" + }, + "_weekday": { + "sunday": "Sunday", + "monday": "Monday", + "tuesday": "Tuesday", + "wednesday": "Wednesday", + "thursday": "Thursday", + "friday": "Friday", + "saturday": "Saturday" + }, + "_widgets": { + "memo": "Sticky notes", + "notifications": "Notifications", + "timeline": "Timeline", + "calendar": "Calendar", + "trends": "Trending", + "clock": "Clock", + "rss": "RSS reader", + "rssMarquee": "RSS ticker", + "activity": "Activity", + "photos": "Photos", + "digitalClock": "Digital clock", + "federation": "Federation", + "postForm": "Posting form", + "slideshow": "Slideshow", + "button": "Button", + "onlineUsers": "Online users", + "jobQueue": "Job Queue", + "serverMetric": "Server metrics", + "aiscript": "AiScript console", + "aichan": "Ai" + }, + "_cw": { + "hide": "Hide", + "show": "Show content", + "chars": "{count} characters", + "files": "{count} file(s)" + }, + "_poll": { + "noOnlyOneChoice": "At least two choices are needed", + "choiceN": "Choice {n}", + "noMore": "You cannot add more choices", + "canMultipleVote": "Allow selecting multiple choices", + "expiration": "End poll", + "infinite": "Never", + "at": "End at...", + "after": "End after...", + "deadlineDate": "End date", + "deadlineTime": "Time", + "duration": "Duration", + "votesCount": "{n} votes", + "totalVotes": "{n} votes in total", + "vote": "Vote", + "showResult": "View results", + "voted": "Voted", + "closed": "Ended", + "remainingDays": "{d} day(s) {h} hour(s) remaining", + "remainingHours": "{h} hour(s) {m} minute(s) remaining", + "remainingMinutes": "{m} minute(s) {s} second(s) remaining", + "remainingSeconds": "{s} second(s) remaining" + }, + "_visibility": { + "public": "Public", + "publicDescription": "Your note will be visible for all users", + "home": "Home", + "homeDescription": "Post to home timeline only", + "followers": "Followers", + "followersDescription": "Make visible to your followers only", + "specified": "Direct", + "specifiedDescription": "Make visible for specified users only", + "localOnly": "Local only", + "localOnlyDescription": "Not visible to remote users" + }, + "_postForm": { + "replyPlaceholder": "Reply to this note...", + "quotePlaceholder": "Quote this note...", + "channelPlaceholder": "Post to a channel...", + "_placeholders": { + "a": "What are you up to?", + "b": "What's happening around you?", + "c": "What's on your mind?", + "d": "What do you want to say?", + "e": "Start writing...", + "f": "Waiting for you to write..." + } + }, + "_profile": { + "name": "Name", + "username": "Username", + "description": "Bio", + "youCanIncludeHashtags": "You can also include hashtags in your bio.", + "metadata": "Additional Information", + "metadataEdit": "Edit additional Information", + "metadataDescription": "Using these, you can display additional information fields in your profile.", + "metadataLabel": "Label", + "metadataContent": "Content", + "changeAvatar": "Change avatar", + "changeBanner": "Change banner" + }, + "_exportOrImport": { + "allNotes": "All notes", + "followingList": "Followed users", + "muteList": "Muted users", + "blockingList": "Blocked users", + "userLists": "User lists", + "excludeMutingUsers": "Exclude muted users", + "excludeInactiveUsers": "Exclude inactive users" + }, + "_charts": { + "federation": "Federation", + "apRequest": "Requests", + "usersIncDec": "Difference in the number of users", + "usersTotal": "Total number of users", + "activeUsers": "Active users", + "notesIncDec": "Difference in the number of notes", + "localNotesIncDec": "Difference in the number of local notes", + "remoteNotesIncDec": "Difference in the number of remote notes", + "notesTotal": "Total number of notes", + "filesIncDec": "Difference in the number of files", + "filesTotal": "Total number of files", + "storageUsageIncDec": "Difference in storage usage", + "storageUsageTotal": "Total storage usage" + }, + "_instanceCharts": { + "requests": "Requests", + "users": "Difference in the number of users", + "usersTotal": "Cumulative number of users", + "notes": "Difference in the number of notes", + "notesTotal": "Cumulative number of notes", + "ff": "Difference in the number of followed users / followers", + "ffTotal": "Cumulative number of followed users / followers", + "cacheSize": "Difference in cache size", + "cacheSizeTotal": "Cumulative total cache size", + "files": "Difference in the number of files", + "filesTotal": "Cumulative number of files" + }, + "_timelines": { + "home": "Home", + "local": "Local", + "social": "Social", + "global": "Global" + }, + "_pages": { + "newPage": "Create a new Page", + "editPage": "Edit this Page", + "readPage": "Viewing this Page's source", + "created": "Page successfully created", + "updated": "Page successfully edited", + "deleted": "Page successfully deleted", + "pageSetting": "Page settings", + "nameAlreadyExists": "The specified Page URL already exists", + "invalidNameTitle": "The specified Page URL is invalid", + "invalidNameText": "Make sure the Page title is not empty", + "editThisPage": "Edit this Page", + "viewSource": "View source", + "viewPage": "View your Pages", + "like": "Like", + "unlike": "Remove like", + "my": "My Pages", + "liked": "Liked Pages", + "featured": "Popular", + "contents": "Contents", + "title": "Title", + "url": "Page URL", + "summary": "Page summary", + "alignCenter": "Center elements", + "hideTitleWhenPinned": "Hide Page title when pinned to profile", + "font": "Font", + "fontSerif": "Serif", + "fontSansSerif": "Sans Serif", + "eyeCatchingImageSet": "Set thumbnail", + "eyeCatchingImageRemove": "Delete thumbnail" + }, + "_relayStatus": { + "requesting": "Pending", + "accepted": "Accepted", + "rejected": "Rejected" + }, + "_notification": { + "youGotMention": "{name} mentioned you", + "youGotReply": "{name} replied to you", + "youGotQuote": "{name} quoted you", + "youRenoted": "Renote from {name}", + "youGotPoll": "{name} voted on your poll", + "youGotMessagingMessageFromUser": "{name} sent you a chat message", + "youGotMessagingMessageFromGroup": "A chat message was sent to the {name} group", + "youWereFollowed": "followed you", + "youReceivedFollowRequest": "You've received a follow request", + "yourFollowRequestAccepted": "Your follow request was accepted", + "youWereInvitedToGroup": "{userName} invited you to a group", + "pollEnded": "Poll results have become available", + "emptyPushNotificationMessage": "Push notifications have been updated", + "_types": { + "follow": "New followers", + "mention": "Mentions", + "reply": "Replies", + "renote": "Renotes", + "quote": "Quotes", + "reaction": "Reactions", + "pollVote": "Votes on polls", + "pollEnded": "Polls ending", + "receiveFollowRequest": "Received follow requests", + "followRequestAccepted": "Accepted follow requests", + "groupInvited": "Group invitations", + "app": "Notifications from linked apps" + }, + "_actions": { + "followBack": "followed you back", + "reply": "Reply", + "renote": "Renote" + } + }, + "_deck": { + "alwaysShowMainColumn": "Always show main column", + "columnAlign": "Align columns", + "columnMargin": "Margin between columns", + "columnHeaderHeight": "Column header height", + "addColumn": "Add column", + "swapLeft": "Swap with the left column", + "swapRight": "Swap with the right column", + "swapUp": "Swap with the above column", + "swapDown": "Swap with the below column", + "stackLeft": "Stack with the left column", + "popRight": "Pop column to the right", + "profile": "Profile", + "_columns": { + "main": "Main", + "widgets": "Widgets", + "notifications": "Notifications", + "tl": "Timeline", + "antenna": "Antennas", + "list": "List", + "mentions": "Mentions", + "direct": "Direct notes" + } + }, + "_translationService": { + "_deepl": { + "authKey": "DeepL Auth Key" + }, + "_libreTranslate": { + "endpoint": "LibreTranslate API Endpoint", + "authKey": "LibreTranslate Auth Key (optional)" + } + } +} diff --git a/package.json b/package.json index 659ba2897..305f71fcf 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "lodash": "^4.17.21" }, "dependencies": { + "@discordapp/twemoji": "^14.0.2", "argon2": "^0.30.2", "execa": "5.1.1", "gulp": "4.0.2", diff --git a/packages/backend/.eslintignore b/packages/backend/.eslintignore deleted file mode 100644 index b268e1398..000000000 --- a/packages/backend/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -/built -/.eslintrc.cjs -/@types/**/* diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs deleted file mode 100644 index c3b829966..000000000 --- a/packages/backend/.eslintrc.cjs +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: [ - '../shared/.eslintrc.js', - ], - plugins: [ - 'foundkey-custom-rules', - ], - rules: { - 'foundkey-custom-rules/typeorm-prefer-count': 'error', - 'import/order': ['warn', { - 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - 'pathGroups': [ - { - 'pattern': '@/**', - 'group': 'external', - 'position': 'after' - } - ], - }], - 'no-restricted-globals': [ - 'error', - { - 'name': '__dirname', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - }, - { - 'name': '__filename', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - } - ] - }, -}; diff --git a/packages/backend/.mocharc.json b/packages/backend/.mocharc.json deleted file mode 100644 index f836f9e90..000000000 --- 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/.vscode/settings.json b/packages/backend/.vscode/settings.json deleted file mode 100644 index 9fb3b29d4..000000000 --- a/packages/backend/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "typescript.tsdk": "node_modules\\typescript\\lib", - "path-intellisense.mappings": { - "@": "${workspaceRoot}/packages/backend/src/" - }, - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": true - } -} diff --git a/packages/backend/jsconfig.json b/packages/backend/jsconfig.json deleted file mode 100644 index 1230aadd1..000000000 --- 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/migration/1000000000000-Init.js b/packages/backend/migration/1000000000000-Init.js deleted file mode 100644 index 1140be7e8..000000000 --- a/packages/backend/migration/1000000000000-Init.js +++ /dev/null @@ -1,482 +0,0 @@ - - -export class Init1000000000000 { - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "log_level_enum" AS ENUM('error', 'warning', 'info', 'success', 'debug')`); - await queryRunner.query(`CREATE TABLE "log" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "domain" character varying(64) array NOT NULL DEFAULT '{}'::varchar[], "level" "log_level_enum" NOT NULL, "worker" character varying(8) NOT NULL, "machine" character varying(128) NOT NULL, "message" character varying(1024) NOT NULL, "data" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_350604cbdf991d5930d9e618fbd" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_8e4eb51a35d81b64dda28eed0a" ON "log" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_8cb40cfc8f3c28261e6f887b03" ON "log" ("domain") `); - await queryRunner.query(`CREATE INDEX "IDX_584b536b49e53ac81beb39a177" ON "log" ("level") `); - await queryRunner.query(`CREATE TABLE "drive_folder" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(128) NOT NULL, "userId" character varying(32), "parentId" character varying(32), CONSTRAINT "PK_7a0c089191f5ebdc214e0af808a" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_02878d441ceae15ce060b73daf" ON "drive_folder" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_f4fc06e49c0171c85f1c48060d" ON "drive_folder" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_00ceffb0cdc238b3233294f08f" ON "drive_folder" ("parentId") `); - await queryRunner.query(`CREATE TABLE "drive_file" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32), "userHost" character varying(128), "md5" character varying(32) NOT NULL, "name" character varying(256) NOT NULL, "type" character varying(128) NOT NULL, "size" integer NOT NULL, "comment" character varying(512), "properties" jsonb NOT NULL DEFAULT '{}', "storedInternal" boolean NOT NULL, "url" character varying(512) NOT NULL, "thumbnailUrl" character varying(512), "webpublicUrl" character varying(512), "accessKey" character varying(256), "thumbnailAccessKey" character varying(256), "webpublicAccessKey" character varying(256), "uri" character varying(512), "src" character varying(512), "folderId" character varying(32), "isSensitive" boolean NOT NULL DEFAULT false, "isLink" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_43ddaaaf18c9e68029b7cbb032e" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_c8dfad3b72196dd1d6b5db168a" ON "drive_file" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_860fa6f6c7df5bb887249fba22" ON "drive_file" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_92779627994ac79277f070c91e" ON "drive_file" ("userHost") `); - await queryRunner.query(`CREATE INDEX "IDX_37bb9a1b4585f8a3beb24c62d6" ON "drive_file" ("md5") `); - await queryRunner.query(`CREATE INDEX "IDX_a40b8df8c989d7db937ea27cf6" ON "drive_file" ("type") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d85a184c2540d2deba33daf642" ON "drive_file" ("accessKey") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e74022ce9a074b3866f70e0d27" ON "drive_file" ("thumbnailAccessKey") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c55b2b7c284d9fef98026fc88e" ON "drive_file" ("webpublicAccessKey") `); - await queryRunner.query(`CREATE INDEX "IDX_e5848eac4940934e23dbc17581" ON "drive_file" ("uri") `); - await queryRunner.query(`CREATE INDEX "IDX_bb90d1956dafc4068c28aa7560" ON "drive_file" ("folderId") `); - await queryRunner.query(`CREATE TABLE "user" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "lastFetchedAt" TIMESTAMP WITH TIME ZONE, "username" character varying(128) NOT NULL, "usernameLower" character varying(128) NOT NULL, "name" character varying(128), "followersCount" integer NOT NULL DEFAULT 0, "followingCount" integer NOT NULL DEFAULT 0, "notesCount" integer NOT NULL DEFAULT 0, "avatarId" character varying(32), "bannerId" character varying(32), "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "avatarUrl" character varying(512), "bannerUrl" character varying(512), "avatarColor" character varying(32), "bannerColor" character varying(32), "isSuspended" boolean NOT NULL DEFAULT false, "isSilenced" boolean NOT NULL DEFAULT false, "isLocked" boolean NOT NULL DEFAULT false, "isBot" boolean NOT NULL DEFAULT false, "isCat" boolean NOT NULL DEFAULT false, "isAdmin" boolean NOT NULL DEFAULT false, "isModerator" boolean NOT NULL DEFAULT false, "isVerified" boolean NOT NULL DEFAULT false, "emojis" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "host" character varying(128), "inbox" character varying(512), "sharedInbox" character varying(512), "featured" character varying(512), "uri" character varying(512), "token" character(16), CONSTRAINT "UQ_a854e557b1b14814750c7c7b0c9" UNIQUE ("token"), CONSTRAINT "REL_58f5c71eaab331645112cf8cfa" UNIQUE ("avatarId"), CONSTRAINT "REL_afc64b53f8db3707ceb34eb28e" UNIQUE ("bannerId"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_e11e649824a45d8ed01d597fd9" ON "user" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_80ca6e6ef65fb9ef34ea8c90f4" ON "user" ("updatedAt") `); - await queryRunner.query(`CREATE INDEX "IDX_a27b942a0d6dcff90e3ee9b5e8" ON "user" ("usernameLower") `); - await queryRunner.query(`CREATE INDEX "IDX_fa99d777623947a5b05f394cae" ON "user" ("tags") `); - await queryRunner.query(`CREATE INDEX "IDX_3252a5df8d5bbd16b281f7799e" ON "user" ("host") `); - await queryRunner.query(`CREATE INDEX "IDX_be623adaa4c566baf5d29ce0c8" ON "user" ("uri") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a854e557b1b14814750c7c7b0c" ON "user" ("token") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5deb01ae162d1d70b80d064c27" ON "user" ("usernameLower", "host") `); - await queryRunner.query(`CREATE TABLE "app" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32), "secret" character varying(64) NOT NULL, "name" character varying(128) NOT NULL, "description" character varying(512) NOT NULL, "permission" character varying(64) array NOT NULL, "callbackUrl" character varying(512), CONSTRAINT "PK_9478629fc093d229df09e560aea" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_048a757923ed8b157e9895da53" ON "app" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_3f5b0899ef90527a3462d7c2cb" ON "app" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_f49922d511d666848f250663c4" ON "app" ("secret") `); - await queryRunner.query(`CREATE TABLE "access_token" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(128) NOT NULL, "hash" character varying(128) NOT NULL, "userId" character varying(32) NOT NULL, "appId" character varying(32) NOT NULL, CONSTRAINT "PK_f20f028607b2603deabd8182d12" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_70ba8f6af34bc924fc9e12adb8" ON "access_token" ("token") `); - await queryRunner.query(`CREATE INDEX "IDX_64c327441248bae40f7d92f34f" ON "access_token" ("hash") `); - await queryRunner.query(`CREATE INDEX "IDX_9949557d0e1b2c19e5344c171e" ON "access_token" ("userId") `); - await queryRunner.query(`CREATE TYPE "note_visibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`); - await queryRunner.query(`CREATE TABLE "note" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "replyId" character varying(32), "renoteId" character varying(32), "text" text, "name" character varying(256), "cw" character varying(512), "appId" character varying(32), "userId" character varying(32) NOT NULL, "viaMobile" boolean NOT NULL DEFAULT false, "localOnly" boolean NOT NULL DEFAULT false, "renoteCount" smallint NOT NULL DEFAULT 0, "repliesCount" smallint NOT NULL DEFAULT 0, "reactions" jsonb NOT NULL DEFAULT '{}', "visibility" "note_visibility_enum" NOT NULL, "uri" character varying(512), "score" integer NOT NULL DEFAULT 0, "fileIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "attachedFileTypes" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "mentions" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "mentionedRemoteUsers" text NOT NULL DEFAULT '[]', "emojis" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "hasPoll" boolean NOT NULL DEFAULT false, "geo" jsonb DEFAULT null, "userHost" character varying(128), "replyUserId" character varying(32), "replyUserHost" character varying(128), "renoteUserId" character varying(32), "renoteUserHost" character varying(128), CONSTRAINT "PK_96d0c172a4fba276b1bbed43058" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_e7c0567f5261063592f022e9b5" ON "note" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_17cb3553c700a4985dff5a30ff" ON "note" ("replyId") `); - await queryRunner.query(`CREATE INDEX "IDX_52ccc804d7c69037d558bac4c9" ON "note" ("renoteId") `); - await queryRunner.query(`CREATE INDEX "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_153536c67d05e9adb24e99fc2b" ON "note" ("uri") `); - await queryRunner.query(`CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds") `); - await queryRunner.query(`CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes") `); - await queryRunner.query(`CREATE INDEX "IDX_796a8c03959361f97dc2be1d5c" ON "note" ("visibleUserIds") `); - await queryRunner.query(`CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions") `); - await queryRunner.query(`CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags") `); - await queryRunner.query(`CREATE INDEX "IDX_7125a826ab192eb27e11d358a5" ON "note" ("userHost") `); - await queryRunner.query(`CREATE TABLE "poll_vote" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "choice" integer NOT NULL, CONSTRAINT "PK_fd002d371201c472490ba89c6a0" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_0fb627e1c2f753262a74f0562d" ON "poll_vote" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_66d2bd2ee31d14bcc23069a89f" ON "poll_vote" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_aecfbd5ef60374918e63ee95fa" ON "poll_vote" ("noteId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_50bd7164c5b78f1f4a42c4d21f" ON "poll_vote" ("userId", "noteId", "choice") `); - await queryRunner.query(`CREATE TABLE "note_reaction" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "reaction" character varying(128) NOT NULL, CONSTRAINT "PK_767ec729b108799b587a3fcc9cf" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_13761f64257f40c5636d0ff95e" ON "note_reaction" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_45145e4953780f3cd5656f0ea6" ON "note_reaction" ("noteId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ad0c221b25672daf2df320a817" ON "note_reaction" ("userId", "noteId") `); - await queryRunner.query(`CREATE TABLE "note_watching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "noteUserId" character varying(32) NOT NULL, CONSTRAINT "PK_49286fdb23725945a74aa27d757" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_318cdf42a9cfc11f479bd802bb" ON "note_watching" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_b0134ec406e8d09a540f818288" ON "note_watching" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_03e7028ab8388a3f5e3ce2a861" ON "note_watching" ("noteId") `); - await queryRunner.query(`CREATE INDEX "IDX_44499765eec6b5489d72c4253b" ON "note_watching" ("noteUserId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a42c93c69989ce1d09959df4cf" ON "note_watching" ("userId", "noteId") `); - await queryRunner.query(`CREATE TABLE "note_unread" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "noteUserId" character varying(32) NOT NULL, "isSpecified" boolean NOT NULL, CONSTRAINT "PK_1904eda61a784f57e6e51fa9c1f" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_56b0166d34ddae49d8ef7610bb" ON "note_unread" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_e637cba4dc4410218c4251260e" ON "note_unread" ("noteId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d908433a4953cc13216cd9c274" ON "note_unread" ("userId", "noteId") `); - await queryRunner.query(`CREATE TABLE "notification" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "notifieeId" character varying(32) NOT NULL, "notifierId" character varying(32) NOT NULL, "type" character varying(32) NOT NULL, "isRead" boolean NOT NULL DEFAULT false, "noteId" character varying(32), "reaction" character varying(128), "choice" integer, CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_b11a5e627c41d4dc3170f1d370" ON "notification" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_3c601b70a1066d2c8b517094cb" ON "notification" ("notifieeId") `); - await queryRunner.query(`CREATE TABLE "meta" ("id" character varying(32) NOT NULL, "name" character varying(128), "description" character varying(1024), "maintainerName" character varying(128), "maintainerEmail" character varying(128), "announcements" jsonb NOT NULL DEFAULT '[]', "disableRegistration" boolean NOT NULL DEFAULT false, "disableLocalTimeline" boolean NOT NULL DEFAULT false, "disableGlobalTimeline" boolean NOT NULL DEFAULT false, "enableEmojiReaction" boolean NOT NULL DEFAULT true, "useStarForReactionFallback" boolean NOT NULL DEFAULT false, "langs" character varying(64) array NOT NULL DEFAULT '{}'::varchar[], "hiddenTags" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "blockedHosts" character varying(256) array NOT NULL DEFAULT '{}'::varchar[], "mascotImageUrl" character varying(512) DEFAULT '/assets/ai.png', "bannerUrl" character varying(512), "errorImageUrl" character varying(512) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png', "iconUrl" character varying(512), "cacheRemoteFiles" boolean NOT NULL DEFAULT true, "proxyAccount" character varying(128), "enableRecaptcha" boolean NOT NULL DEFAULT false, "recaptchaSiteKey" character varying(64), "recaptchaSecretKey" character varying(64), "localDriveCapacityMb" integer NOT NULL DEFAULT 1024, "remoteDriveCapacityMb" integer NOT NULL DEFAULT 32, "maxNoteTextLength" integer NOT NULL DEFAULT 500, "summalyProxy" character varying(128), "enableEmail" boolean NOT NULL DEFAULT false, "email" character varying(128), "smtpSecure" boolean NOT NULL DEFAULT false, "smtpHost" character varying(128), "smtpPort" integer, "smtpUser" character varying(128), "smtpPass" character varying(128), "enableServiceWorker" boolean NOT NULL DEFAULT false, "swPublicKey" character varying(128), "swPrivateKey" character varying(128), "enableTwitterIntegration" boolean NOT NULL DEFAULT false, "twitterConsumerKey" character varying(128), "twitterConsumerSecret" character varying(128), "enableGithubIntegration" boolean NOT NULL DEFAULT false, "githubClientId" character varying(128), "githubClientSecret" character varying(128), "enableDiscordIntegration" boolean NOT NULL DEFAULT false, "discordClientId" character varying(128), "discordClientSecret" character varying(128), CONSTRAINT "PK_c4c17a6c2bd7651338b60fc590b" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TABLE "following" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, "followerHost" character varying(128), "followerInbox" character varying(512), "followerSharedInbox" character varying(512), "followeeHost" character varying(128), "followeeInbox" character varying(512), "followeeSharedInbox" character varying(512), CONSTRAINT "PK_c76c6e044bdf76ecf8bfb82a645" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_582f8fab771a9040a12961f3e7" ON "following" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_24e0042143a18157b234df186c" ON "following" ("followeeId") `); - await queryRunner.query(`CREATE INDEX "IDX_6516c5a6f3c015b4eed39978be" ON "following" ("followerId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_307be5f1d1252e0388662acb96" ON "following" ("followerId", "followeeId") `); - await queryRunner.query(`CREATE TABLE "instance" ("id" character varying(32) NOT NULL, "caughtAt" TIMESTAMP WITH TIME ZONE NOT NULL, "host" character varying(128) NOT NULL, "system" character varying(64), "usersCount" integer NOT NULL DEFAULT 0, "notesCount" integer NOT NULL DEFAULT 0, "followingCount" integer NOT NULL DEFAULT 0, "followersCount" integer NOT NULL DEFAULT 0, "driveUsage" integer NOT NULL DEFAULT 0, "driveFiles" integer NOT NULL DEFAULT 0, "latestRequestSentAt" TIMESTAMP WITH TIME ZONE, "latestStatus" integer, "latestRequestReceivedAt" TIMESTAMP WITH TIME ZONE, "lastCommunicatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "isNotResponding" boolean NOT NULL DEFAULT false, "isMarkedAsClosed" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_eaf60e4a0c399c9935413e06474" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_2cd3b2a6b4cf0b910b260afe08" ON "instance" ("caughtAt") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8d5afc98982185799b160e10eb" ON "instance" ("host") `); - await queryRunner.query(`CREATE TABLE "muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_2e92d06c8b5c602eeb27ca9ba48" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_f86d57fbca33c7a4e6897490cc" ON "muting" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_ec96b4fed9dae517e0dbbe0675" ON "muting" ("muteeId") `); - await queryRunner.query(`CREATE INDEX "IDX_93060675b4a79a577f31d260c6" ON "muting" ("muterId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_1eb9d9824a630321a29fd3b290" ON "muting" ("muterId", "muteeId") `); - await queryRunner.query(`CREATE TABLE "sw_subscription" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "endpoint" character varying(512) NOT NULL, "auth" character varying(256) NOT NULL, "publickey" character varying(128) NOT NULL, CONSTRAINT "PK_e8f763631530051b95eb6279b91" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_97754ca6f2baff9b4abb7f853d" ON "sw_subscription" ("userId") `); - await queryRunner.query(`CREATE TABLE "blocking" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "blockeeId" character varying(32) NOT NULL, "blockerId" character varying(32) NOT NULL, CONSTRAINT "PK_e5d9a541cc1965ee7e048ea09dd" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_b9a354f7941c1e779f3b33aea6" ON "blocking" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_2cd4a2743a99671308f5417759" ON "blocking" ("blockeeId") `); - await queryRunner.query(`CREATE INDEX "IDX_0627125f1a8a42c9a1929edb55" ON "blocking" ("blockerId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_98a1bc5cb30dfd159de056549f" ON "blocking" ("blockerId", "blockeeId") `); - await queryRunner.query(`CREATE TABLE "user_list" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, CONSTRAINT "PK_87bab75775fd9b1ff822b656402" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_b7fcefbdd1c18dce86687531f9" ON "user_list" ("userId") `); - await queryRunner.query(`CREATE TABLE "user_list_joining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userListId" character varying(32) NOT NULL, CONSTRAINT "PK_11abb3768da1c5f8de101c9df45" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_d844bfc6f3f523a05189076efa" ON "user_list_joining" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_605472305f26818cc93d1baaa7" ON "user_list_joining" ("userListId") `); - await queryRunner.query(`CREATE TABLE "hashtag" ("id" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "mentionedUserIds" character varying(32) array NOT NULL, "mentionedUsersCount" integer NOT NULL DEFAULT 0, "mentionedLocalUserIds" character varying(32) array NOT NULL, "mentionedLocalUsersCount" integer NOT NULL DEFAULT 0, "mentionedRemoteUserIds" character varying(32) array NOT NULL, "mentionedRemoteUsersCount" integer NOT NULL DEFAULT 0, "attachedUserIds" character varying(32) array NOT NULL, "attachedUsersCount" integer NOT NULL DEFAULT 0, "attachedLocalUserIds" character varying(32) array NOT NULL, "attachedLocalUsersCount" integer NOT NULL DEFAULT 0, "attachedRemoteUserIds" character varying(32) array NOT NULL, "attachedRemoteUsersCount" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_cb36eb8af8412bfa978f1165d78" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_347fec870eafea7b26c8a73bac" ON "hashtag" ("name") `); - await queryRunner.query(`CREATE INDEX "IDX_2710a55f826ee236ea1a62698f" ON "hashtag" ("mentionedUsersCount") `); - await queryRunner.query(`CREATE INDEX "IDX_0e206cec573f1edff4a3062923" ON "hashtag" ("mentionedLocalUsersCount") `); - await queryRunner.query(`CREATE INDEX "IDX_4c02d38a976c3ae132228c6fce" ON "hashtag" ("mentionedRemoteUsersCount") `); - await queryRunner.query(`CREATE INDEX "IDX_d57f9030cd3af7f63ffb1c267c" ON "hashtag" ("attachedUsersCount") `); - await queryRunner.query(`CREATE INDEX "IDX_0c44bf4f680964145f2a68a341" ON "hashtag" ("attachedLocalUsersCount") `); - await queryRunner.query(`CREATE INDEX "IDX_0b03cbcd7e6a7ce068efa8ecc2" ON "hashtag" ("attachedRemoteUsersCount") `); - await queryRunner.query(`CREATE TABLE "note_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_af0da35a60b9fa4463a62082b36" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_47f4b1892f5d6ba8efb3057d81" ON "note_favorite" ("userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0f4fb9ad355f3effff221ef245" ON "note_favorite" ("userId", "noteId") `); - await queryRunner.query(`CREATE TABLE "abuse_user_report" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "reporterId" character varying(32) NOT NULL, "comment" character varying(512) NOT NULL, CONSTRAINT "PK_87873f5f5cc5c321a1306b2d18c" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_db2098070b2b5a523c58181f74" ON "abuse_user_report" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_d049123c413e68ca52abe73420" ON "abuse_user_report" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_04cc96756f89d0b7f9473e8cdf" ON "abuse_user_report" ("reporterId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5cd442c3b2e74fdd99dae20243" ON "abuse_user_report" ("userId", "reporterId") `); - await queryRunner.query(`CREATE TABLE "registration_ticket" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "code" character varying(64) NOT NULL, CONSTRAINT "PK_f11696b6fafcf3662d4292734f8" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0ff69e8dfa9fe31bb4a4660f59" ON "registration_ticket" ("code") `); - await queryRunner.query(`CREATE TABLE "messaging_message" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "recipientId" character varying(32) NOT NULL, "text" character varying(4096), "isRead" boolean NOT NULL DEFAULT false, "fileId" character varying(32), CONSTRAINT "PK_db398fd79dc95d0eb8c30456eaa" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_e21cd3646e52ef9c94aaf17c2e" ON "messaging_message" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_5377c307783fce2b6d352e1203" ON "messaging_message" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_cac14a4e3944454a5ce7daa514" ON "messaging_message" ("recipientId") `); - await queryRunner.query(`CREATE TABLE "signin" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "ip" character varying(128) NOT NULL, "headers" jsonb NOT NULL, "success" boolean NOT NULL, CONSTRAINT "PK_9e96ddc025712616fc492b3b588" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_2c308dbdc50d94dc625670055f" ON "signin" ("userId") `); - await queryRunner.query(`CREATE TABLE "auth_session" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(128) NOT NULL, "userId" character varying(32), "appId" character varying(32) NOT NULL, CONSTRAINT "PK_19354ed146424a728c1112a8cbf" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_62cb09e1129f6ec024ef66e183" ON "auth_session" ("token") `); - await queryRunner.query(`CREATE TABLE "follow_request" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, "requestId" character varying(128), "followerHost" character varying(128), "followerInbox" character varying(512), "followerSharedInbox" character varying(512), "followeeHost" character varying(128), "followeeInbox" character varying(512), "followeeSharedInbox" character varying(512), CONSTRAINT "PK_53a9aa3725f7a3deb150b39dbfc" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_12c01c0d1a79f77d9f6c15fadd" ON "follow_request" ("followeeId") `); - await queryRunner.query(`CREATE INDEX "IDX_a7fd92dd6dc519e6fb435dd108" ON "follow_request" ("followerId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d54a512b822fac7ed52800f6b4" ON "follow_request" ("followerId", "followeeId") `); - await queryRunner.query(`CREATE TABLE "emoji" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "name" character varying(128) NOT NULL, "host" character varying(128), "url" character varying(512) NOT NULL, "uri" character varying(512), "type" character varying(64), "aliases" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_df74ce05e24999ee01ea0bc50a3" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_b37dafc86e9af007e3295c2781" ON "emoji" ("name") `); - await queryRunner.query(`CREATE INDEX "IDX_5900e907bb46516ddf2871327c" ON "emoji" ("host") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4f4d35e1256c84ae3d1f0eab10" ON "emoji" ("name", "host") `); - await queryRunner.query(`CREATE TABLE "reversi_game" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "startedAt" TIMESTAMP WITH TIME ZONE, "user1Id" character varying(32) NOT NULL, "user2Id" character varying(32) NOT NULL, "user1Accepted" boolean NOT NULL DEFAULT false, "user2Accepted" boolean NOT NULL DEFAULT false, "black" integer, "isStarted" boolean NOT NULL DEFAULT false, "isEnded" boolean NOT NULL DEFAULT false, "winnerId" character varying(32), "surrendered" character varying(32), "logs" jsonb NOT NULL DEFAULT '[]', "map" character varying(64) array NOT NULL, "bw" character varying(32) NOT NULL, "isLlotheo" boolean NOT NULL DEFAULT false, "canPutEverywhere" boolean NOT NULL DEFAULT false, "loopedBoard" boolean NOT NULL DEFAULT false, "form1" jsonb DEFAULT null, "form2" jsonb DEFAULT null, "crc32" character varying(32), CONSTRAINT "PK_76b30eeba71b1193ad7c5311c3f" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt") `); - await queryRunner.query(`CREATE TABLE "reversi_matching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "parentId" character varying(32) NOT NULL, "childId" character varying(32) NOT NULL, CONSTRAINT "PK_880bd0afbab232f21c8b9d146cf" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_3b25402709dd9882048c2bbade" ON "reversi_matching" ("parentId") `); - await queryRunner.query(`CREATE INDEX "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId") `); - await queryRunner.query(`CREATE TABLE "user_note_pining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_a6a2dad4ae000abce2ea9d9b103" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_bfbc6f79ba4007b4ce5097f08d" ON "user_note_pining" ("userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_410cd649884b501c02d6e72738" ON "user_note_pining" ("userId", "noteId") `); - await queryRunner.query(`CREATE TYPE "poll_notevisibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`); - await queryRunner.query(`CREATE TABLE "poll" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE, "multiple" boolean NOT NULL, "choices" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], "votes" integer array NOT NULL, "noteVisibility" "poll_notevisibility_enum" NOT NULL, "userId" character varying(32) NOT NULL, "userHost" character varying(128), CONSTRAINT "REL_da851e06d0dfe2ef397d8b1bf1" UNIQUE ("noteId"), CONSTRAINT "PK_da851e06d0dfe2ef397d8b1bf1b" PRIMARY KEY ("noteId"))`); - await queryRunner.query(`CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_7fa20a12319c7f6dc3aed98c0a" ON "poll" ("userHost") `); - await queryRunner.query(`CREATE TABLE "user_keypair" ("userId" character varying(32) NOT NULL, "publicKey" character varying(4096) NOT NULL, "privateKey" character varying(4096) NOT NULL, CONSTRAINT "REL_f4853eb41ab722fe05f81cedeb" UNIQUE ("userId"), CONSTRAINT "PK_f4853eb41ab722fe05f81cedeb6" PRIMARY KEY ("userId"))`); - await queryRunner.query(`CREATE TABLE "user_publickey" ("userId" character varying(32) NOT NULL, "keyId" character varying(256) NOT NULL, "keyPem" character varying(4096) NOT NULL, CONSTRAINT "REL_10c146e4b39b443ede016f6736" UNIQUE ("userId"), CONSTRAINT "PK_10c146e4b39b443ede016f6736d" PRIMARY KEY ("userId"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_171e64971c780ebd23fae140bb" ON "user_publickey" ("keyId") `); - await queryRunner.query(`CREATE TABLE "user_profile" ("userId" character varying(32) NOT NULL, "location" character varying(128), "birthday" character(10), "description" character varying(1024), "fields" jsonb NOT NULL DEFAULT '[]', "url" character varying(512), "email" character varying(128), "emailVerifyCode" character varying(128), "emailVerified" boolean NOT NULL DEFAULT false, "twoFactorTempSecret" character varying(128), "twoFactorSecret" character varying(128), "twoFactorEnabled" boolean NOT NULL DEFAULT false, "password" character varying(128), "clientData" jsonb NOT NULL DEFAULT '{}', "autoWatch" boolean NOT NULL DEFAULT false, "autoAcceptFollowed" boolean NOT NULL DEFAULT false, "alwaysMarkNsfw" boolean NOT NULL DEFAULT false, "carefulBot" boolean NOT NULL DEFAULT false, "twitter" boolean NOT NULL DEFAULT false, "twitterAccessToken" character varying(64) DEFAULT null, "twitterAccessTokenSecret" character varying(64) DEFAULT null, "twitterUserId" character varying(64) DEFAULT null, "twitterScreenName" character varying(64) DEFAULT null, "github" boolean NOT NULL DEFAULT false, "githubAccessToken" character varying(64) DEFAULT null, "githubId" integer DEFAULT null, "githubLogin" character varying(64) DEFAULT null, "discord" boolean NOT NULL DEFAULT false, "discordAccessToken" character varying(64) DEFAULT null, "discordRefreshToken" character varying(64) DEFAULT null, "discordExpiresDate" integer DEFAULT null, "discordId" character varying(64) DEFAULT null, "discordUsername" character varying(64) DEFAULT null, "discordDiscriminator" character varying(64) DEFAULT null, "userHost" character varying(128), CONSTRAINT "REL_51cb79b5555effaf7d69ba1cff" UNIQUE ("userId"), CONSTRAINT "PK_51cb79b5555effaf7d69ba1cff9" PRIMARY KEY ("userId"))`); - await queryRunner.query(`CREATE INDEX "IDX_dce530b98e454793dac5ec2f5a" ON "user_profile" ("userHost") `); - await queryRunner.query(`CREATE TYPE "__chart__active_users_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__active_users" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__active_users_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "PK_317237a9f733b970604a11e314f" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__drive_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__drive" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__drive_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_totalCount" bigint NOT NULL, "___local_totalSize" bigint NOT NULL, "___local_incCount" bigint NOT NULL, "___local_incSize" bigint NOT NULL, "___local_decCount" bigint NOT NULL, "___local_decSize" bigint NOT NULL, "___remote_totalCount" bigint NOT NULL, "___remote_totalSize" bigint NOT NULL, "___remote_incCount" bigint NOT NULL, "___remote_incSize" bigint NOT NULL, "___remote_decCount" bigint NOT NULL, "___remote_decSize" bigint NOT NULL, CONSTRAINT "PK_f96bc548a765cd4b3b354221ce7" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__federation_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__federation" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__federation_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___instance_total" bigint NOT NULL, "___instance_inc" bigint NOT NULL, "___instance_dec" bigint NOT NULL, CONSTRAINT "PK_b39dcd31a0fe1a7757e348e85fd" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__hashtag_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__hashtag" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__hashtag_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "PK_c32f1ea2b44a5d2f7881e37f8f9" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__instance_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__instance" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__instance_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___requests_failed" bigint NOT NULL, "___requests_succeeded" bigint NOT NULL, "___requests_received" bigint NOT NULL, "___notes_total" bigint NOT NULL, "___notes_inc" bigint NOT NULL, "___notes_dec" bigint NOT NULL, "___notes_diffs_normal" bigint NOT NULL, "___notes_diffs_reply" bigint NOT NULL, "___notes_diffs_renote" bigint NOT NULL, "___users_total" bigint NOT NULL, "___users_inc" bigint NOT NULL, "___users_dec" bigint NOT NULL, "___following_total" bigint NOT NULL, "___following_inc" bigint NOT NULL, "___following_dec" bigint NOT NULL, "___followers_total" bigint NOT NULL, "___followers_inc" bigint NOT NULL, "___followers_dec" bigint NOT NULL, "___drive_totalFiles" bigint NOT NULL, "___drive_totalUsage" bigint NOT NULL, "___drive_incFiles" bigint NOT NULL, "___drive_incUsage" bigint NOT NULL, "___drive_decFiles" bigint NOT NULL, "___drive_decUsage" bigint NOT NULL, CONSTRAINT "PK_1267c67c7c2d47b4903975f2c00" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__network_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__network" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__network_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___incomingRequests" bigint NOT NULL, "___outgoingRequests" bigint NOT NULL, "___totalTime" bigint NOT NULL, "___incomingBytes" bigint NOT NULL, "___outgoingBytes" bigint NOT NULL, CONSTRAINT "PK_bc4290c2e27fad14ef0c1ca93f3" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__notes_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__notes" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__notes_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_total" bigint NOT NULL, "___local_inc" bigint NOT NULL, "___local_dec" bigint NOT NULL, "___local_diffs_normal" bigint NOT NULL, "___local_diffs_reply" bigint NOT NULL, "___local_diffs_renote" bigint NOT NULL, "___remote_total" bigint NOT NULL, "___remote_inc" bigint NOT NULL, "___remote_dec" bigint NOT NULL, "___remote_diffs_normal" bigint NOT NULL, "___remote_diffs_reply" bigint NOT NULL, "___remote_diffs_renote" bigint NOT NULL, CONSTRAINT "PK_0aec823fa85c7f901bdb3863b14" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__per_user_drive_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__per_user_drive" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_drive_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___totalCount" bigint NOT NULL, "___totalSize" bigint NOT NULL, "___incCount" bigint NOT NULL, "___incSize" bigint NOT NULL, "___decCount" bigint NOT NULL, "___decSize" bigint NOT NULL, CONSTRAINT "PK_d0ef23d24d666e1a44a0cd3d208" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__per_user_following_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__per_user_following" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_following_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_followings_total" bigint NOT NULL, "___local_followings_inc" bigint NOT NULL, "___local_followings_dec" bigint NOT NULL, "___local_followers_total" bigint NOT NULL, "___local_followers_inc" bigint NOT NULL, "___local_followers_dec" bigint NOT NULL, "___remote_followings_total" bigint NOT NULL, "___remote_followings_inc" bigint NOT NULL, "___remote_followings_dec" bigint NOT NULL, "___remote_followers_total" bigint NOT NULL, "___remote_followers_inc" bigint NOT NULL, "___remote_followers_dec" bigint NOT NULL, CONSTRAINT "PK_85bb1b540363a29c2fec83bd907" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__per_user_notes_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__per_user_notes" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_notes_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___total" bigint NOT NULL, "___inc" bigint NOT NULL, "___dec" bigint NOT NULL, "___diffs_normal" bigint NOT NULL, "___diffs_reply" bigint NOT NULL, "___diffs_renote" bigint NOT NULL, CONSTRAINT "PK_334acf6e915af2f29edc11b8e50" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__per_user_reaction_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__per_user_reaction" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__per_user_reaction_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "PK_984f54dae441e65b633e8d27a7f" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__test_grouped_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__test_grouped" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__test_grouped_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___foo_total" bigint NOT NULL, "___foo_inc" bigint NOT NULL, "___foo_dec" bigint NOT NULL, CONSTRAINT "PK_f4a2b175d308695af30d4293272" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__test_unique_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__test_unique" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__test_unique_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___foo" bigint NOT NULL, CONSTRAINT "PK_409bac9c97cc612d8500012319d" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__test_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__test" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__test_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___foo_total" bigint NOT NULL, "___foo_inc" bigint NOT NULL, "___foo_dec" bigint NOT NULL, CONSTRAINT "PK_b4bc31dffbd1b785276a3ecfc1e" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE TYPE "__chart__users_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`CREATE TABLE "__chart__users" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128), "span" "__chart__users_span_enum" NOT NULL, "unique" jsonb NOT NULL DEFAULT '{}', "___local_total" bigint NOT NULL, "___local_inc" bigint NOT NULL, "___local_dec" bigint NOT NULL, "___remote_total" bigint NOT NULL, "___remote_inc" bigint NOT NULL, "___remote_dec" bigint NOT NULL, CONSTRAINT "PK_4dfcf2c78d03524b9eb2c99d328" PRIMARY KEY ("id"))`); - await queryRunner.query(`ALTER TABLE "drive_folder" ADD CONSTRAINT "FK_f4fc06e49c0171c85f1c48060d2" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "drive_folder" ADD CONSTRAINT "FK_00ceffb0cdc238b3233294f08f2" FOREIGN KEY ("parentId") REFERENCES "drive_folder"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "drive_file" ADD CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "drive_file" ADD CONSTRAINT "FK_bb90d1956dafc4068c28aa7560a" FOREIGN KEY ("folderId") REFERENCES "drive_folder"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_58f5c71eaab331645112cf8cfa5" FOREIGN KEY ("avatarId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_afc64b53f8db3707ceb34eb28e2" FOREIGN KEY ("bannerId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "app" ADD CONSTRAINT "FK_3f5b0899ef90527a3462d7c2cb3" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_9949557d0e1b2c19e5344c171e9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_ec5c201576192ba8904c345c5cc" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_5b87d9d19127bd5d92026017a7b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "poll_vote" ADD CONSTRAINT "FK_66d2bd2ee31d14bcc23069a89f8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "poll_vote" ADD CONSTRAINT "FK_aecfbd5ef60374918e63ee95fa7" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note_reaction" ADD CONSTRAINT "FK_13761f64257f40c5636d0ff95ee" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note_reaction" ADD CONSTRAINT "FK_45145e4953780f3cd5656f0ea6a" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note_watching" ADD CONSTRAINT "FK_b0134ec406e8d09a540f8182888" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note_watching" ADD CONSTRAINT "FK_03e7028ab8388a3f5e3ce2a8619" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note_unread" ADD CONSTRAINT "FK_56b0166d34ddae49d8ef7610bb9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note_unread" ADD CONSTRAINT "FK_e637cba4dc4410218c4251260e4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3c601b70a1066d2c8b517094cb9" FOREIGN KEY ("notifieeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_769cb6b73a1efe22ddf733ac453" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "following" ADD CONSTRAINT "FK_24e0042143a18157b234df186c3" FOREIGN KEY ("followeeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "following" ADD CONSTRAINT "FK_6516c5a6f3c015b4eed39978be5" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "muting" ADD CONSTRAINT "FK_ec96b4fed9dae517e0dbbe0675c" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "muting" ADD CONSTRAINT "FK_93060675b4a79a577f31d260c67" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "sw_subscription" ADD CONSTRAINT "FK_97754ca6f2baff9b4abb7f853dd" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "blocking" ADD CONSTRAINT "FK_2cd4a2743a99671308f5417759e" FOREIGN KEY ("blockeeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "blocking" ADD CONSTRAINT "FK_0627125f1a8a42c9a1929edb552" FOREIGN KEY ("blockerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_list" ADD CONSTRAINT "FK_b7fcefbdd1c18dce86687531f99" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_list_joining" ADD CONSTRAINT "FK_d844bfc6f3f523a05189076efaa" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_list_joining" ADD CONSTRAINT "FK_605472305f26818cc93d1baaa74" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note_favorite" ADD CONSTRAINT "FK_47f4b1892f5d6ba8efb3057d81a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note_favorite" ADD CONSTRAINT "FK_0e00498f180193423c992bc4370" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_d049123c413e68ca52abe734203" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_04cc96756f89d0b7f9473e8cdf3" FOREIGN KEY ("reporterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_5377c307783fce2b6d352e1203b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_cac14a4e3944454a5ce7daa5142" FOREIGN KEY ("recipientId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_535def119223ac05ad3fa9ef64b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "signin" ADD CONSTRAINT "FK_2c308dbdc50d94dc625670055f7" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "FK_c072b729d71697f959bde66ade0" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "FK_dbe037d4bddd17b03a1dc778dee" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "follow_request" ADD CONSTRAINT "FK_12c01c0d1a79f77d9f6c15fadd2" FOREIGN KEY ("followeeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "follow_request" ADD CONSTRAINT "FK_a7fd92dd6dc519e6fb435dd108f" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_note_pining" ADD CONSTRAINT "FK_bfbc6f79ba4007b4ce5097f08d6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_note_pining" ADD CONSTRAINT "FK_68881008f7c3588ad7ecae471cf" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`); - await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); - await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6"`); - await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b"`); - await queryRunner.query(`ALTER TABLE "user_note_pining" DROP CONSTRAINT "FK_68881008f7c3588ad7ecae471cf"`); - await queryRunner.query(`ALTER TABLE "user_note_pining" DROP CONSTRAINT "FK_bfbc6f79ba4007b4ce5097f08d6"`); - await queryRunner.query(`ALTER TABLE "reversi_matching" DROP CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066"`); - await queryRunner.query(`ALTER TABLE "reversi_matching" DROP CONSTRAINT "FK_3b25402709dd9882048c2bbade0"`); - await queryRunner.query(`ALTER TABLE "reversi_game" DROP CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6"`); - await queryRunner.query(`ALTER TABLE "reversi_game" DROP CONSTRAINT "FK_f7467510c60a45ce5aca6292743"`); - await queryRunner.query(`ALTER TABLE "follow_request" DROP CONSTRAINT "FK_a7fd92dd6dc519e6fb435dd108f"`); - await queryRunner.query(`ALTER TABLE "follow_request" DROP CONSTRAINT "FK_12c01c0d1a79f77d9f6c15fadd2"`); - await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "FK_dbe037d4bddd17b03a1dc778dee"`); - await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "FK_c072b729d71697f959bde66ade0"`); - await queryRunner.query(`ALTER TABLE "signin" DROP CONSTRAINT "FK_2c308dbdc50d94dc625670055f7"`); - await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_535def119223ac05ad3fa9ef64b"`); - await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_cac14a4e3944454a5ce7daa5142"`); - await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_5377c307783fce2b6d352e1203b"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_04cc96756f89d0b7f9473e8cdf3"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_d049123c413e68ca52abe734203"`); - await queryRunner.query(`ALTER TABLE "note_favorite" DROP CONSTRAINT "FK_0e00498f180193423c992bc4370"`); - await queryRunner.query(`ALTER TABLE "note_favorite" DROP CONSTRAINT "FK_47f4b1892f5d6ba8efb3057d81a"`); - await queryRunner.query(`ALTER TABLE "user_list_joining" DROP CONSTRAINT "FK_605472305f26818cc93d1baaa74"`); - await queryRunner.query(`ALTER TABLE "user_list_joining" DROP CONSTRAINT "FK_d844bfc6f3f523a05189076efaa"`); - await queryRunner.query(`ALTER TABLE "user_list" DROP CONSTRAINT "FK_b7fcefbdd1c18dce86687531f99"`); - await queryRunner.query(`ALTER TABLE "blocking" DROP CONSTRAINT "FK_0627125f1a8a42c9a1929edb552"`); - await queryRunner.query(`ALTER TABLE "blocking" DROP CONSTRAINT "FK_2cd4a2743a99671308f5417759e"`); - await queryRunner.query(`ALTER TABLE "sw_subscription" DROP CONSTRAINT "FK_97754ca6f2baff9b4abb7f853dd"`); - await queryRunner.query(`ALTER TABLE "muting" DROP CONSTRAINT "FK_93060675b4a79a577f31d260c67"`); - await queryRunner.query(`ALTER TABLE "muting" DROP CONSTRAINT "FK_ec96b4fed9dae517e0dbbe0675c"`); - await queryRunner.query(`ALTER TABLE "following" DROP CONSTRAINT "FK_6516c5a6f3c015b4eed39978be5"`); - await queryRunner.query(`ALTER TABLE "following" DROP CONSTRAINT "FK_24e0042143a18157b234df186c3"`); - await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_769cb6b73a1efe22ddf733ac453"`); - await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`); - await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3c601b70a1066d2c8b517094cb9"`); - await queryRunner.query(`ALTER TABLE "note_unread" DROP CONSTRAINT "FK_e637cba4dc4410218c4251260e4"`); - await queryRunner.query(`ALTER TABLE "note_unread" DROP CONSTRAINT "FK_56b0166d34ddae49d8ef7610bb9"`); - await queryRunner.query(`ALTER TABLE "note_watching" DROP CONSTRAINT "FK_03e7028ab8388a3f5e3ce2a8619"`); - await queryRunner.query(`ALTER TABLE "note_watching" DROP CONSTRAINT "FK_b0134ec406e8d09a540f8182888"`); - await queryRunner.query(`ALTER TABLE "note_reaction" DROP CONSTRAINT "FK_45145e4953780f3cd5656f0ea6a"`); - await queryRunner.query(`ALTER TABLE "note_reaction" DROP CONSTRAINT "FK_13761f64257f40c5636d0ff95ee"`); - await queryRunner.query(`ALTER TABLE "poll_vote" DROP CONSTRAINT "FK_aecfbd5ef60374918e63ee95fa7"`); - await queryRunner.query(`ALTER TABLE "poll_vote" DROP CONSTRAINT "FK_66d2bd2ee31d14bcc23069a89f8"`); - await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_5b87d9d19127bd5d92026017a7b"`); - await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_ec5c201576192ba8904c345c5cc"`); - await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_52ccc804d7c69037d558bac4c96"`); - await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`); - await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`); - await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_9949557d0e1b2c19e5344c171e9"`); - await queryRunner.query(`ALTER TABLE "app" DROP CONSTRAINT "FK_3f5b0899ef90527a3462d7c2cb3"`); - await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_afc64b53f8db3707ceb34eb28e2"`); - await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_58f5c71eaab331645112cf8cfa5"`); - await queryRunner.query(`ALTER TABLE "drive_file" DROP CONSTRAINT "FK_bb90d1956dafc4068c28aa7560a"`); - await queryRunner.query(`ALTER TABLE "drive_file" DROP CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e"`); - await queryRunner.query(`ALTER TABLE "drive_folder" DROP CONSTRAINT "FK_00ceffb0cdc238b3233294f08f2"`); - await queryRunner.query(`ALTER TABLE "drive_folder" DROP CONSTRAINT "FK_f4fc06e49c0171c85f1c48060d2"`); - await queryRunner.query(`DROP TABLE "__chart__users"`); - await queryRunner.query(`DROP TYPE "__chart__users_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__test"`); - await queryRunner.query(`DROP TYPE "__chart__test_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__test_unique"`); - await queryRunner.query(`DROP TYPE "__chart__test_unique_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__test_grouped"`); - await queryRunner.query(`DROP TYPE "__chart__test_grouped_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__per_user_reaction"`); - await queryRunner.query(`DROP TYPE "__chart__per_user_reaction_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__per_user_notes"`); - await queryRunner.query(`DROP TYPE "__chart__per_user_notes_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__per_user_following"`); - await queryRunner.query(`DROP TYPE "__chart__per_user_following_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__per_user_drive"`); - await queryRunner.query(`DROP TYPE "__chart__per_user_drive_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__notes"`); - await queryRunner.query(`DROP TYPE "__chart__notes_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__network"`); - await queryRunner.query(`DROP TYPE "__chart__network_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__instance"`); - await queryRunner.query(`DROP TYPE "__chart__instance_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__hashtag"`); - await queryRunner.query(`DROP TYPE "__chart__hashtag_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__federation"`); - await queryRunner.query(`DROP TYPE "__chart__federation_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__drive"`); - await queryRunner.query(`DROP TYPE "__chart__drive_span_enum"`); - await queryRunner.query(`DROP TABLE "__chart__active_users"`); - await queryRunner.query(`DROP TYPE "__chart__active_users_span_enum"`); - await queryRunner.query(`DROP INDEX "IDX_dce530b98e454793dac5ec2f5a"`); - await queryRunner.query(`DROP TABLE "user_profile"`); - await queryRunner.query(`DROP INDEX "IDX_171e64971c780ebd23fae140bb"`); - await queryRunner.query(`DROP TABLE "user_publickey"`); - await queryRunner.query(`DROP TABLE "user_keypair"`); - await queryRunner.query(`DROP INDEX "IDX_7fa20a12319c7f6dc3aed98c0a"`); - await queryRunner.query(`DROP INDEX "IDX_0610ebcfcfb4a18441a9bcdab2"`); - await queryRunner.query(`DROP TABLE "poll"`); - await queryRunner.query(`DROP TYPE "poll_notevisibility_enum"`); - await queryRunner.query(`DROP INDEX "IDX_410cd649884b501c02d6e72738"`); - await queryRunner.query(`DROP INDEX "IDX_bfbc6f79ba4007b4ce5097f08d"`); - await queryRunner.query(`DROP TABLE "user_note_pining"`); - await queryRunner.query(`DROP INDEX "IDX_e247b23a3c9b45f89ec1299d06"`); - await queryRunner.query(`DROP INDEX "IDX_3b25402709dd9882048c2bbade"`); - await queryRunner.query(`DROP INDEX "IDX_b604d92d6c7aec38627f6eaf16"`); - await queryRunner.query(`DROP TABLE "reversi_matching"`); - await queryRunner.query(`DROP INDEX "IDX_b46ec40746efceac604142be1c"`); - await queryRunner.query(`DROP TABLE "reversi_game"`); - await queryRunner.query(`DROP INDEX "IDX_4f4d35e1256c84ae3d1f0eab10"`); - await queryRunner.query(`DROP INDEX "IDX_5900e907bb46516ddf2871327c"`); - await queryRunner.query(`DROP INDEX "IDX_b37dafc86e9af007e3295c2781"`); - await queryRunner.query(`DROP TABLE "emoji"`); - await queryRunner.query(`DROP INDEX "IDX_d54a512b822fac7ed52800f6b4"`); - await queryRunner.query(`DROP INDEX "IDX_a7fd92dd6dc519e6fb435dd108"`); - await queryRunner.query(`DROP INDEX "IDX_12c01c0d1a79f77d9f6c15fadd"`); - await queryRunner.query(`DROP TABLE "follow_request"`); - await queryRunner.query(`DROP INDEX "IDX_62cb09e1129f6ec024ef66e183"`); - await queryRunner.query(`DROP TABLE "auth_session"`); - await queryRunner.query(`DROP INDEX "IDX_2c308dbdc50d94dc625670055f"`); - await queryRunner.query(`DROP TABLE "signin"`); - await queryRunner.query(`DROP INDEX "IDX_cac14a4e3944454a5ce7daa514"`); - await queryRunner.query(`DROP INDEX "IDX_5377c307783fce2b6d352e1203"`); - await queryRunner.query(`DROP INDEX "IDX_e21cd3646e52ef9c94aaf17c2e"`); - await queryRunner.query(`DROP TABLE "messaging_message"`); - await queryRunner.query(`DROP INDEX "IDX_0ff69e8dfa9fe31bb4a4660f59"`); - await queryRunner.query(`DROP TABLE "registration_ticket"`); - await queryRunner.query(`DROP INDEX "IDX_5cd442c3b2e74fdd99dae20243"`); - await queryRunner.query(`DROP INDEX "IDX_04cc96756f89d0b7f9473e8cdf"`); - await queryRunner.query(`DROP INDEX "IDX_d049123c413e68ca52abe73420"`); - await queryRunner.query(`DROP INDEX "IDX_db2098070b2b5a523c58181f74"`); - await queryRunner.query(`DROP TABLE "abuse_user_report"`); - await queryRunner.query(`DROP INDEX "IDX_0f4fb9ad355f3effff221ef245"`); - await queryRunner.query(`DROP INDEX "IDX_47f4b1892f5d6ba8efb3057d81"`); - await queryRunner.query(`DROP TABLE "note_favorite"`); - await queryRunner.query(`DROP INDEX "IDX_0b03cbcd7e6a7ce068efa8ecc2"`); - await queryRunner.query(`DROP INDEX "IDX_0c44bf4f680964145f2a68a341"`); - await queryRunner.query(`DROP INDEX "IDX_d57f9030cd3af7f63ffb1c267c"`); - await queryRunner.query(`DROP INDEX "IDX_4c02d38a976c3ae132228c6fce"`); - await queryRunner.query(`DROP INDEX "IDX_0e206cec573f1edff4a3062923"`); - await queryRunner.query(`DROP INDEX "IDX_2710a55f826ee236ea1a62698f"`); - await queryRunner.query(`DROP INDEX "IDX_347fec870eafea7b26c8a73bac"`); - await queryRunner.query(`DROP TABLE "hashtag"`); - await queryRunner.query(`DROP INDEX "IDX_605472305f26818cc93d1baaa7"`); - await queryRunner.query(`DROP INDEX "IDX_d844bfc6f3f523a05189076efa"`); - await queryRunner.query(`DROP TABLE "user_list_joining"`); - await queryRunner.query(`DROP INDEX "IDX_b7fcefbdd1c18dce86687531f9"`); - await queryRunner.query(`DROP TABLE "user_list"`); - await queryRunner.query(`DROP INDEX "IDX_98a1bc5cb30dfd159de056549f"`); - await queryRunner.query(`DROP INDEX "IDX_0627125f1a8a42c9a1929edb55"`); - await queryRunner.query(`DROP INDEX "IDX_2cd4a2743a99671308f5417759"`); - await queryRunner.query(`DROP INDEX "IDX_b9a354f7941c1e779f3b33aea6"`); - await queryRunner.query(`DROP TABLE "blocking"`); - await queryRunner.query(`DROP INDEX "IDX_97754ca6f2baff9b4abb7f853d"`); - await queryRunner.query(`DROP TABLE "sw_subscription"`); - await queryRunner.query(`DROP INDEX "IDX_1eb9d9824a630321a29fd3b290"`); - await queryRunner.query(`DROP INDEX "IDX_93060675b4a79a577f31d260c6"`); - await queryRunner.query(`DROP INDEX "IDX_ec96b4fed9dae517e0dbbe0675"`); - await queryRunner.query(`DROP INDEX "IDX_f86d57fbca33c7a4e6897490cc"`); - await queryRunner.query(`DROP TABLE "muting"`); - await queryRunner.query(`DROP INDEX "IDX_8d5afc98982185799b160e10eb"`); - await queryRunner.query(`DROP INDEX "IDX_2cd3b2a6b4cf0b910b260afe08"`); - await queryRunner.query(`DROP TABLE "instance"`); - await queryRunner.query(`DROP INDEX "IDX_307be5f1d1252e0388662acb96"`); - await queryRunner.query(`DROP INDEX "IDX_6516c5a6f3c015b4eed39978be"`); - await queryRunner.query(`DROP INDEX "IDX_24e0042143a18157b234df186c"`); - await queryRunner.query(`DROP INDEX "IDX_582f8fab771a9040a12961f3e7"`); - await queryRunner.query(`DROP TABLE "following"`); - await queryRunner.query(`DROP TABLE "meta"`); - await queryRunner.query(`DROP INDEX "IDX_3c601b70a1066d2c8b517094cb"`); - await queryRunner.query(`DROP INDEX "IDX_b11a5e627c41d4dc3170f1d370"`); - await queryRunner.query(`DROP TABLE "notification"`); - await queryRunner.query(`DROP INDEX "IDX_d908433a4953cc13216cd9c274"`); - await queryRunner.query(`DROP INDEX "IDX_e637cba4dc4410218c4251260e"`); - await queryRunner.query(`DROP INDEX "IDX_56b0166d34ddae49d8ef7610bb"`); - await queryRunner.query(`DROP TABLE "note_unread"`); - await queryRunner.query(`DROP INDEX "IDX_a42c93c69989ce1d09959df4cf"`); - await queryRunner.query(`DROP INDEX "IDX_44499765eec6b5489d72c4253b"`); - await queryRunner.query(`DROP INDEX "IDX_03e7028ab8388a3f5e3ce2a861"`); - await queryRunner.query(`DROP INDEX "IDX_b0134ec406e8d09a540f818288"`); - await queryRunner.query(`DROP INDEX "IDX_318cdf42a9cfc11f479bd802bb"`); - await queryRunner.query(`DROP TABLE "note_watching"`); - await queryRunner.query(`DROP INDEX "IDX_ad0c221b25672daf2df320a817"`); - await queryRunner.query(`DROP INDEX "IDX_45145e4953780f3cd5656f0ea6"`); - await queryRunner.query(`DROP INDEX "IDX_13761f64257f40c5636d0ff95e"`); - await queryRunner.query(`DROP INDEX "IDX_01f4581f114e0ebd2bbb876f0b"`); - await queryRunner.query(`DROP TABLE "note_reaction"`); - await queryRunner.query(`DROP INDEX "IDX_50bd7164c5b78f1f4a42c4d21f"`); - await queryRunner.query(`DROP INDEX "IDX_aecfbd5ef60374918e63ee95fa"`); - await queryRunner.query(`DROP INDEX "IDX_66d2bd2ee31d14bcc23069a89f"`); - await queryRunner.query(`DROP INDEX "IDX_0fb627e1c2f753262a74f0562d"`); - await queryRunner.query(`DROP TABLE "poll_vote"`); - await queryRunner.query(`DROP INDEX "IDX_7125a826ab192eb27e11d358a5"`); - await queryRunner.query(`DROP INDEX "IDX_88937d94d7443d9a99a76fa5c0"`); - await queryRunner.query(`DROP INDEX "IDX_54ebcb6d27222913b908d56fd8"`); - await queryRunner.query(`DROP INDEX "IDX_796a8c03959361f97dc2be1d5c"`); - await queryRunner.query(`DROP INDEX "IDX_25dfc71b0369b003a4cd434d0b"`); - await queryRunner.query(`DROP INDEX "IDX_51c063b6a133a9cb87145450f5"`); - await queryRunner.query(`DROP INDEX "IDX_153536c67d05e9adb24e99fc2b"`); - await queryRunner.query(`DROP INDEX "IDX_5b87d9d19127bd5d92026017a7"`); - await queryRunner.query(`DROP INDEX "IDX_52ccc804d7c69037d558bac4c9"`); - await queryRunner.query(`DROP INDEX "IDX_17cb3553c700a4985dff5a30ff"`); - await queryRunner.query(`DROP INDEX "IDX_e7c0567f5261063592f022e9b5"`); - await queryRunner.query(`DROP TABLE "note"`); - await queryRunner.query(`DROP TYPE "note_visibility_enum"`); - await queryRunner.query(`DROP INDEX "IDX_9949557d0e1b2c19e5344c171e"`); - await queryRunner.query(`DROP INDEX "IDX_64c327441248bae40f7d92f34f"`); - await queryRunner.query(`DROP INDEX "IDX_70ba8f6af34bc924fc9e12adb8"`); - await queryRunner.query(`DROP TABLE "access_token"`); - await queryRunner.query(`DROP INDEX "IDX_f49922d511d666848f250663c4"`); - await queryRunner.query(`DROP INDEX "IDX_3f5b0899ef90527a3462d7c2cb"`); - await queryRunner.query(`DROP INDEX "IDX_048a757923ed8b157e9895da53"`); - await queryRunner.query(`DROP TABLE "app"`); - await queryRunner.query(`DROP INDEX "IDX_5deb01ae162d1d70b80d064c27"`); - await queryRunner.query(`DROP INDEX "IDX_a854e557b1b14814750c7c7b0c"`); - await queryRunner.query(`DROP INDEX "IDX_be623adaa4c566baf5d29ce0c8"`); - await queryRunner.query(`DROP INDEX "IDX_3252a5df8d5bbd16b281f7799e"`); - await queryRunner.query(`DROP INDEX "IDX_fa99d777623947a5b05f394cae"`); - await queryRunner.query(`DROP INDEX "IDX_a27b942a0d6dcff90e3ee9b5e8"`); - await queryRunner.query(`DROP INDEX "IDX_80ca6e6ef65fb9ef34ea8c90f4"`); - await queryRunner.query(`DROP INDEX "IDX_e11e649824a45d8ed01d597fd9"`); - await queryRunner.query(`DROP TABLE "user"`); - await queryRunner.query(`DROP INDEX "IDX_bb90d1956dafc4068c28aa7560"`); - await queryRunner.query(`DROP INDEX "IDX_e5848eac4940934e23dbc17581"`); - await queryRunner.query(`DROP INDEX "IDX_c55b2b7c284d9fef98026fc88e"`); - await queryRunner.query(`DROP INDEX "IDX_e74022ce9a074b3866f70e0d27"`); - await queryRunner.query(`DROP INDEX "IDX_d85a184c2540d2deba33daf642"`); - await queryRunner.query(`DROP INDEX "IDX_a40b8df8c989d7db937ea27cf6"`); - await queryRunner.query(`DROP INDEX "IDX_37bb9a1b4585f8a3beb24c62d6"`); - await queryRunner.query(`DROP INDEX "IDX_92779627994ac79277f070c91e"`); - await queryRunner.query(`DROP INDEX "IDX_860fa6f6c7df5bb887249fba22"`); - await queryRunner.query(`DROP INDEX "IDX_c8dfad3b72196dd1d6b5db168a"`); - await queryRunner.query(`DROP TABLE "drive_file"`); - await queryRunner.query(`DROP INDEX "IDX_00ceffb0cdc238b3233294f08f"`); - await queryRunner.query(`DROP INDEX "IDX_f4fc06e49c0171c85f1c48060d"`); - await queryRunner.query(`DROP INDEX "IDX_02878d441ceae15ce060b73daf"`); - await queryRunner.query(`DROP TABLE "drive_folder"`); - await queryRunner.query(`DROP INDEX "IDX_584b536b49e53ac81beb39a177"`); - await queryRunner.query(`DROP INDEX "IDX_8cb40cfc8f3c28261e6f887b03"`); - await queryRunner.query(`DROP INDEX "IDX_8e4eb51a35d81b64dda28eed0a"`); - await queryRunner.query(`DROP TABLE "log"`); - await queryRunner.query(`DROP TYPE "log_level_enum"`); - } -} diff --git a/packages/backend/migration/1556348509290-Pages.js b/packages/backend/migration/1556348509290-Pages.js deleted file mode 100644 index 50caa2ce9..000000000 --- a/packages/backend/migration/1556348509290-Pages.js +++ /dev/null @@ -1,28 +0,0 @@ - - -export class Pages1556348509290 { - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "page_visibility_enum" AS ENUM('public', 'followers', 'specified')`); - await queryRunner.query(`CREATE TABLE "page" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "name" character varying(256) NOT NULL, "summary" character varying(256), "alignCenter" boolean NOT NULL, "font" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "eyeCatchingImageId" character varying(32), "content" jsonb NOT NULL DEFAULT '[]', "variables" jsonb NOT NULL DEFAULT '[]', "visibility" "page_visibility_enum" NOT NULL, "visibleUserIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_742f4117e065c5b6ad21b37ba1f" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_fbb4297c927a9b85e9cefa2eb1" ON "page" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_af639b066dfbca78b01a920f8a" ON "page" ("updatedAt") `); - await queryRunner.query(`CREATE INDEX "IDX_b82c19c08afb292de4600d99e4" ON "page" ("name") `); - await queryRunner.query(`CREATE INDEX "IDX_ae1d917992dd0c9d9bbdad06c4" ON "page" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_90148bbc2bf0854428786bfc15" ON "page" ("visibleUserIds") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2133ef8317e4bdb839c0dcbf13" ON "page" ("userId", "name") `); - await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "page" ADD CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" FOREIGN KEY ("eyeCatchingImageId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10"`); - await queryRunner.query(`ALTER TABLE "page" DROP CONSTRAINT "FK_ae1d917992dd0c9d9bbdad06c4a"`); - await queryRunner.query(`DROP INDEX "IDX_2133ef8317e4bdb839c0dcbf13"`); - await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`); - await queryRunner.query(`DROP INDEX "IDX_ae1d917992dd0c9d9bbdad06c4"`); - await queryRunner.query(`DROP INDEX "IDX_b82c19c08afb292de4600d99e4"`); - await queryRunner.query(`DROP INDEX "IDX_af639b066dfbca78b01a920f8a"`); - await queryRunner.query(`DROP INDEX "IDX_fbb4297c927a9b85e9cefa2eb1"`); - await queryRunner.query(`DROP TABLE "page"`); - await queryRunner.query(`DROP TYPE "page_visibility_enum"`); - } -} diff --git a/packages/backend/migration/1556746559567-UserProfile.js b/packages/backend/migration/1556746559567-UserProfile.js deleted file mode 100644 index 50a9d1a8b..000000000 --- a/packages/backend/migration/1556746559567-UserProfile.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class UserProfile1556746559567 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "githubId" TYPE VARCHAR(64) USING "githubId"::VARCHAR(64)`); - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "discordExpiresDate" TYPE VARCHAR(64) USING "discordExpiresDate"::VARCHAR(64)`); - } - async down(queryRunner) { - await queryRunner.query(`UPDATE "user_profile" SET github = FALSE, discord = FALSE`); - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "githubId" TYPE INTEGER USING NULL`); - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "discordExpiresDate" TYPE INTEGER USING NULL`); - } -} diff --git a/packages/backend/migration/1557476068003-PinnedUsers.js b/packages/backend/migration/1557476068003-PinnedUsers.js deleted file mode 100644 index d9cce2543..000000000 --- a/packages/backend/migration/1557476068003-PinnedUsers.js +++ /dev/null @@ -1,10 +0,0 @@ - - -export class PinnedUsers1557476068003 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedUsers" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedUsers"`); - } -} diff --git a/packages/backend/migration/1557761316509-AddSomeUrls.js b/packages/backend/migration/1557761316509-AddSomeUrls.js deleted file mode 100644 index ab8736f7c..000000000 --- a/packages/backend/migration/1557761316509-AddSomeUrls.js +++ /dev/null @@ -1,14 +0,0 @@ - - -export class AddSomeUrls1557761316509 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "ToSUrl" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "repositoryUrl" character varying(512) NOT NULL DEFAULT 'https://github.com/misskey-dev/misskey'`); - await queryRunner.query(`ALTER TABLE "meta" ADD "feedbackUrl" character varying(512) DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "feedbackUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "repositoryUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ToSUrl"`); - } -} diff --git a/packages/backend/migration/1557932705754-ObjectStorageSetting.js b/packages/backend/migration/1557932705754-ObjectStorageSetting.js deleted file mode 100644 index 19a0b9d5c..000000000 --- a/packages/backend/migration/1557932705754-ObjectStorageSetting.js +++ /dev/null @@ -1,28 +0,0 @@ - - -export class ObjectStorageSetting1557932705754 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "useObjectStorage" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBucket" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePrefix" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageBaseUrl" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageEndpoint" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageRegion" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageAccessKey" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSecretKey" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStoragePort" integer`); - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseSSL" boolean NOT NULL DEFAULT true`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseSSL"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePort"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSecretKey"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageAccessKey"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageRegion"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageEndpoint"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBaseUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStoragePrefix"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageBucket"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useObjectStorage"`); - } -} diff --git a/packages/backend/migration/1558072954435-PageLike.js b/packages/backend/migration/1558072954435-PageLike.js deleted file mode 100644 index 31b08418a..000000000 --- a/packages/backend/migration/1558072954435-PageLike.js +++ /dev/null @@ -1,20 +0,0 @@ - - -export class PageLike1558072954435 { - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "page_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "pageId" character varying(32) NOT NULL, CONSTRAINT "PK_813f034843af992d3ae0f43c64c" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_0e61efab7f88dbb79c9166dbb4" ON "page_like" ("userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa" ON "page_like" ("userId", "pageId") `); - await queryRunner.query(`ALTER TABLE "page" ADD "likedCount" integer NOT NULL DEFAULT 0`); - await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "page_like" ADD CONSTRAINT "FK_cf8782626dced3176038176a847" FOREIGN KEY ("pageId") REFERENCES "page"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_cf8782626dced3176038176a847"`); - await queryRunner.query(`ALTER TABLE "page_like" DROP CONSTRAINT "FK_0e61efab7f88dbb79c9166dbb48"`); - await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "likedCount"`); - await queryRunner.query(`DROP INDEX "IDX_4ce6fb9c70529b4c8ac46c9bfa"`); - await queryRunner.query(`DROP INDEX "IDX_0e61efab7f88dbb79c9166dbb4"`); - await queryRunner.query(`DROP TABLE "page_like"`); - } -} diff --git a/packages/backend/migration/1558103093633-UserGroup.js b/packages/backend/migration/1558103093633-UserGroup.js deleted file mode 100644 index b670b31c3..000000000 --- a/packages/backend/migration/1558103093633-UserGroup.js +++ /dev/null @@ -1,38 +0,0 @@ - - -export class UserGroup1558103093633 { - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "user_group" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "isPrivate" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_3c29fba6fe013ec8724378ce7c9" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_20e30aa35180e317e133d75316" ON "user_group" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_3d6b372788ab01be58853003c9" ON "user_group" ("userId") `); - await queryRunner.query(`CREATE TABLE "user_group_joining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_15f2425885253c5507e1599cfe7" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23" ON "user_group_joining" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_67dc758bc0566985d1b3d39986" ON "user_group_joining" ("userGroupId") `); - await queryRunner.query(`ALTER TABLE "messaging_message" ADD "groupId" character varying(32)`); - await queryRunner.query(`ALTER TABLE "messaging_message" ADD "reads" character varying(32) array NOT NULL DEFAULT '{}'::varchar[]`); - await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" DROP NOT NULL`); - await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS 'The recipient user ID.'`); - await queryRunner.query(`CREATE INDEX "IDX_2c4be03b446884f9e9c502135b" ON "messaging_message" ("groupId") `); - await queryRunner.query(`ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_2c4be03b446884f9e9c502135be" FOREIGN KEY ("groupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_group" ADD CONSTRAINT "FK_3d6b372788ab01be58853003c93" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_group_joining" ADD CONSTRAINT "FK_67dc758bc0566985d1b3d399865" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_67dc758bc0566985d1b3d399865"`); - await queryRunner.query(`ALTER TABLE "user_group_joining" DROP CONSTRAINT "FK_f3a1b4bd0c7cabba958a0c0b231"`); - await queryRunner.query(`ALTER TABLE "user_group" DROP CONSTRAINT "FK_3d6b372788ab01be58853003c93"`); - await queryRunner.query(`ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_2c4be03b446884f9e9c502135be"`); - await queryRunner.query(`DROP INDEX "IDX_2c4be03b446884f9e9c502135b"`); - await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."recipientId" IS ''`); - await queryRunner.query(`ALTER TABLE "messaging_message" ALTER COLUMN "recipientId" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "reads"`); - await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "groupId"`); - await queryRunner.query(`DROP INDEX "IDX_67dc758bc0566985d1b3d39986"`); - await queryRunner.query(`DROP INDEX "IDX_f3a1b4bd0c7cabba958a0c0b23"`); - await queryRunner.query(`DROP TABLE "user_group_joining"`); - await queryRunner.query(`DROP INDEX "IDX_3d6b372788ab01be58853003c9"`); - await queryRunner.query(`DROP INDEX "IDX_20e30aa35180e317e133d75316"`); - await queryRunner.query(`DROP TABLE "user_group"`); - } -} diff --git a/packages/backend/migration/1558257926829-UserGroupInvite.js b/packages/backend/migration/1558257926829-UserGroupInvite.js deleted file mode 100644 index e48bd3a7f..000000000 --- a/packages/backend/migration/1558257926829-UserGroupInvite.js +++ /dev/null @@ -1,22 +0,0 @@ - - -export class UserGroupInvite1558257926829 { - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "user_group_invite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_3893884af0d3a5f4d01e7921a97" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_1039988afa3bf991185b277fe0" ON "user_group_invite" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_e10924607d058004304611a436" ON "user_group_invite" ("userGroupId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_78787741f9010886796f2320a4" ON "user_group_invite" ("userId", "userGroupId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `); - await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_1039988afa3bf991185b277fe03" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_e10924607d058004304611a436a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_e10924607d058004304611a436a"`); - await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_1039988afa3bf991185b277fe03"`); - await queryRunner.query(`DROP INDEX "IDX_d9ecaed8c6dc43f3592c229282"`); - await queryRunner.query(`DROP INDEX "IDX_78787741f9010886796f2320a4"`); - await queryRunner.query(`DROP INDEX "IDX_e10924607d058004304611a436"`); - await queryRunner.query(`DROP INDEX "IDX_1039988afa3bf991185b277fe0"`); - await queryRunner.query(`DROP TABLE "user_group_invite"`); - } -} diff --git a/packages/backend/migration/1558266512381-UserListJoining.js b/packages/backend/migration/1558266512381-UserListJoining.js deleted file mode 100644 index 3398aed13..000000000 --- a/packages/backend/migration/1558266512381-UserListJoining.js +++ /dev/null @@ -1,10 +0,0 @@ - - -export class UserListJoining1558266512381 { - async up(queryRunner) { - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_joining" ("userId", "userListId") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_90f7da835e4c10aca6853621e1"`); - } -} diff --git a/packages/backend/migration/1561706992953-webauthn.js b/packages/backend/migration/1561706992953-webauthn.js deleted file mode 100644 index b007ffef1..000000000 --- a/packages/backend/migration/1561706992953-webauthn.js +++ /dev/null @@ -1,26 +0,0 @@ - - -export class webauthn1561706992953 { - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "attestation_challenge" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "challenge" character varying(64) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "registrationChallenge" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_d0ba6786e093f1bcb497572a6b5" PRIMARY KEY ("id", "userId"))`); - await queryRunner.query(`CREATE INDEX "IDX_f1a461a618fa1755692d0e0d59" ON "attestation_challenge" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_47efb914aed1f72dd39a306c7b" ON "attestation_challenge" ("challenge") `); - await queryRunner.query(`CREATE TABLE "user_security_key" ("id" character varying NOT NULL, "userId" character varying(32) NOT NULL, "publicKey" character varying NOT NULL, "lastUsed" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(30) NOT NULL, CONSTRAINT "PK_3e508571121ab39c5f85d10c166" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_ff9ca3b5f3ee3d0681367a9b44" ON "user_security_key" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_0d7718e562dcedd0aa5cf2c9f7" ON "user_security_key" ("publicKey") `); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "securityKeysAvailable" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "attestation_challenge" ADD CONSTRAINT "FK_f1a461a618fa1755692d0e0d592" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_security_key" ADD CONSTRAINT "FK_ff9ca3b5f3ee3d0681367a9b447" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_security_key" DROP CONSTRAINT "FK_ff9ca3b5f3ee3d0681367a9b447"`); - await queryRunner.query(`ALTER TABLE "attestation_challenge" DROP CONSTRAINT "FK_f1a461a618fa1755692d0e0d592"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "securityKeysAvailable"`); - await queryRunner.query(`DROP INDEX "IDX_0d7718e562dcedd0aa5cf2c9f7"`); - await queryRunner.query(`DROP INDEX "IDX_ff9ca3b5f3ee3d0681367a9b44"`); - await queryRunner.query(`DROP TABLE "user_security_key"`); - await queryRunner.query(`DROP INDEX "IDX_47efb914aed1f72dd39a306c7b"`); - await queryRunner.query(`DROP INDEX "IDX_f1a461a618fa1755692d0e0d59"`); - await queryRunner.query(`DROP TABLE "attestation_challenge"`); - } -} diff --git a/packages/backend/migration/1561873850023-ChartIndexes.js b/packages/backend/migration/1561873850023-ChartIndexes.js deleted file mode 100644 index 3ce53567f..000000000 --- a/packages/backend/migration/1561873850023-ChartIndexes.js +++ /dev/null @@ -1,198 +0,0 @@ - - -export class ChartIndexes1561873850023 { - async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc" ON "__chart__active_users" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_15e91a03aeeac9dbccdf43fc06" ON "__chart__active_users" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_00ed5f86db1f7efafb1978bf21" ON "__chart__active_users" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_20f57cc8f142c131340ee16742" ON "__chart__active_users" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_c26e2c1cbb6e911e0554b27416" ON "__chart__active_users" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_13565815f618a1ff53886c5b28" ON "__chart__drive" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_3fa0d0f17ca72e3dc80999a032" ON "__chart__drive" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_7a170f67425e62a8fabb76c872" ON "__chart__drive" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_6e1df243476e20cbf86572ecc0" ON "__chart__drive" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_06690fc959f1c9fdaf21928222" ON "__chart__drive" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_36cb699c49580d4e6c2e6159f9" ON "__chart__federation" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_e447064455928cf627590ef527" ON "__chart__federation" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_76e87c7bfc5d925fcbba405d84" ON "__chart__federation" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_2d416e6af791a82e338c79d480" ON "__chart__federation" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_e9cd07672b37d8966cf3709283" ON "__chart__federation" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_07747a1038c05f532a718fe1de" ON "__chart__hashtag" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_fcc181fb8283009c61cc4083ef" ON "__chart__hashtag" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_99a7d2faaef84a6f728d714ad6" ON "__chart__hashtag" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_49975586f50ed7b800fdd88fbd" ON "__chart__hashtag" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_6d6f156ceefc6bc5f273a0e370" ON "__chart__hashtag" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_6b8f34a1a64b06014b6fb66824" ON "__chart__instance" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_c12f0af4a66cdd30c2287ce8aa" ON "__chart__instance" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63" ON "__chart__instance" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_d0a4f79af5a97b08f37b547197" ON "__chart__instance" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_f5448d9633cff74208d850aabe" ON "__chart__instance" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_a1efd3e0048a5f2793a47360dc" ON "__chart__network" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_f8dd01baeded2ffa833e0a610a" ON "__chart__network" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_7b5da130992ec9df96712d4290" ON "__chart__network" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_08fac0eb3b11f04c200c0b40dd" ON "__chart__network" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_9ff6944f01acb756fdc92d7563" ON "__chart__network" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_42eb716a37d381cdf566192b2b" ON "__chart__notes" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_e69096589f11e3baa98ddd64d0" ON "__chart__notes" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_7036f2957151588b813185c794" ON "__chart__notes" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_0c9a159c5082cbeef3ca6706b5" ON "__chart__notes" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_924fc196c80ca24bae01dd37e4" ON "__chart__notes" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_5f86db6492274e07c1a3cdf286" ON "__chart__per_user_drive" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_328f259961e60c4fa0bfcf55ca" ON "__chart__per_user_drive" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_e496ca8096d28f6b9b509264dc" ON "__chart__per_user_drive" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53" ON "__chart__per_user_drive" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_f2aeafde2ae6fbad38e857631b" ON "__chart__per_user_drive" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_7af07790712aa3438ff6773f3b" ON "__chart__per_user_following" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_f92dd6d03f8d994f29987f6214" ON "__chart__per_user_following" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_4b3593098b6edc9c5afe36b18b" ON "__chart__per_user_following" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f" ON "__chart__per_user_following" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_4db3b84c7be0d3464714f3e0b1" ON "__chart__per_user_following" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_84234bd1abb873f07329681c83" ON "__chart__per_user_notes" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_8d2cbbc8114d90d19b44d626b6" ON "__chart__per_user_notes" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_55bf20f366979f2436de99206b" ON "__chart__per_user_notes" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_046feeb12e9ef5f783f409866a" ON "__chart__per_user_notes" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_f68a5ab958f9f5fa17a32ac23b" ON "__chart__per_user_notes" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_f7bf4c62059764c2c2bb40fdab" ON "__chart__per_user_reaction" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_65633a106bce43fc7c5c30a5c7" ON "__chart__per_user_reaction" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_8cf3156fd7a6b15c43459c6e3b" ON "__chart__per_user_reaction" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_edeb73c09c3143a81bcb34d569" ON "__chart__per_user_reaction" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_e316f01a6d24eb31db27f88262" ON "__chart__per_user_reaction" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_0c641990ecf47d2545df4edb75" ON "__chart__test_grouped" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_2be7ec6cebddc14dc11e206686" ON "__chart__test_grouped" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_234dff3c0b56a6150b95431ab9" ON "__chart__test_grouped" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_a5133470f4825902e170328ca5" ON "__chart__test_grouped" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_b14489029e4b3aaf4bba5fb524" ON "__chart__test_grouped" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_84e661abb7bd1e51b690d4b017" ON "__chart__test_grouped" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_437bab3c6061d90f6bb65fd2cc" ON "__chart__test_unique" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_5c73bf61da4f6e6f15bae88ed1" ON "__chart__test_unique" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_bbfa573a8181018851ed0b6357" ON "__chart__test_unique" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_d70c86baedc68326be11f9c0ce" ON "__chart__test_unique" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_a0cd75442dd10d0643a17c4a49" ON "__chart__test_unique" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_66e1e1ecd2f29e57778af35b59" ON "__chart__test_unique" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_b070a906db04b44c67c6c2144d" ON "__chart__test" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_92255988735563f0fe4aba1f05" ON "__chart__test" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_d41cce6aee1a50bfc062038f9b" ON "__chart__test" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_c5870993e25c3d5771f91f5003" ON "__chart__test" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_a319e5dbf47e8a17497623beae" ON "__chart__test" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_f170de677ea75ad4533de2723e" ON "__chart__test" ("span", "date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_845254b3eaf708ae8a6cac3026" ON "__chart__users" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_7c184198ecf66a8d3ecb253ab3" ON "__chart__users" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_ed9b95919c672a13008e9487ee" ON "__chart__users" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_f091abb24193d50c653c6b77fc" ON "__chart__users" ("span", "date") `); - await queryRunner.query(`CREATE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_a770a57c70e668cc61590c9161" ON "__chart__users" ("span", "date", "group") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_a770a57c70e668cc61590c9161"`); - await queryRunner.query(`DROP INDEX "IDX_337e9599f278bd7537fe30876f"`); - await queryRunner.query(`DROP INDEX "IDX_f091abb24193d50c653c6b77fc"`); - await queryRunner.query(`DROP INDEX "IDX_ed9b95919c672a13008e9487ee"`); - await queryRunner.query(`DROP INDEX "IDX_7c184198ecf66a8d3ecb253ab3"`); - await queryRunner.query(`DROP INDEX "IDX_845254b3eaf708ae8a6cac3026"`); - await queryRunner.query(`DROP INDEX "IDX_f170de677ea75ad4533de2723e"`); - await queryRunner.query(`DROP INDEX "IDX_a319e5dbf47e8a17497623beae"`); - await queryRunner.query(`DROP INDEX "IDX_c5870993e25c3d5771f91f5003"`); - await queryRunner.query(`DROP INDEX "IDX_d41cce6aee1a50bfc062038f9b"`); - await queryRunner.query(`DROP INDEX "IDX_92255988735563f0fe4aba1f05"`); - await queryRunner.query(`DROP INDEX "IDX_b070a906db04b44c67c6c2144d"`); - await queryRunner.query(`DROP INDEX "IDX_66e1e1ecd2f29e57778af35b59"`); - await queryRunner.query(`DROP INDEX "IDX_a0cd75442dd10d0643a17c4a49"`); - await queryRunner.query(`DROP INDEX "IDX_d70c86baedc68326be11f9c0ce"`); - await queryRunner.query(`DROP INDEX "IDX_bbfa573a8181018851ed0b6357"`); - await queryRunner.query(`DROP INDEX "IDX_5c73bf61da4f6e6f15bae88ed1"`); - await queryRunner.query(`DROP INDEX "IDX_437bab3c6061d90f6bb65fd2cc"`); - await queryRunner.query(`DROP INDEX "IDX_84e661abb7bd1e51b690d4b017"`); - await queryRunner.query(`DROP INDEX "IDX_b14489029e4b3aaf4bba5fb524"`); - await queryRunner.query(`DROP INDEX "IDX_a5133470f4825902e170328ca5"`); - await queryRunner.query(`DROP INDEX "IDX_234dff3c0b56a6150b95431ab9"`); - await queryRunner.query(`DROP INDEX "IDX_2be7ec6cebddc14dc11e206686"`); - await queryRunner.query(`DROP INDEX "IDX_0c641990ecf47d2545df4edb75"`); - await queryRunner.query(`DROP INDEX "IDX_e316f01a6d24eb31db27f88262"`); - await queryRunner.query(`DROP INDEX "IDX_229a41ad465f9205f1f5703291"`); - await queryRunner.query(`DROP INDEX "IDX_edeb73c09c3143a81bcb34d569"`); - await queryRunner.query(`DROP INDEX "IDX_8cf3156fd7a6b15c43459c6e3b"`); - await queryRunner.query(`DROP INDEX "IDX_65633a106bce43fc7c5c30a5c7"`); - await queryRunner.query(`DROP INDEX "IDX_f7bf4c62059764c2c2bb40fdab"`); - await queryRunner.query(`DROP INDEX "IDX_f68a5ab958f9f5fa17a32ac23b"`); - await queryRunner.query(`DROP INDEX "IDX_5048e9daccbbbc6d567bb142d3"`); - await queryRunner.query(`DROP INDEX "IDX_046feeb12e9ef5f783f409866a"`); - await queryRunner.query(`DROP INDEX "IDX_55bf20f366979f2436de99206b"`); - await queryRunner.query(`DROP INDEX "IDX_8d2cbbc8114d90d19b44d626b6"`); - await queryRunner.query(`DROP INDEX "IDX_84234bd1abb873f07329681c83"`); - await queryRunner.query(`DROP INDEX "IDX_4db3b84c7be0d3464714f3e0b1"`); - await queryRunner.query(`DROP INDEX "IDX_b77d4dd9562c3a899d9a286fcd"`); - await queryRunner.query(`DROP INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f"`); - await queryRunner.query(`DROP INDEX "IDX_4b3593098b6edc9c5afe36b18b"`); - await queryRunner.query(`DROP INDEX "IDX_f92dd6d03f8d994f29987f6214"`); - await queryRunner.query(`DROP INDEX "IDX_7af07790712aa3438ff6773f3b"`); - await queryRunner.query(`DROP INDEX "IDX_f2aeafde2ae6fbad38e857631b"`); - await queryRunner.query(`DROP INDEX "IDX_30bf67687f483ace115c5ca642"`); - await queryRunner.query(`DROP INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53"`); - await queryRunner.query(`DROP INDEX "IDX_e496ca8096d28f6b9b509264dc"`); - await queryRunner.query(`DROP INDEX "IDX_328f259961e60c4fa0bfcf55ca"`); - await queryRunner.query(`DROP INDEX "IDX_5f86db6492274e07c1a3cdf286"`); - await queryRunner.query(`DROP INDEX "IDX_924fc196c80ca24bae01dd37e4"`); - await queryRunner.query(`DROP INDEX "IDX_f09d543e3acb16c5976bdb31fa"`); - await queryRunner.query(`DROP INDEX "IDX_0c9a159c5082cbeef3ca6706b5"`); - await queryRunner.query(`DROP INDEX "IDX_7036f2957151588b813185c794"`); - await queryRunner.query(`DROP INDEX "IDX_e69096589f11e3baa98ddd64d0"`); - await queryRunner.query(`DROP INDEX "IDX_42eb716a37d381cdf566192b2b"`); - await queryRunner.query(`DROP INDEX "IDX_9ff6944f01acb756fdc92d7563"`); - await queryRunner.query(`DROP INDEX "IDX_0a905b992fecd2b5c3fb98759e"`); - await queryRunner.query(`DROP INDEX "IDX_08fac0eb3b11f04c200c0b40dd"`); - await queryRunner.query(`DROP INDEX "IDX_7b5da130992ec9df96712d4290"`); - await queryRunner.query(`DROP INDEX "IDX_f8dd01baeded2ffa833e0a610a"`); - await queryRunner.query(`DROP INDEX "IDX_a1efd3e0048a5f2793a47360dc"`); - await queryRunner.query(`DROP INDEX "IDX_f5448d9633cff74208d850aabe"`); - await queryRunner.query(`DROP INDEX "IDX_39ee857ab2f23493037c6b6631"`); - await queryRunner.query(`DROP INDEX "IDX_d0a4f79af5a97b08f37b547197"`); - await queryRunner.query(`DROP INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63"`); - await queryRunner.query(`DROP INDEX "IDX_c12f0af4a66cdd30c2287ce8aa"`); - await queryRunner.query(`DROP INDEX "IDX_6b8f34a1a64b06014b6fb66824"`); - await queryRunner.query(`DROP INDEX "IDX_6d6f156ceefc6bc5f273a0e370"`); - await queryRunner.query(`DROP INDEX "IDX_25a97c02003338124b2b75fdbc"`); - await queryRunner.query(`DROP INDEX "IDX_49975586f50ed7b800fdd88fbd"`); - await queryRunner.query(`DROP INDEX "IDX_99a7d2faaef84a6f728d714ad6"`); - await queryRunner.query(`DROP INDEX "IDX_fcc181fb8283009c61cc4083ef"`); - await queryRunner.query(`DROP INDEX "IDX_07747a1038c05f532a718fe1de"`); - await queryRunner.query(`DROP INDEX "IDX_e9cd07672b37d8966cf3709283"`); - await queryRunner.query(`DROP INDEX "IDX_dd907becf76104e4b656659e6b"`); - await queryRunner.query(`DROP INDEX "IDX_2d416e6af791a82e338c79d480"`); - await queryRunner.query(`DROP INDEX "IDX_76e87c7bfc5d925fcbba405d84"`); - await queryRunner.query(`DROP INDEX "IDX_e447064455928cf627590ef527"`); - await queryRunner.query(`DROP INDEX "IDX_36cb699c49580d4e6c2e6159f9"`); - await queryRunner.query(`DROP INDEX "IDX_06690fc959f1c9fdaf21928222"`); - await queryRunner.query(`DROP INDEX "IDX_3313d7288855ec105b5bbf6c21"`); - await queryRunner.query(`DROP INDEX "IDX_6e1df243476e20cbf86572ecc0"`); - await queryRunner.query(`DROP INDEX "IDX_7a170f67425e62a8fabb76c872"`); - await queryRunner.query(`DROP INDEX "IDX_3fa0d0f17ca72e3dc80999a032"`); - await queryRunner.query(`DROP INDEX "IDX_13565815f618a1ff53886c5b28"`); - await queryRunner.query(`DROP INDEX "IDX_c26e2c1cbb6e911e0554b27416"`); - await queryRunner.query(`DROP INDEX "IDX_9a3ed15a30ab7e3a37702e6e08"`); - await queryRunner.query(`DROP INDEX "IDX_20f57cc8f142c131340ee16742"`); - await queryRunner.query(`DROP INDEX "IDX_00ed5f86db1f7efafb1978bf21"`); - await queryRunner.query(`DROP INDEX "IDX_15e91a03aeeac9dbccdf43fc06"`); - await queryRunner.query(`DROP INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc"`); - await queryRunner.query(`DROP INDEX "IDX_90148bbc2bf0854428786bfc15"`); - await queryRunner.query(`DROP INDEX "IDX_88937d94d7443d9a99a76fa5c0"`); - await queryRunner.query(`DROP INDEX "IDX_54ebcb6d27222913b908d56fd8"`); - await queryRunner.query(`DROP INDEX "IDX_796a8c03959361f97dc2be1d5c"`); - await queryRunner.query(`DROP INDEX "IDX_25dfc71b0369b003a4cd434d0b"`); - await queryRunner.query(`DROP INDEX "IDX_51c063b6a133a9cb87145450f5"`); - await queryRunner.query(`DROP INDEX "IDX_fa99d777623947a5b05f394cae"`); - await queryRunner.query(`DROP INDEX "IDX_315c779174fe8247ab324f036e"`); - await queryRunner.query(`DROP INDEX "IDX_c5d46cbfda48b1c33ed852e21b"`); - await queryRunner.query(`DROP INDEX "IDX_8cb40cfc8f3c28261e6f887b03"`); - } -} diff --git a/packages/backend/migration/1562422242907-PasswordLessLogin.js b/packages/backend/migration/1562422242907-PasswordLessLogin.js deleted file mode 100644 index b73c7db4d..000000000 --- a/packages/backend/migration/1562422242907-PasswordLessLogin.js +++ /dev/null @@ -1,10 +0,0 @@ - - -export class PasswordLessLogin1562422242907 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "usePasswordLessLogin" boolean DEFAULT false NOT NULL`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "usePasswordLessLogin"`); - } -} diff --git a/packages/backend/migration/1562444565093-PinnedPage.js b/packages/backend/migration/1562444565093-PinnedPage.js deleted file mode 100644 index 9a999a915..000000000 --- a/packages/backend/migration/1562444565093-PinnedPage.js +++ /dev/null @@ -1,14 +0,0 @@ - - -export class PinnedPage1562444565093 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "pinnedPageId" character varying(32)`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27" UNIQUE ("pinnedPageId")`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27" FOREIGN KEY ("pinnedPageId") REFERENCES "page"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "pinnedPageId"`); - } -} diff --git a/packages/backend/migration/1562448332510-PageTitleHideOption.js b/packages/backend/migration/1562448332510-PageTitleHideOption.js deleted file mode 100644 index 8fc78d202..000000000 --- a/packages/backend/migration/1562448332510-PageTitleHideOption.js +++ /dev/null @@ -1,10 +0,0 @@ - - -export class PageTitleHideOption1562448332510 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "page" ADD "hideTitleWhenPinned" boolean NOT NULL DEFAULT false`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "hideTitleWhenPinned"`); - } -} diff --git a/packages/backend/migration/1562869971568-ModerationLog.js b/packages/backend/migration/1562869971568-ModerationLog.js deleted file mode 100644 index dd66d16ee..000000000 --- a/packages/backend/migration/1562869971568-ModerationLog.js +++ /dev/null @@ -1,14 +0,0 @@ - - -export class ModerationLog1562869971568 { - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "moderation_log" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "type" character varying(128) NOT NULL, "info" jsonb NOT NULL, CONSTRAINT "PK_d0adca6ecfd068db83e4526cc26" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_a08ad074601d204e0f69da9a95" ON "moderation_log" ("userId") `); - await queryRunner.query(`ALTER TABLE "moderation_log" ADD CONSTRAINT "FK_a08ad074601d204e0f69da9a954" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "moderation_log" DROP CONSTRAINT "FK_a08ad074601d204e0f69da9a954"`); - await queryRunner.query(`DROP INDEX "IDX_a08ad074601d204e0f69da9a95"`); - await queryRunner.query(`DROP TABLE "moderation_log"`); - } -} diff --git a/packages/backend/migration/1563757595828-UsedUsername.js b/packages/backend/migration/1563757595828-UsedUsername.js deleted file mode 100644 index 8972df297..000000000 --- a/packages/backend/migration/1563757595828-UsedUsername.js +++ /dev/null @@ -1,10 +0,0 @@ - - -export class UsedUsername1563757595828 { - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "used_username" ("username" character varying(128) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_78fd79d2d24c6ac2f4cc9a31a5d" PRIMARY KEY ("username"))`); - } - async down(queryRunner) { - await queryRunner.query(`DROP TABLE "used_username"`); - } -} diff --git a/packages/backend/migration/1565634203341-room.js b/packages/backend/migration/1565634203341-room.js deleted file mode 100644 index 679940f24..000000000 --- a/packages/backend/migration/1565634203341-room.js +++ /dev/null @@ -1,10 +0,0 @@ - - -export class room1565634203341 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}'`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "room"`); - } -} diff --git a/packages/backend/migration/1571220798684-CustomEmojiCategory.js b/packages/backend/migration/1571220798684-CustomEmojiCategory.js deleted file mode 100644 index 37c07366e..000000000 --- a/packages/backend/migration/1571220798684-CustomEmojiCategory.js +++ /dev/null @@ -1,10 +0,0 @@ - - -export class CustomEmojiCategory1571220798684 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "emoji" ADD "category" character varying(128)`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "category"`, undefined); - } -} diff --git a/packages/backend/migration/1572760203493-nodeinfo.js b/packages/backend/migration/1572760203493-nodeinfo.js deleted file mode 100644 index 54d5f914a..000000000 --- a/packages/backend/migration/1572760203493-nodeinfo.js +++ /dev/null @@ -1,26 +0,0 @@ - - -export class nodeinfo1572760203493 { - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "system"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "softwareName" character varying(64) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "softwareVersion" character varying(64) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "openRegistrations" boolean DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "name" character varying(256) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "description" character varying(4096) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "maintainerName" character varying(128) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "maintainerEmail" character varying(256) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "infoUpdatedAt" TIMESTAMP WITH TIME ZONE`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "infoUpdatedAt"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "maintainerEmail"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "maintainerName"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "description"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "name"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "openRegistrations"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "softwareVersion"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "softwareName"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "system" character varying(64)`, undefined); - } -} diff --git a/packages/backend/migration/1576269851876-TalkFederationId.js b/packages/backend/migration/1576269851876-TalkFederationId.js deleted file mode 100644 index 35861d571..000000000 --- a/packages/backend/migration/1576269851876-TalkFederationId.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class TalkFederationId1576269851876 { - constructor() { - this.name = 'TalkFederationId1576269851876'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "messaging_message" ADD "uri" character varying(512)`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "messaging_message" DROP COLUMN "uri"`, undefined); - } -} diff --git a/packages/backend/migration/1576869585998-ProxyRemoteFiles.js b/packages/backend/migration/1576869585998-ProxyRemoteFiles.js deleted file mode 100644 index d6d134be4..000000000 --- a/packages/backend/migration/1576869585998-ProxyRemoteFiles.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class ProxyRemoteFiles1576869585998 { - constructor() { - this.name = 'ProxyRemoteFiles1576869585998'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT false`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyRemoteFiles"`, undefined); - } -} diff --git a/packages/backend/migration/1579267006611-v12.js b/packages/backend/migration/1579267006611-v12.js deleted file mode 100644 index 7f6318a19..000000000 --- a/packages/backend/migration/1579267006611-v12.js +++ /dev/null @@ -1,33 +0,0 @@ - - -export class v121579267006611 { - constructor() { - this.name = 'v121579267006611'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "announcement" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "text" character varying(8192) NOT NULL, "title" character varying(256) NOT NULL, "imageUrl" character varying(1024), CONSTRAINT "PK_e0ef0550174fd1099a308fd18a0" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_118ec703e596086fc4515acb39" ON "announcement" ("createdAt") `, undefined); - await queryRunner.query(`CREATE TABLE "announcement_read" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "announcementId" character varying(32) NOT NULL, CONSTRAINT "PK_4b90ad1f42681d97b2683890c5e" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_8288151386172b8109f7239ab2" ON "announcement_read" ("userId") `, undefined); - await queryRunner.query(`CREATE INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf" ON "announcement_read" ("announcementId") `, undefined); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_924fa71815cfa3941d003702a0" ON "announcement_read" ("userId", "announcementId") `, undefined); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isVerified"`, undefined); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "announcements"`, undefined); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableEmojiReaction"`, undefined); - await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_8288151386172b8109f7239ab28" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "announcement_read" ADD CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe" FOREIGN KEY ("announcementId") REFERENCES "announcement"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_603a7b1e7aa0533c6c88e9bfafe"`, undefined); - await queryRunner.query(`ALTER TABLE "announcement_read" DROP CONSTRAINT "FK_8288151386172b8109f7239ab28"`, undefined); - await queryRunner.query(`ALTER TABLE "meta" ADD "enableEmojiReaction" boolean NOT NULL DEFAULT true`, undefined); - await queryRunner.query(`ALTER TABLE "meta" ADD "announcements" jsonb NOT NULL DEFAULT '[]'`, undefined); - await queryRunner.query(`ALTER TABLE "user" ADD "isVerified" boolean NOT NULL DEFAULT false`, undefined); - await queryRunner.query(`DROP INDEX "IDX_924fa71815cfa3941d003702a0"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_603a7b1e7aa0533c6c88e9bfaf"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_8288151386172b8109f7239ab2"`, undefined); - await queryRunner.query(`DROP TABLE "announcement_read"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_118ec703e596086fc4515acb39"`, undefined); - await queryRunner.query(`DROP TABLE "announcement"`, undefined); - } -} diff --git a/packages/backend/migration/1579270193251-v12-2.js b/packages/backend/migration/1579270193251-v12-2.js deleted file mode 100644 index c51ce6306..000000000 --- a/packages/backend/migration/1579270193251-v12-2.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class v1221579270193251 { - constructor() { - this.name = 'v1221579270193251'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement_read" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement_read" DROP COLUMN "createdAt"`, undefined); - } -} diff --git a/packages/backend/migration/1579282808087-v12-3.js b/packages/backend/migration/1579282808087-v12-3.js deleted file mode 100644 index aeb4f5a87..000000000 --- a/packages/backend/migration/1579282808087-v12-3.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class v1231579282808087 { - constructor() { - this.name = 'v1231579282808087'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "updatedAt"`, undefined); - } -} diff --git a/packages/backend/migration/1579544426412-v12-4.js b/packages/backend/migration/1579544426412-v12-4.js deleted file mode 100644 index f1e093413..000000000 --- a/packages/backend/migration/1579544426412-v12-4.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class v1241579544426412 { - constructor() { - this.name = 'v1241579544426412'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "notification" ADD "followRequestId" character varying(32)`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_bd7fab507621e635b32cd31892c" FOREIGN KEY ("followRequestId") REFERENCES "follow_request"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_bd7fab507621e635b32cd31892c"`, undefined); - await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "followRequestId"`, undefined); - } -} diff --git a/packages/backend/migration/1579977526288-v12-5.js b/packages/backend/migration/1579977526288-v12-5.js deleted file mode 100644 index 6d2b5c584..000000000 --- a/packages/backend/migration/1579977526288-v12-5.js +++ /dev/null @@ -1,53 +0,0 @@ - - -export class v1251579977526288 { - constructor() { - this.name = 'v1251579977526288'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "clip" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "isPublic" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_f0685dac8d4dd056d7255670b75" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_2b5ec6c574d6802c94c80313fb" ON "clip" ("userId") `, undefined); - await queryRunner.query(`CREATE TABLE "clip_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_e94cda2f40a99b57e032a1a738b" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_a012eaf5c87c65da1deb5fdbfa" ON "clip_note" ("noteId") `, undefined); - await queryRunner.query(`CREATE INDEX "IDX_ebe99317bbbe9968a0c6f579ad" ON "clip_note" ("clipId") `, undefined); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6fc0ec357d55a18646262fdfff" ON "clip_note" ("noteId", "clipId") `, undefined); - await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'list')`, undefined); - await queryRunner.query(`CREATE TABLE "antenna" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "src" "antenna_src_enum" NOT NULL, "userListId" character varying(32), "keywords" jsonb NOT NULL DEFAULT '[]', "withFile" boolean NOT NULL, "expression" character varying(2048), "notify" boolean NOT NULL, "hasNewNote" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_c170b99775e1dccca947c9f2d5f" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_6446c571a0e8d0f05f01c78909" ON "antenna" ("userId") `, undefined); - await queryRunner.query(`CREATE TABLE "antenna_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "antennaId" character varying(32) NOT NULL, CONSTRAINT "PK_fb28d94d0989a3872df19fd6ef8" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_bd0397be22147e17210940e125" ON "antenna_note" ("noteId") `, undefined); - await queryRunner.query(`CREATE INDEX "IDX_0d775946662d2575dfd2068a5f" ON "antenna_note" ("antennaId") `, undefined); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_335a0bf3f904406f9ef3dd51c2" ON "antenna_note" ("noteId", "antennaId") `, undefined); - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "geo"`, undefined); - await queryRunner.query(`ALTER TABLE "clip" ADD CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "clip_note" ADD CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_6446c571a0e8d0f05f01c789096" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_0d775946662d2575dfd2068a5f5"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna_note" DROP CONSTRAINT "FK_bd0397be22147e17210940e125b"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_709d7d32053d0dd7620f678eeb9"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_6446c571a0e8d0f05f01c789096"`, undefined); - await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_ebe99317bbbe9968a0c6f579adf"`, undefined); - await queryRunner.query(`ALTER TABLE "clip_note" DROP CONSTRAINT "FK_a012eaf5c87c65da1deb5fdbfa3"`, undefined); - await queryRunner.query(`ALTER TABLE "clip" DROP CONSTRAINT "FK_2b5ec6c574d6802c94c80313fb2"`, undefined); - await queryRunner.query(`ALTER TABLE "note" ADD "geo" jsonb`, undefined); - await queryRunner.query(`DROP INDEX "IDX_335a0bf3f904406f9ef3dd51c2"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_0d775946662d2575dfd2068a5f"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_bd0397be22147e17210940e125"`, undefined); - await queryRunner.query(`DROP TABLE "antenna_note"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_6446c571a0e8d0f05f01c78909"`, undefined); - await queryRunner.query(`DROP TABLE "antenna"`, undefined); - await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_6fc0ec357d55a18646262fdfff"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_ebe99317bbbe9968a0c6f579ad"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_a012eaf5c87c65da1deb5fdbfa"`, undefined); - await queryRunner.query(`DROP TABLE "clip_note"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_2b5ec6c574d6802c94c80313fb"`, undefined); - await queryRunner.query(`DROP TABLE "clip"`, undefined); - } -} diff --git a/packages/backend/migration/1579993013959-v12-6.js b/packages/backend/migration/1579993013959-v12-6.js deleted file mode 100644 index 3941c1391..000000000 --- a/packages/backend/migration/1579993013959-v12-6.js +++ /dev/null @@ -1,17 +0,0 @@ - - -export class v1261579993013959 { - constructor() { - this.name = 'v1261579993013959'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "hasNewNote"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna_note" ADD "read" boolean NOT NULL DEFAULT false`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_9937ea48d7ae97ffb4f3f063a4" ON "antenna_note" ("read") `, undefined); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_9937ea48d7ae97ffb4f3f063a4"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna_note" DROP COLUMN "read"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ADD "hasNewNote" boolean NOT NULL DEFAULT false`, undefined); - } -} diff --git a/packages/backend/migration/1580069531114-v12-7.js b/packages/backend/migration/1580069531114-v12-7.js deleted file mode 100644 index 4b4790cb7..000000000 --- a/packages/backend/migration/1580069531114-v12-7.js +++ /dev/null @@ -1,23 +0,0 @@ - - -export class v1271580069531114 { - constructor() { - this.name = 'v1271580069531114'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ADD "caseSensitive" boolean NOT NULL DEFAULT false`, undefined); - await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined); - await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list')`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined); - await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'list')`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined); - await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined); - await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO "antenna_src_enum"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "caseSensitive"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined); - } -} diff --git a/packages/backend/migration/1580148575182-v12-8.js b/packages/backend/migration/1580148575182-v12-8.js deleted file mode 100644 index cc30200c1..000000000 --- a/packages/backend/migration/1580148575182-v12-8.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class v1281580148575182 { - constructor() { - this.name = 'v1281580148575182'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_ec5c201576192ba8904c345c5cc"`, undefined); - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "appId"`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "appId" character varying(32)`, undefined); - await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_ec5c201576192ba8904c345c5cc" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, undefined); - } -} diff --git a/packages/backend/migration/1580154400017-v12-9.js b/packages/backend/migration/1580154400017-v12-9.js deleted file mode 100644 index 3715798f1..000000000 --- a/packages/backend/migration/1580154400017-v12-9.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class v1291580154400017 { - constructor() { - this.name = 'v1291580154400017'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "withReplies" boolean NOT NULL DEFAULT false`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "withReplies"`, undefined); - } -} diff --git a/packages/backend/migration/1580276619901-v12-10.js b/packages/backend/migration/1580276619901-v12-10.js deleted file mode 100644 index d5decb882..000000000 --- a/packages/backend/migration/1580276619901-v12-10.js +++ /dev/null @@ -1,18 +0,0 @@ - - -export class v12101580276619901 { - constructor() { - this.name = 'v12101580276619901'; - } - async up(queryRunner) { - await queryRunner.query(`TRUNCATE TABLE "notification"`, undefined); - await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined); - await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted')`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD "type" "notification_type_enum" NOT NULL`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "type"`, undefined); - await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD "type" character varying(32) NOT NULL`, undefined); - } -} diff --git a/packages/backend/migration/1580331224276-v12-11.js b/packages/backend/migration/1580331224276-v12-11.js deleted file mode 100644 index 129720adb..000000000 --- a/packages/backend/migration/1580331224276-v12-11.js +++ /dev/null @@ -1,17 +0,0 @@ - - -export class v12111580331224276 { - constructor() { - this.name = 'v12111580331224276'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "isMarkedAsClosed"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "isSuspended" boolean NOT NULL DEFAULT false`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_34500da2e38ac393f7bb6b299c" ON "instance" ("isSuspended") `, undefined); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_34500da2e38ac393f7bb6b299c"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "isSuspended"`, undefined); - await queryRunner.query(`ALTER TABLE "instance" ADD "isMarkedAsClosed" boolean NOT NULL DEFAULT false`, undefined); - } -} diff --git a/packages/backend/migration/1580508795118-v12-12.js b/packages/backend/migration/1580508795118-v12-12.js deleted file mode 100644 index c5cec23a3..000000000 --- a/packages/backend/migration/1580508795118-v12-12.js +++ /dev/null @@ -1,45 +0,0 @@ - - -export class v12121580508795118 { - constructor() { - this.name = 'v12121580508795118'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitter"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterAccessToken"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterAccessTokenSecret"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterUserId"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "twitterScreenName"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "github"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubAccessToken"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubId"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "githubLogin"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discord"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordAccessToken"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordRefreshToken"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordExpiresDate"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordId"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordUsername"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "discordDiscriminator"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "integrations" jsonb NOT NULL DEFAULT '{}'`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "integrations"`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordDiscriminator" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordUsername" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordId" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordExpiresDate" character varying(64)`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordRefreshToken" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "discordAccessToken" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "discord" boolean NOT NULL DEFAULT false`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "githubLogin" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "githubId" character varying(64)`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "githubAccessToken" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "github" boolean NOT NULL DEFAULT false`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterScreenName" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterUserId" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterAccessTokenSecret" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitterAccessToken" character varying(64) DEFAULT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "twitter" boolean NOT NULL DEFAULT false`, undefined); - } -} diff --git a/packages/backend/migration/1580543501339-v12-13.js b/packages/backend/migration/1580543501339-v12-13.js deleted file mode 100644 index 2fa490392..000000000 --- a/packages/backend/migration/1580543501339-v12-13.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class v12131580543501339 { - constructor() { - this.name = 'v12131580543501339'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_NOTE_TAGS" ON "note" USING gin ("tags")`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_NOTE_TAGS"`, undefined); - } -} diff --git a/packages/backend/migration/1580864313253-v12-14.js b/packages/backend/migration/1580864313253-v12-14.js deleted file mode 100644 index a3756ad02..000000000 --- a/packages/backend/migration/1580864313253-v12-14.js +++ /dev/null @@ -1,19 +0,0 @@ - - -export class v12141580864313253 { - constructor() { - this.name = 'v12141580864313253'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "proxyAccount" TO "proxyAccountId"`, undefined); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`, undefined); - await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(32)`, undefined); - await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad" FOREIGN KEY ("proxyAccountId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad"`, undefined); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`, undefined); - await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(128)`, undefined); - await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "proxyAccountId" TO "proxyAccount"`, undefined); - } -} diff --git a/packages/backend/migration/1581526429287-user-group-invitation.js b/packages/backend/migration/1581526429287-user-group-invitation.js deleted file mode 100644 index 181b0aba8..000000000 --- a/packages/backend/migration/1581526429287-user-group-invitation.js +++ /dev/null @@ -1,37 +0,0 @@ - - -export class userGroupInvitation1581526429287 { - constructor() { - this.name = 'userGroupInvitation1581526429287'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "user_group_invitation" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_160c63ec02bf23f6a5c5e8140d6" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_bfbc6305547539369fe73eb144" ON "user_group_invitation" ("userId") `, undefined); - await queryRunner.query(`CREATE INDEX "IDX_5cc8c468090e129857e9fecce5" ON "user_group_invitation" ("userGroupId") `, undefined); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e9793f65f504e5a31fbaedbf2f" ON "user_group_invitation" ("userId", "userGroupId") `, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD "userGroupInvitationId" character varying(32)`, undefined); - await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`, undefined); - await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited')`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::"text"::"notification_type_enum"`, undefined); - await queryRunner.query(`DROP TYPE "notification_type_enum_old"`, undefined); - await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS 'The type of the Notification.'`, undefined); - await queryRunner.query(`ALTER TABLE "user_group_invitation" ADD CONSTRAINT "FK_bfbc6305547539369fe73eb144a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "user_group_invitation" ADD CONSTRAINT "FK_5cc8c468090e129857e9fecce5a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_8fe87814e978053a53b1beb7e98" FOREIGN KEY ("userGroupInvitationId") REFERENCES "user_group_invitation"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_8fe87814e978053a53b1beb7e98"`, undefined); - await queryRunner.query(`ALTER TABLE "user_group_invitation" DROP CONSTRAINT "FK_5cc8c468090e129857e9fecce5a"`, undefined); - await queryRunner.query(`ALTER TABLE "user_group_invitation" DROP CONSTRAINT "FK_bfbc6305547539369fe73eb144a"`, undefined); - await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS ''`, undefined); - await queryRunner.query(`CREATE TYPE "notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted')`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum_old" USING "type"::"text"::"notification_type_enum_old"`, undefined); - await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined); - await queryRunner.query(`ALTER TYPE "notification_type_enum_old" RENAME TO "notification_type_enum"`, undefined); - await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "userGroupInvitationId"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_e9793f65f504e5a31fbaedbf2f"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_5cc8c468090e129857e9fecce5"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_bfbc6305547539369fe73eb144"`, undefined); - await queryRunner.query(`DROP TABLE "user_group_invitation"`, undefined); - } -} diff --git a/packages/backend/migration/1581695816408-user-group-antenna.js b/packages/backend/migration/1581695816408-user-group-antenna.js deleted file mode 100644 index 267b58cd9..000000000 --- a/packages/backend/migration/1581695816408-user-group-antenna.js +++ /dev/null @@ -1,27 +0,0 @@ - - -export class userGroupAntenna1581695816408 { - constructor() { - this.name = 'userGroupAntenna1581695816408'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "userGroupJoiningId" character varying(32)`, undefined); - await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`, undefined); - await queryRunner.query(`CREATE TYPE "antenna_src_enum" AS ENUM('home', 'all', 'users', 'list', 'group')`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum" USING "src"::"text"::"antenna_src_enum"`, undefined); - await queryRunner.query(`DROP TYPE "antenna_src_enum_old"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[]`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb" FOREIGN KEY ("userGroupJoiningId") REFERENCES "user_group_joining"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "users"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ADD "users" character varying array NOT NULL DEFAULT '{}'`, undefined); - await queryRunner.query(`CREATE TYPE "antenna_src_enum_old" AS ENUM('home', 'all', 'users', 'list')`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "antenna_src_enum_old" USING "src"::"text"::"antenna_src_enum_old"`, undefined); - await queryRunner.query(`DROP TYPE "antenna_src_enum"`, undefined); - await queryRunner.query(`ALTER TYPE "antenna_src_enum_old" RENAME TO "antenna_src_enum"`, undefined); - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "userGroupJoiningId"`, undefined); - } -} diff --git a/packages/backend/migration/1581708415836-drive-user-folder-id-index.js b/packages/backend/migration/1581708415836-drive-user-folder-id-index.js deleted file mode 100644 index 43c2ce6ce..000000000 --- a/packages/backend/migration/1581708415836-drive-user-folder-id-index.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class driveUserFolderIdIndex1581708415836 { - constructor() { - this.name = 'driveUserFolderIdIndex1581708415836'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_55720b33a61a7c806a8215b825" ON "drive_file" ("userId", "folderId", "id") `, undefined); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_55720b33a61a7c806a8215b825"`, undefined); - } -} diff --git a/packages/backend/migration/1581979837262-promo.js b/packages/backend/migration/1581979837262-promo.js deleted file mode 100644 index 4813a5f48..000000000 --- a/packages/backend/migration/1581979837262-promo.js +++ /dev/null @@ -1,27 +0,0 @@ - - -export class promo1581979837262 { - constructor() { - this.name = 'promo1581979837262'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "promo_note" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "REL_e263909ca4fe5d57f8d4230dd5" UNIQUE ("noteId"), CONSTRAINT "PK_e263909ca4fe5d57f8d4230dd5c" PRIMARY KEY ("noteId"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_83f0862e9bae44af52ced7099e" ON "promo_note" ("userId") `, undefined); - await queryRunner.query(`CREATE TABLE "promo_read" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_61917c1541002422b703318b7c9" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_9657d55550c3d37bfafaf7d4b0" ON "promo_read" ("userId") `, undefined); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2882b8a1a07c7d281a98b6db16" ON "promo_read" ("userId", "noteId") `, undefined); - await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4"`, undefined); - await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05"`, undefined); - await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_2882b8a1a07c7d281a98b6db16"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_9657d55550c3d37bfafaf7d4b0"`, undefined); - await queryRunner.query(`DROP TABLE "promo_read"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_83f0862e9bae44af52ced7099e"`, undefined); - await queryRunner.query(`DROP TABLE "promo_note"`, undefined); - } -} diff --git a/packages/backend/migration/1582019042083-featured-injecttion.js b/packages/backend/migration/1582019042083-featured-injecttion.js deleted file mode 100644 index 7f8790b01..000000000 --- a/packages/backend/migration/1582019042083-featured-injecttion.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class featuredInjecttion1582019042083 { - constructor() { - this.name = 'featuredInjecttion1582019042083'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "injectFeaturedNote" boolean NOT NULL DEFAULT true`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "injectFeaturedNote"`, undefined); - } -} diff --git a/packages/backend/migration/1582210532752-antenna-exclude.js b/packages/backend/migration/1582210532752-antenna-exclude.js deleted file mode 100644 index ff8d7b80d..000000000 --- a/packages/backend/migration/1582210532752-antenna-exclude.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class antennaExclude1582210532752 { - constructor() { - this.name = 'antennaExclude1582210532752'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeKeywords" jsonb NOT NULL DEFAULT '[]'`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeKeywords"`, undefined); - } -} diff --git a/packages/backend/migration/1582875306439-note-reaction-length.js b/packages/backend/migration/1582875306439-note-reaction-length.js deleted file mode 100644 index e99501f01..000000000 --- a/packages/backend/migration/1582875306439-note-reaction-length.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class noteReactionLength1582875306439 { - constructor() { - this.name = 'noteReactionLength1582875306439'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(128)`, undefined); - } -} diff --git a/packages/backend/migration/1585361548360-miauth.js b/packages/backend/migration/1585361548360-miauth.js deleted file mode 100644 index e59aa3b6e..000000000 --- a/packages/backend/migration/1585361548360-miauth.js +++ /dev/null @@ -1,35 +0,0 @@ - - -export class miauth1585361548360 { - constructor() { - this.name = 'miauth1585361548360'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "access_token" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ADD "session" character varying(128) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ADD "name" character varying(128) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ADD "description" character varying(512) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ADD "iconUrl" character varying(512) DEFAULT null`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ADD "permission" character varying(64) array NOT NULL DEFAULT '{}'::varchar[]`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ADD "fetched" boolean NOT NULL DEFAULT false`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP NOT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET DEFAULT null`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_bf3a053c07d9fb5d87317c56ee" ON "access_token" ("session") `, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_bf3a053c07d9fb5d87317c56ee"`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET NOT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "fetched"`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "permission"`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "iconUrl"`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "description"`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "name"`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "session"`, undefined); - await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "lastUsedAt"`, undefined); - } -} diff --git a/packages/backend/migration/1585385921215-custom-notification.js b/packages/backend/migration/1585385921215-custom-notification.js deleted file mode 100644 index c3ddb2be1..000000000 --- a/packages/backend/migration/1585385921215-custom-notification.js +++ /dev/null @@ -1,47 +0,0 @@ - - -export class customNotification1585385921215 { - constructor() { - this.name = 'customNotification1585385921215'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "notification" ADD "customBody" character varying(2048)`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD "customHeader" character varying(256)`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD "customIcon" character varying(1024)`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD "appAccessTokenId" character varying(32)`, undefined); - await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" DROP NOT NULL`, undefined); - await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS 'The ID of sender user of the Notification.'`, undefined); - await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`, undefined); - await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::"text"::"notification_type_enum"`, undefined); - await queryRunner.query(`DROP TYPE "notification_type_enum_old"`, undefined); - await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS 'The type of the Notification.'`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71" ON "notification" ("notifierId") `, undefined); - await queryRunner.query(`CREATE INDEX "IDX_33f33cc8ef29d805a97ff4628b" ON "notification" ("type") `, undefined); - await queryRunner.query(`CREATE INDEX "IDX_080ab397c379af09b9d2169e5b" ON "notification" ("isRead") `, undefined); - await queryRunner.query(`CREATE INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c" ON "notification" ("appAccessTokenId") `, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9" FOREIGN KEY ("appAccessTokenId") REFERENCES "access_token"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9"`, undefined); - await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_080ab397c379af09b9d2169e5b"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_33f33cc8ef29d805a97ff4628b"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71"`, undefined); - await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS ''`, undefined); - await queryRunner.query(`CREATE TYPE "notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited')`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum_old" USING "type"::"text"::"notification_type_enum_old"`, undefined); - await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined); - await queryRunner.query(`ALTER TYPE "notification_type_enum_old" RENAME TO "notification_type_enum"`, undefined); - await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS ''`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" SET NOT NULL`, undefined); - await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "appAccessTokenId"`, undefined); - await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customIcon"`, undefined); - await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customHeader"`, undefined); - await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customBody"`, undefined); - } -} diff --git a/packages/backend/migration/1585772678853-ap-url.js b/packages/backend/migration/1585772678853-ap-url.js deleted file mode 100644 index 5fb809ff5..000000000 --- a/packages/backend/migration/1585772678853-ap-url.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class apUrl1585772678853 { - constructor() { - this.name = 'apUrl1585772678853'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "url" character varying(512)`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "url"`, undefined); - } -} diff --git a/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js b/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js deleted file mode 100644 index e13bb217e..000000000 --- a/packages/backend/migration/1586624197029-AddObjectStorageUseProxy.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class AddObjectStorageUseProxy1586624197029 { - constructor() { - this.name = 'AddObjectStorageUseProxy1586624197029'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageUseProxy" boolean NOT NULL DEFAULT true`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageUseProxy"`, undefined); - } -} diff --git a/packages/backend/migration/1586641139527-remote-reaction.js b/packages/backend/migration/1586641139527-remote-reaction.js deleted file mode 100644 index 5b23103a1..000000000 --- a/packages/backend/migration/1586641139527-remote-reaction.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class remoteReaction1586641139527 { - constructor() { - this.name = 'remoteReaction1586641139527'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(260)`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined); - } -} diff --git a/packages/backend/migration/1586708940386-pageAiScript.js b/packages/backend/migration/1586708940386-pageAiScript.js deleted file mode 100644 index eed616c11..000000000 --- a/packages/backend/migration/1586708940386-pageAiScript.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class pageAiScript1586708940386 { - constructor() { - this.name = 'pageAiScript1586708940386'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "page" ADD "script" character varying(16384) NOT NULL DEFAULT ''`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "script"`, undefined); - } -} diff --git a/packages/backend/migration/1588044505511-hCaptcha.js b/packages/backend/migration/1588044505511-hCaptcha.js deleted file mode 100644 index a33dbd713..000000000 --- a/packages/backend/migration/1588044505511-hCaptcha.js +++ /dev/null @@ -1,17 +0,0 @@ - - -export class hCaptcha1588044505511 { - constructor() { - this.name = 'hCaptcha1588044505511'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "enableHcaptcha" boolean NOT NULL DEFAULT false`, undefined); - await queryRunner.query(`ALTER TABLE "meta" ADD "hcaptchaSiteKey" character varying(64)`, undefined); - await queryRunner.query(`ALTER TABLE "meta" ADD "hcaptchaSecretKey" character varying(64)`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hcaptchaSecretKey"`, undefined); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hcaptchaSiteKey"`, undefined); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableHcaptcha"`, undefined); - } -} diff --git a/packages/backend/migration/1589023282116-pubRelay.js b/packages/backend/migration/1589023282116-pubRelay.js deleted file mode 100644 index 48a1028d3..000000000 --- a/packages/backend/migration/1589023282116-pubRelay.js +++ /dev/null @@ -1,17 +0,0 @@ - - -export class pubRelay1589023282116 { - constructor() { - this.name = 'pubRelay1589023282116'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "relay_status_enum" AS ENUM('requesting', 'accepted', 'rejected')`, undefined); - await queryRunner.query(`CREATE TABLE "relay" ("id" character varying(32) NOT NULL, "inbox" character varying(512) NOT NULL, "status" "relay_status_enum" NOT NULL, CONSTRAINT "PK_78ebc9cfddf4292633b7ba57aee" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab" ON "relay" ("inbox") `, undefined); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab"`, undefined); - await queryRunner.query(`DROP TABLE "relay"`, undefined); - await queryRunner.query(`DROP TYPE "relay_status_enum"`, undefined); - } -} diff --git a/packages/backend/migration/1595075960584-blurhash.js b/packages/backend/migration/1595075960584-blurhash.js deleted file mode 100644 index f24d3722c..000000000 --- a/packages/backend/migration/1595075960584-blurhash.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class blurhash1595075960584 { - constructor() { - this.name = 'blurhash1595075960584'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" ADD "blurhash" character varying(128)`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "blurhash"`); - } -} diff --git a/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js b/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js deleted file mode 100644 index f18f6f972..000000000 --- a/packages/backend/migration/1595077605646-blurhash-for-avatar-banner.js +++ /dev/null @@ -1,19 +0,0 @@ - - -export class blurhashForAvatarBanner1595077605646 { - constructor() { - this.name = 'blurhashForAvatarBanner1595077605646'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarColor"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerColor"`); - await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`); - await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`); - await queryRunner.query(`ALTER TABLE "user" ADD "bannerColor" character varying(32)`); - await queryRunner.query(`ALTER TABLE "user" ADD "avatarColor" character varying(32)`); - } -} diff --git a/packages/backend/migration/1595676934834-instance-icon-url.js b/packages/backend/migration/1595676934834-instance-icon-url.js deleted file mode 100644 index df9d8199b..000000000 --- a/packages/backend/migration/1595676934834-instance-icon-url.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class instanceIconUrl1595676934834 { - constructor() { - this.name = 'instanceIconUrl1595676934834'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" ADD "iconUrl" character varying(256) DEFAULT null`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "iconUrl"`); - } -} diff --git a/packages/backend/migration/1595771249699-word-mute.js b/packages/backend/migration/1595771249699-word-mute.js deleted file mode 100644 index e8e4ac838..000000000 --- a/packages/backend/migration/1595771249699-word-mute.js +++ /dev/null @@ -1,29 +0,0 @@ - - -export class wordMute1595771249699 { - constructor() { - this.name = 'wordMute1595771249699'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "muted_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_897e2eff1c0b9b64e55ca1418a4" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_70ab9786313d78e4201d81cdb8" ON "muted_note" ("noteId") `); - await queryRunner.query(`CREATE INDEX "IDX_d8e07aa18c2d64e86201601aec" ON "muted_note" ("userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a8c6bfd637d3f1d67a27c48e27" ON "muted_note" ("noteId", "userId") `); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "enableWordMute" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutedWords" jsonb NOT NULL DEFAULT '[]'`); - await queryRunner.query(`CREATE INDEX "IDX_3befe6f999c86aff06eb0257b4" ON "user_profile" ("enableWordMute") `); - await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_70ab9786313d78e4201d81cdb89" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1"`); - await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_70ab9786313d78e4201d81cdb89"`); - await queryRunner.query(`DROP INDEX "IDX_3befe6f999c86aff06eb0257b4"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutedWords"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "enableWordMute"`); - await queryRunner.query(`DROP INDEX "IDX_a8c6bfd637d3f1d67a27c48e27"`); - await queryRunner.query(`DROP INDEX "IDX_d8e07aa18c2d64e86201601aec"`); - await queryRunner.query(`DROP INDEX "IDX_70ab9786313d78e4201d81cdb8"`); - await queryRunner.query(`DROP TABLE "muted_note"`); - } -} diff --git a/packages/backend/migration/1595782306083-word-mute2.js b/packages/backend/migration/1595782306083-word-mute2.js deleted file mode 100644 index ab1e40a04..000000000 --- a/packages/backend/migration/1595782306083-word-mute2.js +++ /dev/null @@ -1,17 +0,0 @@ - - -export class wordMute21595782306083 { - constructor() { - this.name = 'wordMute21595782306083'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "muted_note_reason_enum" AS ENUM('word', 'manual', 'spam', 'other')`); - await queryRunner.query(`ALTER TABLE "muted_note" ADD "reason" "muted_note_reason_enum" NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_636e977ff90b23676fb5624b25" ON "muted_note" ("reason") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_636e977ff90b23676fb5624b25"`); - await queryRunner.query(`ALTER TABLE "muted_note" DROP COLUMN "reason"`); - await queryRunner.query(`DROP TYPE "muted_note_reason_enum"`); - } -} diff --git a/packages/backend/migration/1596548170836-channel.js b/packages/backend/migration/1596548170836-channel.js deleted file mode 100644 index 242db7d45..000000000 --- a/packages/backend/migration/1596548170836-channel.js +++ /dev/null @@ -1,57 +0,0 @@ - - -export class channel1596548170836 { - constructor() { - this.name = 'channel1596548170836'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "channel" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "lastNotedAt" TIMESTAMP WITH TIME ZONE, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "description" character varying(2048), "bannerId" character varying(32), "notesCount" integer NOT NULL DEFAULT 0, "usersCount" integer NOT NULL DEFAULT 0, CONSTRAINT "PK_590f33ee6ee7d76437acf362e39" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_71cb7b435b7c0d4843317e7e16" ON "channel" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_29ef80c6f13bcea998447fce43" ON "channel" ("lastNotedAt") `); - await queryRunner.query(`CREATE INDEX "IDX_823bae55bd81b3be6e05cff438" ON "channel" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_0f58c11241e649d2a638a8de94" ON "channel" ("notesCount") `); - await queryRunner.query(`CREATE INDEX "IDX_094b86cd36bb805d1aa1e8cc9a" ON "channel" ("usersCount") `); - await queryRunner.query(`CREATE TABLE "channel_following" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "followeeId" character varying(32) NOT NULL, "followerId" character varying(32) NOT NULL, CONSTRAINT "PK_8b104be7f7415113f2a02cd5bdd" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_11e71f2511589dcc8a4d3214f9" ON "channel_following" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_0e43068c3f92cab197c3d3cd86" ON "channel_following" ("followeeId") `); - await queryRunner.query(`CREATE INDEX "IDX_6d8084ec9496e7334a4602707e" ON "channel_following" ("followerId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2e230dd45a10e671d781d99f3e" ON "channel_following" ("followerId", "followeeId") `); - await queryRunner.query(`CREATE TABLE "channel_note_pining" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_44f7474496bcf2e4b741681146d" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_8125f950afd3093acb10d2db8a" ON "channel_note_pining" ("channelId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f36fed37d6d4cdcc68c803cd9c" ON "channel_note_pining" ("channelId", "noteId") `); - await queryRunner.query(`ALTER TABLE "note" ADD "channelId" character varying(32) DEFAULT null`); - await queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId") `); - await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_823bae55bd81b3be6e05cff4383" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "channel" ADD CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19" FOREIGN KEY ("bannerId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_f22169eb10657bded6d875ac8f9" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e" FOREIGN KEY ("followeeId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "channel_following" ADD CONSTRAINT "FK_6d8084ec9496e7334a4602707e1" FOREIGN KEY ("followerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_8125f950afd3093acb10d2db8a8" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "channel_note_pining" ADD CONSTRAINT "FK_10b19ef67d297ea9de325cd4502" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_10b19ef67d297ea9de325cd4502"`); - await queryRunner.query(`ALTER TABLE "channel_note_pining" DROP CONSTRAINT "FK_8125f950afd3093acb10d2db8a8"`); - await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_6d8084ec9496e7334a4602707e1"`); - await queryRunner.query(`ALTER TABLE "channel_following" DROP CONSTRAINT "FK_0e43068c3f92cab197c3d3cd86e"`); - await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_f22169eb10657bded6d875ac8f9"`); - await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_999da2bcc7efadbfe0e92d3bc19"`); - await queryRunner.query(`ALTER TABLE "channel" DROP CONSTRAINT "FK_823bae55bd81b3be6e05cff4383"`); - await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`); - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "channelId"`); - await queryRunner.query(`DROP INDEX "IDX_f36fed37d6d4cdcc68c803cd9c"`); - await queryRunner.query(`DROP INDEX "IDX_8125f950afd3093acb10d2db8a"`); - await queryRunner.query(`DROP TABLE "channel_note_pining"`); - await queryRunner.query(`DROP INDEX "IDX_2e230dd45a10e671d781d99f3e"`); - await queryRunner.query(`DROP INDEX "IDX_6d8084ec9496e7334a4602707e"`); - await queryRunner.query(`DROP INDEX "IDX_0e43068c3f92cab197c3d3cd86"`); - await queryRunner.query(`DROP INDEX "IDX_11e71f2511589dcc8a4d3214f9"`); - await queryRunner.query(`DROP TABLE "channel_following"`); - await queryRunner.query(`DROP INDEX "IDX_094b86cd36bb805d1aa1e8cc9a"`); - await queryRunner.query(`DROP INDEX "IDX_0f58c11241e649d2a638a8de94"`); - await queryRunner.query(`DROP INDEX "IDX_823bae55bd81b3be6e05cff438"`); - await queryRunner.query(`DROP INDEX "IDX_29ef80c6f13bcea998447fce43"`); - await queryRunner.query(`DROP INDEX "IDX_71cb7b435b7c0d4843317e7e16"`); - await queryRunner.query(`DROP TABLE "channel"`); - } -} diff --git a/packages/backend/migration/1596786425167-channel2.js b/packages/backend/migration/1596786425167-channel2.js deleted file mode 100644 index 4b17048fe..000000000 --- a/packages/backend/migration/1596786425167-channel2.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class channel21596786425167 { - constructor() { - this.name = 'channel21596786425167'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`); - } -} diff --git a/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js b/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js deleted file mode 100644 index 07283e31d..000000000 --- a/packages/backend/migration/1597230137744-objectStorageSetPublicRead.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class objectStorageSetPublicRead1597230137744 { - constructor() { - this.name = 'objectStorageSetPublicRead1597230137744'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageSetPublicRead" boolean NOT NULL DEFAULT false`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageSetPublicRead"`); - } -} diff --git a/packages/backend/migration/1597236229720-IncludingNotificationTypes.js b/packages/backend/migration/1597236229720-IncludingNotificationTypes.js deleted file mode 100644 index f498fa7d9..000000000 --- a/packages/backend/migration/1597236229720-IncludingNotificationTypes.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class IncludingNotificationTypes1597236229720 { - constructor() { - this.name = 'IncludingNotificationTypes1597236229720'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "user_profile_includingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "includingNotificationTypes" "user_profile_includingnotificationtypes_enum" array`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "includingNotificationTypes"`); - await queryRunner.query(`DROP TYPE "user_profile_includingnotificationtypes_enum"`); - } -} diff --git a/packages/backend/migration/1597385880794-add-sensitive-index.js b/packages/backend/migration/1597385880794-add-sensitive-index.js deleted file mode 100644 index 8c5c040ba..000000000 --- a/packages/backend/migration/1597385880794-add-sensitive-index.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class addSensitiveIndex1597385880794 { - constructor() { - this.name = 'addSensitiveIndex1597385880794'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_a7eba67f8b3fa27271e85d2e26" ON "drive_file" ("isSensitive") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_a7eba67f8b3fa27271e85d2e26"`); - } -} diff --git a/packages/backend/migration/1597459042300-channel-unread.js b/packages/backend/migration/1597459042300-channel-unread.js deleted file mode 100644 index 3157ab779..000000000 --- a/packages/backend/migration/1597459042300-channel-unread.js +++ /dev/null @@ -1,26 +0,0 @@ - - -export class channelUnread1597459042300 { - constructor() { - this.name = 'channelUnread1597459042300'; - } - async up(queryRunner) { - await queryRunner.query(`TRUNCATE TABLE "note_unread"`, undefined); - await queryRunner.query(`ALTER TABLE "channel_following" DROP COLUMN "readCursor"`); - await queryRunner.query(`ALTER TABLE "note_unread" ADD "isMentioned" boolean NOT NULL`); - await queryRunner.query(`ALTER TABLE "note_unread" ADD "noteChannelId" character varying(32)`); - await queryRunner.query(`CREATE INDEX "IDX_25b1dd384bec391b07b74b861c" ON "note_unread" ("isMentioned") `); - await queryRunner.query(`CREATE INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30" ON "note_unread" ("isSpecified") `); - await queryRunner.query(`CREATE INDEX "IDX_29e8c1d579af54d4232939f994" ON "note_unread" ("noteUserId") `); - await queryRunner.query(`CREATE INDEX "IDX_6a57f051d82c6d4036c141e107" ON "note_unread" ("noteChannelId") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_6a57f051d82c6d4036c141e107"`); - await queryRunner.query(`DROP INDEX "IDX_29e8c1d579af54d4232939f994"`); - await queryRunner.query(`DROP INDEX "IDX_89a29c9237b8c3b6b3cbb4cb30"`); - await queryRunner.query(`DROP INDEX "IDX_25b1dd384bec391b07b74b861c"`); - await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "noteChannelId"`); - await queryRunner.query(`ALTER TABLE "note_unread" DROP COLUMN "isMentioned"`); - await queryRunner.query(`ALTER TABLE "channel_following" ADD "readCursor" TIMESTAMP WITH TIME ZONE NOT NULL`); - } -} diff --git a/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js b/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js deleted file mode 100644 index 2bd8aee35..000000000 --- a/packages/backend/migration/1597893996136-ChannelNoteIdDescIndex.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class ChannelNoteIdDescIndex1597893996136 { - constructor() { - this.name = 'ChannelNoteIdDescIndex1597893996136'; - } - async up(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`); - await queryRunner.query(`CREATE INDEX "IDX_note_on_channelId_and_id_desc" ON "note" ("channelId", "id" desc)`); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_note_on_channelId_and_id_desc"`); - await queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId") `); - } -} diff --git a/packages/backend/migration/1600353287890-mutingNotificationTypes.js b/packages/backend/migration/1600353287890-mutingNotificationTypes.js deleted file mode 100644 index ed3eb7d14..000000000 --- a/packages/backend/migration/1600353287890-mutingNotificationTypes.js +++ /dev/null @@ -1,19 +0,0 @@ - - -export class mutingNotificationTypes1600353287890 { - constructor() { - this.name = 'mutingNotificationTypes1600353287890'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "includingNotificationTypes"`); - await queryRunner.query(`DROP TYPE "public"."user_profile_includingnotificationtypes_enum"`); - await queryRunner.query(`CREATE TYPE "user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutingNotificationTypes" "user_profile_mutingnotificationtypes_enum" array NOT NULL DEFAULT '{}'`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutingNotificationTypes"`); - await queryRunner.query(`DROP TYPE "user_profile_mutingnotificationtypes_enum"`); - await queryRunner.query(`CREATE TYPE "public"."user_profile_includingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "includingNotificationTypes" "user_profile_includingnotificationtypes_enum" array`); - } -} diff --git a/packages/backend/migration/1603094348345-refine-abuse-user-report.js b/packages/backend/migration/1603094348345-refine-abuse-user-report.js deleted file mode 100644 index 4918032a2..000000000 --- a/packages/backend/migration/1603094348345-refine-abuse-user-report.js +++ /dev/null @@ -1,31 +0,0 @@ - - -export class refineAbuseUserReport1603094348345 { - constructor() { - this.name = 'refineAbuseUserReport1603094348345'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_d049123c413e68ca52abe734203"`); - await queryRunner.query(`DROP INDEX "IDX_d049123c413e68ca52abe73420"`); - await queryRunner.query(`DROP INDEX "IDX_5cd442c3b2e74fdd99dae20243"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" RENAME COLUMN "userId" TO "targetUserId"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "assigneeId" character varying(32)`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolved" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "comment"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "comment" character varying(2048) NOT NULL DEFAULT '{}'::varchar[]`); - await queryRunner.query(`CREATE INDEX "IDX_2b15aaf4a0dc5be3499af7ab6a" ON "abuse_user_report" ("resolved") `); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_08b883dd5fdd6f9c4c1572b36de" FOREIGN KEY ("assigneeId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_08b883dd5fdd6f9c4c1572b36de"`); - await queryRunner.query(`DROP INDEX "IDX_2b15aaf4a0dc5be3499af7ab6a"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "comment"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "comment" character varying(512) NOT NULL DEFAULT '{}'::varchar[]`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolved"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "assigneeId"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" RENAME COLUMN "targetUserId" TO "userId"`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5cd442c3b2e74fdd99dae20243" ON "abuse_user_report" ("userId", "reporterId") `); - await queryRunner.query(`CREATE INDEX "IDX_d049123c413e68ca52abe73420" ON "abuse_user_report" ("userId") `); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_d049123c413e68ca52abe734203" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } -} diff --git a/packages/backend/migration/1603095701770-refine-abuse-user-report2.js b/packages/backend/migration/1603095701770-refine-abuse-user-report2.js deleted file mode 100644 index 64e92672f..000000000 --- a/packages/backend/migration/1603095701770-refine-abuse-user-report2.js +++ /dev/null @@ -1,19 +0,0 @@ - - -export class refineAbuseUserReport21603095701770 { - constructor() { - this.name = 'refineAbuseUserReport21603095701770'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "targetUserHost" character varying(128)`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "reporterHost" character varying(128)`); - await queryRunner.query(`CREATE INDEX "IDX_4ebbf7f93cdc10e8d1ef2fc6cd" ON "abuse_user_report" ("targetUserHost") `); - await queryRunner.query(`CREATE INDEX "IDX_f8d8b93740ad12c4ce8213a199" ON "abuse_user_report" ("reporterHost") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_f8d8b93740ad12c4ce8213a199"`); - await queryRunner.query(`DROP INDEX "IDX_4ebbf7f93cdc10e8d1ef2fc6cd"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "reporterHost"`); - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "targetUserHost"`); - } -} diff --git a/packages/backend/migration/1603776877564-instance-theme-color.js b/packages/backend/migration/1603776877564-instance-theme-color.js deleted file mode 100644 index 92440d3f6..000000000 --- a/packages/backend/migration/1603776877564-instance-theme-color.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class instanceThemeColor1603776877564 { - constructor() { - this.name = 'instanceThemeColor1603776877564'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" ADD "themeColor" character varying(64) DEFAULT null`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "themeColor"`); - } -} diff --git a/packages/backend/migration/1603781553011-instance-favicon.js b/packages/backend/migration/1603781553011-instance-favicon.js deleted file mode 100644 index f607c49ff..000000000 --- a/packages/backend/migration/1603781553011-instance-favicon.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class instanceFavicon1603781553011 { - constructor() { - this.name = 'instanceFavicon1603781553011'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" ADD "faviconUrl" character varying(256) DEFAULT null`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "faviconUrl"`); - } -} diff --git a/packages/backend/migration/1604821689616-delete-auto-watch.js b/packages/backend/migration/1604821689616-delete-auto-watch.js deleted file mode 100644 index 4706e8bae..000000000 --- a/packages/backend/migration/1604821689616-delete-auto-watch.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class deleteAutoWatch1604821689616 { - constructor() { - this.name = 'deleteAutoWatch1604821689616'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "autoWatch"`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "autoWatch" boolean NOT NULL DEFAULT false`); - } -} diff --git a/packages/backend/migration/1605408848373-clip-description.js b/packages/backend/migration/1605408848373-clip-description.js deleted file mode 100644 index edd5505b3..000000000 --- a/packages/backend/migration/1605408848373-clip-description.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class clipDescription1605408848373 { - constructor() { - this.name = 'clipDescription1605408848373'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "clip" ADD "description" character varying(2048) DEFAULT null`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "description"`); - } -} diff --git a/packages/backend/migration/1605408971051-comments.js b/packages/backend/migration/1605408971051-comments.js deleted file mode 100644 index 400efd5e7..000000000 --- a/packages/backend/migration/1605408971051-comments.js +++ /dev/null @@ -1,433 +0,0 @@ - - -export class comments1605408971051 { - constructor() { - this.name = 'comments1605408971051'; - } - async up(queryRunner) { - await queryRunner.query(`COMMENT ON COLUMN "log"."createdAt" IS 'The created date of the Log.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."createdAt" IS 'The created date of the DriveFolder.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."name" IS 'The name of the DriveFolder.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."userId" IS 'The owner ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."parentId" IS 'The parent folder ID. If null, it means the DriveFolder is located in root.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."createdAt" IS 'The created date of the DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userId" IS 'The owner ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userHost" IS 'The host of owner. It will be null if the user in local.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."md5" IS 'The MD5 hash of the DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."name" IS 'The file name of the DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."type" IS 'The content type (MIME) of the DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."size" IS 'The file size (bytes) of the DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."comment" IS 'The comment of the DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."blurhash" IS 'The BlurHash string.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."properties" IS 'The any properties of the DriveFile. For example, it includes image width/height.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."thumbnailUrl" IS 'The URL of the thumbnail of the DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."webpublicUrl" IS 'The URL of the webpublic of the DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."folderId" IS 'The parent folder ID. If null, it means the DriveFile is located in root.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isSensitive" IS 'Whether the DriveFile is NSFW.'`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isLink" IS 'Whether the DriveFile is direct link to remote server.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."createdAt" IS 'The created date of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."updatedAt" IS 'The updated date of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."username" IS 'The username of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."usernameLower" IS 'The username (lowercased) of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."name" IS 'The name of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."followersCount" IS 'The count of followers.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."followingCount" IS 'The count of following.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."notesCount" IS 'The count of notes.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."avatarId" IS 'The ID of avatar DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."bannerId" IS 'The ID of banner DriveFile.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isSuspended" IS 'Whether the User is suspended.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isSilenced" IS 'Whether the User is silenced.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isLocked" IS 'Whether the User is locked.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isBot" IS 'Whether the User is a bot.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isCat" IS 'Whether the User is a cat.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isAdmin" IS 'Whether the User is the admin.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isModerator" IS 'Whether the User is a moderator.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."host" IS 'The host of the User. It will be null if the origin of the user is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."inbox" IS 'The inbox URL of the User. It will be null if the origin of the user is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."sharedInbox" IS 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."featured" IS 'The featured URL of the User. It will be null if the origin of the user is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."uri" IS 'The URI of the User. It will be null if the origin of the user is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS 'The native access token of the User. It will be null if the origin of the user is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "app"."createdAt" IS 'The created date of the App.'`); - await queryRunner.query(`COMMENT ON COLUMN "app"."userId" IS 'The owner ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "app"."secret" IS 'The secret key of the App.'`); - await queryRunner.query(`COMMENT ON COLUMN "app"."name" IS 'The name of the App.'`); - await queryRunner.query(`COMMENT ON COLUMN "app"."description" IS 'The description of the App.'`); - await queryRunner.query(`COMMENT ON COLUMN "app"."permission" IS 'The permission of the App.'`); - await queryRunner.query(`COMMENT ON COLUMN "app"."callbackUrl" IS 'The callbackUrl of the App.'`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."createdAt" IS 'The created date of the AccessToken.'`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."lastUsedAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."session" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."appId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."description" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."iconUrl" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."createdAt" IS 'The created date of the Channel.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."userId" IS 'The owner ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."name" IS 'The name of the Channel.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."description" IS 'The description of the Channel.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."bannerId" IS 'The ID of banner Channel.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."notesCount" IS 'The count of notes.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."usersCount" IS 'The count of users.'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."createdAt" IS 'The created date of the Note.'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."replyId" IS 'The ID of reply target.'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."renoteId" IS 'The ID of renote target.'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."userId" IS 'The ID of author.'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."uri" IS 'The URI of a note. it will be null when the note is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."url" IS 'The human readable url of a note. it will be null when the note is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."channelId" IS 'The ID of source channel.'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."userHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserId" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserId" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "poll_vote"."createdAt" IS 'The created date of the PollVote.'`); - await queryRunner.query(`COMMENT ON COLUMN "note_reaction"."createdAt" IS 'The created date of the NoteReaction.'`); - await queryRunner.query(`COMMENT ON COLUMN "note_watching"."createdAt" IS 'The created date of the NoteWatching.'`); - await queryRunner.query(`COMMENT ON COLUMN "note_watching"."userId" IS 'The watcher ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteId" IS 'The target Note ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteUserId" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteUserId" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteChannelId" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."createdAt" IS 'The created date of the FollowRequest.'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeId" IS 'The followee user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerId" IS 'The follower user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."requestId" IS 'id of Follow Activity.'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerInbox" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerSharedInbox" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeInbox" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeSharedInbox" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "user_group"."createdAt" IS 'The created date of the UserGroup.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_group"."userId" IS 'The ID of owner.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."createdAt" IS 'The created date of the UserGroupInvitation.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userId" IS 'The user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userGroupId" IS 'The group ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "notification"."createdAt" IS 'The created date of the Notification.'`); - await queryRunner.query(`COMMENT ON COLUMN "notification"."notifieeId" IS 'The ID of recipient user of the Notification.'`); - await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`); - await queryRunner.query(`COMMENT ON COLUMN "meta"."localDriveCapacityMb" IS 'Drive capacity of a local user (MB)'`); - await queryRunner.query(`COMMENT ON COLUMN "meta"."remoteDriveCapacityMb" IS 'Drive capacity of a remote user (MB)'`); - await queryRunner.query(`COMMENT ON COLUMN "meta"."maxNoteTextLength" IS 'Max allowed note text length in characters'`); - await queryRunner.query(`COMMENT ON COLUMN "following"."createdAt" IS 'The created date of the Following.'`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followeeId" IS 'The followee user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followerId" IS 'The follower user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followerHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followerInbox" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followerSharedInbox" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followeeHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followeeInbox" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followeeSharedInbox" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."caughtAt" IS 'The caught date of the Instance.'`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."host" IS 'The host of the Instance.'`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."usersCount" IS 'The count of the users of the Instance.'`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."notesCount" IS 'The count of the notes of the Instance.'`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareName" IS 'The software of the Instance.'`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareVersion" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."openRegistrations" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."description" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerName" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerEmail" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."iconUrl" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."faviconUrl" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."themeColor" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "muting"."createdAt" IS 'The created date of the Muting.'`); - await queryRunner.query(`COMMENT ON COLUMN "muting"."muteeId" IS 'The mutee user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "muting"."muterId" IS 'The muter user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "blocking"."createdAt" IS 'The created date of the Blocking.'`); - await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockeeId" IS 'The blockee user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockerId" IS 'The blocker user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_list"."createdAt" IS 'The created date of the UserList.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_list"."userId" IS 'The owner ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_list"."name" IS 'The name of the UserList.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."createdAt" IS 'The created date of the UserListJoining.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userId" IS 'The user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userListId" IS 'The list ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."createdAt" IS 'The created date of the UserGroupJoining.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userId" IS 'The user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userGroupId" IS 'The group ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "note_favorite"."createdAt" IS 'The created date of the NoteFavorite.'`); - await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."createdAt" IS 'The created date of the AbuseUserReport.'`); - await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."targetUserHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."reporterHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."createdAt" IS 'The created date of the MessagingMessage.'`); - await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."userId" IS 'The sender user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."groupId" IS 'The recipient group ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "signin"."createdAt" IS 'The created date of the Signin.'`); - await queryRunner.query(`COMMENT ON COLUMN "auth_session"."createdAt" IS 'The created date of the AuthSession.'`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."createdAt" IS 'The created date of the ReversiGame.'`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."startedAt" IS 'The started date of the ReversiGame.'`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form1" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form2" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_matching"."createdAt" IS 'The created date of the ReversiMatching.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_note_pining"."createdAt" IS 'The created date of the UserNotePinings.'`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."noteId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."noteVisibility" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."userId" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."userHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "user_keypair"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_publickey"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "page"."createdAt" IS 'The created date of the Page.'`); - await queryRunner.query(`COMMENT ON COLUMN "page"."updatedAt" IS 'The updated date of the Page.'`); - await queryRunner.query(`COMMENT ON COLUMN "page"."userId" IS 'The ID of author.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."location" IS 'The location of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."birthday" IS 'The birthday (YYYY-MM-DD) of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."description" IS 'The description (bio) of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."url" IS 'Remote URL of the user.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."email" IS 'The email address of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."password" IS 'The password hash of the User. It will be null if the origin of the user is local.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."clientData" IS 'The client-specific data of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."room" IS 'The room data of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userHost" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."id" IS 'Variable-length id given to navigator.credentials.get()'`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS 'Variable-length public key used to verify attestations (hex-encoded).'`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS 'The date of the last time the UserSecurityKey was successfully validated.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."name" IS 'User-defined name for this key'`); - await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS 'Hex-encoded sha256 hash of the challenge.'`); - await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS 'The date challenge was created for expiry purposes.'`); - await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS 'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.'`); - await queryRunner.query(`COMMENT ON COLUMN "moderation_log"."createdAt" IS 'The created date of the ModerationLog.'`); - await queryRunner.query(`COMMENT ON COLUMN "announcement"."createdAt" IS 'The created date of the Announcement.'`); - await queryRunner.query(`COMMENT ON COLUMN "announcement"."updatedAt" IS 'The updated date of the Announcement.'`); - await queryRunner.query(`COMMENT ON COLUMN "announcement_read"."createdAt" IS 'The created date of the AnnouncementRead.'`); - await queryRunner.query(`COMMENT ON COLUMN "clip"."createdAt" IS 'The created date of the Clip.'`); - await queryRunner.query(`COMMENT ON COLUMN "clip"."userId" IS 'The owner ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "clip"."name" IS 'The name of the Clip.'`); - await queryRunner.query(`COMMENT ON COLUMN "clip"."description" IS 'The description of the Clip.'`); - await queryRunner.query(`COMMENT ON COLUMN "clip_note"."noteId" IS 'The note ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "clip_note"."clipId" IS 'The clip ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "antenna"."createdAt" IS 'The created date of the Antenna.'`); - await queryRunner.query(`COMMENT ON COLUMN "antenna"."userId" IS 'The owner ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "antenna"."name" IS 'The name of the Antenna.'`); - await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."noteId" IS 'The note ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."antennaId" IS 'The antenna ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "promo_note"."noteId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "promo_note"."userId" IS '[Denormalized]'`); - await queryRunner.query(`COMMENT ON COLUMN "promo_read"."createdAt" IS 'The created date of the PromoRead.'`); - await queryRunner.query(`COMMENT ON COLUMN "muted_note"."noteId" IS 'The note ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "muted_note"."userId" IS 'The user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "muted_note"."reason" IS 'The reason of the MutedNote.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel_following"."createdAt" IS 'The created date of the ChannelFollowing.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followeeId" IS 'The followee channel ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followerId" IS 'The follower user ID.'`); - await queryRunner.query(`COMMENT ON COLUMN "channel_note_pining"."createdAt" IS 'The created date of the ChannelNotePining.'`); - } - async down(queryRunner) { - await queryRunner.query(`COMMENT ON COLUMN "channel_note_pining"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followerId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel_following"."followeeId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel_following"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "muted_note"."reason" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "muted_note"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "muted_note"."noteId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "promo_read"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "promo_note"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "promo_note"."noteId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."antennaId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "antenna_note"."noteId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "antenna"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "antenna"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "antenna"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "clip_note"."clipId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "clip_note"."noteId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "clip"."description" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "clip"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "clip"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "clip"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "announcement_read"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "announcement"."updatedAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "announcement"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "moderation_log"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."registrationChallenge" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "attestation_challenge"."challenge" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."lastUsed" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."publicKey" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_security_key"."id" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."room" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."clientData" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."password" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."email" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."url" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."description" IS 'The description (bio) of the User.'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."birthday" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."location" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "page"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "page"."updatedAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "page"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_publickey"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_keypair"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."userHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."noteVisibility" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "poll"."noteId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_note_pining"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_matching"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form2" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."form1" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."startedAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "auth_session"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "signin"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."groupId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "messaging_message"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."reporterHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."targetUserHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "abuse_user_report"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note_favorite"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userGroupId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_joining"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userListId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_list_joining"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_list"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_list"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_list"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockerId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockeeId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "blocking"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "muting"."muterId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "muting"."muteeId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "muting"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."themeColor" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."faviconUrl" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."iconUrl" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerEmail" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."maintainerName" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."description" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."openRegistrations" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareVersion" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."softwareName" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."notesCount" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."usersCount" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."host" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "instance"."caughtAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followeeSharedInbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followeeInbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followeeHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followerSharedInbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followerInbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followerHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followerId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "following"."followeeId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "following"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "meta"."maxNoteTextLength" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "meta"."remoteDriveCapacityMb" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "meta"."localDriveCapacityMb" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "notification"."isRead" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "notification"."notifieeId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "notification"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userGroupId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_group_invitation"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_group"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user_group"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeSharedInbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeInbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerSharedInbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerInbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."requestId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followerId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."followeeId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "follow_request"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteChannelId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note_unread"."noteUserId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteUserId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note_watching"."noteId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note_watching"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note_watching"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note_reaction"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "poll_vote"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."renoteUserId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."replyUserId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."userHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."channelId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."url" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."uri" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."renoteId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."replyId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "note"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."usersCount" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."notesCount" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."bannerId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."description" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "channel"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."iconUrl" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."description" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."appId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."session" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."lastUsedAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "access_token"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "app"."callbackUrl" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "app"."permission" IS 'The permission of the App.'`); - await queryRunner.query(`COMMENT ON COLUMN "app"."description" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "app"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "app"."secret" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "app"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "app"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."uri" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."featured" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."sharedInbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."inbox" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."host" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isModerator" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isAdmin" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isCat" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isBot" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isLocked" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isSilenced" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isSuspended" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."bannerId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."avatarId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."notesCount" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."followingCount" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."followersCount" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."usernameLower" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."username" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."updatedAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isLink" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."isSensitive" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."folderId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."webpublicUrl" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."thumbnailUrl" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."properties" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."blurhash" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."comment" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."size" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."type" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."md5" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userHost" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_file"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."parentId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."userId" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."name" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "drive_folder"."createdAt" IS NULL`); - await queryRunner.query(`COMMENT ON COLUMN "log"."createdAt" IS NULL`); - } -} diff --git a/packages/backend/migration/1605585339718-instance-pinned-pages.js b/packages/backend/migration/1605585339718-instance-pinned-pages.js deleted file mode 100644 index 56ccd44c8..000000000 --- a/packages/backend/migration/1605585339718-instance-pinned-pages.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class instancePinnedPages1605585339718 { - constructor() { - this.name = 'instancePinnedPages1605585339718'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'::varchar[]`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`); - } -} diff --git a/packages/backend/migration/1605965516823-instance-images.js b/packages/backend/migration/1605965516823-instance-images.js deleted file mode 100644 index 710c75981..000000000 --- a/packages/backend/migration/1605965516823-instance-images.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class instanceImages1605965516823 { - constructor() { - this.name = 'instanceImages1605965516823'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "backgroundImageUrl" character varying(512)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "logoImageUrl" character varying(512)`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "logoImageUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "backgroundImageUrl"`); - } -} diff --git a/packages/backend/migration/1606191203881-no-crawle.js b/packages/backend/migration/1606191203881-no-crawle.js deleted file mode 100644 index b9ada4354..000000000 --- a/packages/backend/migration/1606191203881-no-crawle.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class noCrawle1606191203881 { - constructor() { - this.name = 'noCrawle1606191203881'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "noCrawle" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`); - } - async down(queryRunner) { - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "noCrawle"`); - } -} diff --git a/packages/backend/migration/1607151207216-instance-pinned-clip.js b/packages/backend/migration/1607151207216-instance-pinned-clip.js deleted file mode 100644 index 9a4195e74..000000000 --- a/packages/backend/migration/1607151207216-instance-pinned-clip.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class instancePinnedClip1607151207216 { - constructor() { - this.name = 'instancePinnedClip1607151207216'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedClipId" character varying(32)`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedClipId"`); - } -} diff --git a/packages/backend/migration/1607353487793-isExplorable.js b/packages/backend/migration/1607353487793-isExplorable.js deleted file mode 100644 index d9f1ff4c6..000000000 --- a/packages/backend/migration/1607353487793-isExplorable.js +++ /dev/null @@ -1,17 +0,0 @@ - - -export class isExplorable1607353487793 { - constructor() { - this.name = 'isExplorable1607353487793'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "isExplorable" boolean NOT NULL DEFAULT true`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isExplorable" IS 'Whether the User is explorable.'`); - await queryRunner.query(`CREATE INDEX "IDX_d5a1b83c7cab66f167e6888188" ON "user" ("isExplorable") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_d5a1b83c7cab66f167e6888188"`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isExplorable" IS 'Whether the User is explorable.'`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isExplorable"`); - } -} diff --git a/packages/backend/migration/1610277136869-registry.js b/packages/backend/migration/1610277136869-registry.js deleted file mode 100644 index 184c062dd..000000000 --- a/packages/backend/migration/1610277136869-registry.js +++ /dev/null @@ -1,21 +0,0 @@ - - -export class registry1610277136869 { - constructor() { - this.name = 'registry1610277136869'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "registry_item" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "key" character varying(1024) NOT NULL, "scope" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[], "domain" character varying(512), CONSTRAINT "PK_64b3f7e6008b4d89b826cd3af95" PRIMARY KEY ("id")); COMMENT ON COLUMN "registry_item"."createdAt" IS 'The created date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."updatedAt" IS 'The updated date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."userId" IS 'The owner ID.'; COMMENT ON COLUMN "registry_item"."key" IS 'The key of the RegistryItem.'`); - await queryRunner.query(`CREATE INDEX "IDX_fb9d21ba0abb83223263df6bcb" ON "registry_item" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_22baca135bb8a3ea1a83d13df3" ON "registry_item" ("scope") `); - await queryRunner.query(`CREATE INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad" ON "registry_item" ("domain") `); - await queryRunner.query(`ALTER TABLE "registry_item" ADD CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "registry_item" DROP CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3"`); - await queryRunner.query(`DROP INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad"`); - await queryRunner.query(`DROP INDEX "IDX_22baca135bb8a3ea1a83d13df3"`); - await queryRunner.query(`DROP INDEX "IDX_fb9d21ba0abb83223263df6bcb"`); - await queryRunner.query(`DROP TABLE "registry_item"`); - } -} diff --git a/packages/backend/migration/1610277585759-registry2.js b/packages/backend/migration/1610277585759-registry2.js deleted file mode 100644 index 591bafae3..000000000 --- a/packages/backend/migration/1610277585759-registry2.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class registry21610277585759 { - constructor() { - this.name = 'registry21610277585759'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "registry_item" ADD "value" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`); - } - async down(queryRunner) { - await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`); - await queryRunner.query(`ALTER TABLE "registry_item" DROP COLUMN "value"`); - } -} diff --git a/packages/backend/migration/1610283021566-registry3.js b/packages/backend/migration/1610283021566-registry3.js deleted file mode 100644 index e0289f17e..000000000 --- a/packages/backend/migration/1610283021566-registry3.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class registry31610283021566 { - constructor() { - this.name = 'registry31610283021566'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" DROP NOT NULL`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" SET NOT NULL`); - } -} diff --git a/packages/backend/migration/1611354329133-followersUri.js b/packages/backend/migration/1611354329133-followersUri.js deleted file mode 100644 index 669ddb480..000000000 --- a/packages/backend/migration/1611354329133-followersUri.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class followersUri1611354329133 { - constructor() { - this.name = 'followersUri1611354329133'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "followersUri" varchar(512) DEFAULT NULL`); - await queryRunner.query(`COMMENT ON COLUMN "user"."followersUri" IS 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'`); - } - async down(queryRunner) { - await queryRunner.query(`COMMENT ON COLUMN "user"."followersUri" IS 'The URI of the user Follower Collection. It will be null if the origin of the user is local.'`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "followersUri"`); - } -} diff --git a/packages/backend/migration/1611397665007-gallery.js b/packages/backend/migration/1611397665007-gallery.js deleted file mode 100644 index f49b2df46..000000000 --- a/packages/backend/migration/1611397665007-gallery.js +++ /dev/null @@ -1,39 +0,0 @@ - - -export class gallery1611397665007 { - constructor() { - this.name = 'gallery1611397665007'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "gallery_post" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "description" character varying(2048), "userId" character varying(32) NOT NULL, "fileIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "isSensitive" boolean NOT NULL DEFAULT false, "likedCount" integer NOT NULL DEFAULT '0', "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_8e90d7b6015f2c4518881b14753" PRIMARY KEY ("id")); COMMENT ON COLUMN "gallery_post"."createdAt" IS 'The created date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."updatedAt" IS 'The updated date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."userId" IS 'The ID of author.'; COMMENT ON COLUMN "gallery_post"."isSensitive" IS 'Whether the post is sensitive.'`); - await queryRunner.query(`CREATE INDEX "IDX_8f1a239bd077c8864a20c62c2c" ON "gallery_post" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_f631d37835adb04792e361807c" ON "gallery_post" ("updatedAt") `); - await queryRunner.query(`CREATE INDEX "IDX_985b836dddd8615e432d7043dd" ON "gallery_post" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_3ca50563facd913c425e7a89ee" ON "gallery_post" ("fileIds") `); - await queryRunner.query(`CREATE INDEX "IDX_f2d744d9a14d0dfb8b96cb7fc5" ON "gallery_post" ("isSensitive") `); - await queryRunner.query(`CREATE INDEX "IDX_1a165c68a49d08f11caffbd206" ON "gallery_post" ("likedCount") `); - await queryRunner.query(`CREATE INDEX "IDX_05cca34b985d1b8edc1d1e28df" ON "gallery_post" ("tags") `); - await queryRunner.query(`CREATE TABLE "gallery_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "postId" character varying(32) NOT NULL, CONSTRAINT "PK_853ab02be39b8de45cd720cc15f" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_8fd5215095473061855ceb948c" ON "gallery_like" ("userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_df1b5f4099e99fb0bc5eae53b6" ON "gallery_like" ("userId", "postId") `); - await queryRunner.query(`ALTER TABLE "gallery_post" ADD CONSTRAINT "FK_985b836dddd8615e432d7043ddb" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_8fd5215095473061855ceb948cf" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_b1cb568bfe569e47b7051699fc8" FOREIGN KEY ("postId") REFERENCES "gallery_post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "gallery_like" DROP CONSTRAINT "FK_b1cb568bfe569e47b7051699fc8"`); - await queryRunner.query(`ALTER TABLE "gallery_like" DROP CONSTRAINT "FK_8fd5215095473061855ceb948cf"`); - await queryRunner.query(`ALTER TABLE "gallery_post" DROP CONSTRAINT "FK_985b836dddd8615e432d7043ddb"`); - await queryRunner.query(`DROP INDEX "IDX_df1b5f4099e99fb0bc5eae53b6"`); - await queryRunner.query(`DROP INDEX "IDX_8fd5215095473061855ceb948c"`); - await queryRunner.query(`DROP TABLE "gallery_like"`); - await queryRunner.query(`DROP INDEX "IDX_05cca34b985d1b8edc1d1e28df"`); - await queryRunner.query(`DROP INDEX "IDX_1a165c68a49d08f11caffbd206"`); - await queryRunner.query(`DROP INDEX "IDX_f2d744d9a14d0dfb8b96cb7fc5"`); - await queryRunner.query(`DROP INDEX "IDX_3ca50563facd913c425e7a89ee"`); - await queryRunner.query(`DROP INDEX "IDX_985b836dddd8615e432d7043dd"`); - await queryRunner.query(`DROP INDEX "IDX_f631d37835adb04792e361807c"`); - await queryRunner.query(`DROP INDEX "IDX_8f1a239bd077c8864a20c62c2c"`); - await queryRunner.query(`DROP TABLE "gallery_post"`); - } -} diff --git a/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js b/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js deleted file mode 100644 index e4d3c0e8e..000000000 --- a/packages/backend/migration/1611547387175-objectStorageS3ForcePathStyle.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class objectStorageS3ForcePathStyle1611547387175 { - constructor() { - this.name = 'objectStorageS3ForcePathStyle1611547387175'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "objectStorageS3ForcePathStyle" boolean NOT NULL DEFAULT true`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "objectStorageS3ForcePathStyle"`); - } -} diff --git a/packages/backend/migration/1612619156584-announcement-email.js b/packages/backend/migration/1612619156584-announcement-email.js deleted file mode 100644 index bcc718d1c..000000000 --- a/packages/backend/migration/1612619156584-announcement-email.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class announcementEmail1612619156584 { - constructor() { - this.name = 'announcementEmail1612619156584'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "receiveAnnouncementEmail" boolean NOT NULL DEFAULT true`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "receiveAnnouncementEmail"`); - } -} diff --git a/packages/backend/migration/1613155914446-emailNotificationTypes.js b/packages/backend/migration/1613155914446-emailNotificationTypes.js deleted file mode 100644 index cd49924d2..000000000 --- a/packages/backend/migration/1613155914446-emailNotificationTypes.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class emailNotificationTypes1613155914446 { - constructor() { - this.name = 'emailNotificationTypes1613155914446'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "emailNotificationTypes" jsonb NOT NULL DEFAULT '["follow","receiveFollowRequest","groupInvited"]'`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "emailNotificationTypes"`); - } -} diff --git a/packages/backend/migration/1613181457597-user-lang.js b/packages/backend/migration/1613181457597-user-lang.js deleted file mode 100644 index d2cd06848..000000000 --- a/packages/backend/migration/1613181457597-user-lang.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class userLang1613181457597 { - constructor() { - this.name = 'userLang1613181457597'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "lang" character varying(32)`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "lang"`); - } -} diff --git a/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js b/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js deleted file mode 100644 index f2e2c5d35..000000000 --- a/packages/backend/migration/1613503367223-use-bigint-for-driveUsage.js +++ /dev/null @@ -1,14 +0,0 @@ - - -export class useBigintForDriveUsage1613503367223 { - constructor() { - this.name = 'useBigintForDriveUsage1613503367223'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "driveUsage" TYPE bigint`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "driveUsage"`); - await queryRunner.query(`ALTER TABLE "instance" ADD "driveUsage" integer NOT NULL DEFAULT 0`); - } -} diff --git a/packages/backend/migration/1615965918224-chart-v2.js b/packages/backend/migration/1615965918224-chart-v2.js deleted file mode 100644 index 86fa5b0c0..000000000 --- a/packages/backend/migration/1615965918224-chart-v2.js +++ /dev/null @@ -1,216 +0,0 @@ - - -export class chartV21615965918224 { - constructor() { - this.name = 'chartV21615965918224'; - } - async up(queryRunner) { - await queryRunner.query(`DELETE FROM "__chart__active_users" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__drive" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__federation" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__hashtag" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__instance" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__network" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__notes" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__per_user_drive" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__per_user_following" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__per_user_notes" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__per_user_reaction" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__test" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__test_grouped" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__test_unique" WHERE "span" = 'day'`); - await queryRunner.query(`DELETE FROM "__chart__users" WHERE "span" = 'day'`); - await queryRunner.query(`DROP INDEX "IDX_15e91a03aeeac9dbccdf43fc06"`); - await queryRunner.query(`DROP INDEX "IDX_20f57cc8f142c131340ee16742"`); - await queryRunner.query(`DROP INDEX "IDX_c26e2c1cbb6e911e0554b27416"`); - await queryRunner.query(`DROP INDEX "IDX_3fa0d0f17ca72e3dc80999a032"`); - await queryRunner.query(`DROP INDEX "IDX_6e1df243476e20cbf86572ecc0"`); - await queryRunner.query(`DROP INDEX "IDX_06690fc959f1c9fdaf21928222"`); - await queryRunner.query(`DROP INDEX "IDX_e447064455928cf627590ef527"`); - await queryRunner.query(`DROP INDEX "IDX_2d416e6af791a82e338c79d480"`); - await queryRunner.query(`DROP INDEX "IDX_e9cd07672b37d8966cf3709283"`); - await queryRunner.query(`DROP INDEX "IDX_fcc181fb8283009c61cc4083ef"`); - await queryRunner.query(`DROP INDEX "IDX_49975586f50ed7b800fdd88fbd"`); - await queryRunner.query(`DROP INDEX "IDX_6d6f156ceefc6bc5f273a0e370"`); - await queryRunner.query(`DROP INDEX "IDX_c12f0af4a66cdd30c2287ce8aa"`); - await queryRunner.query(`DROP INDEX "IDX_d0a4f79af5a97b08f37b547197"`); - await queryRunner.query(`DROP INDEX "IDX_f5448d9633cff74208d850aabe"`); - await queryRunner.query(`DROP INDEX "IDX_f8dd01baeded2ffa833e0a610a"`); - await queryRunner.query(`DROP INDEX "IDX_08fac0eb3b11f04c200c0b40dd"`); - await queryRunner.query(`DROP INDEX "IDX_9ff6944f01acb756fdc92d7563"`); - await queryRunner.query(`DROP INDEX "IDX_e69096589f11e3baa98ddd64d0"`); - await queryRunner.query(`DROP INDEX "IDX_0c9a159c5082cbeef3ca6706b5"`); - await queryRunner.query(`DROP INDEX "IDX_924fc196c80ca24bae01dd37e4"`); - await queryRunner.query(`DROP INDEX "IDX_328f259961e60c4fa0bfcf55ca"`); - await queryRunner.query(`DROP INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53"`); - await queryRunner.query(`DROP INDEX "IDX_f2aeafde2ae6fbad38e857631b"`); - await queryRunner.query(`DROP INDEX "IDX_f92dd6d03f8d994f29987f6214"`); - await queryRunner.query(`DROP INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f"`); - await queryRunner.query(`DROP INDEX "IDX_4db3b84c7be0d3464714f3e0b1"`); - await queryRunner.query(`DROP INDEX "IDX_8d2cbbc8114d90d19b44d626b6"`); - await queryRunner.query(`DROP INDEX "IDX_046feeb12e9ef5f783f409866a"`); - await queryRunner.query(`DROP INDEX "IDX_f68a5ab958f9f5fa17a32ac23b"`); - await queryRunner.query(`DROP INDEX "IDX_65633a106bce43fc7c5c30a5c7"`); - await queryRunner.query(`DROP INDEX "IDX_edeb73c09c3143a81bcb34d569"`); - await queryRunner.query(`DROP INDEX "IDX_e316f01a6d24eb31db27f88262"`); - await queryRunner.query(`DROP INDEX "IDX_2be7ec6cebddc14dc11e206686"`); - await queryRunner.query(`DROP INDEX "IDX_a5133470f4825902e170328ca5"`); - await queryRunner.query(`DROP INDEX "IDX_84e661abb7bd1e51b690d4b017"`); - await queryRunner.query(`DROP INDEX "IDX_5c73bf61da4f6e6f15bae88ed1"`); - await queryRunner.query(`DROP INDEX "IDX_d70c86baedc68326be11f9c0ce"`); - await queryRunner.query(`DROP INDEX "IDX_66e1e1ecd2f29e57778af35b59"`); - await queryRunner.query(`DROP INDEX "IDX_92255988735563f0fe4aba1f05"`); - await queryRunner.query(`DROP INDEX "IDX_c5870993e25c3d5771f91f5003"`); - await queryRunner.query(`DROP INDEX "IDX_f170de677ea75ad4533de2723e"`); - await queryRunner.query(`DROP INDEX "IDX_7c184198ecf66a8d3ecb253ab3"`); - await queryRunner.query(`DROP INDEX "IDX_f091abb24193d50c653c6b77fc"`); - await queryRunner.query(`DROP INDEX "IDX_a770a57c70e668cc61590c9161"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__active_users_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_count"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_count"`); - await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__drive_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__federation_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__hashtag_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_count"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_count"`); - await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__instance_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__network" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__network_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__network" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__notes_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__per_user_drive_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__per_user_following_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__per_user_notes_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__per_user_reaction_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__test_grouped" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__test_grouped_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__test_grouped" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__test_unique_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "___foo"`); - await queryRunner.query(`ALTER TABLE "__chart__test" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__test_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__test" DROP COLUMN "unique"`); - await queryRunner.query(`ALTER TABLE "__chart__users" DROP COLUMN "span"`); - await queryRunner.query(`DROP TYPE "public"."__chart__users_span_enum"`); - await queryRunner.query(`ALTER TABLE "__chart__users" DROP COLUMN "unique"`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__users" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__users_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__users" ADD "span" "__chart__users_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__test" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__test_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__test" ADD "span" "__chart__test_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "___foo" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__test_unique_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "span" "__chart__test_unique_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__test_grouped" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__test_grouped_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__test_grouped" ADD "span" "__chart__test_grouped_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_reaction_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ADD "span" "__chart__per_user_reaction_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_notes_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD "span" "__chart__per_user_notes_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_following_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ADD "span" "__chart__per_user_following_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__per_user_drive_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ADD "span" "__chart__per_user_drive_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__notes_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "span" "__chart__notes_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__network" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__network_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__network" ADD "span" "__chart__network_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__instance_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "span" "__chart__instance_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_count" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_count" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__hashtag_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "span" "__chart__hashtag_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__federation_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "span" "__chart__federation_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__drive_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "span" "__chart__drive_span_enum" NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_count" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_count" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`CREATE TYPE "public"."__chart__active_users_span_enum" AS ENUM('hour', 'day')`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "span" "__chart__active_users_span_enum" NOT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_a770a57c70e668cc61590c9161" ON "__chart__users" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_f091abb24193d50c653c6b77fc" ON "__chart__users" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_7c184198ecf66a8d3ecb253ab3" ON "__chart__users" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_f170de677ea75ad4533de2723e" ON "__chart__test" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_c5870993e25c3d5771f91f5003" ON "__chart__test" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_92255988735563f0fe4aba1f05" ON "__chart__test" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_66e1e1ecd2f29e57778af35b59" ON "__chart__test_unique" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_d70c86baedc68326be11f9c0ce" ON "__chart__test_unique" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_5c73bf61da4f6e6f15bae88ed1" ON "__chart__test_unique" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_84e661abb7bd1e51b690d4b017" ON "__chart__test_grouped" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_a5133470f4825902e170328ca5" ON "__chart__test_grouped" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_2be7ec6cebddc14dc11e206686" ON "__chart__test_grouped" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_e316f01a6d24eb31db27f88262" ON "__chart__per_user_reaction" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_edeb73c09c3143a81bcb34d569" ON "__chart__per_user_reaction" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_65633a106bce43fc7c5c30a5c7" ON "__chart__per_user_reaction" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_f68a5ab958f9f5fa17a32ac23b" ON "__chart__per_user_notes" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_046feeb12e9ef5f783f409866a" ON "__chart__per_user_notes" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_8d2cbbc8114d90d19b44d626b6" ON "__chart__per_user_notes" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_4db3b84c7be0d3464714f3e0b1" ON "__chart__per_user_following" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_57b5458d0d3d6d1e7f13d4e57f" ON "__chart__per_user_following" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_f92dd6d03f8d994f29987f6214" ON "__chart__per_user_following" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_f2aeafde2ae6fbad38e857631b" ON "__chart__per_user_drive" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_42ea9381f0fda8dfe0fa1c8b53" ON "__chart__per_user_drive" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_328f259961e60c4fa0bfcf55ca" ON "__chart__per_user_drive" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_924fc196c80ca24bae01dd37e4" ON "__chart__notes" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_0c9a159c5082cbeef3ca6706b5" ON "__chart__notes" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_e69096589f11e3baa98ddd64d0" ON "__chart__notes" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_9ff6944f01acb756fdc92d7563" ON "__chart__network" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_08fac0eb3b11f04c200c0b40dd" ON "__chart__network" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_f8dd01baeded2ffa833e0a610a" ON "__chart__network" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_f5448d9633cff74208d850aabe" ON "__chart__instance" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_d0a4f79af5a97b08f37b547197" ON "__chart__instance" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_c12f0af4a66cdd30c2287ce8aa" ON "__chart__instance" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_6d6f156ceefc6bc5f273a0e370" ON "__chart__hashtag" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_49975586f50ed7b800fdd88fbd" ON "__chart__hashtag" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_fcc181fb8283009c61cc4083ef" ON "__chart__hashtag" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_e9cd07672b37d8966cf3709283" ON "__chart__federation" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_2d416e6af791a82e338c79d480" ON "__chart__federation" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_e447064455928cf627590ef527" ON "__chart__federation" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_06690fc959f1c9fdaf21928222" ON "__chart__drive" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_6e1df243476e20cbf86572ecc0" ON "__chart__drive" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_3fa0d0f17ca72e3dc80999a032" ON "__chart__drive" ("span") `); - await queryRunner.query(`CREATE INDEX "IDX_c26e2c1cbb6e911e0554b27416" ON "__chart__active_users" ("date", "group", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_20f57cc8f142c131340ee16742" ON "__chart__active_users" ("date", "span") `); - await queryRunner.query(`CREATE INDEX "IDX_15e91a03aeeac9dbccdf43fc06" ON "__chart__active_users" ("span") `); - } -} diff --git a/packages/backend/migration/1615966519402-chart-v2-2.js b/packages/backend/migration/1615966519402-chart-v2-2.js deleted file mode 100644 index c62f1b875..000000000 --- a/packages/backend/migration/1615966519402-chart-v2-2.js +++ /dev/null @@ -1,21 +0,0 @@ - - -export class chartV221615966519402 { - constructor() { - this.name = 'chartV221615966519402'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_users" character varying array NOT NULL DEFAULT '{}'::varchar[]`); - await queryRunner.query(`ALTER TABLE "__chart__test_unique" ADD "___foo" character varying array NOT NULL DEFAULT '{}'::varchar[]`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__test_unique" DROP COLUMN "___foo"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_users"`); - } -} diff --git a/packages/backend/migration/1618637372000-user-last-active-date.js b/packages/backend/migration/1618637372000-user-last-active-date.js deleted file mode 100644 index 6c77ace46..000000000 --- a/packages/backend/migration/1618637372000-user-last-active-date.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class userLastActiveDate1618637372000 { - constructor() { - this.name = 'userLastActiveDate1618637372000'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "lastActiveDate" TIMESTAMP WITH TIME ZONE DEFAULT NULL`); - await queryRunner.query(`CREATE INDEX "IDX_seoignmeoprigmkpodgrjmkpormg" ON "user" ("lastActiveDate") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_seoignmeoprigmkpodgrjmkpormg"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "lastActiveDate"`); - } -} diff --git a/packages/backend/migration/1618639857000-user-hide-online-status.js b/packages/backend/migration/1618639857000-user-hide-online-status.js deleted file mode 100644 index e63c8ae11..000000000 --- a/packages/backend/migration/1618639857000-user-hide-online-status.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class userHideOnlineStatus1618639857000 { - constructor() { - this.name = 'userHideOnlineStatus1618639857000'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "hideOnlineStatus" boolean NOT NULL DEFAULT false`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hideOnlineStatus"`); - } -} diff --git a/packages/backend/migration/1619942102890-password-reset.js b/packages/backend/migration/1619942102890-password-reset.js deleted file mode 100644 index 922d225dc..000000000 --- a/packages/backend/migration/1619942102890-password-reset.js +++ /dev/null @@ -1,19 +0,0 @@ - - -export class passwordReset1619942102890 { - constructor() { - this.name = 'passwordReset1619942102890'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "password_reset_request" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "token" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_fcf4b02eae1403a2edaf87fd074" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0b575fa9a4cfe638a925949285" ON "password_reset_request" ("token") `); - await queryRunner.query(`CREATE INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac" ON "password_reset_request" ("userId") `); - await queryRunner.query(`ALTER TABLE "password_reset_request" ADD CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "password_reset_request" DROP CONSTRAINT "FK_4bb7fd4a34492ae0e6cc8d30ac8"`); - await queryRunner.query(`DROP INDEX "IDX_4bb7fd4a34492ae0e6cc8d30ac"`); - await queryRunner.query(`DROP INDEX "IDX_0b575fa9a4cfe638a925949285"`); - await queryRunner.query(`DROP TABLE "password_reset_request"`); - } -} diff --git a/packages/backend/migration/1620019354680-ad.js b/packages/backend/migration/1620019354680-ad.js deleted file mode 100644 index c96d2bfb3..000000000 --- a/packages/backend/migration/1620019354680-ad.js +++ /dev/null @@ -1,17 +0,0 @@ - - -export class ad1620019354680 { - constructor() { - this.name = 'ad1620019354680'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "ad" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "place" character varying(32) NOT NULL, "priority" character varying(32) NOT NULL, "url" character varying(1024) NOT NULL, "imageUrl" character varying(1024) NOT NULL, "memo" character varying(8192) NOT NULL, CONSTRAINT "PK_0193d5ef09746e88e9ea92c634d" PRIMARY KEY ("id")); COMMENT ON COLUMN "ad"."createdAt" IS 'The created date of the Ad.'; COMMENT ON COLUMN "ad"."expiresAt" IS 'The expired date of the Ad.'`); - await queryRunner.query(`CREATE INDEX "IDX_1129c2ef687fc272df040bafaa" ON "ad" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_2da24ce20ad209f1d9dc032457" ON "ad" ("expiresAt") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_2da24ce20ad209f1d9dc032457"`); - await queryRunner.query(`DROP INDEX "IDX_1129c2ef687fc272df040bafaa"`); - await queryRunner.query(`DROP TABLE "ad"`); - } -} diff --git a/packages/backend/migration/1620364649428-ad2.js b/packages/backend/migration/1620364649428-ad2.js deleted file mode 100644 index db1c3e1de..000000000 --- a/packages/backend/migration/1620364649428-ad2.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class ad21620364649428 { - constructor() { - this.name = 'ad21620364649428'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "ad" ADD "ratio" integer NOT NULL DEFAULT '1'`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "ratio"`); - } -} diff --git a/packages/backend/migration/1621479946000-add-note-indexes.js b/packages/backend/migration/1621479946000-add-note-indexes.js deleted file mode 100644 index dcf97fa4d..000000000 --- a/packages/backend/migration/1621479946000-add-note-indexes.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class addNoteIndexes1621479946000 { - constructor() { - this.name = 'addNoteIndexes1621479946000'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_NOTE_MENTIONS" ON "note" USING gin ("mentions")`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_NOTE_VISIBLE_USER_IDS" ON "note" USING gin ("visibleUserIds")`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_NOTE_MENTIONS"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_NOTE_VISIBLE_USER_IDS"`, undefined); - } -} diff --git a/packages/backend/migration/1622679304522-user-profile-description-length.js b/packages/backend/migration/1622679304522-user-profile-description-length.js deleted file mode 100644 index 22f6c1c5d..000000000 --- a/packages/backend/migration/1622679304522-user-profile-description-length.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class userProfileDescriptionLength1622679304522 { - constructor() { - this.name = 'userProfileDescriptionLength1622679304522'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE character varying(2048)`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "description" TYPE character varying(1024)`, undefined); - } -} diff --git a/packages/backend/migration/1622681548499-log-message-length.js b/packages/backend/migration/1622681548499-log-message-length.js deleted file mode 100644 index ac16c0e1b..000000000 --- a/packages/backend/migration/1622681548499-log-message-length.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class logMessageLength1622681548499 { - constructor() { - this.name = 'logMessageLength1622681548499'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "log" ALTER COLUMN "message" TYPE character varying(2048)`, undefined); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "log" ALTER COLUMN "message" TYPE character varying(1024)`, undefined); - } -} diff --git a/packages/backend/migration/1626509500668-fix-remote-file-proxy.js b/packages/backend/migration/1626509500668-fix-remote-file-proxy.js deleted file mode 100644 index 30c562007..000000000 --- a/packages/backend/migration/1626509500668-fix-remote-file-proxy.js +++ /dev/null @@ -1,22 +0,0 @@ - - -export class fixRemoteFileProxy1626509500668 { - constructor() { - this.name = 'fixRemoteFileProxy1626509500668'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarUrl"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerUrl"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`); - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyRemoteFiles"`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`); - await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`); - await queryRunner.query(`ALTER TABLE "user" ADD "bannerUrl" character varying(512)`); - await queryRunner.query(`ALTER TABLE "user" ADD "avatarUrl" character varying(512)`); - } -} - diff --git a/packages/backend/migration/1629004542760-chart-reindex.js b/packages/backend/migration/1629004542760-chart-reindex.js deleted file mode 100644 index a7d459276..000000000 --- a/packages/backend/migration/1629004542760-chart-reindex.js +++ /dev/null @@ -1,181 +0,0 @@ - - -export class chartReindex1629004542760 { - constructor() { - this.name = 'chartReindex1629004542760'; - } - async up(queryRunner) { - await queryRunner.query(`DELETE FROM "__chart__active_users" a USING "__chart__active_users" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__drive" a USING "__chart__drive" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__federation" a USING "__chart__federation" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__hashtag" a USING "__chart__hashtag" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__instance" a USING "__chart__instance" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__network" a USING "__chart__network" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__notes" a USING "__chart__notes" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__per_user_drive" a USING "__chart__per_user_drive" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__per_user_following" a USING "__chart__per_user_following" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__per_user_notes" a USING "__chart__per_user_notes" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__per_user_reaction" a USING "__chart__per_user_reaction" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__test_grouped" a USING "__chart__test_grouped" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__test_unique" a USING "__chart__test_unique" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DELETE FROM "__chart__users" a USING "__chart__users" b WHERE a.id < b.id AND ((a.group IS NULL AND b.group IS NULL) OR a.group = b.group) AND a.date = b.date;`); - await queryRunner.query(`DROP INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc"`); - await queryRunner.query(`DROP INDEX "IDX_00ed5f86db1f7efafb1978bf21"`); - await queryRunner.query(`DROP INDEX "IDX_9a3ed15a30ab7e3a37702e6e08"`); - await queryRunner.query(`DROP INDEX "IDX_13565815f618a1ff53886c5b28"`); - await queryRunner.query(`DROP INDEX "IDX_7a170f67425e62a8fabb76c872"`); - await queryRunner.query(`DROP INDEX "IDX_3313d7288855ec105b5bbf6c21"`); - await queryRunner.query(`DROP INDEX "IDX_36cb699c49580d4e6c2e6159f9"`); - await queryRunner.query(`DROP INDEX "IDX_76e87c7bfc5d925fcbba405d84"`); - await queryRunner.query(`DROP INDEX "IDX_dd907becf76104e4b656659e6b"`); - await queryRunner.query(`DROP INDEX "IDX_07747a1038c05f532a718fe1de"`); - await queryRunner.query(`DROP INDEX "IDX_99a7d2faaef84a6f728d714ad6"`); - await queryRunner.query(`DROP INDEX "IDX_25a97c02003338124b2b75fdbc"`); - await queryRunner.query(`DROP INDEX "IDX_6b8f34a1a64b06014b6fb66824"`); - await queryRunner.query(`DROP INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63"`); - await queryRunner.query(`DROP INDEX "IDX_39ee857ab2f23493037c6b6631"`); - await queryRunner.query(`DROP INDEX "IDX_a1efd3e0048a5f2793a47360dc"`); - await queryRunner.query(`DROP INDEX "IDX_7b5da130992ec9df96712d4290"`); - await queryRunner.query(`DROP INDEX "IDX_0a905b992fecd2b5c3fb98759e"`); - await queryRunner.query(`DROP INDEX "IDX_42eb716a37d381cdf566192b2b"`); - await queryRunner.query(`DROP INDEX "IDX_7036f2957151588b813185c794"`); - await queryRunner.query(`DROP INDEX "IDX_f09d543e3acb16c5976bdb31fa"`); - await queryRunner.query(`DROP INDEX "IDX_5f86db6492274e07c1a3cdf286"`); - await queryRunner.query(`DROP INDEX "IDX_e496ca8096d28f6b9b509264dc"`); - await queryRunner.query(`DROP INDEX "IDX_30bf67687f483ace115c5ca642"`); - await queryRunner.query(`DROP INDEX "IDX_7af07790712aa3438ff6773f3b"`); - await queryRunner.query(`DROP INDEX "IDX_4b3593098b6edc9c5afe36b18b"`); - await queryRunner.query(`DROP INDEX "IDX_b77d4dd9562c3a899d9a286fcd"`); - await queryRunner.query(`DROP INDEX "IDX_84234bd1abb873f07329681c83"`); - await queryRunner.query(`DROP INDEX "IDX_55bf20f366979f2436de99206b"`); - await queryRunner.query(`DROP INDEX "IDX_5048e9daccbbbc6d567bb142d3"`); - await queryRunner.query(`DROP INDEX "IDX_f7bf4c62059764c2c2bb40fdab"`); - await queryRunner.query(`DROP INDEX "IDX_8cf3156fd7a6b15c43459c6e3b"`); - await queryRunner.query(`DROP INDEX "IDX_229a41ad465f9205f1f5703291"`); - await queryRunner.query(`DROP INDEX "IDX_0c641990ecf47d2545df4edb75"`); - await queryRunner.query(`DROP INDEX "IDX_234dff3c0b56a6150b95431ab9"`); - await queryRunner.query(`DROP INDEX "IDX_b14489029e4b3aaf4bba5fb524"`); - await queryRunner.query(`DROP INDEX "IDX_437bab3c6061d90f6bb65fd2cc"`); - await queryRunner.query(`DROP INDEX "IDX_bbfa573a8181018851ed0b6357"`); - await queryRunner.query(`DROP INDEX "IDX_a0cd75442dd10d0643a17c4a49"`); - await queryRunner.query(`DROP INDEX "IDX_b070a906db04b44c67c6c2144d"`); - await queryRunner.query(`DROP INDEX "IDX_d41cce6aee1a50bfc062038f9b"`); - await queryRunner.query(`DROP INDEX "IDX_a319e5dbf47e8a17497623beae"`); - await queryRunner.query(`DROP INDEX "IDX_845254b3eaf708ae8a6cac3026"`); - await queryRunner.query(`DROP INDEX "IDX_ed9b95919c672a13008e9487ee"`); - await queryRunner.query(`DROP INDEX "IDX_337e9599f278bd7537fe30876f"`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_60c5c6e7e538c09aa274ecd1cf" ON "__chart__active_users" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ceab80a6729f8e2e6f5b8a1a3d" ON "__chart__drive" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_eddfed8fb40305a04c6f941050" ON "__chart__federation" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_53a3604b939e2b479eb2cfaac8" ON "__chart__hashtag" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8111b817b9818c04d7eb8475b1" ON "__chart__instance" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2082327b2699ce924fa654afc5" ON "__chart__network" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e60c358aaced5aab8900a4af31" ON "__chart__notes" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a9a806d466b314f253a1a611c4" ON "__chart__per_user_drive" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dabbb38a51ab86ee3cab291326" ON "__chart__per_user_following" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_583a157ed0cf0ed1b5ec2a833f" ON "__chart__per_user_notes" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3b7697a96f522d0478972e6d6f" ON "__chart__per_user_reaction" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b14489029e4b3aaf4bba5fb524" ON "__chart__test_grouped" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_da522b4008a9f5d7743b87ad55" ON "__chart__test_grouped" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a0cd75442dd10d0643a17c4a49" ON "__chart__test_unique" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_16effb2e888f6763673b579f80" ON "__chart__test_unique" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a319e5dbf47e8a17497623beae" ON "__chart__test" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dab383a36f3c9db4a0c9b02cf3" ON "__chart__test" ("date") WHERE "group" IS NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_66feba81e1795d176d06c0b1e6" ON "__chart__users" ("date") WHERE "group" IS NULL`); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_66feba81e1795d176d06c0b1e6"`); - await queryRunner.query(`DROP INDEX "IDX_337e9599f278bd7537fe30876f"`); - await queryRunner.query(`DROP INDEX "IDX_dab383a36f3c9db4a0c9b02cf3"`); - await queryRunner.query(`DROP INDEX "IDX_a319e5dbf47e8a17497623beae"`); - await queryRunner.query(`DROP INDEX "IDX_16effb2e888f6763673b579f80"`); - await queryRunner.query(`DROP INDEX "IDX_a0cd75442dd10d0643a17c4a49"`); - await queryRunner.query(`DROP INDEX "IDX_da522b4008a9f5d7743b87ad55"`); - await queryRunner.query(`DROP INDEX "IDX_b14489029e4b3aaf4bba5fb524"`); - await queryRunner.query(`DROP INDEX "IDX_3b7697a96f522d0478972e6d6f"`); - await queryRunner.query(`DROP INDEX "IDX_229a41ad465f9205f1f5703291"`); - await queryRunner.query(`DROP INDEX "IDX_583a157ed0cf0ed1b5ec2a833f"`); - await queryRunner.query(`DROP INDEX "IDX_5048e9daccbbbc6d567bb142d3"`); - await queryRunner.query(`DROP INDEX "IDX_dabbb38a51ab86ee3cab291326"`); - await queryRunner.query(`DROP INDEX "IDX_b77d4dd9562c3a899d9a286fcd"`); - await queryRunner.query(`DROP INDEX "IDX_a9a806d466b314f253a1a611c4"`); - await queryRunner.query(`DROP INDEX "IDX_30bf67687f483ace115c5ca642"`); - await queryRunner.query(`DROP INDEX "IDX_e60c358aaced5aab8900a4af31"`); - await queryRunner.query(`DROP INDEX "IDX_f09d543e3acb16c5976bdb31fa"`); - await queryRunner.query(`DROP INDEX "IDX_2082327b2699ce924fa654afc5"`); - await queryRunner.query(`DROP INDEX "IDX_0a905b992fecd2b5c3fb98759e"`); - await queryRunner.query(`DROP INDEX "IDX_8111b817b9818c04d7eb8475b1"`); - await queryRunner.query(`DROP INDEX "IDX_39ee857ab2f23493037c6b6631"`); - await queryRunner.query(`DROP INDEX "IDX_53a3604b939e2b479eb2cfaac8"`); - await queryRunner.query(`DROP INDEX "IDX_25a97c02003338124b2b75fdbc"`); - await queryRunner.query(`DROP INDEX "IDX_eddfed8fb40305a04c6f941050"`); - await queryRunner.query(`DROP INDEX "IDX_dd907becf76104e4b656659e6b"`); - await queryRunner.query(`DROP INDEX "IDX_ceab80a6729f8e2e6f5b8a1a3d"`); - await queryRunner.query(`DROP INDEX "IDX_3313d7288855ec105b5bbf6c21"`); - await queryRunner.query(`DROP INDEX "IDX_60c5c6e7e538c09aa274ecd1cf"`); - await queryRunner.query(`DROP INDEX "IDX_9a3ed15a30ab7e3a37702e6e08"`); - await queryRunner.query(`DROP INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5"`); - await queryRunner.query(`DROP INDEX "IDX_f22169eb10657bded6d875ac8f"`); - await queryRunner.query(`DROP INDEX "IDX_c8cc87bd0f2f4487d17c651fbf"`); - await queryRunner.query(`DROP INDEX "IDX_754499f9b2642336433769518d"`); - await queryRunner.query(`DROP INDEX "IDX_315c779174fe8247ab324f036e"`); - await queryRunner.query(`DROP INDEX "IDX_c5d46cbfda48b1c33ed852e21b"`); - await queryRunner.query(`CREATE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_ed9b95919c672a13008e9487ee" ON "__chart__users" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_845254b3eaf708ae8a6cac3026" ON "__chart__users" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_a319e5dbf47e8a17497623beae" ON "__chart__test" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_d41cce6aee1a50bfc062038f9b" ON "__chart__test" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_b070a906db04b44c67c6c2144d" ON "__chart__test" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_a0cd75442dd10d0643a17c4a49" ON "__chart__test_unique" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_bbfa573a8181018851ed0b6357" ON "__chart__test_unique" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_437bab3c6061d90f6bb65fd2cc" ON "__chart__test_unique" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_b14489029e4b3aaf4bba5fb524" ON "__chart__test_grouped" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_234dff3c0b56a6150b95431ab9" ON "__chart__test_grouped" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_0c641990ecf47d2545df4edb75" ON "__chart__test_grouped" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_8cf3156fd7a6b15c43459c6e3b" ON "__chart__per_user_reaction" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_f7bf4c62059764c2c2bb40fdab" ON "__chart__per_user_reaction" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_55bf20f366979f2436de99206b" ON "__chart__per_user_notes" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_84234bd1abb873f07329681c83" ON "__chart__per_user_notes" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_4b3593098b6edc9c5afe36b18b" ON "__chart__per_user_following" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_7af07790712aa3438ff6773f3b" ON "__chart__per_user_following" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_e496ca8096d28f6b9b509264dc" ON "__chart__per_user_drive" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_5f86db6492274e07c1a3cdf286" ON "__chart__per_user_drive" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_7036f2957151588b813185c794" ON "__chart__notes" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_42eb716a37d381cdf566192b2b" ON "__chart__notes" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_7b5da130992ec9df96712d4290" ON "__chart__network" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_a1efd3e0048a5f2793a47360dc" ON "__chart__network" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_da8a46ba84ca1d8bb5a29bfb63" ON "__chart__instance" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_6b8f34a1a64b06014b6fb66824" ON "__chart__instance" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_99a7d2faaef84a6f728d714ad6" ON "__chart__hashtag" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_07747a1038c05f532a718fe1de" ON "__chart__hashtag" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_76e87c7bfc5d925fcbba405d84" ON "__chart__federation" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_36cb699c49580d4e6c2e6159f9" ON "__chart__federation" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_7a170f67425e62a8fabb76c872" ON "__chart__drive" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_13565815f618a1ff53886c5b28" ON "__chart__drive" ("date") `); - await queryRunner.query(`CREATE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `); - await queryRunner.query(`CREATE INDEX "IDX_00ed5f86db1f7efafb1978bf21" ON "__chart__active_users" ("group") `); - await queryRunner.query(`CREATE INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc" ON "__chart__active_users" ("date") `); - } -} diff --git a/packages/backend/migration/1629024377804-deepl-integration.js b/packages/backend/migration/1629024377804-deepl-integration.js deleted file mode 100644 index 19c49ffcd..000000000 --- a/packages/backend/migration/1629024377804-deepl-integration.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class deeplIntegration1629024377804 { - constructor() { - this.name = 'deeplIntegration1629024377804'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "deeplAuthKey" character varying(128)`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplAuthKey"`); - } -} diff --git a/packages/backend/migration/1629288472000-fix-channel-userId.js b/packages/backend/migration/1629288472000-fix-channel-userId.js deleted file mode 100644 index 02a1199b0..000000000 --- a/packages/backend/migration/1629288472000-fix-channel-userId.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class fixChannelUserId1629288472000 { - constructor() { - this.name = 'fixChannelUserId1629288472000'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "channel" ALTER COLUMN "userId" DROP NOT NULL;`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "channel" ALTER COLUMN "userId" SET NOT NULL;`); - } -} diff --git a/packages/backend/migration/1629512953000-user-is-deleted.js b/packages/backend/migration/1629512953000-user-is-deleted.js deleted file mode 100644 index a7848d569..000000000 --- a/packages/backend/migration/1629512953000-user-is-deleted.js +++ /dev/null @@ -1,14 +0,0 @@ - - -export class isUserDeleted1629512953000 { - constructor() { - this.name = 'isUserDeleted1629512953000'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "isDeleted" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`COMMENT ON COLUMN "user"."isDeleted" IS 'Whether the User is deleted.'`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isDeleted"`); - } -} diff --git a/packages/backend/migration/1629778475000-deepl-integration2.js b/packages/backend/migration/1629778475000-deepl-integration2.js deleted file mode 100644 index 699f06c76..000000000 --- a/packages/backend/migration/1629778475000-deepl-integration2.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class deeplIntegration21629778475000 { - constructor() { - this.name = 'deeplIntegration21629778475000'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "deeplIsPro" boolean NOT NULL DEFAULT false`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplIsPro"`); - } -} diff --git a/packages/backend/migration/1629833361000-AddShowTLReplies.js b/packages/backend/migration/1629833361000-AddShowTLReplies.js deleted file mode 100644 index 5d4c938a7..000000000 --- a/packages/backend/migration/1629833361000-AddShowTLReplies.js +++ /dev/null @@ -1,14 +0,0 @@ - - -export class addShowTLReplies1629833361000 { - constructor() { - this.name = 'addShowTLReplies1629833361000'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "showTimelineReplies" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`COMMENT ON COLUMN "user"."showTimelineReplies" IS 'Whether to show users replying to other users in the timeline.'`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "showTimelineReplies"`); - } -} diff --git a/packages/backend/migration/1629968054000_userInstanceBlocks.js b/packages/backend/migration/1629968054000_userInstanceBlocks.js deleted file mode 100644 index 1f202d9f6..000000000 --- a/packages/backend/migration/1629968054000_userInstanceBlocks.js +++ /dev/null @@ -1,14 +0,0 @@ - - -export class userInstanceBlocks1629968054000 { - constructor() { - this.name = 'userInstanceBlocks1629968054000'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutedInstances" jsonb NOT NULL DEFAULT '[]'`); - await queryRunner.query(`COMMENT ON COLUMN "user_profile"."mutedInstances" IS 'List of instances muted by the user.'`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutedInstances"`); - } -} diff --git a/packages/backend/migration/1631880003000-user-block-federation.js b/packages/backend/migration/1631880003000-user-block-federation.js deleted file mode 100644 index 1a90d5ae4..000000000 --- a/packages/backend/migration/1631880003000-user-block-federation.js +++ /dev/null @@ -1,12 +0,0 @@ -export class userBlockFederation1631880003000 { - name = 'userBlockFederation1631880003000'; - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" ADD "federateBlocks" boolean NOT NULL DEFAULT true`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "federateBlocks"`); - } - -} diff --git a/packages/backend/migration/1633068642000-email-required-for-signup.js b/packages/backend/migration/1633068642000-email-required-for-signup.js deleted file mode 100644 index d592f3ca2..000000000 --- a/packages/backend/migration/1633068642000-email-required-for-signup.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class emailRequiredForSignup1633068642000 { - constructor() { - this.name = 'emailRequiredForSignup1633068642000'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "emailRequiredForSignup" boolean NOT NULL DEFAULT false`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "emailRequiredForSignup"`); - } -} diff --git a/packages/backend/migration/1633071909016-user-pending.js b/packages/backend/migration/1633071909016-user-pending.js deleted file mode 100644 index 17cf5c11b..000000000 --- a/packages/backend/migration/1633071909016-user-pending.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class userPending1633071909016 { - constructor() { - this.name = 'userPending1633071909016'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "user_pending" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "code" character varying(128) NOT NULL, "username" character varying(128) NOT NULL, "email" character varying(128) NOT NULL, "password" character varying(128) NOT NULL, CONSTRAINT "PK_d4c84e013c98ec02d19b8fbbafa" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4e5c4c99175638ec0761714ab0" ON "user_pending" ("code") `); - } - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_4e5c4c99175638ec0761714ab0"`); - await queryRunner.query(`DROP TABLE "user_pending"`); - } -} diff --git a/packages/backend/migration/1634486652000-user-public-reactions.js b/packages/backend/migration/1634486652000-user-public-reactions.js deleted file mode 100644 index e74112249..000000000 --- a/packages/backend/migration/1634486652000-user-public-reactions.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class userPublicReactions1634486652000 { - constructor() { - this.name = 'userPublicReactions1634486652000'; - } - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "publicReactions" boolean NOT NULL DEFAULT false`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "publicReactions"`); - } -} diff --git a/packages/backend/migration/1634902659689-delete-log.js b/packages/backend/migration/1634902659689-delete-log.js deleted file mode 100644 index 555a0020c..000000000 --- a/packages/backend/migration/1634902659689-delete-log.js +++ /dev/null @@ -1,12 +0,0 @@ - - -export class deleteLog1634902659689 { - constructor() { - this.name = 'deleteLog1634902659689'; - } - async up(queryRunner) { - await queryRunner.query(`DROP TABLE "log"`); - } - async down(queryRunner) { - } -} diff --git a/packages/backend/migration/1635500777168-note-thread-mute.js b/packages/backend/migration/1635500777168-note-thread-mute.js deleted file mode 100644 index a790cace3..000000000 --- a/packages/backend/migration/1635500777168-note-thread-mute.js +++ /dev/null @@ -1,25 +0,0 @@ - - -export class noteThreadMute1635500777168 { - constructor() { - this.name = 'noteThreadMute1635500777168'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "note_thread_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "threadId" character varying(256) NOT NULL, CONSTRAINT "PK_ec5936d94d1a0369646d12a3a47" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_29c11c7deb06615076f8c95b80" ON "note_thread_muting" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_c426394644267453e76f036926" ON "note_thread_muting" ("threadId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ae7aab18a2641d3e5f25e0c4ea" ON "note_thread_muting" ("userId", "threadId") `); - await queryRunner.query(`ALTER TABLE "note" ADD "threadId" character varying(256)`); - await queryRunner.query(`CREATE INDEX "IDX_d4ebdef929896d6dc4a3c5bb48" ON "note" ("threadId") `); - await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD CONSTRAINT "FK_29c11c7deb06615076f8c95b80a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP CONSTRAINT "FK_29c11c7deb06615076f8c95b80a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_d4ebdef929896d6dc4a3c5bb48"`); - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "threadId"`); - await queryRunner.query(`DROP INDEX "public"."IDX_ae7aab18a2641d3e5f25e0c4ea"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c426394644267453e76f036926"`); - await queryRunner.query(`DROP INDEX "public"."IDX_29c11c7deb06615076f8c95b80"`); - await queryRunner.query(`DROP TABLE "note_thread_muting"`); - } -} diff --git a/packages/backend/migration/1636197624383-ff-visibility.js b/packages/backend/migration/1636197624383-ff-visibility.js deleted file mode 100644 index 89028f3c2..000000000 --- a/packages/backend/migration/1636197624383-ff-visibility.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class ffVisibility1636197624383 { - constructor() { - this.name = 'ffVisibility1636197624383'; - } - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`); - await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`); - } - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`); - await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`); - } -} diff --git a/packages/backend/migration/1636697408073-remove-via-mobile.js b/packages/backend/migration/1636697408073-remove-via-mobile.js deleted file mode 100644 index 36e96fd21..000000000 --- a/packages/backend/migration/1636697408073-remove-via-mobile.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class removeViaMobile1636697408073 { - name = 'removeViaMobile1636697408073' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "viaMobile"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "viaMobile" boolean NOT NULL DEFAULT false`); - } -} diff --git a/packages/backend/migration/1637320813000-forwarded-report.js b/packages/backend/migration/1637320813000-forwarded-report.js deleted file mode 100644 index 1e39bd5c3..000000000 --- a/packages/backend/migration/1637320813000-forwarded-report.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class forwardedReport1637320813000 { - name = 'forwardedReport1637320813000'; - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "forwarded" boolean NOT NULL DEFAULT false`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "forwarded"`); - } -}; diff --git a/packages/backend/migration/1639325650583-chart-v3.js b/packages/backend/migration/1639325650583-chart-v3.js deleted file mode 100644 index e2a4e920c..000000000 --- a/packages/backend/migration/1639325650583-chart-v3.js +++ /dev/null @@ -1,189 +0,0 @@ - - -export class chartV31639325650583 { - name = 'chartV31639325650583' - - async up(queryRunner) { - await queryRunner.query(`DELETE FROM "__chart__per_user_drive" WHERE "group" IS NULL`); - - await queryRunner.query(`DROP INDEX "public"."IDX_dd907becf76104e4b656659e6b"`); - await queryRunner.query(`DROP INDEX "public"."IDX_eddfed8fb40305a04c6f941050"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f09d543e3acb16c5976bdb31fa"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e60c358aaced5aab8900a4af31"`); - await queryRunner.query(`DROP INDEX "public"."IDX_337e9599f278bd7537fe30876f"`); - await queryRunner.query(`DROP INDEX "public"."IDX_66feba81e1795d176d06c0b1e6"`); - await queryRunner.query(`DROP INDEX "public"."IDX_0a905b992fecd2b5c3fb98759e"`); - await queryRunner.query(`DROP INDEX "public"."IDX_2082327b2699ce924fa654afc5"`); - await queryRunner.query(`DROP INDEX "public"."IDX_9a3ed15a30ab7e3a37702e6e08"`); - await queryRunner.query(`DROP INDEX "public"."IDX_60c5c6e7e538c09aa274ecd1cf"`); - await queryRunner.query(`DROP INDEX "public"."IDX_8111b817b9818c04d7eb8475b1"`); - await queryRunner.query(`DROP INDEX "public"."IDX_583a157ed0cf0ed1b5ec2a833f"`); - await queryRunner.query(`DROP INDEX "public"."IDX_3313d7288855ec105b5bbf6c21"`); - await queryRunner.query(`DROP INDEX "public"."IDX_ceab80a6729f8e2e6f5b8a1a3d"`); - await queryRunner.query(`DROP INDEX "public"."IDX_3b7697a96f522d0478972e6d6f"`); - await queryRunner.query(`DROP INDEX "public"."IDX_53a3604b939e2b479eb2cfaac8"`); - await queryRunner.query(`DROP INDEX "public"."IDX_dabbb38a51ab86ee3cab291326"`); - await queryRunner.query(`DROP INDEX "public"."IDX_a9a806d466b314f253a1a611c4"`); - await queryRunner.query(`CREATE TABLE "__chart_day__federation" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "___instance_total" bigint NOT NULL, "___instance_inc" bigint NOT NULL, "___instance_dec" bigint NOT NULL, CONSTRAINT "UQ_617a8fe225a6e701d89e02d2c74" UNIQUE ("date"), CONSTRAINT "PK_7ca721c769f31698e0e1331e8e6" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_617a8fe225a6e701d89e02d2c7" ON "__chart_day__federation" ("date") `); - await queryRunner.query(`CREATE TABLE "__chart_day__notes" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "___local_total" bigint NOT NULL, "___local_inc" bigint NOT NULL, "___local_dec" bigint NOT NULL, "___local_diffs_normal" bigint NOT NULL, "___local_diffs_reply" bigint NOT NULL, "___local_diffs_renote" bigint NOT NULL, "___remote_total" bigint NOT NULL, "___remote_inc" bigint NOT NULL, "___remote_dec" bigint NOT NULL, "___remote_diffs_normal" bigint NOT NULL, "___remote_diffs_reply" bigint NOT NULL, "___remote_diffs_renote" bigint NOT NULL, CONSTRAINT "UQ_1a527b423ad0858a1af5a056d43" UNIQUE ("date"), CONSTRAINT "PK_1fa4139e1f338272b758d05e090" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_1a527b423ad0858a1af5a056d4" ON "__chart_day__notes" ("date") `); - await queryRunner.query(`CREATE TABLE "__chart_day__users" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "___local_total" bigint NOT NULL, "___local_inc" bigint NOT NULL, "___local_dec" bigint NOT NULL, "___remote_total" bigint NOT NULL, "___remote_inc" bigint NOT NULL, "___remote_dec" bigint NOT NULL, CONSTRAINT "UQ_cad6e07c20037f31cdba8a350c3" UNIQUE ("date"), CONSTRAINT "PK_d7f7185abb9851f70c4726c54bd" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_cad6e07c20037f31cdba8a350c" ON "__chart_day__users" ("date") `); - await queryRunner.query(`CREATE TABLE "__chart_day__network" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "___incomingRequests" bigint NOT NULL, "___outgoingRequests" bigint NOT NULL, "___totalTime" bigint NOT NULL, "___incomingBytes" bigint NOT NULL, "___outgoingBytes" bigint NOT NULL, CONSTRAINT "UQ_8bfa548c2b31f9e07db113773ee" UNIQUE ("date"), CONSTRAINT "PK_cac499d6f471042dfed1e7e0132" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8bfa548c2b31f9e07db113773e" ON "__chart_day__network" ("date") `); - await queryRunner.query(`CREATE TABLE "__chart_day__active_users" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "___local_users" character varying array NOT NULL, "___remote_users" character varying array NOT NULL, CONSTRAINT "UQ_d5954f3df5e5e3bdfc3c03f3906" UNIQUE ("date"), CONSTRAINT "PK_b1790489b14f005ae8f404f5795" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d5954f3df5e5e3bdfc3c03f390" ON "__chart_day__active_users" ("date") `); - await queryRunner.query(`CREATE TABLE "__chart_day__instance" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128) NOT NULL, "___requests_failed" bigint NOT NULL, "___requests_succeeded" bigint NOT NULL, "___requests_received" bigint NOT NULL, "___notes_total" bigint NOT NULL, "___notes_inc" bigint NOT NULL, "___notes_dec" bigint NOT NULL, "___notes_diffs_normal" bigint NOT NULL, "___notes_diffs_reply" bigint NOT NULL, "___notes_diffs_renote" bigint NOT NULL, "___users_total" bigint NOT NULL, "___users_inc" bigint NOT NULL, "___users_dec" bigint NOT NULL, "___following_total" bigint NOT NULL, "___following_inc" bigint NOT NULL, "___following_dec" bigint NOT NULL, "___followers_total" bigint NOT NULL, "___followers_inc" bigint NOT NULL, "___followers_dec" bigint NOT NULL, "___drive_totalFiles" bigint NOT NULL, "___drive_totalUsage" bigint NOT NULL, "___drive_incFiles" bigint NOT NULL, "___drive_incUsage" bigint NOT NULL, "___drive_decFiles" bigint NOT NULL, "___drive_decUsage" bigint NOT NULL, CONSTRAINT "UQ_fea7c0278325a1a2492f2d6acbf" UNIQUE ("date", "group"), CONSTRAINT "PK_479a8ff9d959274981087043023" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_fea7c0278325a1a2492f2d6acb" ON "__chart_day__instance" ("date", "group") `); - await queryRunner.query(`CREATE TABLE "__chart_day__per_user_notes" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128) NOT NULL, "___total" bigint NOT NULL, "___inc" bigint NOT NULL, "___dec" bigint NOT NULL, "___diffs_normal" bigint NOT NULL, "___diffs_reply" bigint NOT NULL, "___diffs_renote" bigint NOT NULL, CONSTRAINT "UQ_c5545d4b31cdc684034e33b81c3" UNIQUE ("date", "group"), CONSTRAINT "PK_58bab6b6d3ad9310cbc7460fd28" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c5545d4b31cdc684034e33b81c" ON "__chart_day__per_user_notes" ("date", "group") `); - await queryRunner.query(`CREATE TABLE "__chart_day__drive" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "___local_totalCount" bigint NOT NULL, "___local_totalSize" bigint NOT NULL, "___local_incCount" bigint NOT NULL, "___local_incSize" bigint NOT NULL, "___local_decCount" bigint NOT NULL, "___local_decSize" bigint NOT NULL, "___remote_totalCount" bigint NOT NULL, "___remote_totalSize" bigint NOT NULL, "___remote_incCount" bigint NOT NULL, "___remote_incSize" bigint NOT NULL, "___remote_decCount" bigint NOT NULL, "___remote_decSize" bigint NOT NULL, CONSTRAINT "UQ_0b60ebb3aa0065f10b0616c1171" UNIQUE ("date"), CONSTRAINT "PK_e7ec0de057c77c40fc8d8b62151" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0b60ebb3aa0065f10b0616c117" ON "__chart_day__drive" ("date") `); - await queryRunner.query(`CREATE TABLE "__chart_day__per_user_reaction" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128) NOT NULL, "___local_count" bigint NOT NULL, "___remote_count" bigint NOT NULL, CONSTRAINT "UQ_d54b653660d808b118e36c184c0" UNIQUE ("date", "group"), CONSTRAINT "PK_8af24e2d51ff781a354fe595eda" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d54b653660d808b118e36c184c" ON "__chart_day__per_user_reaction" ("date", "group") `); - await queryRunner.query(`CREATE TABLE "__chart_day__hashtag" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128) NOT NULL, "___local_users" character varying array NOT NULL, "___remote_users" character varying array NOT NULL, CONSTRAINT "UQ_8f589cf056ff51f09d6096f6450" UNIQUE ("date", "group"), CONSTRAINT "PK_13d5a3b089344e5557f8e0980b4" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8f589cf056ff51f09d6096f645" ON "__chart_day__hashtag" ("date", "group") `); - await queryRunner.query(`CREATE TABLE "__chart_day__per_user_following" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128) NOT NULL, "___local_followings_total" bigint NOT NULL, "___local_followings_inc" bigint NOT NULL, "___local_followings_dec" bigint NOT NULL, "___local_followers_total" bigint NOT NULL, "___local_followers_inc" bigint NOT NULL, "___local_followers_dec" bigint NOT NULL, "___remote_followings_total" bigint NOT NULL, "___remote_followings_inc" bigint NOT NULL, "___remote_followings_dec" bigint NOT NULL, "___remote_followers_total" bigint NOT NULL, "___remote_followers_inc" bigint NOT NULL, "___remote_followers_dec" bigint NOT NULL, CONSTRAINT "UQ_e4849a3231f38281280ea4c0eee" UNIQUE ("date", "group"), CONSTRAINT "PK_68ce6b67da57166da66fc8fb27e" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e4849a3231f38281280ea4c0ee" ON "__chart_day__per_user_following" ("date", "group") `); - await queryRunner.query(`CREATE TABLE "__chart_day__per_user_drive" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "group" character varying(128) NOT NULL, "___totalCount" bigint NOT NULL, "___totalSize" bigint NOT NULL, "___incCount" bigint NOT NULL, "___incSize" bigint NOT NULL, "___decCount" bigint NOT NULL, "___decSize" bigint NOT NULL, CONSTRAINT "UQ_62aa5047b5aec92524f24c701d7" UNIQUE ("date", "group"), CONSTRAINT "PK_1ae135254c137011645da7f4045" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_62aa5047b5aec92524f24c701d" ON "__chart_day__per_user_drive" ("date", "group") `); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "group"`); - await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "group"`); - await queryRunner.query(`ALTER TABLE "__chart__users" DROP COLUMN "group"`); - await queryRunner.query(`ALTER TABLE "__chart__network" DROP COLUMN "group"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "group"`); - await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "group"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD CONSTRAINT "UQ_36cb699c49580d4e6c2e6159f97" UNIQUE ("date")`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ADD CONSTRAINT "UQ_42eb716a37d381cdf566192b2be" UNIQUE ("date")`); - await queryRunner.query(`ALTER TABLE "__chart__users" ADD CONSTRAINT "UQ_845254b3eaf708ae8a6cac30265" UNIQUE ("date")`); - await queryRunner.query(`ALTER TABLE "__chart__network" ADD CONSTRAINT "UQ_a1efd3e0048a5f2793a47360dc6" UNIQUE ("date")`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD CONSTRAINT "UQ_0ad37b7ef50f4ddc84363d7ccca" UNIQUE ("date")`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___local_users" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___remote_users" DROP DEFAULT`); - await queryRunner.query(`DROP INDEX "public"."IDX_39ee857ab2f23493037c6b6631"`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "group" SET NOT NULL`); - await queryRunner.query(`DROP INDEX "public"."IDX_5048e9daccbbbc6d567bb142d3"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "group" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ADD CONSTRAINT "UQ_13565815f618a1ff53886c5b28a" UNIQUE ("date")`); - await queryRunner.query(`DROP INDEX "public"."IDX_229a41ad465f9205f1f5703291"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "group" SET NOT NULL`); - await queryRunner.query(`DROP INDEX "public"."IDX_25a97c02003338124b2b75fdbc"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "group" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___local_users" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___remote_users" DROP DEFAULT`); - await queryRunner.query(`DROP INDEX "public"."IDX_b77d4dd9562c3a899d9a286fcd"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "group" SET NOT NULL`); - await queryRunner.query(`DROP INDEX "public"."IDX_30bf67687f483ace115c5ca642"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "group" SET NOT NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_36cb699c49580d4e6c2e6159f9" ON "__chart__federation" ("date") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_42eb716a37d381cdf566192b2b" ON "__chart__notes" ("date") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_845254b3eaf708ae8a6cac3026" ON "__chart__users" ("date") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a1efd3e0048a5f2793a47360dc" ON "__chart__network" ("date") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0ad37b7ef50f4ddc84363d7ccc" ON "__chart__active_users" ("date") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_13565815f618a1ff53886c5b28" ON "__chart__drive" ("date") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `); - await queryRunner.query(`ALTER TABLE "__chart__instance" ADD CONSTRAINT "UQ_39ee857ab2f23493037c6b66311" UNIQUE ("date", "group")`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD CONSTRAINT "UQ_5048e9daccbbbc6d567bb142d34" UNIQUE ("date", "group")`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ADD CONSTRAINT "UQ_229a41ad465f9205f1f57032910" UNIQUE ("date", "group")`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD CONSTRAINT "UQ_25a97c02003338124b2b75fdbc8" UNIQUE ("date", "group")`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ADD CONSTRAINT "UQ_b77d4dd9562c3a899d9a286fcd7" UNIQUE ("date", "group")`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ADD CONSTRAINT "UQ_30bf67687f483ace115c5ca6429" UNIQUE ("date", "group")`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" DROP CONSTRAINT "UQ_30bf67687f483ace115c5ca6429"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" DROP CONSTRAINT "UQ_b77d4dd9562c3a899d9a286fcd7"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP CONSTRAINT "UQ_25a97c02003338124b2b75fdbc8"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" DROP CONSTRAINT "UQ_229a41ad465f9205f1f57032910"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP CONSTRAINT "UQ_5048e9daccbbbc6d567bb142d34"`); - await queryRunner.query(`ALTER TABLE "__chart__instance" DROP CONSTRAINT "UQ_39ee857ab2f23493037c6b66311"`); - await queryRunner.query(`DROP INDEX "public"."IDX_30bf67687f483ace115c5ca642"`); - await queryRunner.query(`DROP INDEX "public"."IDX_b77d4dd9562c3a899d9a286fcd"`); - await queryRunner.query(`DROP INDEX "public"."IDX_25a97c02003338124b2b75fdbc"`); - await queryRunner.query(`DROP INDEX "public"."IDX_229a41ad465f9205f1f5703291"`); - await queryRunner.query(`DROP INDEX "public"."IDX_13565815f618a1ff53886c5b28"`); - await queryRunner.query(`DROP INDEX "public"."IDX_5048e9daccbbbc6d567bb142d3"`); - await queryRunner.query(`DROP INDEX "public"."IDX_39ee857ab2f23493037c6b6631"`); - await queryRunner.query(`DROP INDEX "public"."IDX_0ad37b7ef50f4ddc84363d7ccc"`); - await queryRunner.query(`DROP INDEX "public"."IDX_a1efd3e0048a5f2793a47360dc"`); - await queryRunner.query(`DROP INDEX "public"."IDX_845254b3eaf708ae8a6cac3026"`); - await queryRunner.query(`DROP INDEX "public"."IDX_42eb716a37d381cdf566192b2b"`); - await queryRunner.query(`DROP INDEX "public"."IDX_36cb699c49580d4e6c2e6159f9"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "group" DROP NOT NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_30bf67687f483ace115c5ca642" ON "__chart__per_user_drive" ("date", "group") `); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "group" DROP NOT NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b77d4dd9562c3a899d9a286fcd" ON "__chart__per_user_following" ("date", "group") `); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___remote_users" SET DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___local_users" SET DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "group" DROP NOT NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_25a97c02003338124b2b75fdbc" ON "__chart__hashtag" ("date", "group") `); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "group" DROP NOT NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_229a41ad465f9205f1f5703291" ON "__chart__per_user_reaction" ("date", "group") `); - await queryRunner.query(`ALTER TABLE "__chart__drive" DROP CONSTRAINT "UQ_13565815f618a1ff53886c5b28a"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "group" DROP NOT NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_5048e9daccbbbc6d567bb142d3" ON "__chart__per_user_notes" ("date", "group") `); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "group" DROP NOT NULL`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_39ee857ab2f23493037c6b6631" ON "__chart__instance" ("date", "group") `); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___remote_users" SET DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___local_users" SET DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP CONSTRAINT "UQ_0ad37b7ef50f4ddc84363d7ccca"`); - await queryRunner.query(`ALTER TABLE "__chart__network" DROP CONSTRAINT "UQ_a1efd3e0048a5f2793a47360dc6"`); - await queryRunner.query(`ALTER TABLE "__chart__users" DROP CONSTRAINT "UQ_845254b3eaf708ae8a6cac30265"`); - await queryRunner.query(`ALTER TABLE "__chart__notes" DROP CONSTRAINT "UQ_42eb716a37d381cdf566192b2be"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP CONSTRAINT "UQ_36cb699c49580d4e6c2e6159f97"`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "group" character varying(128)`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "group" character varying(128)`); - await queryRunner.query(`ALTER TABLE "__chart__network" ADD "group" character varying(128)`); - await queryRunner.query(`ALTER TABLE "__chart__users" ADD "group" character varying(128)`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "group" character varying(128)`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "group" character varying(128)`); - await queryRunner.query(`DROP INDEX "public"."IDX_62aa5047b5aec92524f24c701d"`); - await queryRunner.query(`DROP TABLE "__chart_day__per_user_drive"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e4849a3231f38281280ea4c0ee"`); - await queryRunner.query(`DROP TABLE "__chart_day__per_user_following"`); - await queryRunner.query(`DROP INDEX "public"."IDX_8f589cf056ff51f09d6096f645"`); - await queryRunner.query(`DROP TABLE "__chart_day__hashtag"`); - await queryRunner.query(`DROP INDEX "public"."IDX_d54b653660d808b118e36c184c"`); - await queryRunner.query(`DROP TABLE "__chart_day__per_user_reaction"`); - await queryRunner.query(`DROP INDEX "public"."IDX_0b60ebb3aa0065f10b0616c117"`); - await queryRunner.query(`DROP TABLE "__chart_day__drive"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c5545d4b31cdc684034e33b81c"`); - await queryRunner.query(`DROP TABLE "__chart_day__per_user_notes"`); - await queryRunner.query(`DROP INDEX "public"."IDX_fea7c0278325a1a2492f2d6acb"`); - await queryRunner.query(`DROP TABLE "__chart_day__instance"`); - await queryRunner.query(`DROP INDEX "public"."IDX_d5954f3df5e5e3bdfc3c03f390"`); - await queryRunner.query(`DROP TABLE "__chart_day__active_users"`); - await queryRunner.query(`DROP INDEX "public"."IDX_8bfa548c2b31f9e07db113773e"`); - await queryRunner.query(`DROP TABLE "__chart_day__network"`); - await queryRunner.query(`DROP INDEX "public"."IDX_cad6e07c20037f31cdba8a350c"`); - await queryRunner.query(`DROP TABLE "__chart_day__users"`); - await queryRunner.query(`DROP INDEX "public"."IDX_1a527b423ad0858a1af5a056d4"`); - await queryRunner.query(`DROP TABLE "__chart_day__notes"`); - await queryRunner.query(`DROP INDEX "public"."IDX_617a8fe225a6e701d89e02d2c7"`); - await queryRunner.query(`DROP TABLE "__chart_day__federation"`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a9a806d466b314f253a1a611c4" ON "__chart__per_user_drive" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dabbb38a51ab86ee3cab291326" ON "__chart__per_user_following" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_53a3604b939e2b479eb2cfaac8" ON "__chart__hashtag" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3b7697a96f522d0478972e6d6f" ON "__chart__per_user_reaction" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ceab80a6729f8e2e6f5b8a1a3d" ON "__chart__drive" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_3313d7288855ec105b5bbf6c21" ON "__chart__drive" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_583a157ed0cf0ed1b5ec2a833f" ON "__chart__per_user_notes" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_8111b817b9818c04d7eb8475b1" ON "__chart__instance" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_60c5c6e7e538c09aa274ecd1cf" ON "__chart__active_users" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_9a3ed15a30ab7e3a37702e6e08" ON "__chart__active_users" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2082327b2699ce924fa654afc5" ON "__chart__network" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0a905b992fecd2b5c3fb98759e" ON "__chart__network" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_66feba81e1795d176d06c0b1e6" ON "__chart__users" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_337e9599f278bd7537fe30876f" ON "__chart__users" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e60c358aaced5aab8900a4af31" ON "__chart__notes" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_f09d543e3acb16c5976bdb31fa" ON "__chart__notes" ("date", "group") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_eddfed8fb40305a04c6f941050" ON "__chart__federation" ("date") WHERE ("group" IS NULL)`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_dd907becf76104e4b656659e6b" ON "__chart__federation" ("date", "group") `); - } -} diff --git a/packages/backend/migration/1642611822809-emoji-url.js b/packages/backend/migration/1642611822809-emoji-url.js deleted file mode 100644 index d38f8cc08..000000000 --- a/packages/backend/migration/1642611822809-emoji-url.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class emojiUrl1642611822809 { - name = 'emojiUrl1642611822809' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "emoji" RENAME COLUMN "url" TO "originalUrl"`); - await queryRunner.query(`ALTER TABLE "emoji" ADD "publicUrl" character varying(512) NOT NULL DEFAULT ''`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "publicUrl"`); - await queryRunner.query(`ALTER TABLE "emoji" RENAME COLUMN "originalUrl" TO "url"`); - } -} diff --git a/packages/backend/migration/1642613870898-drive-file-webpublic-type.js b/packages/backend/migration/1642613870898-drive-file-webpublic-type.js deleted file mode 100644 index 15434f7d0..000000000 --- a/packages/backend/migration/1642613870898-drive-file-webpublic-type.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class driveFileWebpublicType1642613870898 { - name = 'driveFileWebpublicType1642613870898' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" ADD "webpublicType" character varying(128)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "webpublicType"`); - } -} diff --git a/packages/backend/migration/1642698156335-reported-urls.js b/packages/backend/migration/1642698156335-reported-urls.js deleted file mode 100644 index 416b69dfb..000000000 --- a/packages/backend/migration/1642698156335-reported-urls.js +++ /dev/null @@ -1,11 +0,0 @@ -export class reportedUrls1642698156335 { - name = 'reportedUrls1642698156335'; - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "urls" character varying (512) array NOT NULL DEFAULT '{}'::varchar[]`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "urls"`); - } -}; diff --git a/packages/backend/migration/1643963705770-chart-v4.js b/packages/backend/migration/1643963705770-chart-v4.js deleted file mode 100644 index 8b320c2b4..000000000 --- a/packages/backend/migration/1643963705770-chart-v4.js +++ /dev/null @@ -1,63 +0,0 @@ - - -export class chartV41643963705770 { - name = 'chartV41643963705770' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "___drive_totalUsage"`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" DROP COLUMN "___drive_totalUsage"`); - await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "___local_totalCount"`); - await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "___local_totalSize"`); - await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "___remote_totalCount"`); - await queryRunner.query(`ALTER TABLE "__chart__drive" DROP COLUMN "___remote_totalSize"`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" DROP COLUMN "___local_totalCount"`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" DROP COLUMN "___local_totalSize"`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" DROP COLUMN "___remote_totalCount"`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" DROP COLUMN "___remote_totalSize"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_users" bigint NOT NULL DEFAULT 0`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_users" bigint NOT NULL DEFAULT 0`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___local_users" bigint NOT NULL DEFAULT 0`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___remote_users" bigint NOT NULL DEFAULT 0`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_users" bigint NOT NULL DEFAULT 0`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_users" bigint NOT NULL DEFAULT 0`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ADD "___local_users" bigint NOT NULL DEFAULT 0`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ADD "___remote_users" bigint NOT NULL DEFAULT 0`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ADD "___remote_users" character varying array NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ADD "___local_users" character varying array NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___remote_users" character varying array NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "___local_users" character varying array NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___remote_users" character varying array NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___local_users" character varying array NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_users" character varying array NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_users" character varying array NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ADD "___remote_totalSize" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ADD "___remote_totalCount" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ADD "___local_totalSize" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ADD "___local_totalCount" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "___remote_totalSize" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "___remote_totalCount" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "___local_totalSize" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ADD "___local_totalCount" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ADD "___drive_totalUsage" bigint NOT NULL`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "___drive_totalUsage" bigint NOT NULL`); - } -} diff --git a/packages/backend/migration/1643966656277-chart-v5.js b/packages/backend/migration/1643966656277-chart-v5.js deleted file mode 100644 index df84002f7..000000000 --- a/packages/backend/migration/1643966656277-chart-v5.js +++ /dev/null @@ -1,27 +0,0 @@ - - -export class chartV51643966656277 { - name = 'chartV51643966656277' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___local_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___remote_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___local_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___remote_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "unique_temp___local_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ADD "unique_temp___remote_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ADD "unique_temp___local_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ADD "unique_temp___remote_users" character varying array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" DROP COLUMN "unique_temp___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" DROP COLUMN "unique_temp___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "unique_temp___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" DROP COLUMN "unique_temp___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___local_users"`); - } -} diff --git a/packages/backend/migration/1643967331284-chart-v6.js b/packages/backend/migration/1643967331284-chart-v6.js deleted file mode 100644 index 119198f4a..000000000 --- a/packages/backend/migration/1643967331284-chart-v6.js +++ /dev/null @@ -1,343 +0,0 @@ - - -export class chartV61643967331284 { - name = 'chartV61643967331284' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_normal" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_reply" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_renote" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_normal" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_reply" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_renote" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_normal" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_reply" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_renote" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_normal" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_reply" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_renote" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___incomingRequests" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___outgoingRequests" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___totalTime" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___incomingBytes" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___outgoingBytes" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___incomingRequests" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___outgoingRequests" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___totalTime" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___incomingBytes" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___outgoingBytes" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_failed" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_succeeded" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_received" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_normal" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_reply" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_renote" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_totalFiles" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_incFiles" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_decFiles" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_incUsage" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_decUsage" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_failed" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_succeeded" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_received" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_normal" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_reply" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_renote" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_totalFiles" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_incFiles" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_decFiles" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_incUsage" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_decUsage" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_normal" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_reply" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_renote" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_normal" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_reply" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_renote" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_incCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_incSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_decCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_decSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_incCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_incSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_decCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_decSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_incCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_incSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_decCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_decSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_incCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_incSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_decCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_decSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "___local_count" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "___remote_count" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_reaction" ALTER COLUMN "___local_count" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_reaction" ALTER COLUMN "___remote_count" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_total" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_inc" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_dec" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___totalCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___totalSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___incCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___incSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___decCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___decSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___totalCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___totalSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___incCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___incSize" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___decCount" SET DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___decSize" SET DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___decSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___decCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___incSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___incCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___totalSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___totalCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___decSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___decCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___incSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___incCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___totalSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___totalCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_reaction" ALTER COLUMN "___remote_count" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_reaction" ALTER COLUMN "___local_count" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "___remote_count" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "___local_count" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_decSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_decCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_incSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_incCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_decSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_decCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_incSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_incCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_decSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_decCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_incSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_incCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_decSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_decCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_incSize" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_incCount" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_renote" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_reply" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_normal" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_renote" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_reply" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_normal" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_decUsage" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_incUsage" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_decFiles" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_incFiles" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_totalFiles" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_renote" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_reply" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_normal" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_received" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_succeeded" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_failed" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_decUsage" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_incUsage" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_decFiles" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_incFiles" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_totalFiles" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_renote" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_reply" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_normal" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_received" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_succeeded" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_failed" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___outgoingBytes" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___incomingBytes" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___totalTime" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___outgoingRequests" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___incomingRequests" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___outgoingBytes" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___incomingBytes" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___totalTime" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___outgoingRequests" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___incomingRequests" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_renote" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_reply" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_normal" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_renote" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_reply" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_normal" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_renote" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_reply" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_normal" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_renote" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_reply" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_normal" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_total" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_dec" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_inc" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_total" DROP DEFAULT`); - } -} diff --git a/packages/backend/migration/1644010796173-convert-hard-mutes.js b/packages/backend/migration/1644010796173-convert-hard-mutes.js deleted file mode 100644 index 207a759b8..000000000 --- a/packages/backend/migration/1644010796173-convert-hard-mutes.js +++ /dev/null @@ -1,65 +0,0 @@ -import RE2 from 're2'; - - -export class convertHardMutes1644010796173 { - name = 'convertHardMutes1644010796173' - - async up(queryRunner) { - let entries = await queryRunner.query(`SELECT "userId", "mutedWords" FROM "user_profile" WHERE "userHost" IS NULL`); - for(let i = 0; i < entries.length; i++) { - let words = entries[i].mutedWords - .map(line => { - if (typeof line === 'string') return []; - const regexp = line.join(" ").match(/^\/(.+)\/(.*)$/); - if (regexp) { - // convert regexp's - try { - new RE2(regexp[1], regexp[2]); - return `/${regexp[1]}/${regexp[2]}`; - } catch (err) { - // invalid regex, ignore it - return []; - } - } else { - // remove empty segments - return line.filter(x => x !== ''); - } - }) - // remove empty lines - .filter(x => !(Array.isArray(x) && x.length === 0)); - - await queryRunner.connection.createQueryBuilder() - .update('user_profile') - .set({ - mutedWords: words - }) - .where('userId = :id', { id: entries[i].userId }) - .execute(); - } - } - - async down(queryRunner) { - let entries = await queryRunner.query(`SELECT "userId", "mutedWords" FROM "user_profile"`); - for(let i = 0; i < entries.length; i++) { - let words = entries[i].mutedWords - .map(line => { - if (Array.isArray(line)) { - return line; - } else { - // do not split regex at spaces again - return [line]; - } - }) - // remove empty lines - .filter(x => !(Array.isArray(x) && x.length === 0)); - - await queryRunner.connection.createQueryBuilder() - .update('user_profile') - .set({ - mutedWords: words - }) - .where('userId = :id', { id: entries[i].userId }) - .execute(); - } - } -} diff --git a/packages/backend/migration/1644058404077-chart-v7.js b/packages/backend/migration/1644058404077-chart-v7.js deleted file mode 100644 index f05ad003d..000000000 --- a/packages/backend/migration/1644058404077-chart-v7.js +++ /dev/null @@ -1,501 +0,0 @@ - - -export class chartV71644058404077 { - name = 'chartV71644058404077' - - async up(queryRunner) { - await queryRunner.query(`UPDATE "__chart__federation" SET "___instance_total"=2147483647 WHERE "___instance_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__federation" SET "___instance_inc"=32767 WHERE "___instance_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__federation" SET "___instance_dec"=32767 WHERE "___instance_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__federation" SET "___instance_total"=2147483647 WHERE "___instance_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__federation" SET "___instance_inc"=32767 WHERE "___instance_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__federation" SET "___instance_dec"=32767 WHERE "___instance_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___local_total"=2147483647 WHERE "___local_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___local_inc"=2147483647 WHERE "___local_inc" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___local_dec"=2147483647 WHERE "___local_dec" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___local_diffs_normal"=2147483647 WHERE "___local_diffs_normal" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___local_diffs_reply"=2147483647 WHERE "___local_diffs_reply" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___local_diffs_renote"=2147483647 WHERE "___local_diffs_renote" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___remote_total"=2147483647 WHERE "___remote_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___remote_inc"=2147483647 WHERE "___remote_inc" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___remote_dec"=2147483647 WHERE "___remote_dec" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___remote_diffs_normal"=2147483647 WHERE "___remote_diffs_normal" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___remote_diffs_reply"=2147483647 WHERE "___remote_diffs_reply" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__notes" SET "___remote_diffs_renote"=2147483647 WHERE "___remote_diffs_renote" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___local_total"=2147483647 WHERE "___local_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___local_inc"=2147483647 WHERE "___local_inc" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___local_dec"=2147483647 WHERE "___local_dec" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___local_diffs_normal"=2147483647 WHERE "___local_diffs_normal" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___local_diffs_reply"=2147483647 WHERE "___local_diffs_reply" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___local_diffs_renote"=2147483647 WHERE "___local_diffs_renote" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___remote_total"=2147483647 WHERE "___remote_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___remote_inc"=2147483647 WHERE "___remote_inc" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___remote_dec"=2147483647 WHERE "___remote_dec" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___remote_diffs_normal"=2147483647 WHERE "___remote_diffs_normal" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___remote_diffs_reply"=2147483647 WHERE "___remote_diffs_reply" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__notes" SET "___remote_diffs_renote"=2147483647 WHERE "___remote_diffs_renote" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__users" SET "___local_total"=2147483647 WHERE "___local_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__users" SET "___local_inc"=32767 WHERE "___local_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__users" SET "___local_dec"=32767 WHERE "___local_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__users" SET "___remote_total"=2147483647 WHERE "___remote_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__users" SET "___remote_inc"=32767 WHERE "___remote_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__users" SET "___remote_dec"=32767 WHERE "___remote_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__users" SET "___local_total"=2147483647 WHERE "___local_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__users" SET "___local_inc"=32767 WHERE "___local_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__users" SET "___local_dec"=32767 WHERE "___local_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__users" SET "___remote_total"=2147483647 WHERE "___remote_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__users" SET "___remote_inc"=32767 WHERE "___remote_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__users" SET "___remote_dec"=32767 WHERE "___remote_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__network" SET "___incomingRequests"=2147483647 WHERE "___incomingRequests" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__network" SET "___outgoingRequests"=2147483647 WHERE "___outgoingRequests" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__network" SET "___totalTime"=2147483647 WHERE "___totalTime" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__network" SET "___incomingBytes"=2147483647 WHERE "___incomingBytes" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__network" SET "___outgoingBytes"=2147483647 WHERE "___outgoingBytes" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__network" SET "___incomingRequests"=2147483647 WHERE "___incomingRequests" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__network" SET "___outgoingRequests"=2147483647 WHERE "___outgoingRequests" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__network" SET "___totalTime"=2147483647 WHERE "___totalTime" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__network" SET "___incomingBytes"=2147483647 WHERE "___incomingBytes" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__network" SET "___outgoingBytes"=2147483647 WHERE "___outgoingBytes" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___requests_failed"=32767 WHERE "___requests_failed" > 32767`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___requests_succeeded"=32767 WHERE "___requests_succeeded" > 32767`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___requests_received"=32767 WHERE "___requests_received" > 32767`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___notes_total"=2147483647 WHERE "___notes_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___notes_inc"=2147483647 WHERE "___notes_inc" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___notes_dec"=2147483647 WHERE "___notes_dec" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___notes_diffs_normal"=2147483647 WHERE "___notes_diffs_normal" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___notes_diffs_reply"=2147483647 WHERE "___notes_diffs_reply" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___notes_diffs_renote"=2147483647 WHERE "___notes_diffs_renote" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___users_total"=2147483647 WHERE "___users_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___users_inc"=32767 WHERE "___users_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___users_dec"=32767 WHERE "___users_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___following_total"=2147483647 WHERE "___following_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___following_inc"=32767 WHERE "___following_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___following_dec"=32767 WHERE "___following_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___followers_total"=2147483647 WHERE "___followers_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___followers_inc"=32767 WHERE "___followers_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___followers_dec"=32767 WHERE "___followers_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___drive_totalFiles"=2147483647 WHERE "___drive_totalFiles" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___drive_incFiles"=2147483647 WHERE "___drive_incFiles" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___drive_decFiles"=2147483647 WHERE "___drive_decFiles" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___drive_incUsage"=2147483647 WHERE "___drive_incUsage" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__instance" SET "___drive_decUsage"=2147483647 WHERE "___drive_decUsage" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___requests_failed"=32767 WHERE "___requests_failed" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___requests_succeeded"=32767 WHERE "___requests_succeeded" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___requests_received"=32767 WHERE "___requests_received" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___notes_total"=2147483647 WHERE "___notes_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___notes_inc"=2147483647 WHERE "___notes_inc" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___notes_dec"=2147483647 WHERE "___notes_dec" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___notes_diffs_normal"=2147483647 WHERE "___notes_diffs_normal" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___notes_diffs_reply"=2147483647 WHERE "___notes_diffs_reply" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___notes_diffs_renote"=2147483647 WHERE "___notes_diffs_renote" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___users_total"=2147483647 WHERE "___users_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___users_inc"=32767 WHERE "___users_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___users_dec"=32767 WHERE "___users_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___following_total"=2147483647 WHERE "___following_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___following_inc"=32767 WHERE "___following_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___following_dec"=32767 WHERE "___following_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___followers_total"=2147483647 WHERE "___followers_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___followers_inc"=32767 WHERE "___followers_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___followers_dec"=32767 WHERE "___followers_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___drive_totalFiles"=2147483647 WHERE "___drive_totalFiles" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___drive_incFiles"=2147483647 WHERE "___drive_incFiles" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___drive_decFiles"=2147483647 WHERE "___drive_decFiles" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___drive_incUsage"=2147483647 WHERE "___drive_incUsage" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__instance" SET "___drive_decUsage"=2147483647 WHERE "___drive_decUsage" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__per_user_notes" SET "___total"=2147483647 WHERE "___total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__per_user_notes" SET "___inc"=32767 WHERE "___inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_notes" SET "___dec"=32767 WHERE "___dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_notes" SET "___diffs_normal"=32767 WHERE "___diffs_normal" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_notes" SET "___diffs_reply"=32767 WHERE "___diffs_reply" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_notes" SET "___diffs_renote"=32767 WHERE "___diffs_renote" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_notes" SET "___total"=2147483647 WHERE "___total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__per_user_notes" SET "___inc"=32767 WHERE "___inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_notes" SET "___dec"=32767 WHERE "___dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_notes" SET "___diffs_normal"=32767 WHERE "___diffs_normal" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_notes" SET "___diffs_reply"=32767 WHERE "___diffs_reply" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_notes" SET "___diffs_renote"=32767 WHERE "___diffs_renote" > 32767`); - await queryRunner.query(`UPDATE "__chart__drive" SET "___local_incCount"=2147483647 WHERE "___local_incCount" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__drive" SET "___local_incSize"=2147483647 WHERE "___local_incSize" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__drive" SET "___local_decCount"=2147483647 WHERE "___local_decCount" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__drive" SET "___local_decSize"=2147483647 WHERE "___local_decSize" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__drive" SET "___remote_incCount"=2147483647 WHERE "___remote_incCount" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__drive" SET "___remote_incSize"=2147483647 WHERE "___remote_incSize" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__drive" SET "___remote_decCount"=2147483647 WHERE "___remote_decCount" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__drive" SET "___remote_decSize"=2147483647 WHERE "___remote_decSize" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__drive" SET "___local_incCount"=2147483647 WHERE "___local_incCount" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__drive" SET "___local_incSize"=2147483647 WHERE "___local_incSize" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__drive" SET "___local_decCount"=2147483647 WHERE "___local_decCount" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__drive" SET "___local_decSize"=2147483647 WHERE "___local_decSize" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__drive" SET "___remote_incCount"=2147483647 WHERE "___remote_incCount" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__drive" SET "___remote_incSize"=2147483647 WHERE "___remote_incSize" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__drive" SET "___remote_decCount"=2147483647 WHERE "___remote_decCount" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__drive" SET "___remote_decSize"=2147483647 WHERE "___remote_decSize" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__per_user_reaction" SET "___local_count"=32767 WHERE "___local_count" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_reaction" SET "___remote_count"=32767 WHERE "___remote_count" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_reaction" SET "___local_count"=32767 WHERE "___local_count" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_reaction" SET "___remote_count"=32767 WHERE "___remote_count" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___local_followings_total"=2147483647 WHERE "___local_followings_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___local_followings_inc"=32767 WHERE "___local_followings_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___local_followings_dec"=32767 WHERE "___local_followings_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___local_followers_total"=2147483647 WHERE "___local_followers_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___local_followers_inc"=32767 WHERE "___local_followers_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___local_followers_dec"=32767 WHERE "___local_followers_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___remote_followings_total"=2147483647 WHERE "___remote_followings_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___remote_followings_inc"=32767 WHERE "___remote_followings_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___remote_followings_dec"=32767 WHERE "___remote_followings_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___remote_followers_total"=2147483647 WHERE "___remote_followers_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___remote_followers_inc"=32767 WHERE "___remote_followers_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart__per_user_following" SET "___remote_followers_dec"=32767 WHERE "___remote_followers_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___local_followings_total"=2147483647 WHERE "___local_followings_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___local_followings_inc"=32767 WHERE "___local_followings_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___local_followings_dec"=32767 WHERE "___local_followings_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___local_followers_total"=2147483647 WHERE "___local_followers_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___local_followers_inc"=32767 WHERE "___local_followers_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___local_followers_dec"=32767 WHERE "___local_followers_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___remote_followings_total"=2147483647 WHERE "___remote_followings_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___remote_followings_inc"=32767 WHERE "___remote_followings_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___remote_followings_dec"=32767 WHERE "___remote_followings_dec" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___remote_followers_total"=2147483647 WHERE "___remote_followers_total" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___remote_followers_inc"=32767 WHERE "___remote_followers_inc" > 32767`); - await queryRunner.query(`UPDATE "__chart_day__per_user_following" SET "___remote_followers_dec"=32767 WHERE "___remote_followers_dec" > 32767`); - await queryRunner.query(`TRUNCATE TABLE "__chart__per_user_drive"`); - await queryRunner.query(`TRUNCATE TABLE "__chart_day__per_user_drive"`); - - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_total" TYPE integer USING "___instance_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_inc" TYPE smallint USING "___instance_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_dec" TYPE smallint USING "___instance_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_total" TYPE integer USING "___instance_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_inc" TYPE smallint USING "___instance_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_dec" TYPE smallint USING "___instance_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_total" TYPE integer USING "___local_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_inc" TYPE integer USING "___local_inc"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_dec" TYPE integer USING "___local_dec"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_normal" TYPE integer USING "___local_diffs_normal"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_reply" TYPE integer USING "___local_diffs_reply"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_renote" TYPE integer USING "___local_diffs_renote"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_total" TYPE integer USING "___remote_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_inc" TYPE integer USING "___remote_inc"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_dec" TYPE integer USING "___remote_dec"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_normal" TYPE integer USING "___remote_diffs_normal"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_reply" TYPE integer USING "___remote_diffs_reply"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_renote" TYPE integer USING "___remote_diffs_renote"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_total" TYPE integer USING "___local_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_inc" TYPE integer USING "___local_inc"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_dec" TYPE integer USING "___local_dec"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_normal" TYPE integer USING "___local_diffs_normal"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_reply" TYPE integer USING "___local_diffs_reply"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_renote" TYPE integer USING "___local_diffs_renote"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_total" TYPE integer USING "___remote_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_inc" TYPE integer USING "___remote_inc"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_dec" TYPE integer USING "___remote_dec"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_normal" TYPE integer USING "___remote_diffs_normal"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_reply" TYPE integer USING "___remote_diffs_reply"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_renote" TYPE integer USING "___remote_diffs_renote"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_total" TYPE integer USING "___local_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_inc" TYPE smallint USING "___local_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_dec" TYPE smallint USING "___local_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_total" TYPE integer USING "___remote_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_inc" TYPE smallint USING "___remote_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_dec" TYPE smallint USING "___remote_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_total" TYPE integer USING "___local_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_inc" TYPE smallint USING "___local_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_dec" TYPE smallint USING "___local_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_total" TYPE integer USING "___remote_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_inc" TYPE smallint USING "___remote_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_dec" TYPE smallint USING "___remote_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___incomingRequests" TYPE integer USING "___incomingRequests"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___outgoingRequests" TYPE integer USING "___outgoingRequests"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___totalTime" TYPE integer USING "___totalTime"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___incomingBytes" TYPE integer USING "___incomingBytes"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___outgoingBytes" TYPE integer USING "___outgoingBytes"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___incomingRequests" TYPE integer USING "___incomingRequests"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___outgoingRequests" TYPE integer USING "___outgoingRequests"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___totalTime" TYPE integer USING "___totalTime"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___incomingBytes" TYPE integer USING "___incomingBytes"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___outgoingBytes" TYPE integer USING "___outgoingBytes"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_failed" TYPE smallint USING "___requests_failed"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_succeeded" TYPE smallint USING "___requests_succeeded"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_received" TYPE smallint USING "___requests_received"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_total" TYPE integer USING "___notes_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_inc" TYPE integer USING "___notes_inc"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_dec" TYPE integer USING "___notes_dec"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_normal" TYPE integer USING "___notes_diffs_normal"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_reply" TYPE integer USING "___notes_diffs_reply"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_renote" TYPE integer USING "___notes_diffs_renote"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_total" TYPE integer USING "___users_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_inc" TYPE smallint USING "___users_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_dec" TYPE smallint USING "___users_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_total" TYPE integer USING "___following_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_inc" TYPE smallint USING "___following_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_dec" TYPE smallint USING "___following_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_total" TYPE integer USING "___followers_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_inc" TYPE smallint USING "___followers_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_dec" TYPE smallint USING "___followers_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_totalFiles" TYPE integer USING "___drive_totalFiles"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_incFiles" TYPE integer USING "___drive_incFiles"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_decFiles" TYPE integer USING "___drive_decFiles"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_incUsage" TYPE integer USING "___drive_incUsage"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_decUsage" TYPE integer USING "___drive_decUsage"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_failed" TYPE smallint USING "___requests_failed"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_succeeded" TYPE smallint USING "___requests_succeeded"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_received" TYPE smallint USING "___requests_received"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_total" TYPE integer USING "___notes_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_inc" TYPE integer USING "___notes_inc"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_dec" TYPE integer USING "___notes_dec"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_normal" TYPE integer USING "___notes_diffs_normal"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_reply" TYPE integer USING "___notes_diffs_reply"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_renote" TYPE integer USING "___notes_diffs_renote"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_total" TYPE integer USING "___users_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_inc" TYPE smallint USING "___users_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_dec" TYPE smallint USING "___users_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_total" TYPE integer USING "___following_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_inc" TYPE smallint USING "___following_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_dec" TYPE smallint USING "___following_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_total" TYPE integer USING "___followers_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_inc" TYPE smallint USING "___followers_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_dec" TYPE smallint USING "___followers_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_totalFiles" TYPE integer USING "___drive_totalFiles"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_incFiles" TYPE integer USING "___drive_incFiles"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_decFiles" TYPE integer USING "___drive_decFiles"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_incUsage" TYPE integer USING "___drive_incUsage"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_decUsage" TYPE integer USING "___drive_decUsage"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___total" TYPE integer USING "___total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___inc" TYPE smallint USING "___inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___dec" TYPE smallint USING "___dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_normal" TYPE smallint USING "___diffs_normal"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_reply" TYPE smallint USING "___diffs_reply"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_renote" TYPE smallint USING "___diffs_renote"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___total" TYPE integer USING "___total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___inc" TYPE smallint USING "___inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___dec" TYPE smallint USING "___dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_normal" TYPE smallint USING "___diffs_normal"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_reply" TYPE smallint USING "___diffs_reply"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_renote" TYPE smallint USING "___diffs_renote"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_incCount" TYPE integer USING "___local_incCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_incSize" TYPE integer USING "___local_incSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_decCount" TYPE integer USING "___local_decCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_decSize" TYPE integer USING "___local_decSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_incCount" TYPE integer USING "___remote_incCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_incSize" TYPE integer USING "___remote_incSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_decCount" TYPE integer USING "___remote_decCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_decSize" TYPE integer USING "___remote_decSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_incCount" TYPE integer USING "___local_incCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_incSize" TYPE integer USING "___local_incSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_decCount" TYPE integer USING "___local_decCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_decSize" TYPE integer USING "___local_decSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_incCount" TYPE integer USING "___remote_incCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_incSize" TYPE integer USING "___remote_incSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_decCount" TYPE integer USING "___remote_decCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_decSize" TYPE integer USING "___remote_decSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "___local_count" TYPE smallint USING "___local_count"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "___remote_count" TYPE smallint USING "___remote_count"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_reaction" ALTER COLUMN "___local_count" TYPE smallint USING "___local_count"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_reaction" ALTER COLUMN "___remote_count" TYPE smallint USING "___remote_count"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_total" TYPE integer USING "___local_followings_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_inc" TYPE smallint USING "___local_followings_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_dec" TYPE smallint USING "___local_followings_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_total" TYPE integer USING "___local_followers_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_inc" TYPE smallint USING "___local_followers_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_dec" TYPE smallint USING "___local_followers_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_total" TYPE integer USING "___remote_followings_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_inc" TYPE smallint USING "___remote_followings_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_dec" TYPE smallint USING "___remote_followings_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_total" TYPE integer USING "___remote_followers_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_inc" TYPE smallint USING "___remote_followers_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_dec" TYPE smallint USING "___remote_followers_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_total" TYPE integer USING "___local_followings_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_inc" TYPE smallint USING "___local_followings_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_dec" TYPE smallint USING "___local_followings_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_total" TYPE integer USING "___local_followers_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_inc" TYPE smallint USING "___local_followers_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_dec" TYPE smallint USING "___local_followers_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_total" TYPE integer USING "___remote_followings_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_inc" TYPE smallint USING "___remote_followings_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_dec" TYPE smallint USING "___remote_followings_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_total" TYPE integer USING "___remote_followers_total"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_inc" TYPE smallint USING "___remote_followers_inc"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_dec" TYPE smallint USING "___remote_followers_dec"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___totalCount" TYPE integer USING "___totalCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___totalSize" TYPE integer USING "___totalSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___incCount" TYPE smallint USING "___incCount"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___incSize" TYPE integer USING "___incSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___decCount" TYPE smallint USING "___decCount"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___decSize" TYPE integer USING "___decSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___totalCount" TYPE integer USING "___totalCount"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___totalSize" TYPE integer USING "___totalSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___incCount" TYPE smallint USING "___incCount"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___incSize" TYPE integer USING "___incSize"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___decCount" TYPE smallint USING "___decCount"::smallint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___decSize" TYPE integer USING "___decSize"::integer`); - } - - async down(queryRunner) { - - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_total" TYPE bigint USING "___instance_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_inc" TYPE bigint USING "___instance_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ALTER COLUMN "___instance_dec" TYPE bigint USING "___instance_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_total" TYPE bigint USING "___instance_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_inc" TYPE bigint USING "___instance_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ALTER COLUMN "___instance_dec" TYPE bigint USING "___instance_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_total" TYPE bigint USING "___local_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_inc" TYPE bigint USING "___local_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_dec" TYPE bigint USING "___local_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_normal" TYPE bigint USING "___local_diffs_normal"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_reply" TYPE bigint USING "___local_diffs_reply"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___local_diffs_renote" TYPE bigint USING "___local_diffs_renote"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_total" TYPE bigint USING "___remote_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_inc" TYPE bigint USING "___remote_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_dec" TYPE bigint USING "___remote_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_normal" TYPE bigint USING "___remote_diffs_normal"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_reply" TYPE bigint USING "___remote_diffs_reply"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ALTER COLUMN "___remote_diffs_renote" TYPE bigint USING "___remote_diffs_renote"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_total" TYPE bigint USING "___local_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_inc" TYPE bigint USING "___local_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_dec" TYPE bigint USING "___local_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_normal" TYPE bigint USING "___local_diffs_normal"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_reply" TYPE bigint USING "___local_diffs_reply"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___local_diffs_renote" TYPE bigint USING "___local_diffs_renote"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_total" TYPE bigint USING "___remote_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_inc" TYPE bigint USING "___remote_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_dec" TYPE bigint USING "___remote_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_normal" TYPE bigint USING "___remote_diffs_normal"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_reply" TYPE bigint USING "___remote_diffs_reply"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ALTER COLUMN "___remote_diffs_renote" TYPE bigint USING "___remote_diffs_renote"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_total" TYPE bigint USING "___local_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_inc" TYPE bigint USING "___local_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___local_dec" TYPE bigint USING "___local_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_total" TYPE bigint USING "___remote_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_inc" TYPE bigint USING "___remote_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__users" ALTER COLUMN "___remote_dec" TYPE bigint USING "___remote_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_total" TYPE bigint USING "___local_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_inc" TYPE bigint USING "___local_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___local_dec" TYPE bigint USING "___local_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_total" TYPE bigint USING "___remote_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_inc" TYPE bigint USING "___remote_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__users" ALTER COLUMN "___remote_dec" TYPE bigint USING "___remote_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___incomingRequests" TYPE bigint USING "___incomingRequests"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___outgoingRequests" TYPE bigint USING "___outgoingRequests"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___totalTime" TYPE bigint USING "___totalTime"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___incomingBytes" TYPE bigint USING "___incomingBytes"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__network" ALTER COLUMN "___outgoingBytes" TYPE bigint USING "___outgoingBytes"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___incomingRequests" TYPE bigint USING "___incomingRequests"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___outgoingRequests" TYPE bigint USING "___outgoingRequests"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___totalTime" TYPE bigint USING "___totalTime"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___incomingBytes" TYPE bigint USING "___incomingBytes"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__network" ALTER COLUMN "___outgoingBytes" TYPE bigint USING "___outgoingBytes"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_failed" TYPE bigint USING "___requests_failed"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_succeeded" TYPE bigint USING "___requests_succeeded"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___requests_received" TYPE bigint USING "___requests_received"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_total" TYPE bigint USING "___notes_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_inc" TYPE bigint USING "___notes_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_dec" TYPE bigint USING "___notes_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_normal" TYPE bigint USING "___notes_diffs_normal"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_reply" TYPE bigint USING "___notes_diffs_reply"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___notes_diffs_renote" TYPE bigint USING "___notes_diffs_renote"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_total" TYPE bigint USING "___users_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_inc" TYPE bigint USING "___users_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___users_dec" TYPE bigint USING "___users_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_total" TYPE bigint USING "___following_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_inc" TYPE bigint USING "___following_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___following_dec" TYPE bigint USING "___following_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_total" TYPE bigint USING "___followers_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_inc" TYPE bigint USING "___followers_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___followers_dec" TYPE bigint USING "___followers_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_totalFiles" TYPE bigint USING "___drive_totalFiles"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_incFiles" TYPE bigint USING "___drive_incFiles"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_decFiles" TYPE bigint USING "___drive_decFiles"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_incUsage" TYPE bigint USING "___drive_incUsage"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ALTER COLUMN "___drive_decUsage" TYPE bigint USING "___drive_decUsage"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_failed" TYPE bigint USING "___requests_failed"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_succeeded" TYPE bigint USING "___requests_succeeded"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___requests_received" TYPE bigint USING "___requests_received"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_total" TYPE bigint USING "___notes_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_inc" TYPE bigint USING "___notes_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_dec" TYPE bigint USING "___notes_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_normal" TYPE bigint USING "___notes_diffs_normal"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_reply" TYPE bigint USING "___notes_diffs_reply"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___notes_diffs_renote" TYPE bigint USING "___notes_diffs_renote"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_total" TYPE bigint USING "___users_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_inc" TYPE bigint USING "___users_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___users_dec" TYPE bigint USING "___users_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_total" TYPE bigint USING "___following_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_inc" TYPE bigint USING "___following_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___following_dec" TYPE bigint USING "___following_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_total" TYPE bigint USING "___followers_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_inc" TYPE bigint USING "___followers_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___followers_dec" TYPE bigint USING "___followers_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_totalFiles" TYPE bigint USING "___drive_totalFiles"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_incFiles" TYPE bigint USING "___drive_incFiles"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_decFiles" TYPE bigint USING "___drive_decFiles"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_incUsage" TYPE bigint USING "___drive_incUsage"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ALTER COLUMN "___drive_decUsage" TYPE bigint USING "___drive_decUsage"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___total" TYPE bigint USING "___total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___inc" TYPE bigint USING "___inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___dec" TYPE bigint USING "___dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_normal" TYPE bigint USING "___diffs_normal"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_reply" TYPE bigint USING "___diffs_reply"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ALTER COLUMN "___diffs_renote" TYPE bigint USING "___diffs_renote"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___total" TYPE bigint USING "___total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___inc" TYPE bigint USING "___inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___dec" TYPE bigint USING "___dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_normal" TYPE bigint USING "___diffs_normal"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_reply" TYPE bigint USING "___diffs_reply"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ALTER COLUMN "___diffs_renote" TYPE bigint USING "___diffs_renote"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_incCount" TYPE bigint USING "___local_incCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_incSize" TYPE bigint USING "___local_incSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_decCount" TYPE bigint USING "___local_decCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___local_decSize" TYPE bigint USING "___local_decSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_incCount" TYPE bigint USING "___remote_incCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_incSize" TYPE bigint USING "___remote_incSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_decCount" TYPE bigint USING "___remote_decCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__drive" ALTER COLUMN "___remote_decSize" TYPE bigint USING "___remote_decSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_incCount" TYPE bigint USING "___local_incCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_incSize" TYPE bigint USING "___local_incSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_decCount" TYPE bigint USING "___local_decCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___local_decSize" TYPE bigint USING "___local_decSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_incCount" TYPE bigint USING "___remote_incCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_incSize" TYPE bigint USING "___remote_incSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_decCount" TYPE bigint USING "___remote_decCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__drive" ALTER COLUMN "___remote_decSize" TYPE bigint USING "___remote_decSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "___local_count" TYPE bigint USING "___local_count"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_reaction" ALTER COLUMN "___remote_count" TYPE bigint USING "___remote_count"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_reaction" ALTER COLUMN "___local_count" TYPE bigint USING "___local_count"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_reaction" ALTER COLUMN "___remote_count" TYPE bigint USING "___remote_count"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_total" TYPE bigint USING "___local_followings_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_inc" TYPE bigint USING "___local_followings_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followings_dec" TYPE bigint USING "___local_followings_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_total" TYPE bigint USING "___local_followers_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_inc" TYPE bigint USING "___local_followers_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___local_followers_dec" TYPE bigint USING "___local_followers_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_total" TYPE bigint USING "___remote_followings_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_inc" TYPE bigint USING "___remote_followings_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followings_dec" TYPE bigint USING "___remote_followings_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_total" TYPE bigint USING "___remote_followers_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_inc" TYPE bigint USING "___remote_followers_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_following" ALTER COLUMN "___remote_followers_dec" TYPE bigint USING "___remote_followers_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_total" TYPE bigint USING "___local_followings_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_inc" TYPE bigint USING "___local_followings_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followings_dec" TYPE bigint USING "___local_followings_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_total" TYPE bigint USING "___local_followers_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_inc" TYPE bigint USING "___local_followers_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___local_followers_dec" TYPE bigint USING "___local_followers_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_total" TYPE bigint USING "___remote_followings_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_inc" TYPE bigint USING "___remote_followings_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followings_dec" TYPE bigint USING "___remote_followings_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_total" TYPE bigint USING "___remote_followers_total"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_inc" TYPE bigint USING "___remote_followers_inc"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_following" ALTER COLUMN "___remote_followers_dec" TYPE bigint USING "___remote_followers_dec"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___totalCount" TYPE bigint USING "___totalCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___totalSize" TYPE bigint USING "___totalSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___incCount" TYPE bigint USING "___incCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___incSize" TYPE bigint USING "___incSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___decCount" TYPE bigint USING "___decCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_drive" ALTER COLUMN "___decSize" TYPE bigint USING "___decSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___totalCount" TYPE bigint USING "___totalCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___totalSize" TYPE bigint USING "___totalSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___incCount" TYPE bigint USING "___incCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___incSize" TYPE bigint USING "___incSize"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___decCount" TYPE bigint USING "___decCount"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_drive" ALTER COLUMN "___decSize" TYPE bigint USING "___decSize"::bigint`); - } -} diff --git a/packages/backend/migration/1644059847460-chart-v8.js b/packages/backend/migration/1644059847460-chart-v8.js deleted file mode 100644 index a5339c0eb..000000000 --- a/packages/backend/migration/1644059847460-chart-v8.js +++ /dev/null @@ -1,25 +0,0 @@ - - -export class chartV81644059847460 { - name = 'chartV81644059847460' - - async up(queryRunner) { - await queryRunner.query(`UPDATE "__chart__active_users" SET "___local_users"=2147483647 WHERE "___local_users" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__active_users" SET "___remote_users"=2147483647 WHERE "___remote_users" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__active_users" SET "___local_users"=2147483647 WHERE "___local_users" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__active_users" SET "___remote_users"=2147483647 WHERE "___remote_users" > 2147483647`); - - await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___local_users" TYPE integer USING "___local_users"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___remote_users" TYPE integer USING "___remote_users"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___local_users" TYPE integer USING "___local_users"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___remote_users" TYPE integer USING "___remote_users"::integer`); - } - - async down(queryRunner) { - - await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ALTER COLUMN "___remote_users" TYPE bigint USING "___remote_users"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ALTER COLUMN "___remote_users" TYPE bigint USING "___remote_users"::bigint`); - } -} diff --git a/packages/backend/migration/1644060125705-chart-v9.js b/packages/backend/migration/1644060125705-chart-v9.js deleted file mode 100644 index da35d4231..000000000 --- a/packages/backend/migration/1644060125705-chart-v9.js +++ /dev/null @@ -1,25 +0,0 @@ - - -export class chartV91644060125705 { - name = 'chartV91644060125705' - - async up(queryRunner) { - await queryRunner.query(`UPDATE "__chart__hashtag" SET "___local_users"=2147483647 WHERE "___local_users" > 2147483647`); - await queryRunner.query(`UPDATE "__chart__hashtag" SET "___remote_users"=2147483647 WHERE "___remote_users" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__hashtag" SET "___local_users"=2147483647 WHERE "___local_users" > 2147483647`); - await queryRunner.query(`UPDATE "__chart_day__hashtag" SET "___remote_users"=2147483647 WHERE "___remote_users" > 2147483647`); - - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___local_users" TYPE integer USING "___local_users"::integer`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___remote_users" TYPE integer USING "___remote_users"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___local_users" TYPE integer USING "___local_users"::integer`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___remote_users" TYPE integer USING "___remote_users"::integer`); - } - - async down(queryRunner) { - - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart__hashtag" ALTER COLUMN "___remote_users" TYPE bigint USING "___remote_users"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___local_users" TYPE bigint USING "___local_users"::bigint`); - await queryRunner.query(`ALTER TABLE "__chart_day__hashtag" ALTER COLUMN "___remote_users" TYPE bigint USING "___remote_users"::bigint`); - } -} diff --git a/packages/backend/migration/1644073149413-chart-v10.js b/packages/backend/migration/1644073149413-chart-v10.js deleted file mode 100644 index 7260bbeca..000000000 --- a/packages/backend/migration/1644073149413-chart-v10.js +++ /dev/null @@ -1,35 +0,0 @@ - - -export class chartV101644073149413 { - name = 'chartV101644073149413' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "__chart__ap_request" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "___deliverFailed" integer NOT NULL DEFAULT '0', "___deliverSucceeded" integer NOT NULL DEFAULT '0', "___inboxReceived" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_e56f4beac5746d44bc3e19c80d0" UNIQUE ("date"), CONSTRAINT "PK_56a25cd447c7ee08876b3baf8d8" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e56f4beac5746d44bc3e19c80d" ON "__chart__ap_request" ("date") `); - await queryRunner.query(`CREATE TABLE "__chart_day__ap_request" ("id" SERIAL NOT NULL, "date" integer NOT NULL, "___deliverFailed" integer NOT NULL DEFAULT '0', "___deliverSucceeded" integer NOT NULL DEFAULT '0', "___inboxReceived" integer NOT NULL DEFAULT '0', CONSTRAINT "UQ_a848f66d6cec11980a5dd595822" UNIQUE ("date"), CONSTRAINT "PK_9318b49daee320194e23f712e69" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a848f66d6cec11980a5dd59582" ON "__chart_day__ap_request" ("date") `); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "unique_temp___deliveredInstances" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___deliveredInstances" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "unique_temp___inboxInstances" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___inboxInstances" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "unique_temp___deliveredInstances" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___deliveredInstances" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "unique_temp___inboxInstances" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___inboxInstances" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___inboxInstances"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "unique_temp___inboxInstances"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___deliveredInstances"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "unique_temp___deliveredInstances"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___inboxInstances"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "unique_temp___inboxInstances"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___deliveredInstances"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "unique_temp___deliveredInstances"`); - await queryRunner.query(`DROP INDEX "public"."IDX_a848f66d6cec11980a5dd59582"`); - await queryRunner.query(`DROP TABLE "__chart_day__ap_request"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e56f4beac5746d44bc3e19c80d"`); - await queryRunner.query(`DROP TABLE "__chart__ap_request"`); - } -} diff --git a/packages/backend/migration/1644095659741-chart-v11.js b/packages/backend/migration/1644095659741-chart-v11.js deleted file mode 100644 index 309fff1d9..000000000 --- a/packages/backend/migration/1644095659741-chart-v11.js +++ /dev/null @@ -1,91 +0,0 @@ - - -export class chartV111644095659741 { - name = 'chartV111644095659741' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___local_users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___remote_users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___users" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___notedUsers" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___notedUsers" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___registeredWithinWeek" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___registeredWithinWeek" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___registeredWithinMonth" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___registeredWithinMonth" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___registeredWithinYear" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___registeredWithinYear" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___registeredOutsideWeek" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___registeredOutsideWeek" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___registeredOutsideMonth" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___registeredOutsideMonth" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___registeredOutsideYear" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___registeredOutsideYear" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___users" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___notedUsers" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___notedUsers" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___registeredWithinWeek" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___registeredWithinWeek" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___registeredWithinMonth" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___registeredWithinMonth" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___registeredWithinYear" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___registeredWithinYear" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___registeredOutsideWeek" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___registeredOutsideWeek" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___registeredOutsideMonth" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___registeredOutsideMonth" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___registeredOutsideYear" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___registeredOutsideYear" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___registeredOutsideYear"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___registeredOutsideYear"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___registeredOutsideMonth"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___registeredOutsideMonth"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___registeredOutsideWeek"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___registeredOutsideWeek"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___registeredWithinYear"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___registeredWithinYear"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___registeredWithinMonth"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___registeredWithinMonth"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___registeredWithinWeek"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___registeredWithinWeek"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___notedUsers"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___notedUsers"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___registeredOutsideYear"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___registeredOutsideYear"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___registeredOutsideMonth"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___registeredOutsideMonth"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___registeredOutsideWeek"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___registeredOutsideWeek"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___registeredWithinYear"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___registeredWithinYear"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___registeredWithinMonth"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___registeredWithinMonth"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___registeredWithinWeek"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___registeredWithinWeek"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___notedUsers"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___notedUsers"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___remote_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___local_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___remote_users" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___local_users" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___remote_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___local_users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___remote_users" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___local_users" integer NOT NULL DEFAULT '0'`); - } -} diff --git a/packages/backend/migration/1644328606241-chart-v12.js b/packages/backend/migration/1644328606241-chart-v12.js deleted file mode 100644 index c3c7e44f9..000000000 --- a/packages/backend/migration/1644328606241-chart-v12.js +++ /dev/null @@ -1,27 +0,0 @@ - - -export class chartV121644328606241 { - name = 'chartV121644328606241' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "___local_diffs_withFile" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__notes" ADD "___remote_diffs_withFile" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ADD "___local_diffs_withFile" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" ADD "___remote_diffs_withFile" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__instance" ADD "___notes_diffs_withFile" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" ADD "___notes_diffs_withFile" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" ADD "___diffs_withFile" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" ADD "___diffs_withFile" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__per_user_notes" DROP COLUMN "___diffs_withFile"`); - await queryRunner.query(`ALTER TABLE "__chart__per_user_notes" DROP COLUMN "___diffs_withFile"`); - await queryRunner.query(`ALTER TABLE "__chart_day__instance" DROP COLUMN "___notes_diffs_withFile"`); - await queryRunner.query(`ALTER TABLE "__chart__instance" DROP COLUMN "___notes_diffs_withFile"`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" DROP COLUMN "___remote_diffs_withFile"`); - await queryRunner.query(`ALTER TABLE "__chart_day__notes" DROP COLUMN "___local_diffs_withFile"`); - await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "___remote_diffs_withFile"`); - await queryRunner.query(`ALTER TABLE "__chart__notes" DROP COLUMN "___local_diffs_withFile"`); - } -} diff --git a/packages/backend/migration/1644331238153-chart-v13.js b/packages/backend/migration/1644331238153-chart-v13.js deleted file mode 100644 index 639f7b4e2..000000000 --- a/packages/backend/migration/1644331238153-chart-v13.js +++ /dev/null @@ -1,19 +0,0 @@ - - -export class chartV131644331238153 { - name = 'chartV131644331238153' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "unique_temp___stalled" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___stalled" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "unique_temp___stalled" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___stalled" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___stalled"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "unique_temp___stalled"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___stalled"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "unique_temp___stalled"`); - } -} diff --git a/packages/backend/migration/1644344266289-chart-v14.js b/packages/backend/migration/1644344266289-chart-v14.js deleted file mode 100644 index a0d9cfc38..000000000 --- a/packages/backend/migration/1644344266289-chart-v14.js +++ /dev/null @@ -1,47 +0,0 @@ - - -export class chartV141644344266289 { - name = 'chartV141644344266289' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___users"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___notedUsers"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___notedUsers"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___users"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___notedUsers"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___notedUsers"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___readWrite" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___read" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___read" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___write" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___write" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___readWrite" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___read" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___read" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___write" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___write" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___write"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___write"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___read"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "unique_temp___read"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" DROP COLUMN "___readWrite"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___write"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___write"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___read"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "unique_temp___read"`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" DROP COLUMN "___readWrite"`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___notedUsers" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___notedUsers" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "___users" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__active_users" ADD "unique_temp___users" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___notedUsers" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___notedUsers" character varying array NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "___users" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__active_users" ADD "unique_temp___users" character varying array NOT NULL DEFAULT '{}'`); - } -} diff --git a/packages/backend/migration/1644395759931-instance-theme-color.js b/packages/backend/migration/1644395759931-instance-theme-color.js deleted file mode 100644 index 8f335ad21..000000000 --- a/packages/backend/migration/1644395759931-instance-theme-color.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class instanceThemeColor1644395759931 { - name = 'instanceThemeColor1644395759931' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "themeColor" character varying(512)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "themeColor"`); - } -} diff --git a/packages/backend/migration/1644481657998-chart-v15.js b/packages/backend/migration/1644481657998-chart-v15.js deleted file mode 100644 index b50ca87c4..000000000 --- a/packages/backend/migration/1644481657998-chart-v15.js +++ /dev/null @@ -1,31 +0,0 @@ - - -export class chartV151644481657998 { - name = 'chartV151644481657998' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___instance_total"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___instance_inc"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___instance_dec"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___instance_total"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___instance_inc"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___instance_dec"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___sub" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___pub" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___sub" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___pub" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___pub"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___sub"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___pub"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___sub"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___instance_dec" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___instance_inc" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___instance_total" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___instance_dec" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___instance_inc" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___instance_total" integer NOT NULL DEFAULT '0'`); - } -} diff --git a/packages/backend/migration/1644551208096-following-indexes.js b/packages/backend/migration/1644551208096-following-indexes.js deleted file mode 100644 index 276473ff6..000000000 --- a/packages/backend/migration/1644551208096-following-indexes.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class followingIndexes1644551208096 { - name = 'followingIndexes1644551208096' - - async up(queryRunner) { - await queryRunner.query(`CREATE INDEX "IDX_4ccd2239268ebbd1b35e318754" ON "following" ("followerHost") `); - await queryRunner.query(`CREATE INDEX "IDX_fcdafee716dfe9c3b5fde90f30" ON "following" ("followeeHost") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_fcdafee716dfe9c3b5fde90f30"`); - await queryRunner.query(`DROP INDEX "public"."IDX_4ccd2239268ebbd1b35e318754"`); - } -} diff --git a/packages/backend/migration/1645340161439-remove-max-note-text-length.js b/packages/backend/migration/1645340161439-remove-max-note-text-length.js deleted file mode 100644 index c88cb70bf..000000000 --- a/packages/backend/migration/1645340161439-remove-max-note-text-length.js +++ /dev/null @@ -1,13 +0,0 @@ - - -export class removeMaxNoteTextLength1645340161439 { - name = 'removeMaxNoteTextLength1645340161439' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "maxNoteTextLength"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "maxNoteTextLength" integer NOT NULL DEFAULT '500'`); - } -} diff --git a/packages/backend/migration/1645599900873-federation-chart-pubsub.js b/packages/backend/migration/1645599900873-federation-chart-pubsub.js deleted file mode 100644 index fd7cb6d5a..000000000 --- a/packages/backend/migration/1645599900873-federation-chart-pubsub.js +++ /dev/null @@ -1,15 +0,0 @@ - - -export class federationChartPubsub1645599900873 { - name = 'federationChartPubsub1645599900873' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___pubsub" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___pubsub" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___pubsub"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___pubsub"`); - } -} diff --git a/packages/backend/migration/1646143552768-instance-default-theme.js b/packages/backend/migration/1646143552768-instance-default-theme.js deleted file mode 100644 index 029354fd9..000000000 --- a/packages/backend/migration/1646143552768-instance-default-theme.js +++ /dev/null @@ -1,13 +0,0 @@ -export class instanceDefaultTheme1646143552768 { - name = 'instanceDefaultTheme1646143552768' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "defaultLightTheme" character varying(8192)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "defaultDarkTheme" character varying(8192)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "defaultDarkTheme"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "defaultLightTheme"`); - } -} diff --git a/packages/backend/migration/1646387162108-mute-expires-at.js b/packages/backend/migration/1646387162108-mute-expires-at.js deleted file mode 100644 index c8be8f3c5..000000000 --- a/packages/backend/migration/1646387162108-mute-expires-at.js +++ /dev/null @@ -1,13 +0,0 @@ -export class muteExpiresAt1646387162108 { - name = 'muteExpiresAt1646387162108' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "muting" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`); - await queryRunner.query(`CREATE INDEX "IDX_c1fd1c3dfb0627aa36c253fd14" ON "muting" ("expiresAt") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "public"."IDX_c1fd1c3dfb0627aa36c253fd14"`); - await queryRunner.query(`ALTER TABLE "muting" DROP COLUMN "expiresAt"`); - } -} diff --git a/packages/backend/migration/1646549089451-poll-ended-notification.js b/packages/backend/migration/1646549089451-poll-ended-notification.js deleted file mode 100644 index 38a38ce64..000000000 --- a/packages/backend/migration/1646549089451-poll-ended-notification.js +++ /dev/null @@ -1,18 +0,0 @@ - -export class pollEndedNotification1646549089451 { - name = 'pollEndedNotification1646549089451' - - async up(queryRunner) { - await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`); - await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); - await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`); - await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`); - } - - async down(queryRunner) { - await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`); - await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`); - await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`); - await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`); - } -} diff --git a/packages/backend/migration/1646633030285-chart-federation-active.js b/packages/backend/migration/1646633030285-chart-federation-active.js deleted file mode 100644 index 952289c8f..000000000 --- a/packages/backend/migration/1646633030285-chart-federation-active.js +++ /dev/null @@ -1,13 +0,0 @@ -export class chartFederationActive1646633030285 { - name = 'chartFederationActive1646633030285' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___active" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___active" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___active"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___active"`); - } -} diff --git a/packages/backend/migration/1646655454495-remove-instance-drive-columns.js b/packages/backend/migration/1646655454495-remove-instance-drive-columns.js deleted file mode 100644 index a0ee1b2c4..000000000 --- a/packages/backend/migration/1646655454495-remove-instance-drive-columns.js +++ /dev/null @@ -1,13 +0,0 @@ -export class removeInstanceDriveColumns1646655454495 { - name = 'removeInstanceDriveColumns1646655454495' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "driveUsage"`); - await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "driveFiles"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "instance" ADD "driveFiles" integer NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "instance" ADD "driveUsage" bigint NOT NULL DEFAULT '0'`); - } -} diff --git a/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js b/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js deleted file mode 100644 index c9a847cbc..000000000 --- a/packages/backend/migration/1646732390560-chart-federation-active-sub-pub.js +++ /dev/null @@ -1,21 +0,0 @@ -export class chartFederationActiveSubPub1646732390560 { - name = 'chartFederationActiveSubPub1646732390560' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___active"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___active"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___subActive" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___pubActive" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___subActive" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___pubActive" smallint NOT NULL DEFAULT '0'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___pubActive"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" DROP COLUMN "___subActive"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___pubActive"`); - await queryRunner.query(`ALTER TABLE "__chart__federation" DROP COLUMN "___subActive"`); - await queryRunner.query(`ALTER TABLE "__chart_day__federation" ADD "___active" smallint NOT NULL DEFAULT '0'`); - await queryRunner.query(`ALTER TABLE "__chart__federation" ADD "___active" smallint NOT NULL DEFAULT '0'`); - } -} diff --git a/packages/backend/migration/1648548247382-webhook.js b/packages/backend/migration/1648548247382-webhook.js deleted file mode 100644 index aea369a5c..000000000 --- a/packages/backend/migration/1648548247382-webhook.js +++ /dev/null @@ -1,19 +0,0 @@ -export class webhook1648548247382 { - name = 'webhook1648548247382' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "webhook" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "on" character varying(128) array NOT NULL DEFAULT '{}', "url" character varying(1024) NOT NULL, "secret" character varying(1024) NOT NULL, "active" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_e6765510c2d078db49632b59020" PRIMARY KEY ("id")); COMMENT ON COLUMN "webhook"."createdAt" IS 'The created date of the Antenna.'; COMMENT ON COLUMN "webhook"."userId" IS 'The owner ID.'; COMMENT ON COLUMN "webhook"."name" IS 'The name of the Antenna.'`); - await queryRunner.query(`CREATE INDEX "IDX_f272c8c8805969e6a6449c77b3" ON "webhook" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_8063a0586ed1dfbe86e982d961" ON "webhook" ("on") `); - await queryRunner.query(`CREATE INDEX "IDX_5a056076f76b2efe08216ba655" ON "webhook" ("active") `); - await queryRunner.query(`ALTER TABLE "webhook" ADD CONSTRAINT "FK_f272c8c8805969e6a6449c77b3c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "webhook" DROP CONSTRAINT "FK_f272c8c8805969e6a6449c77b3c"`); - await queryRunner.query(`DROP INDEX "public"."IDX_5a056076f76b2efe08216ba655"`); - await queryRunner.query(`DROP INDEX "public"."IDX_8063a0586ed1dfbe86e982d961"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f272c8c8805969e6a6449c77b3"`); - await queryRunner.query(`DROP TABLE "webhook"`); - } -} diff --git a/packages/backend/migration/1648816172177-webhook-2.js b/packages/backend/migration/1648816172177-webhook-2.js deleted file mode 100644 index 2feb68d61..000000000 --- a/packages/backend/migration/1648816172177-webhook-2.js +++ /dev/null @@ -1,14 +0,0 @@ - -export class webhook21648816172177 { - name = 'webhook21648816172177' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "webhook" ADD "latestSentAt" TIMESTAMP WITH TIME ZONE`); - await queryRunner.query(`ALTER TABLE "webhook" ADD "latestStatus" integer`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "latestStatus"`); - await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "latestSentAt"`); - } -} diff --git a/packages/backend/migration/1651224615271-foreign-key.js b/packages/backend/migration/1651224615271-foreign-key.js deleted file mode 100644 index 44ba7fb6c..000000000 --- a/packages/backend/migration/1651224615271-foreign-key.js +++ /dev/null @@ -1,89 +0,0 @@ -export class foreignKeyReports1651224615271 { - name = 'foreignKeyReports1651224615271' - - async up(queryRunner) { - await Promise.all([ - queryRunner.query(`ALTER INDEX "public"."IDX_seoignmeoprigmkpodgrjmkpormg" RENAME TO "IDX_c8cc87bd0f2f4487d17c651fbf"`), - queryRunner.query(`DROP INDEX "public"."IDX_note_on_channelId_and_id_desc"`), - - // remove unnecessary default null, see also down - queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "followersUri" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "session" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "name" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "description" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "iconUrl" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareName" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareVersion" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "name" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "description" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerName" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerEmail" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "iconUrl" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "faviconUrl" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "themeColor" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "clip" ALTER COLUMN "description" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "channelId" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" DROP DEFAULT`), - - queryRunner.query(`CREATE INDEX "IDX_315c779174fe8247ab324f036e" ON "drive_file" ("isLink")`), - queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId")`), - queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`), - - queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`).then(() => { - queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - }), - - queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b" UNIQUE ("noteId")`), - queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6" UNIQUE ("userId")`), - queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9" UNIQUE ("userId")`), - queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`), - queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c" UNIQUE ("noteId")`), - - queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" TO "FK_a9ca79ad939bf06066b81c9d3aa"`), - - queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" ADD VALUE 'pollEnded' AFTER 'pollVote'`), - ]); - } - - async down(queryRunner) { - await Promise.all([ - // There is no ALTER TYPE REMOVE VALUE query, so the reverse operation is a bit more complex - queryRunner.query(`UPDATE "user_profile" SET "mutingNotificationTypes" = array_remove("mutingNotificationTypes", 'pollEnded')`) - .then(() => - queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`) - ).then(() => - queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`) - ).then(() => - queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`) - ).then(() => - queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`) - ).then(() => - queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`) - ).then(() => - queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`) - ), - - queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa" TO "FK_3126dd7c502c9e4d7597ef7ef10"`), - - queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c"`), - queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`), - queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9"`), - queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6"`), - queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b"`), - - queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" SET DEFAULT '{}'`), - queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`), - - queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`), - queryRunner.query(`DROP INDEX "public"."IDX_f22169eb10657bded6d875ac8f"`), - queryRunner.query(`DROP INDEX "public"."IDX_315c779174fe8247ab324f036e"`), - - /* DEFAULT's are not set again because if the column can be NULL, then DEFAULT NULL is not necessary. - see also https://github.com/typeorm/typeorm/issues/7579#issuecomment-835423615 */ - - queryRunner.query(`CREATE INDEX "IDX_note_on_channelId_and_id_desc" ON "note" ("id", "channelId") `), - queryRunner.query(`ALTER INDEX "public"."IDX_c8cc87bd0f2f4487d17c651fbf" RENAME TO "IDX_seoignmeoprigmkpodgrjmkpormg"`), - ]); - } -} diff --git a/packages/backend/migration/1652859567549-uniform-themecolor.js b/packages/backend/migration/1652859567549-uniform-themecolor.js deleted file mode 100644 index 8da1fd7fb..000000000 --- a/packages/backend/migration/1652859567549-uniform-themecolor.js +++ /dev/null @@ -1,36 +0,0 @@ -import tinycolor from 'tinycolor2'; - -export class uniformThemecolor1652859567549 { - name = 'uniformThemecolor1652859567549' - - async up(queryRunner) { - const formatColor = (color) => { - let tc = new tinycolor(color); - if (tc.isValid()) { - return tc.toHexString(); - } else { - return null; - } - }; - - await queryRunner.query('SELECT "id", "themeColor" FROM "instance" WHERE "themeColor" IS NOT NULL') - .then(instances => Promise.all(instances.map(instance => { - // update theme color to uniform format, e.g. #00ff00 - // invalid theme colors get set to null - return queryRunner.query('UPDATE "instance" SET "themeColor" = $1 WHERE "id" = $2', [formatColor(instance.themeColor), instance.id]); - }))); - - // also fix own theme color - await queryRunner.query('SELECT "themeColor" FROM "meta" WHERE "themeColor" IS NOT NULL LIMIT 1') - .then(metas => { - if (metas.length > 0) { - return queryRunner.query('UPDATE "meta" SET "themeColor" = $1', [formatColor(metas[0].themeColor)]); - } - }); - } - - async down(queryRunner) { - // The original representation is not stored, so migrating back is not possible. - // The new format also works in older versions so this is not a problem. - } -} diff --git a/packages/backend/migration/1654124623992-remove-unused-image-urls.js b/packages/backend/migration/1654124623992-remove-unused-image-urls.js deleted file mode 100644 index 14ba51e3f..000000000 --- a/packages/backend/migration/1654124623992-remove-unused-image-urls.js +++ /dev/null @@ -1,14 +0,0 @@ -export class removeUnusedImageUrls1654124623992 { - name = 'removeUnusedImageUrls1654124623992' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mascotImageUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "errorImageUrl"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "mascotImageUrl" character varying(512) DEFAULT '/assets/ai.png'`); - await queryRunner.query(`ALTER TABLE "meta" ADD "errorImageUrl" character varying(512) DEFAULT 'https://xn--931a.moe/aiart/yubitun.png'`); - } - -} diff --git a/packages/backend/migration/1655793461890-thread-mute-notifications.js b/packages/backend/migration/1655793461890-thread-mute-notifications.js deleted file mode 100644 index 89f340d5f..000000000 --- a/packages/backend/migration/1655793461890-thread-mute-notifications.js +++ /dev/null @@ -1,13 +0,0 @@ -export class threadMuteNotifications1655793461890 { - name = 'threadMuteNotifications1655793461890' - - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "public"."note_thread_muting_mutingnotificationtypes_enum" AS ENUM('mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded')`); - await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD "mutingNotificationTypes" "public"."note_thread_muting_mutingnotificationtypes_enum" array NOT NULL DEFAULT '{}'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP COLUMN "mutingNotificationTypes"`); - await queryRunner.query(`DROP TYPE "public"."note_thread_muting_mutingnotificationtypes_enum"`); - } -} diff --git a/packages/backend/migration/1657570176749-remove-ads.js b/packages/backend/migration/1657570176749-remove-ads.js deleted file mode 100644 index 585581730..000000000 --- a/packages/backend/migration/1657570176749-remove-ads.js +++ /dev/null @@ -1,12 +0,0 @@ -export class removeAds1657570176749 { - name = 'removeAds1657570176749'; - - async up(queryRunner) { - await queryRunner.query(`DROP TABLE "ad"`); - } - - async down(queryRunner) { - await queryRunner.query(`CREATE TABLE public.ad ("id" character varying(32) NOT NULL, "createdAt" timestamp with time zone NOT NULL, "expiresAt" timestamp with time zone NOT NULL, "place" character varying(32) NOT NULL, "priority" character varying(32) NOT NULL, "url" character varying(1024) NOT NULL, "imageUrl" character varying(1024) NOT NULL, "memo" character varying(8192) NOT NULL, "ratio" integer DEFAULT 1 NOT NULL)`); - } - -} diff --git a/packages/backend/migration/1658146000392-remove-repo-url.js b/packages/backend/migration/1658146000392-remove-repo-url.js deleted file mode 100644 index 71309650b..000000000 --- a/packages/backend/migration/1658146000392-remove-repo-url.js +++ /dev/null @@ -1,13 +0,0 @@ -export class removeRepoUrl1658146000392 { - name = 'removeRepoUrl1658146000392'; - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "repositoryUrl"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "feedbackUrl"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "repositoryUrl" character varying(512) not null default 'https://github.com/misskey-dev/misskey'`); - await queryRunner.query(`ALTER TABLE "meta" ADD "feedbackUrl" character varying(512) default 'https://github.com/misskey-dev/misskey/issues/new'`); - } -} diff --git a/packages/backend/migration/1658656633972-note-replies-function.js b/packages/backend/migration/1658656633972-note-replies-function.js deleted file mode 100644 index de2e28c6f..000000000 --- a/packages/backend/migration/1658656633972-note-replies-function.js +++ /dev/null @@ -1,52 +0,0 @@ -export class noteRepliesFunction1658656633972 { - name = 'noteRepliesFunction1658656633972' - - async up(queryRunner) { - await queryRunner.query(` - CREATE OR REPLACE FUNCTION note_replies(start_id varchar, max_depth integer, max_breadth integer) RETURNS TABLE (id VARCHAR) AS - $$ - SELECT DISTINCT id FROM ( - WITH RECURSIVE tree (id, ancestors, depth) AS ( - SELECT start_id, '{}'::VARCHAR[], 0 - UNION - SELECT - note.id, - CASE - WHEN note."replyId" = tree.id THEN tree.ancestors || note."replyId" - ELSE tree.ancestors || note."renoteId" - END, - depth + 1 - FROM note, tree - WHERE ( - note."replyId" = tree.id - OR - ( - -- get renotes but not pure renotes - note."renoteId" = tree.id - AND - ( - note.text IS NOT NULL - OR - CARDINALITY(note."fileIds") != 0 - OR - note."hasPoll" = TRUE - ) - ) - ) AND depth < max_depth - ) - SELECT - id, - -- apply the limit per node - row_number() OVER (PARTITION BY ancestors[array_upper(ancestors, 1)]) AS nth_child - FROM tree - WHERE depth > 0 - ) AS recursive WHERE nth_child < max_breadth - $$ - LANGUAGE SQL - `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP FUNCTION note_replies`); - } -} diff --git a/packages/backend/migration/1659335999000-pages-to-plaintext.js b/packages/backend/migration/1659335999000-pages-to-plaintext.js deleted file mode 100644 index 2f0a2f194..000000000 --- a/packages/backend/migration/1659335999000-pages-to-plaintext.js +++ /dev/null @@ -1,82 +0,0 @@ -export class pagesToPlaintext1659335999000 { - name = 'pagesToPlaintext1659335999000' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "page" ADD "text" text`); - - async function noteUrl(noteId) { - const note = await queryRunner.query(`SELECT "uri", "userHost" FROM "note" WHERE "id" = $1`, [noteId]); - if (note.uri) return note.uri; - // don't really have access to the configuration here so just guess - else return `https://${note.userHost}/notes/${noteId}`; - } - - async function fileUrl(fileId) { - const file = await queryRunner.query(`SELECT "url" from "drive_file" WHERE "id" = $1`, [fileId]); - return file.url; - } - - async function convertBlock(block) { - switch (block.type) { - case 'note': - if (block.note) return await noteUrl(block.note); - else break; - case 'section': - return (await Promise.all(block.children.map(convertBlock))).join('\n'); - case 'text': - return block.text; - case 'textarea': - return '```\n' + block.text + '```'; - case 'image': - if (block.fileId) return '![image](' + await fileUrl(block.fileId) + ')'; - else break; - case 'if': // no idea how to convert these - case 'post': // new note form, why? - case 'canvas': // there is some aiscript api for these but dont think anyone ever used it - // interactive elements can also not be converted - case 'button': - case 'numberInput': - case 'textInput': - case 'switch': - case 'radioButton': - case 'counter': - break; - } - - return `(There was a/an ${block.type} here in a previous version but it is no longer supported.)`; - } - - await queryRunner.query(`SELECT id, "content" FROM "page"`) - .then(pages => Promise.all(pages.map(page => { - return Promise.all(page.content.map(convertBlock)) - .then(texts => { - queryRunner.query(`UPDATE "page" SET "text" = $1 WHERE "id" = $2`, [texts.join('\n'), page.id]); - }); - }))); - - await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "content"`); - await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "variables"`); - await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "script"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "page" ADD "content" jsonb default '[]'::jsonb`); - await queryRunner.query(`ALTER TABLE "page" ADD "variables" jsonb default '[]'::jsonb`); - await queryRunner.query(`ALTER TABLE "page" ADD "script" character varying(16384) default ''`); - - // The conversion from the previous page content to text is lossy, - // so we can just convert it back to a big text block. - await queryRunner.query(`SELECT "id", "text" FROM "page"`) - .then(pages => Promise.all(pages.map(page => { - const content = [{ - // just a random UUID to keep the data structure - id: '0730b23f-ab5b-4d56-8bd1-f4ead3f72af7', - type: 'text', - text: page.text, - }]; - return queryRunner.query(`UPDATE "page" SET "content" = $1 WHERE "id" = $2`, [JSON.stringify(content), page.id]); - }))); - - await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "text"`); - } -} diff --git a/packages/backend/migration/1659516638000-drive-file-user-constraint.js b/packages/backend/migration/1659516638000-drive-file-user-constraint.js deleted file mode 100644 index 42a5cfbba..000000000 --- a/packages/backend/migration/1659516638000-drive-file-user-constraint.js +++ /dev/null @@ -1,13 +0,0 @@ -export class driveFileUserConstraint1659516638000 { - name = 'driveFileUserConstraint1659516638000'; - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" DROP CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e"`); - await queryRunner.query(`ALTER TABLE "drive_file" ADD CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" DROP CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e"`); - await queryRunner.query(`ALTER TABLE "drive_file" ADD CONSTRAINT "FK_860fa6f6c7df5bb887249fba22e" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE SET NULL`); - } -} diff --git a/packages/backend/migration/1660251834642-remove-promo-entities.js b/packages/backend/migration/1660251834642-remove-promo-entities.js deleted file mode 100644 index 5dd735aec..000000000 --- a/packages/backend/migration/1660251834642-remove-promo-entities.js +++ /dev/null @@ -1,25 +0,0 @@ -export class removePromoEntities1660251834642 { - name = 'removePromoEntities1660251834642'; - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4"`, undefined); - await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05"`, undefined); - await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_2882b8a1a07c7d281a98b6db16"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_9657d55550c3d37bfafaf7d4b0"`, undefined); - await queryRunner.query(`DROP TABLE "promo_read"`, undefined); - await queryRunner.query(`DROP INDEX "IDX_83f0862e9bae44af52ced7099e"`, undefined); - await queryRunner.query(`DROP TABLE "promo_note"`, undefined); - } - - async down(queryRunner) { - await queryRunner.query(`CREATE TABLE "promo_note" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "REL_e263909ca4fe5d57f8d4230dd5" UNIQUE ("noteId"), CONSTRAINT "PK_e263909ca4fe5d57f8d4230dd5c" PRIMARY KEY ("noteId"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_83f0862e9bae44af52ced7099e" ON "promo_note" ("userId") `, undefined); - await queryRunner.query(`CREATE TABLE "promo_read" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_61917c1541002422b703318b7c9" PRIMARY KEY ("id"))`, undefined); - await queryRunner.query(`CREATE INDEX "IDX_9657d55550c3d37bfafaf7d4b0" ON "promo_read" ("userId") `, undefined); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2882b8a1a07c7d281a98b6db16" ON "promo_read" ("userId", "noteId") `, undefined); - await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); - } -} diff --git a/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js b/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js deleted file mode 100644 index 42d79b5b5..000000000 --- a/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js +++ /dev/null @@ -1,12 +0,0 @@ -export class removeMentionedRemoteUsersColumn1661376843000 { - name = 'removeMentionedRemoteUsersColumn1661376843000'; - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "mentionedRemoteUsers"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`); - await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`); - } -} diff --git a/packages/backend/migration/1662489803045-remove-rooms.js b/packages/backend/migration/1662489803045-remove-rooms.js deleted file mode 100644 index 77ea85bac..000000000 --- a/packages/backend/migration/1662489803045-remove-rooms.js +++ /dev/null @@ -1,11 +0,0 @@ -export class removeRooms1662489803045 { - name = 'removeRooms1662489803045' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "room"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}'`); - } -} diff --git a/packages/backend/migration/1662943835603-larger-follow-request-ids.js b/packages/backend/migration/1662943835603-larger-follow-request-ids.js deleted file mode 100644 index f13401d51..000000000 --- a/packages/backend/migration/1662943835603-larger-follow-request-ids.js +++ /dev/null @@ -1,12 +0,0 @@ -export class largerFollowRequestIds1662943835603 { - name = 'largerFollowRequestIds1662943835603'; - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "follow_request" ALTER COLUMN "requestId" TYPE VARCHAR(2048)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "follow_request" ALTER COLUMN "requestId" TYPE VARCHAR(128)`); - } - -} diff --git a/packages/backend/migration/1662999442223-update-pinned-pages.js b/packages/backend/migration/1662999442223-update-pinned-pages.js deleted file mode 100644 index 4ce9b5591..000000000 --- a/packages/backend/migration/1662999442223-update-pinned-pages.js +++ /dev/null @@ -1,11 +0,0 @@ -export class updatePinnedPages1662999442223 { - name = 'updatePinnedPages1662999442223' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{"/featured", "/channels", "/explore", "/pages", "/about-foundkey"}'::varchar[]`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'::varchar[]`); - } -} diff --git a/packages/backend/migration/1663399074403-resize-comments-drive-file.js b/packages/backend/migration/1663399074403-resize-comments-drive-file.js deleted file mode 100644 index a037f1655..000000000 --- a/packages/backend/migration/1663399074403-resize-comments-drive-file.js +++ /dev/null @@ -1,14 +0,0 @@ - -export class resizeCommentsDriveFile1663399074403 { - constructor() { - this.name = 'resizeCommentsDriveFile1663399074403'; - } - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE character varying(2048)`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE character varying(512)`); - } -} diff --git a/packages/backend/migration/1665091090561-add-renote-muting.js b/packages/backend/migration/1665091090561-add-renote-muting.js deleted file mode 100644 index f87114851..000000000 --- a/packages/backend/migration/1665091090561-add-renote-muting.js +++ /dev/null @@ -1,20 +0,0 @@ - -export class addRenoteMuting1665091090561 { - constructor() { - this.name = 'addRenoteMuting1665091090561'; - } - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "renote_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_renoteMuting_id" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `); - await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `); - await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `); - } - - async down(queryRunner) { - await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`); - await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`); - await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`); - await queryRunner.query(`DROP TABLE "renote_muting"`); - } -} diff --git a/packages/backend/migration/1667503570994-sync.js b/packages/backend/migration/1667503570994-sync.js deleted file mode 100644 index 97dfa2d2c..000000000 --- a/packages/backend/migration/1667503570994-sync.js +++ /dev/null @@ -1,44 +0,0 @@ -export class sync1667503570994 { - name = 'sync1667503570994' - - async up(queryRunner) { - await Promise.all([ - // the migration for renote mutes added the index to the wrong table - queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`), - queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`), - queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`), - queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `), - queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `), - queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `), - - queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`), - queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`), - queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`), - queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET NOT NULL`), - queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" SET DEFAULT ''`), - queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `), - queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`), - queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`), - ]); - } - - async down(queryRunner) { - await Promise.all([ - queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`), - queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`), - queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`), - queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP DEFAULT`), - queryRunner.query(`ALTER TABLE "page" ALTER COLUMN "text" DROP NOT NULL`), - queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`), - queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`), - queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`), - - queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`), - queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`), - queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`), - queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `), - queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `), - queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `), - ]); - } -} diff --git a/packages/backend/migration/1667653936442-token-permissions.js b/packages/backend/migration/1667653936442-token-permissions.js deleted file mode 100644 index df6925bee..000000000 --- a/packages/backend/migration/1667653936442-token-permissions.js +++ /dev/null @@ -1,33 +0,0 @@ -export class tokenPermissions1667653936442 { - name = 'tokenPermissions1667653936442' - - async up(queryRunner) { - // Carry over the permissions from the app for tokens that have an associated app. - await queryRunner.query(`UPDATE "access_token" SET permission = (SELECT permission FROM "app" WHERE "app"."id" = "access_token"."appId") WHERE "appId" IS NOT NULL AND CARDINALITY("permission") = 0`); - // The permission column should now always be set explicitly, so the default is not needed any more. - await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "permission" DROP DEFAULT`); - // Drop all currently running authorization sessions. Already created tokens remain untouched. - // If you were registering an app just before upgrade started, try again later. ¯\_(ツ)_/¯ - await queryRunner.query(`TRUNCATE TABLE "auth_session"`); - // Refactor scheme to allow multiple access tokens per app. - await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "FK_c072b729d71697f959bde66ade0"`); - await queryRunner.query(`ALTER TABLE "auth_session" RENAME COLUMN "userId" TO "accessTokenId"`); - await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "UQ_8e001e5a101c6dca37df1a76d66" UNIQUE ("accessTokenId")`); - await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "FK_8e001e5a101c6dca37df1a76d66" FOREIGN KEY ("accessTokenId") REFERENCES "access_token"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - // Drop all currently running authorization sessions. Already created tokens remain untouched. - // If you were registering an app just before downgrade started, try again later. ¯\_(ツ)_/¯ - await queryRunner.query(`TRUNCATE TABLE "auth_session"`); - - await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "FK_8e001e5a101c6dca37df1a76d66"`); - await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "UQ_8e001e5a101c6dca37df1a76d66"`); - await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "permission" DROP DEFAULT`); - await queryRunner.query(`ALTER TABLE "auth_session" RENAME COLUMN "accessTokenId" TO "userId"`); - await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "FK_c072b729d71697f959bde66ade0" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - - await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "permission" SET DEFAULT '{}'::varchar[]`); - await queryRunner.query(`UPDATE "access_token" SET permission = '{}'::varchar[] WHERE "appId" IS NOT NULL`); - } -} diff --git a/packages/backend/migration/1667738304733-pkce.js b/packages/backend/migration/1667738304733-pkce.js deleted file mode 100644 index 4036bd649..000000000 --- a/packages/backend/migration/1667738304733-pkce.js +++ /dev/null @@ -1,12 +0,0 @@ -export class pkce1667738304733 { - name = 'pkce1667738304733' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "auth_session" ADD "pkceChallenge" text`); - await queryRunner.query(`COMMENT ON COLUMN "auth_session"."pkceChallenge" IS 'PKCE code_challenge value, if provided (OAuth only)'`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "auth_session" DROP COLUMN "pkceChallenge"`); - } -} diff --git a/packages/backend/migration/1668374092227-forceEnablePush.js b/packages/backend/migration/1668374092227-forceEnablePush.js deleted file mode 100644 index c3d63f2a4..000000000 --- a/packages/backend/migration/1668374092227-forceEnablePush.js +++ /dev/null @@ -1,24 +0,0 @@ -import push from 'web-push'; - -export class forceEnablePush1668374092227 { - name = 'forceEnablePush1668374092227'; - - async up(queryRunner) { - // set VAPID keys if not yet set - const { publicKey, privateKey } = push.generateVAPIDKeys(); - await queryRunner.query(`UPDATE "meta" SET "swPublicKey" = $1, "swPrivateKey" = $2 WHERE "swPublicKey" IS NULL OR "swPrivateKey" IS NULL`, [publicKey, privateKey]); - - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableServiceWorker"`); - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPublicKey" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPrivateKey" SET NOT NULL`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPrivateKey" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "swPublicKey" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "meta" ADD "enableServiceWorker" boolean NOT NULL DEFAULT false`); - // since VAPID keys are set and the service worker may have been enabled before, make sure it is now enabled - await queryRunner.query(`UPDATE "meta" SET "enableServiceWorker" = true`); - // can't unset the VAPID keys because we do not know if we set them in the migration - } -} diff --git a/packages/backend/migration/1668661888188-add-libretranslate.js b/packages/backend/migration/1668661888188-add-libretranslate.js deleted file mode 100644 index cc5f47939..000000000 --- a/packages/backend/migration/1668661888188-add-libretranslate.js +++ /dev/null @@ -1,19 +0,0 @@ -export class addLibretranslate1668661888188 { - name = 'addLibretranslate1668661888188' - - async up(queryRunner) { - await queryRunner.query(`CREATE TYPE "public"."meta_translationservice_enum" AS ENUM('deepl', 'libretranslate')`); - await queryRunner.query(`ALTER TABLE "meta" ADD "translationService" "public"."meta_translationservice_enum"`); - await queryRunner.query(`ALTER TABLE "meta" ADD "libreTranslateAuthKey" character varying(128)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "libreTranslateEndpoint" character varying(2048)`); - // Set translationService to 'deepl' if auth key is already set - await queryRunner.query(`UPDATE "meta" SET "translationService"='deepl' WHERE "deeplAuthKey" IS NOT NULL`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "libreTranslateEndpoint"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "libreTranslateAuthKey"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "translationService"`); - await queryRunner.query(`DROP TYPE "public"."meta_translationservice_enum"`); - } -} diff --git a/packages/backend/migration/1669545702493-detectDeeplPro.js b/packages/backend/migration/1669545702493-detectDeeplPro.js deleted file mode 100644 index 2a75ff228..000000000 --- a/packages/backend/migration/1669545702493-detectDeeplPro.js +++ /dev/null @@ -1,12 +0,0 @@ -export class detectDeeplPro1669545702493 { - name = 'detectDeeplPro1669545702493'; - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplIsPro"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" ADD "deeplIsPro" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`UPDATE "meta" SET "deeplIsPro" = true WHERE "deeplAuthKey" IS NOT NULL AND "deeplAuthKey" NOT LIKE '%:fx'`); - } -} diff --git a/packages/backend/migration/1670359028055-removeIntegrations.js b/packages/backend/migration/1670359028055-removeIntegrations.js deleted file mode 100644 index fd0888a6e..000000000 --- a/packages/backend/migration/1670359028055-removeIntegrations.js +++ /dev/null @@ -1,29 +0,0 @@ -export class removeIntegrations1670359028055 { - name = 'removeIntegrations1670359028055' - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTwitterIntegration"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "twitterConsumerKey"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "twitterConsumerSecret"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableGithubIntegration"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "githubClientId"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "githubClientSecret"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableDiscordIntegration"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "discordClientId"`); - await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "discordClientSecret"`); - await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "integrations"`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_profile" ADD "integrations" jsonb NOT NULL DEFAULT '{}'`); - await queryRunner.query(`ALTER TABLE "meta" ADD "discordClientSecret" character varying(128)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "discordClientId" character varying(128)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "enableDiscordIntegration" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "meta" ADD "githubClientSecret" character varying(128)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "githubClientId" character varying(128)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "enableGithubIntegration" boolean NOT NULL DEFAULT false`); - await queryRunner.query(`ALTER TABLE "meta" ADD "twitterConsumerSecret" character varying(128)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "twitterConsumerKey" character varying(128)`); - await queryRunner.query(`ALTER TABLE "meta" ADD "enableTwitterIntegration" boolean NOT NULL DEFAULT false`); - } -} diff --git a/packages/backend/migration/1672607891750-remove-reversi.js b/packages/backend/migration/1672607891750-remove-reversi.js deleted file mode 100644 index 2906e884a..000000000 --- a/packages/backend/migration/1672607891750-remove-reversi.js +++ /dev/null @@ -1,21 +0,0 @@ -export class removeReversi1672607891750 { - name = 'removeReversi1672607891750'; - - async up(queryRunner) { - await queryRunner.query(`DROP TABLE "reversi_matching"`); - await queryRunner.query(`DROP TABLE "reversi_game"`); - } - - async down(queryRunner) { - await queryRunner.query(`CREATE TABLE "reversi_game" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "startedAt" TIMESTAMP WITH TIME ZONE, "user1Id" character varying(32) NOT NULL, "user2Id" character varying(32) NOT NULL, "user1Accepted" boolean NOT NULL DEFAULT false, "user2Accepted" boolean NOT NULL DEFAULT false, "black" integer, "isStarted" boolean NOT NULL DEFAULT false, "isEnded" boolean NOT NULL DEFAULT false, "winnerId" character varying(32), "surrendered" character varying(32), "logs" jsonb NOT NULL DEFAULT '[]', "map" character varying(64) array NOT NULL, "bw" character varying(32) NOT NULL, "isLlotheo" boolean NOT NULL DEFAULT false, "canPutEverywhere" boolean NOT NULL DEFAULT false, "loopedBoard" boolean NOT NULL DEFAULT false, "form1" jsonb DEFAULT null, "form2" jsonb DEFAULT null, "crc32" character varying(32), CONSTRAINT "PK_76b30eeba71b1193ad7c5311c3f" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt")`); - await queryRunner.query(`CREATE TABLE "reversi_matching" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "parentId" character varying(32) NOT NULL, "childId" character varying(32) NOT NULL, CONSTRAINT "PK_880bd0afbab232f21c8b9d146cf" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt")`); - await queryRunner.query(`CREATE INDEX "IDX_3b25402709dd9882048c2bbade" ON "reversi_matching" ("parentId")`); - await queryRunner.query(`CREATE INDEX "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId")`); - await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } -} diff --git a/packages/backend/migration/1672991292018-remove-user-group-invite.js b/packages/backend/migration/1672991292018-remove-user-group-invite.js deleted file mode 100644 index adc5d935e..000000000 --- a/packages/backend/migration/1672991292018-remove-user-group-invite.js +++ /dev/null @@ -1,23 +0,0 @@ -export class removeUserGroupInvite1672991292018 { - - async up(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_e10924607d058004304611a436a"`); - await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_1039988afa3bf991185b277fe03"`); - await queryRunner.query(`DROP INDEX "IDX_d9ecaed8c6dc43f3592c229282"`); - await queryRunner.query(`DROP INDEX "IDX_78787741f9010886796f2320a4"`); - await queryRunner.query(`DROP INDEX "IDX_e10924607d058004304611a436"`); - await queryRunner.query(`DROP INDEX "IDX_1039988afa3bf991185b277fe0"`); - await queryRunner.query(`DROP TABLE "user_group_invite"`); - } - - async down(queryRunner) { - await queryRunner.query(`CREATE TABLE "user_group_invite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_3893884af0d3a5f4d01e7921a97" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_1039988afa3bf991185b277fe0" ON "user_group_invite" ("userId") `); - await queryRunner.query(`CREATE INDEX "IDX_e10924607d058004304611a436" ON "user_group_invite" ("userGroupId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_78787741f9010886796f2320a4" ON "user_group_invite" ("userId", "userGroupId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `); - await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_1039988afa3bf991185b277fe03" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_e10924607d058004304611a436a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - -} diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js deleted file mode 100644 index a4e903aba..000000000 --- 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 deleted file mode 100644 index 5d4eca072..000000000 --- a/packages/backend/package.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "name": "backend", - "version": "13.0.0-preview3", - "main": "./index.js", - "private": true, - "type": "module", - "scripts": { - "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", - "watch": "node watch.mjs", - "lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts", - "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha", - "migrate": "npx typeorm migration:run -d ormconfig.js", - "start": "node --experimental-json-modules ./built/index.js", - "start:test": "cross-env NODE_ENV=test node --experimental-json-modules ./built/index.js", - "test": "npm run mocha" - }, - "dependencies": { - "@bull-board/api": "^4.3.1", - "@bull-board/koa": "^4.3.1", - "@discordapp/twemoji": "14.0.2", - "@elastic/elasticsearch": "7.11.0", - "@koa/cors": "3.1.0", - "@koa/multer": "3.0.0", - "@koa/router": "9.0.1", - "@peertube/http-signature": "1.7.0", - "@sinonjs/fake-timers": "9.1.2", - "@syuilo/aiscript": "0.11.1", - "abort-controller": "3.0.0", - "ajv": "8.11.0", - "archiver": "5.3.1", - "autobind-decorator": "2.4.0", - "aws-sdk": "2.1165.0", - "bcryptjs": "2.4.3", - "blurhash": "1.1.5", - "bull": "4.8.4", - "cacheable-lookup": "6.0.4", - "cbor": "8.1.0", - "chalk": "5.0.1", - "chalk-template": "0.4.0", - "cli-highlight": "2.1.11", - "color-convert": "2.0.1", - "content-disposition": "0.5.4", - "date-fns": "2.28.0", - "deep-email-validator": "0.1.21", - "escape-regexp": "0.0.1", - "feed": "4.2.2", - "file-type": "18.0.0", - "fluent-ffmpeg": "2.1.2", - "foundkey-js": "workspace:*", - "got": "12.3.1", - "hpagent": "0.1.2", - "ioredis": "4.28.5", - "ip-cidr": "3.0.10", - "is-svg": "4.3.2", - "js-yaml": "4.1.0", - "jsdom": "20.0.0", - "json5": "2.2.1", - "json5-loader": "4.0.1", - "jsonld": "6.0.0", - "jsrsasign": "10.5.25", - "koa": "2.13.4", - "koa-bodyparser": "4.3.0", - "koa-favicon": "2.1.0", - "koa-json-body": "5.3.0", - "koa-logger": "3.2.1", - "koa-mount": "4.0.0", - "koa-send": "5.0.1", - "koa-slow": "2.1.0", - "koa-views": "7.0.2", - "mfm-js": "0.22.1", - "mime-types": "2.1.35", - "mocha": "10.0.0", - "multer": "1.4.5-lts.1", - "nested-property": "4.0.0", - "node-fetch": "3.2.6", - "nodemailer": "6.7.6", - "os-utils": "0.0.14", - "parse5": "7.0.0", - "pg": "8.7.3", - "private-ip": "2.3.3", - "probe-image-size": "7.2.3", - "promise-limit": "2.7.0", - "pug": "3.0.2", - "punycode": "2.1.1", - "pureimage": "0.3.14", - "qrcode": "1.5.1", - "random-seed": "0.3.0", - "ratelimiter": "3.4.1", - "re2": "1.17.8", - "redis-lock": "0.1.4", - "reflect-metadata": "0.1.13", - "rename": "1.0.4", - "require-all": "3.0.0", - "rss-parser": "3.12.0", - "sanitize-html": "2.7.0", - "semver": "7.3.7", - "sharp": "0.31.2", - "speakeasy": "2.0.0", - "strict-event-emitter-types": "2.0.0", - "stringz": "2.1.0", - "style-loader": "3.3.1", - "summaly": "2.6.0", - "syslog-pro": "1.0.0", - "systeminformation": "5.11.22", - "tinycolor2": "1.4.2", - "tmp": "0.2.1", - "ts-loader": "9.3.1", - "ts-node": "10.9.1", - "tsc-alias": "1.7.0", - "tsconfig-paths": "4.1.0", - "twemoji-parser": "14.0.0", - "typeorm": "0.3.7", - "unzipper": "0.10.11", - "uuid": "8.3.2", - "web-push": "3.5.0", - "ws": "8.8.0", - "xev": "3.0.2" - }, - "devDependencies": { - "@redocly/openapi-core": "1.0.0-beta.97", - "@types/bcryptjs": "2.4.2", - "@types/bull": "3.15.8", - "@types/cbor": "6.0.0", - "@types/color-convert": "^2.0.0", - "@types/escape-regexp": "0.0.1", - "@types/fluent-ffmpeg": "2.1.20", - "@types/is-url": "1.2.30", - "@types/js-yaml": "4.0.5", - "@types/jsdom": "16.2.14", - "@types/jsonld": "1.5.6", - "@types/jsrsasign": "10.5.1", - "@types/koa": "2.13.5", - "@types/koa-bodyparser": "4.3.7", - "@types/koa-cors": "0.0.2", - "@types/koa-favicon": "2.0.21", - "@types/koa-logger": "3.1.2", - "@types/koa-mount": "4.0.1", - "@types/koa-send": "4.1.3", - "@types/koa-views": "7.0.0", - "@types/koa__cors": "3.1.1", - "@types/koa__multer": "2.0.4", - "@types/koa__router": "8.0.11", - "@types/mocha": "9.1.1", - "@types/node": "18.7.16", - "@types/node-fetch": "3.0.3", - "@types/nodemailer": "6.4.5", - "@types/pg": "^8.6.5", - "@types/pug": "2.0.6", - "@types/punycode": "2.1.0", - "@types/qrcode": "1.5.0", - "@types/random-seed": "0.3.3", - "@types/ratelimiter": "3.4.3", - "@types/redis": "4.0.11", - "@types/rename": "1.0.4", - "@types/sanitize-html": "2.6.2", - "@types/semver": "7.3.12", - "@types/sharp": "0.30.5", - "@types/sinon": "^10.0.13", - "@types/sinonjs__fake-timers": "8.1.2", - "@types/speakeasy": "2.0.7", - "@types/syslog-pro": "^1.0.0", - "@types/tinycolor2": "1.4.3", - "@types/tmp": "0.2.3", - "@types/uuid": "8.3.4", - "@types/web-push": "3.3.2", - "@types/ws": "8.5.3", - "@typescript-eslint/eslint-plugin": "^5.46.1", - "@typescript-eslint/parser": "^5.46.1", - "cross-env": "7.0.3", - "eslint": "^8.29.0", - "eslint-plugin-foundkey-custom-rules": "file:../shared/custom-rules", - "eslint-plugin-import": "^2.26.0", - "execa": "6.1.0", - "form-data": "^4.0.0", - "sinon": "^14.0.2", - "typescript": "^4.9.4" - } -} diff --git a/packages/backend/src/@types/hcaptcha.d.ts b/packages/backend/src/@types/hcaptcha.d.ts deleted file mode 100644 index afed58756..000000000 --- a/packages/backend/src/@types/hcaptcha.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare module 'hcaptcha' { - interface IVerifyResponse { - success: boolean; - challenge_ts: string; - hostname: string; - credit?: boolean; - 'error-codes'?: unknown[]; - } - - export function verify(secret: string, token: string): Promise; -} diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts deleted file mode 100644 index d1f9cd955..000000000 --- a/packages/backend/src/@types/http-signature.d.ts +++ /dev/null @@ -1,77 +0,0 @@ -declare module '@peertube/http-signature' { - import { 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 5aa8179c5..000000000 --- a/packages/backend/src/@types/koa-json-body.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare module 'koa-json-body' { - import { Middleware } from 'koa'; - - interface IKoaJsonBodyOptions { - strict: boolean; - limit: string; - fallback: boolean; - } - - function koaJsonBody(opt?: IKoaJsonBodyOptions): Middleware; - - namespace koaJsonBody {} // Hack - - export = koaJsonBody; -} diff --git a/packages/backend/src/@types/koa-slow.d.ts b/packages/backend/src/@types/koa-slow.d.ts deleted file mode 100644 index e748e2cc9..000000000 --- a/packages/backend/src/@types/koa-slow.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module 'koa-slow' { - import { Middleware } from 'koa'; - - interface ISlowOptions { - url?: RegExp; - delay?: number; - } - - function slow(options?: ISlowOptions): Middleware; - - namespace slow {} // Hack - - export = slow; -} diff --git a/packages/backend/src/@types/os-utils.d.ts b/packages/backend/src/@types/os-utils.d.ts deleted file mode 100644 index 390df17d3..000000000 --- a/packages/backend/src/@types/os-utils.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -declare module 'os-utils' { - type FreeCommandCallback = (usedmem: number) => void; - - type HarddriveCallback = (total: number, free: number, used: number) => void; - - type GetProcessesCallback = (result: string) => void; - - type CPUCallback = (perc: number) => void; - - export function platform(): NodeJS.Platform; - export function cpuCount(): number; - export function sysUptime(): number; - export function processUptime(): number; - - export function freemem(): number; - export function totalmem(): number; - export function freememPercentage(): number; - export function freeCommand(callback: FreeCommandCallback): void; - - export function harddrive(callback: HarddriveCallback): void; - - export function getProcesses(callback: GetProcessesCallback): void; - export function getProcesses(nProcess: number, callback: GetProcessesCallback): void; - - export function allLoadavg(): string; - export function loadavg(_time?: number): number; - - export function cpuFree(callback: CPUCallback): void; - export function cpuUsage(callback: CPUCallback): void; -} diff --git a/packages/backend/src/@types/package.json.d.ts b/packages/backend/src/@types/package.json.d.ts deleted file mode 100644 index abe5fae68..000000000 --- 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 11bb6c620..000000000 --- a/packages/backend/src/@types/probe-image-size.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -declare module 'probe-image-size' { - import { 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/boot/index.ts b/packages/backend/src/boot/index.ts deleted file mode 100644 index ef98f0eea..000000000 --- a/packages/backend/src/boot/index.ts +++ /dev/null @@ -1,81 +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'; - -const logger = new Logger('core', 'cyan'); -const clusterLogger = logger.createSubLogger('cluster', 'orange', false); -const ev = new Xev(); - -/** - * Init process - */ -export async function boot(): Promise { - process.title = `FoundKey (${cluster.isPrimary ? 'master' : 'worker'})`; - - if (cluster.isPrimary || envOption.disableClustering) { - await masterMain(); - - if (cluster.isPrimary) { - ev.mount(); - } - } - - if (cluster.isWorker || envOption.disableClustering) { - await workerMain(); - } - - // for when FoundKey is launched as a child process during unit testing - // otherwise, process.send cannot be used, so it is suppressed - 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 { - // if that fails too there is nothing we can do - } -}); - -// 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 4e5327641..000000000 --- a/packages/backend/src/boot/master.ts +++ /dev/null @@ -1,171 +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 { Config } from '@/config/types.js'; -import { showMachineInfo } from '@/misc/show-machine-info.js'; -import { envOption } from '@/env.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('#86b300'); - -function greet(): void { - if (!envOption.quiet) { - //#region FoundKey logo - console.log(themeColor(' ___ _ _ __ ')); - console.log(themeColor(' | __|__ _ _ _ _ __| | |/ /___ _ _ ')); - console.log(themeColor(' | _/ _ \\ || | \' \\/ _` | \' { - 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', {}, true); - process.exit(1); - } - - bootLogger.succ('FoundKey initialized'); - - if (!envOption.disableClustering) { - await spawnWorkers(config.clusterLimits); - } - - bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); - - if (!envOption.noDaemons) { - import('../daemons/server-stats.js').then(x => x.serverStats()); - import('../daemons/queue-stats.js').then(x => x.queueStats()); - } -} - -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!', {}, 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) { - const e = exception as Partial | Error; - if ('code' in e && e.code === 'ENOENT') { - configLogger.error('Configuration file not found', {}, 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', {}, true); - dbLogger.error(e as Error | string); - process.exit(1); - } -} - -async function spawnWorkers(clusterLimits: Required): Promise { - const modes = ['web' as const, 'queue' as const]; - const cpus = os.cpus().length; - for (const mode of modes.filter(mode => clusterLimits[mode] > cpus)) { - bootLogger.warn(`configuration warning: cluster limit for ${mode} exceeds number of cores (${cpus})`); - } - - const total = modes.reduce((acc, mode) => acc + clusterLimits[mode], 0); - const workers = new Array(total); - workers.fill('web', 0, clusterLimits.web); - workers.fill('queue', clusterLimits.web); - - bootLogger.info(`Starting ${total} workers...`); - await Promise.all(workers.map(mode => spawnWorker(mode))); - bootLogger.succ('All workers started'); -} - -function spawnWorker(mode: 'web' | 'queue'): Promise { - return new Promise(res => { - const worker = cluster.fork({ mode }); - 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 567e929e0..000000000 --- a/packages/backend/src/boot/worker.ts +++ /dev/null @@ -1,30 +0,0 @@ -import os from 'node:os'; -import cluster from 'node:cluster'; -import { initDb } from '@/db/postgre.js'; - -/** - * Init worker process - */ -export async function workerMain(): Promise { - await initDb(); - - if (!process.env.mode || process.env.mode === 'web') { - // start server - await import('../server/index.js').then(x => x.default()); - } - - if (!process.env.mode || process.env.mode === 'queue') { - // start job queue - import('../queue/index.js').then(x => x.default()); - - if (process.env.mode === 'queue') { - // if this is an exclusive queue worker, renice to have higher priority - os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL); - } - } - - 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 3e53b0003..000000000 --- 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 95286585d..000000000 --- a/packages/backend/src/config/load.ts +++ /dev/null @@ -1,89 +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 { Source, Mixin, Config } 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(): Config { - 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; - - if (config.id && config.id !== 'aid') throw new Error('Unsupported ID algorithm. Only "aid" is supported.'); - - const mixin = {} as Mixin; - - const url = tryCreateUrl(config.url); - - config.url = url.origin; - - config.port = config.port || parseInt(process.env.PORT || '', 10); - - config.images = Object.assign({ - info: '/twemoji/1f440.svg', - notFound: '/twemoji/2049.svg', - error: '/twemoji/1f480.svg', - }, config.images ?? {}); - - if (!config.maxNoteTextLength) config.maxNoteTextLength = 3000; - - 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 = `FoundKey/${meta.version} (${config.url})`; - mixin.clientEntry = clientManifest['src/init.ts']; - - if (!config.redis.prefix) config.redis.prefix = mixin.host; - - if (!config.clusterLimits) { - config.clusterLimits = { - web: 1, - queue: 1, - }; - } else { - config.clusterLimits = { - web: 1, - queue: 1, - ...config.clusterLimits, - }; - - if (config.clusterLimits.web < 1 || config.clusterLimits.queue < 1) { - throw new Error('invalid cluster limits'); - } - } - - return Object.assign(config, mixin); -} - -function tryCreateUrl(url: string): URL { - try { - return new URL(url); - } catch (e) { - throw new Error(`url="${url}" is not a valid URL.`); - } -} diff --git a/packages/backend/src/config/redis.ts b/packages/backend/src/config/redis.ts deleted file mode 100644 index 5b7d51c32..000000000 --- a/packages/backend/src/config/redis.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Logger from '@/services/logger.js'; -import config from './index.js'; - -const logger = new Logger('config:redis', 'gray', false); - -function getRedisFamily(family?: string | number): number { - const familyMap = { - ipv4: 4, - ipv6: 6, - dual: 0, - }; - if (typeof family === 'string' && family in familyMap) { - return familyMap[family as keyof typeof familyMap]; - } else if (typeof family === 'number' && Object.values(familyMap).includes(family)) { - return family; - } - - if (typeof family !== 'undefined') { - logger.warn(`redis family "${family}" is invalid, defaulting to "dual"`); - } - - return 0; -} - -export function getRedisOptions(keyPrefix?: string): Record { - return { - port: config.redis.port, - host: config.redis.host, - family: getRedisFamily(config.redis.family), - password: config.redis.pass, - db: config.redis.db || 0, - keyPrefix, - }; -} diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts deleted file mode 100644 index 55226ca47..000000000 --- a/packages/backend/src/config/types.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Configuration options set up by the user - */ -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 | 'dual' | 'ipv4' | 'ipv6'; - pass: string; - db?: number; - prefix?: string; - }; - elasticsearch?: { - host: string; - port: number; - ssl?: boolean; - user?: string; - pass?: string; - index?: string; - }; - - proxy?: string; - proxySmtp?: string; - proxyBypassHosts?: string[]; - - allowedPrivateNetworks?: string[]; - - maxFileSize?: number; - - maxNoteTextLength?: number; - - accesslog?: string; - - clusterLimits?: { - web?: number; - queue?: number; - }; - - id: string; - - deliverJobConcurrency?: number; - inboxJobConcurrency?: number; - deliverJobPerSec?: number; - inboxJobPerSec?: number; - deliverJobMaxAttempts?: number; - inboxJobMaxAttempts?: number; - - syslog?: { - host: string; - port: number; - }; - - mediaProxy?: string; - proxyRemoteFiles?: boolean; - internalStoragePath?: string; - - images?: { - info?: string; - notFound?: string; - error?: string; - }; -}; - -/** - * Information that FoundKey automatically sets (by inference from information set by the user) - */ -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 8dcefc319..000000000 --- a/packages/backend/src/const.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Time constants -export const SECOND = 1000; -export const MINUTE = 60 * SECOND; -export const HOUR = 60 * MINUTE; -export const DAY = 24 * HOUR; -export const WEEK = 7 * DAY; -export const MONTH = 30 * DAY; -export const YEAR = 365 * DAY; - -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 reported as application/octet-stream -// SVG is not allowed because it can lead to XSS -export const FILE_TYPE_BROWSERSAFE = [ - // Images - 'image/png', - 'image/gif', - 'image/jpeg', - 'image/webp', - 'image/apng', - 'image/bmp', - 'image/tiff', - 'image/x-icon', - - // OggS - 'audio/opus', - 'video/ogg', - 'audio/ogg', - 'application/ogg', - - // ISO/IEC base media file format - 'video/quicktime', - 'video/mp4', - 'audio/mp4', - 'video/x-m4v', - 'audio/x-m4a', - 'video/3gpp', - 'video/3gpp2', - - 'video/mpeg', - 'audio/mpeg', - - 'video/webm', - 'audio/webm', - - 'audio/aac', - 'audio/x-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/queue-stats.ts b/packages/backend/src/daemons/queue-stats.ts deleted file mode 100644 index 63e1388db..000000000 --- a/packages/backend/src/daemons/queue-stats.ts +++ /dev/null @@ -1,61 +0,0 @@ -import Xev from 'xev'; -import { deliverQueue, inboxQueue } from '@/queue/queues.js'; -import { SECOND } from '@/const.js'; - -const ev = new Xev(); - -const interval = 10 * SECOND; - -/** - * Report queue stats regularly - */ -export function queueStats(): void { - const log: Record>[] = []; - - 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(): Promise { - 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 573e2919e..000000000 --- a/packages/backend/src/daemons/server-stats.ts +++ /dev/null @@ -1,82 +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): number => Math.round(num * 1000) / 1000; -const round = (num: number): number => Math.round(num * 10) / 10; - -/** - * Report server stats regularly - */ -export function serverStats(): void { - const log: Record | number>[] = []; - - ev.on('requestServerStatsLog', x => { - ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50)); - }); - - async function tick(): Promise { - 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) => { - osUtils.cpuUsage((cpuUsage) => { - res(cpuUsage); - }); - }); -} - -// MEMORY STAT -async function mem(): Promise { - const data = await si.mem(); - return data; -} - -// NETWORK STAT -async function net(): Promise { - const iface = await si.networkInterfaceDefault(); - const data = await si.networkStats(iface); - return data[0]; -} - -// FS STAT -async function fs(): Promise { - const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 })); - return data; -} diff --git a/packages/backend/src/db/elasticsearch.ts b/packages/backend/src/db/elasticsearch.ts deleted file mode 100644 index d98c5d180..000000000 --- a/packages/backend/src/db/elasticsearch.ts +++ /dev/null @@ -1,56 +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 22f4c6b1b..000000000 --- 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 90b123ae8..000000000 --- a/packages/backend/src/db/postgre.ts +++ /dev/null @@ -1,245 +0,0 @@ -// https://github.com/typeorm/typeorm/issues/2400 -import pg from 'pg'; - -pg.types.setTypeParser(20, Number); - -import { Logger, DataSource } from 'typeorm'; -import * as highlight from 'cli-highlight'; -import config from '@/config/index.js'; - -import { SECOND } from '@/const.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 { 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 { PasswordResetRequest } from '@/models/entities/password-reset-request.js'; -import { UserPending } from '@/models/entities/user-pending.js'; - -import { entities as charts } from '@/services/chart/entities.js'; -import { Webhook } from '@/models/entities/webhook.js'; -import { getRedisOptions } from '@/config/redis.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): string { - return highlight.highlight(sql, { - language: 'sql', ignoreIllegals: true, - }); - } - - public logQuery(query: string): void { - sqlLogger.info(this.highlight(query).substring(0, 100)); - } - - public logQueryError(error: string, query: string): void { - sqlLogger.error(this.highlight(query)); - } - - public logQuerySlow(time: number, query: string): void { - sqlLogger.warn(this.highlight(query)); - } - - public logSchemaBuild(message: string): void { - sqlLogger.info(message); - } - - public log(message: string): void { - sqlLogger.info(message); - } - - public logMigration(message: string): void { - 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, - Relay, - MutedNote, - Channel, - ChannelFollowing, - ChannelNotePining, - RegistryItem, - PasswordResetRequest, - UserPending, - Webhook, - ...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: 10 * SECOND, - ...config.db.extra, - }, - synchronize: process.env.NODE_ENV === 'test', - dropSchema: process.env.NODE_ENV === 'test', - cache: !config.db.disableCache ? { - type: 'ioredis', - options: getRedisOptions(`${config.redis.prefix}:query:`), - } : false, - logging: log, - logger: log ? new MyCustomLogger() : undefined, - maxQueryExecutionTime: 300, - 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, SECOND)); - continue; - } - } - break; - } -} diff --git a/packages/backend/src/db/redis.ts b/packages/backend/src/db/redis.ts deleted file mode 100644 index f4eadce58..000000000 --- a/packages/backend/src/db/redis.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Redis from 'ioredis'; -import config from '@/config/index.js'; -import { getRedisOptions } from '@/config/redis.js'; - -export function createConnection(): Redis.Redis { - return new Redis(getRedisOptions(`${config.redis.prefix}:`)); -} - -export const subscriber = createConnection(); -subscriber.subscribe(config.host); - -export const redisClient = createConnection(); diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts deleted file mode 100644 index 1b678edc4..000000000 --- a/packages/backend/src/env.ts +++ /dev/null @@ -1,20 +0,0 @@ -const envOption = { - onlyQueue: false, - onlyServer: false, - noDaemons: false, - disableClustering: false, - verbose: false, - withLogTime: false, - quiet: false, - slow: false, -}; - -for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) { - if (process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]) envOption[key] = true; -} - -if (process.env.NODE_ENV === 'test') envOption.disableClustering = true; -if (process.env.NODE_ENV === 'test') envOption.quiet = true; -if (process.env.NODE_ENV === 'test') envOption.noDaemons = true; - -export { envOption }; diff --git a/packages/backend/src/global.d.ts b/packages/backend/src/global.d.ts deleted file mode 100644 index 7343aa199..000000000 --- a/packages/backend/src/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -type FIXME = any; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts deleted file mode 100644 index a25b735d4..000000000 --- a/packages/backend/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * FoundKey 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/mfm/from-html.ts b/packages/backend/src/mfm/from-html.ts deleted file mode 100644 index 0ec5b962e..000000000 --- a/packages/backend/src/mfm/from-html.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { URL } from 'node:url'; -import * as parse5 from 'parse5'; -import * as TreeAdapter from 'parse5/dist/tree-adapters/default'; - -const treeAdapter = parse5.defaultTreeAdapter; - -const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; -const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; - -function getAttr(node: TreeAdapter.Node, attr: string): string { - return node.attrs.find(({ name }) => name === attr)?.value; -} -function attrHas(node: TreeAdapter.Node, attr: string, value: string): boolean { - const attrValue = getAttr(node, attr); - if (!attrValue) return false; - - return new RegExp('\\b' + value + '\\b').test(attrValue); -} - -export function fromHtml(html: string, quoteUri?: string | null): string { - const dom = parse5.parseFragment( - // some AP servers like Pixelfed use br tags as well as newlines - html.replace(/\r?\n/gi, '\n'), - ); - - 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.length > 0) { - return node.childNodes.map(n => getText(n)).join(''); - } - - return ''; - } - - function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { - if (childNodes.length > 0) { - for (const n of childNodes) { - analyze(n); - } - } - } - - function analyze(node: TreeAdapter.Node): void { - 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 href = getAttr(node, 'href'); - - // hashtags - if (txt.startsWith('#') && href && (attrHas(node, 'rel', 'tag') || attrHas(node, 'class', 'hashtag'))) { - text += txt; - // mentions - } else if (txt.startsWith('@') && !attrHas(node, 'rel', 'me')) { - const part = txt.split('@'); - - if (part.length === 2 && href) { - // restore the host name part - const acct = `${txt}@${(new URL(href)).hostname}`; - text += acct; - } else if (part.length === 3) { - text += txt; - } - // other - } else { - const generateLink = () => { - if (!href && !txt) { - return ''; - } - if (!href) { - return txt; - } - if (!txt || txt === href) { // #6383: Missing text node - if (href.match(urlRegexFull)) { - return href; - } else { - return `<${href}>`; - } - } - if (href.match(urlRegex) && !href.match(urlRegexFull)) { - return `[${txt}](<${href}>)`; // #6846 - } else { - return `[${txt}](${href})`; - } - }; - - 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;
-			}
-
-			case 'span':
-			{
-				if (attrHas(node, 'class', 'quote-inline') && quoteUri && getText(node).trim() === `RE: ${quoteUri}`) {
-					// embedded quote thingy for backwards compatibility, don't show it
-				} else {
-					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 830bfac52..000000000
--- a/packages/backend/src/mfm/to-html.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-import { JSDOM } from 'jsdom';
-import * as mfm from 'mfm-js';
-import config from '@/config/index.js';
-import { UserProfiles } from '@/models/index.js';
-import { extractMentions } from '@/misc/extract-mentions.js';
-import { intersperse } from '@/prelude/array.js';
-
-// Transforms MFM to HTML, given the MFM text and a list of user IDs that are
-// mentioned in the text. If the list of mentions is not given, all mentions
-// from the text will be extracted.
-export async function toHtml(mfmText: string, mentions?: string[]): Promise {
-	const nodes = mfm.parse(mfmText);
-	if (nodes.length === 0) {
-		return null;
-	}
-
-	const doc = new JSDOM('').window.document;
-
-	const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => Promise } = {
-		async bold(node) {
-			const el = doc.createElement('b');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		async small(node) {
-			const el = doc.createElement('small');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		async strike(node) {
-			const el = doc.createElement('del');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		async italic(node) {
-			const el = doc.createElement('i');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		async fn(node) {
-			const el = doc.createElement('i');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		async blockCode(node) {
-			const pre = doc.createElement('pre');
-			const inner = doc.createElement('code');
-			inner.textContent = node.props.code;
-			pre.appendChild(inner);
-			return pre;
-		},
-
-		async center(node) {
-			const el = doc.createElement('div');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		async emojiCode(node) {
-			return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
-		},
-
-		async unicodeEmoji(node) {
-			return doc.createTextNode(node.props.emoji);
-		},
-
-		async 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;
-		},
-
-		async inlineCode(node) {
-			const el = doc.createElement('code');
-			el.textContent = node.props.code;
-			return el;
-		},
-
-		async mathInline(node) {
-			const el = doc.createElement('code');
-			el.textContent = node.props.formula;
-			return el;
-		},
-
-		async mathBlock(node) {
-			const el = doc.createElement('code');
-			el.textContent = node.props.formula;
-			return el;
-		},
-
-		async link(node) {
-			const a = doc.createElement('a');
-			a.href = node.props.url;
-			appendChildren(node.children, a);
-			return a;
-		},
-
-		async mention(node): Promise {
-			const { username, host, acct } = node.props;
-			const ids = mentions ?? extractMentions(nodes);
-			if (ids.length > 0) {
-				const mentionedUsers = await UserProfiles.createQueryBuilder('user_profile')
-					.leftJoin('user_profile.user', 'user')
-					.select('user.username', 'username')
-					.addSelect('user.host', 'host')
-					// links should preferably use user friendly urls, only fall back to AP ids
-					.addSelect('COALESCE(user_profile.url, user.uri)', 'url')
-					.where('"userId" IN (:...ids)', { ids })
-					.getRawMany();
-				const userInfo = mentionedUsers.find(user => user.username === username && user.host === host);
-				if (userInfo != null) {
-					// Mastodon microformat: span.h-card > a.u-url.mention
-					const a = doc.createElement('a');
-					a.href = userInfo.url ?? `${config.url}/${acct}`;
-					a.className = 'u-url mention';
-					a.textContent = acct;
-
-					const card = doc.createElement('span');
-					card.className = 'h-card';
-					card.appendChild(a);
-					return card;
-				}
-			}
-			// this user does not actually exist
-			return doc.createTextNode(acct);
-		},
-
-		async quote(node) {
-			const el = doc.createElement('blockquote');
-			appendChildren(node.children, el);
-			return el;
-		},
-
-		async 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;
-		},
-
-		async url(node) {
-			const a = doc.createElement('a');
-			a.href = node.props.url;
-			a.textContent = node.props.url;
-			return a;
-		},
-
-		async search(node) {
-			const a = doc.createElement('a');
-			a.href = `https://www.google.com/search?q=${node.props.query}`;
-			a.textContent = node.props.content;
-			return a;
-		},
-	};
-
-	async function appendChildren(children: mfm.MfmNode[], targetElement: HTMLElement): Promise {
-		type HandlerFunc = (node: mfm.MfmNode) => Promise;
-		const htmlChildren = await Promise.all(children.map(x => (handlers[x.type] as HandlerFunc)(x)));
-
-		for (const child of htmlChildren) {
-			targetElement.appendChild(child);
-		}
-	}
-
-	await 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 60160cc4f..000000000 --- a/packages/backend/src/misc/acct.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type Acct = { - username: string; - host: string | null; -}; - -export function parse(acct: string): Acct { - const split = acct.split('@'); - if (split[0].length === 0) { - // there was an initial at - split.shift(); - } - 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 6523d89b6..000000000 --- a/packages/backend/src/misc/antenna-cache.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Antennas } from '@/models/index.js'; -import { Antenna } from '@/models/entities/antenna.js'; -import { subscriber } from '@/db/redis.js'; - -let antennasFetched = false; -let antennas: Antenna[] = []; - -export async function getAntennas(): Promise { - 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 160cdf9fd..000000000 --- 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 25627fd05..000000000 --- a/packages/backend/src/misc/app-lock.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { promisify } from 'node:util'; -import redisLock from 'redis-lock'; -import { redisClient } from '@/db/redis.js'; -import { SECOND } from '@/const.js'; - -/** - * Retry delay (ms) for lock acquisition - */ -const retryDelay = 100; - -const lock: (key: string, timeout?: number) => Promise<() => void> = promisify(redisLock(redisClient, retryDelay)); - -/** - * 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 * SECOND) { - return lock(`ap-object:${uri}`, timeout); -} - -export function getFetchInstanceMetadataLock(host: string, timeout = 30 * SECOND) { - return lock(`instance:${host}`, timeout); -} - -export function getChartInsertLock(lockKey: string, timeout = 30 * SECOND) { - 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 93ac7a1f3..000000000 --- a/packages/backend/src/misc/before-shutdown.ts +++ /dev/null @@ -1,94 +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 ee6e90a66..000000000 --- a/packages/backend/src/misc/cache.ts +++ /dev/null @@ -1,56 +0,0 @@ -export class Cache { - public cache: Map; - private lifetime: number; - public fetcher: (key: string) => Promise; - - constructor(lifetime: number, fetcher: Cache['fetcher']) { - this.cache = new Map(); - this.lifetime = lifetime; - this.fetcher = fetcher; - } - - public set(key: string, value: T): void { - this.cache.set(key, { - date: Date.now(), - value, - }); - } - - public get(key: string): T | undefined { - const cached = this.cache.get(key); - if (cached == null) return undefined; - - // discard if past the cache lifetime - if ((Date.now() - cached.date) > this.lifetime) { - this.cache.delete(key); - return undefined; - } - - return cached.value; - } - - public delete(key: string): void { - this.cache.delete(key); - } - - /** - * If the value is cached, it is returned. Otherwise the fetcher is - * run to get the value. If the fetcher returns undefined, it is - * returned but not cached. - */ - public async fetch(key: string): Promise { - const cached = this.get(key); - if (cached !== undefined) { - return cached; - } else { - const value = await this.fetcher(key); - - // don't cache undefined - 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 4f0e757e7..000000000 --- a/packages/backend/src/misc/captcha.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { URLSearchParams } from 'node:url'; -import fetch from 'node-fetch'; -import config from '@/config/index.js'; -import { getAgentByUrl } from './fetch.js'; - -export async function verifyRecaptcha(secret: string, response: string): Promise { - 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): Promise { - 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 7091dcd07..000000000 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Antenna } from '@/models/entities/antenna.js'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; -import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js'; -import * as Acct from '@/misc/acct.js'; -import { MINUTE } from '@/const.js'; -import { getFullApAccount } from './convert-host.js'; -import { Packed } from './schema.js'; -import { Cache } from './cache.js'; - -const blockingCache = new Cache( - 5 * MINUTE, - (blockerId) => Blockings.findBy({ blockerId }).then(res => res.map(x => x.blockeeId)), -); - -// designation for users you follow, list users and groups is disabled for performance reasons - -/** - * either noteUserFollowers or antennaUserFollowing must be specified - */ -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; - - // skip if the antenna creator is blocked by the note author - const blockings = (await blockingCache.fetch(noteUser.id)) ?? []; - 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; - } - - 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 c49e95ace..000000000 --- a/packages/backend/src/misc/check-word-mute.ts +++ /dev/null @@ -1,47 +0,0 @@ -import RE2 from 're2'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; - -type NoteLike = { - userId: Note['userId']; - text: Note['text']; - cw: Note['cw']; -}; - -type UserLike = { - id: User['id']; -}; - -export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: Array): Promise { - // 自分自身 - if (me && (note.userId === me.id)) return false; - - if (mutedWords.length > 0) { - const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); - - if (text === '') return false; - - const matched = mutedWords.some(filter => { - if (Array.isArray(filter)) { - return filter.every(keyword => text.includes(keyword)); - } else { - // represents RegExp - const regexp = filter.match(/^\/(.+)\/(.*)$/); - - // This should never happen due to input sanitisation. - if (!regexp) return false; - - try { - return new RE2(regexp[1], regexp[2]).test(text); - } catch (err) { - // This should never happen due to input sanitisation. - return false; - } - } - }); - - if (matched) return true; - } - - return false; -} diff --git a/packages/backend/src/misc/content-disposition.ts b/packages/backend/src/misc/content-disposition.ts deleted file mode 100644 index b2aec471d..000000000 --- a/packages/backend/src/misc/content-disposition.ts +++ /dev/null @@ -1,6 +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 705edaedd..000000000 --- a/packages/backend/src/misc/convert-host.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { toASCII } from 'punycode'; -import { URL } from 'node:url'; -import config from '@/config/index.js'; - -export function getFullApAccount(username: string, host: string | null): string { - return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`; -} - -export function isSelfHost(host: string | null): boolean { - if (host == null) return true; - return toPuny(config.host) === toPuny(host); -} - -export function extractDbHost(uri: string): string { - const url = new URL(uri); - return toPuny(url.hostname); -} - -export function toPuny(host: string): 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 b7f8ce90c..000000000 --- a/packages/backend/src/misc/count-same-renotes.ts +++ /dev/null @@ -1,15 +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 429977669..000000000 --- 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 bb4049c2e..000000000 --- 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): Promise { - 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 c62c70ee3..000000000 --- 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 079e84e8a..000000000 --- a/packages/backend/src/misc/download-url.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as fs from 'node:fs'; -import * as stream from 'node:stream'; -import * as util from 'node:util'; -import chalk from 'chalk'; -import got, * as Got from 'got'; -import IPCIDR from 'ip-cidr'; -import PrivateIp from 'private-ip'; -import { SECOND, MINUTE } from '@/const.js'; -import config from '@/config/index.js'; -import Logger from '@/services/logger.js'; -import { httpAgent, httpsAgent, StatusError } from './fetch.js'; - -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 * SECOND; - const operationTimeout = MINUTE; - 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 ca224971c..000000000 --- a/packages/backend/src/misc/emoji-regex.ts +++ /dev/null @@ -1,4 +0,0 @@ -import twemoji from 'twemoji-parser/dist/lib/regex.js'; -const twemojiRegex = twemoji.default; - -export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); diff --git a/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts b/packages/backend/src/misc/extract-custom-emojis-from-mfm.ts deleted file mode 100644 index a0319d8dd..000000000 --- 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 0b0418eef..000000000 --- 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 cc19b161a..000000000 --- a/packages/backend/src/misc/extract-mentions.ts +++ /dev/null @@ -1,11 +0,0 @@ -// test is located in test/extract-mentions - -import * as mfm from 'mfm-js'; - -export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { - // TODO: 重複を削除 - const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention'); - const mentions = mentionNodes.map(x => x.props); - - return mentions; -} diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts deleted file mode 100644 index ab8c81eef..000000000 --- a/packages/backend/src/misc/fetch-meta.ts +++ /dev/null @@ -1,61 +0,0 @@ -import push from 'web-push'; -import { db } from '@/db/postgre.js'; -import { Meta } from '@/models/entities/meta.js'; -import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js'; - -let cache: Meta | undefined; - -/** - * Performs the primitive database operation to set the server configuration - */ -export async function setMeta(meta: Meta): Promise { - const unlock = await getFetchInstanceMetadataLock('localhost'); - - // try to mitigate older bugs where multiple meta entries may have been created - await db.manager.clear(Meta); - await db.manager.insert(Meta, meta); - - cache = meta; - - unlock(); -} - -/** - * Performs the primitive database operation to fetch server configuration. - * If there is no entry yet, inserts a new one. - * Writes to `cache` instead of returning. - */ -async function getMeta(): Promise { - const unlock = await getFetchInstanceMetadataLock('localhost'); - - // new IDs are prioritised because multiple records may have been created due to past bugs - let metas = await db.manager.find(Meta, { - order: { - id: 'DESC', - }, - }); - if (metas.length === 0) { - const { publicKey, privateKey } = push.generateVAPIDKeys(); - await db.manager.insert(Meta, { - id: 'x', - swPublicKey: publicKey, - swPrivateKey: privateKey, - }); - metas = await db.manager.find(Meta, { - order: { - id: 'DESC', - }, - }); - } - cache = metas[0]; - - unlock(); -} - -export async function fetchMeta(noCache = false): Promise { - if (!noCache && cache) return cache; - - await getMeta(); - - return cache!; -} 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 491a5221c..000000000 --- a/packages/backend/src/misc/fetch-proxy-account.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ILocalUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { fetchMeta } from './fetch-meta.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 42eb445d9..000000000 --- a/packages/backend/src/misc/fetch.ts +++ /dev/null @@ -1,147 +0,0 @@ -import * as http from 'node:http'; -import * as https from 'node:https'; -import { URL } from 'node:url'; -import CacheableLookup from 'cacheable-lookup'; -import fetch from 'node-fetch'; -import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; -import { SECOND } from '@/const.js'; -import config from '@/config/index.js'; - -export async function getJson(url: string, accept = 'application/json, */*', timeout = 10 * SECOND, 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 = 10 * SECOND, 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, redirect: 'follow' | 'manual' | 'error' = 'follow' }) { - const timeout = args.timeout || 10 * SECOND; - - const controller = new AbortController(); - setTimeout(() => { - controller.abort(); - }, timeout * 6); - - const res = await fetch(args.url, { - method: args.method, - headers: args.headers, - body: args.body, - redirect: args.redirect, - timeout, - size: args.size || 10 * 1024 * 1024, // 10 MiB - agent: getAgentByUrl, - signal: controller.signal, - }); - - if ( - !res.ok - && - // intended redirect is not an error - !(args.redirect != 'follow' && res.status >= 300 && res.status < 400)) { - 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 * SECOND, - lookup: cache.lookup, -} as http.AgentOptions); - -/** - * Get https non-proxy agent - */ -const _https = new https.Agent({ - keepAlive: true, - keepAliveMsecs: 30 * SECOND, - 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 * SECOND, - 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 * SECOND, - 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 b3a808f0a..000000000 --- a/packages/backend/src/misc/gen-id.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as crypto from 'node:crypto'; - -// AID generation -// 8 chars: milliseconds elapsed since 2000-01-01 00:00:00.000Z encoded as base36 -// + 2 random chars - -const TIME2000 = 946684800000; -let counter = crypto.randomBytes(2).readUInt16LE(0); - -export function genId(date: Date = new Date()): string { - let t = Math.min(date.valueOf(), new Date().valueOf()); - t -= TIME2000; - if (t < 0) t = 0; - if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date'); - const time = t.toString(36).padStart(8, '0'); - - counter++; - const noise = counter.toString(36).padStart(2, '0').slice(-2); - - return time + noise; -} diff --git a/packages/backend/src/misc/gen-identicon.ts b/packages/backend/src/misc/gen-identicon.ts deleted file mode 100644 index 9b30e5dbd..000000000 --- a/packages/backend/src/misc/gen-identicon.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Identicon generator - * https://en.wikipedia.org/wiki/Identicon - */ - -import { WriteStream } from 'node:fs'; -import * as p from 'pureimage'; -import gen from 'random-seed'; - -const size = 256; // px -const n = 5; // resolution -const margin = (size / n); -const colors = [ - '#e57373', - '#F06292', - '#BA68C8', - '#9575CD', - '#7986CB', - '#64B5F6', - '#4FC3F7', - '#4DD0E1', - '#4DB6AC', - '#81C784', - '#8BC34A', - '#AFB42B', - '#F57F17', - '#FF5722', - '#795548', - '#455A64', -]; -const bg = '#e9e9e9'; - -const actualSize = size - (margin * 2); -const cellSize = actualSize / n; -const sideN = Math.floor(n / 2); - -/** - * Generate buffer of 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'); - - ctx.fillStyle = bg; - ctx.beginPath(); - ctx.fillRect(0, 0, size, size); - - ctx.fillStyle = colors[rand(colors.length)]; - - // side bitmap (filled by false) - const side: boolean[][] = new Array(sideN); - for (let i = 0; i < side.length; i++) { - side[i] = new Array(n).fill(false); - } - - // 1*n (filled by false) - const center: boolean[] = new Array(n).fill(false); - - // eslint:disable-next-line:prefer-for-of - for (let x = 0; x < side.length; x++) { - for (let y = 0; y < side[x].length; y++) { - side[x][y] = rand(3) === 0; - } - } - - for (let i = 0; i < center.length; i++) { - center[i] = rand(3) === 0; - } - - // Draw - for (let x = 0; x < n; x++) { - for (let y = 0; y < n; y++) { - const isXCenter = x === ((n - 1) / 2); - if (isXCenter && !center[y]) continue; - - const isLeftSide = x < ((n - 1) / 2); - if (isLeftSide && !side[x][y]) continue; - - const isRightSide = x > ((n - 1) / 2); - if (isRightSide && !side[sideN - (x - sideN)][y]) continue; - - const actualX = margin + (cellSize * x); - const actualY = margin + (cellSize * y); - ctx.beginPath(); - ctx.fillRect(actualX, actualY, cellSize, cellSize); - } - } - - return p.encodePNGToStream(canvas, stream); -} diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts deleted file mode 100644 index e2ad59850..000000000 --- a/packages/backend/src/misc/gen-key-pair.ts +++ /dev/null @@ -1,36 +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 2618a639f..000000000 --- a/packages/backend/src/misc/get-file-info.ts +++ /dev/null @@ -1,204 +0,0 @@ -import * as fs from 'node:fs'; -import * as crypto from 'node:crypto'; -import * as stream from 'node:stream'; -import * as util from 'node:util'; -import { fileTypeFromFile } from 'file-type'; -import isSvg from 'is-svg'; -import probeImageSize from 'probe-image-size'; -import sharp from 'sharp'; -import { encode } from 'blurhash'; - -const pipeline = util.promisify(stream.pipeline); - -export type FileInfo = { - size: number; - md5: string; - type: { - mime: string; - ext: string | null; - }; - width?: number; - height?: number; - orientation?: number; - blurhash?: string; - warnings: string[]; -}; - -const TYPE_OCTET_STREAM = { - mime: 'application/octet-stream', - ext: null, -}; - -const TYPE_SVG = { - mime: 'image/svg+xml', - ext: 'svg', -}; - -/** - * Get file information - */ -export async function getFileInfo(path: string): Promise { - const warnings = [] as string[]; - - const size = await getFileSize(path); - const md5 = await calcHash(path); - - let type = await detectType(path); - - // image dimensions - let width: number | undefined; - let height: number | undefined; - 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'].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'].includes(type.mime)) { - blurhash = await getBlurhash(path).catch(e => { - warnings.push(`getBlurhash failed: ${e}`); - return undefined; - }); - } - - return { - size, - md5, - type, - width, - height, - orientation, - blurhash, - warnings, - }; -} - -/** - * 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 379325bb1..000000000 --- a/packages/backend/src/misc/get-ip-hash.ts +++ /dev/null @@ -1,9 +0,0 @@ -import IPCIDR from 'ip-cidr'; - -export function getIpHash(ip: string) { - // 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); -} 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 469b9cfa4..000000000 --- a/packages/backend/src/misc/get-note-summary.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { 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 c2e0b9858..000000000 --- a/packages/backend/src/misc/get-reaction-emoji.ts +++ /dev/null @@ -1,16 +0,0 @@ -export default function(reaction: string): string { - switch (reaction) { - case 'like': return '👍'; - case 'love': return '❤️'; - case 'laugh': return '😆'; - case 'hmm': return '🤔'; - case 'surprise': return '😮'; - case 'congrats': return '🎉'; - case 'angry': return '💢'; - case 'confused': return '😥'; - case 'rip': return '😇'; - case 'pudding': return '🍮'; - case 'star': return '⭐'; - default: return reaction; - } -} diff --git a/packages/backend/src/misc/hard-limits.ts b/packages/backend/src/misc/hard-limits.ts deleted file mode 100644 index 7ea45bb34..000000000 --- a/packages/backend/src/misc/hard-limits.ts +++ /dev/null @@ -1,14 +0,0 @@ - -// If you change DB_* values, you must also change the DB schema. - -/** - * Maximum note text length that can be stored in DB. - * Surrogate pairs count as one - */ -export const DB_MAX_NOTE_TEXT_LENGTH = 8192; - -/** - * Maximum image description length that can be stored in DB. - * Surrogate pairs count as one - */ -export const DB_MAX_IMAGE_COMMENT_LENGTH = 2048; diff --git a/packages/backend/src/misc/i18n.ts b/packages/backend/src/misc/i18n.ts deleted file mode 100644 index 6fab0b2ff..000000000 --- a/packages/backend/src/misc/i18n.ts +++ /dev/null @@ -1,28 +0,0 @@ -const locales = await import('../../../../locales/index.js').then(mod => mod.default); - -export class I18n { - public ts: Record; - - constructor(locale: string) { - this.ts = locales[locale]; - this.t = this.t.bind(this); - } - - // string にしているのは、ドット区切りでのパス指定を許可するため - // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - public t(key: string, args?: Record): string { - try { - let str = key.split('.').reduce((o, i) => o[i], this.ts) 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/identifiable-error.ts b/packages/backend/src/misc/identifiable-error.ts deleted file mode 100644 index 2d7c6bd0c..000000000 --- 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 04ff191e4..000000000 --- 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 any).message && (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 d744ba656..000000000 --- a/packages/backend/src/misc/is-instance-muted.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { 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 8993ede33..000000000 --- a/packages/backend/src/misc/is-mime-image.ts +++ /dev/null @@ -1,8 +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'], -}; - -export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime); 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 79eedf914..000000000 --- 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 && 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 910f96258..000000000 --- a/packages/backend/src/misc/keypair-store.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { UserKeypairs } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { UserKeypair } from '@/models/entities/user-keypair.js'; -import { Cache } from './cache.js'; - -const cache = new Cache( - Infinity, - (userId) => UserKeypairs.findOneByOrFail({ userId }), -); - -export async function getUserKeypair(userId: User['id']): Promise { - return await cache.fetch(userId); -} diff --git a/packages/backend/src/misc/langmap.ts b/packages/backend/src/misc/langmap.ts deleted file mode 100644 index 5ee85e6c0..000000000 --- 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 200540566..000000000 --- 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 b6b9a9b76..000000000 --- a/packages/backend/src/misc/nyaize.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function nyaize(text: string): string { - return text - // ja-JP - .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ') - // en-US - .replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya') - .replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan') - .replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan') - // ko-KR - .replace(/[나-낳]/g, match => String.fromCharCode( - match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0), - )) - .replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥') - .replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥'); -} diff --git a/packages/backend/src/misc/password.ts b/packages/backend/src/misc/password.ts deleted file mode 100644 index 99db8f61a..000000000 --- a/packages/backend/src/misc/password.ts +++ /dev/null @@ -1,17 +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 f0a168369..000000000 --- a/packages/backend/src/misc/populate-emojis.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { In, IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import { Emojis } from '@/models/index.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { Note } from '@/models/entities/note.js'; -import { query } from '@/prelude/url.js'; -import { HOUR } from '@/const.js'; -import { Cache } from './cache.js'; -import { isSelfHost, toPunyNullable } from './convert-host.js'; -import { decodeReaction } from './reaction-lib.js'; - -/** - * composite cache key: `${host ?? ''}:${name}` - */ -const cache = new Cache( - 12 * HOUR, - async (key) => { - const [host, name] = key.split(':'); - return (await Emojis.findOneBy({ - name, - host: host || IsNull(), - })) || null; - }, -); - -/** - * Information needed to attach in ActivityPub - */ -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 }; -} - -/** - * Resolve emoji information from ActivityPub attachment. - * @param emojiName custom emoji names attached to notes, user profiles or in rections. Colons should not be included. Localhost is denote by @. (see also `decodeReaction`) - * @param noteUserHost host that the content is from, to default to - * @returns emoji information. `null` means not found. - */ -export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise { - const { name, host } = parseEmojiStr(emojiName, noteUserHost); - if (name == null) return null; - - const emoji = await cache.fetch(`${host ?? ''}:${name}`); - - 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, - }; -} - -/** - * Retrieve list of emojis from the cache. Uncached emoji are dropped. - */ -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; }[]; -} - -/** - * Query list of emojis in bulk and add them to the cache. - */ -export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise { - const notCachedEmojis = emojis.filter(emoji => { - // check if the cache has this emoji - return cache.get(`${emoji.host ?? ''}:${emoji.name}`) == null; - }); - - // check if there even are any uncached emoji to handle - if (notCachedEmojis.length === 0) return; - - // query all uncached emoji - const emojisQuery: any[] = []; - // group by hosts to try to reduce query size - 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(), - }); - } - - await Emojis.find({ - where: emojisQuery, - select: ['name', 'host', 'originalUrl', 'publicUrl'], - }).then(emojis => { - // store all emojis into the cache - emojis.forEach(emoji => { - cache.set(`${emoji.host ?? ''}:${emoji.name}`, emoji); - }); - }); -} diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts deleted file mode 100644 index 51be49a67..000000000 --- a/packages/backend/src/misc/reaction-lib.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* eslint-disable key-spacing */ -import { IsNull } from 'typeorm'; -import { Emojis } from '@/models/index.js'; -import { toPunyNullable } from './convert-host.js'; -import { emojiRegex } from './emoji-regex.js'; -import { fetchMeta } from './fetch-meta.js'; - -const legacies: Record = { - 'like': '👍', - 'love': '❤', // ここに記述する場合は異体字セレクタを入れない - 'laugh': '😆', - 'hmm': '🤔', - 'surprise': '😮', - 'congrats': '🎉', - 'angry': '💢', - 'confused': '😥', - 'rip': '😇', - 'pudding': '🍮', - 'star': '⭐', -}; - -export async function getFallbackReaction(): Promise { - const meta = await fetchMeta(); - return meta.useStarForReactionFallback ? '⭐' : '👍'; -} - -export function convertLegacyReactions(reactions: Record) { - const _reactions = {} as Record; - - for (const reaction of Object.keys(reactions)) { - if (reactions[reaction] <= 0) continue; - - if (Object.keys(legacies).includes(reaction)) { - if (_reactions[legacies[reaction]]) { - _reactions[legacies[reaction]] += reactions[reaction]; - } else { - _reactions[legacies[reaction]] = reactions[reaction]; - } - } else { - if (_reactions[reaction]) { - _reactions[reaction] += reactions[reaction]; - } else { - _reactions[reaction] = reactions[reaction]; - } - } - } - - const _reactions2 = {} as Record; - - for (const reaction of Object.keys(_reactions)) { - _reactions2[decodeReaction(reaction).reaction] = _reactions[reaction]; - } - - return _reactions2; -} - -export async function toDbReaction(reaction?: string | null, idnReacterHost?: string | null): Promise { - if (reaction == null) return await getFallbackReaction(); - - const reacterHost = toPunyNullable(idnReacterHost); - - // 文字列タイプのリアクションを絵文字に変換 - if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; - - // Unicode絵文字 - const match = emojiRegex.exec(reaction); - if (match) { - // 合字を含む1つの絵文字 - const unicode = match[0]; - - // 異体字セレクタ除去 - return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); - } - - const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); - if (custom) { - const name = custom[1]; - const emoji = await Emojis.countBy({ - 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 reaction = decodeReaction(_reaction).reaction; - if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; - return reaction; -} diff --git a/packages/backend/src/misc/renote.ts b/packages/backend/src/misc/renote.ts deleted file mode 100644 index cd51cd04a..000000000 --- a/packages/backend/src/misc/renote.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Note } from '@/models/entities/note.js'; - -export function isPureRenote(note: Note): note is Note & { renoteId: string, text: null, fileIds: null | never[], hasPoll: false } { - return note.renoteId != null - && note.text == null - && ( - note.fileIds == null - || note.fileIds.length === 0 - ) - && !note.hasPoll; -} 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 02eb7f0a2..000000000 --- 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 ad5dcb067..000000000 --- a/packages/backend/src/misc/schema.ts +++ /dev/null @@ -1,174 +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, 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 = - 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 = 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 963e3c00c..000000000 --- a/packages/backend/src/misc/secure-rndstr.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as crypto from 'node:crypto'; - -const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'; -const LU_CHARS = L_CHARS + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - -export function secureRndstrCustom(length = 32, chars: string): string { - 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; -} - -export function secureRndstr(length = 32, useLU = true): string { - const chars = useLU ? LU_CHARS : L_CHARS; - return secureRndstrCustom(length, chars); -} 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 ddd25eeee..000000000 --- a/packages/backend/src/misc/should-block-instance.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Instance } from '@/models/entities/instance.js'; -import { 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 bc71cfbe9..000000000 --- a/packages/backend/src/misc/show-machine-info.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as os from 'node:os'; -import sysUtils from 'systeminformation'; -import 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 0058c9bcf..000000000 --- a/packages/backend/src/misc/skipped-instances.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Instance } from '@/models/entities/instance.js'; -import { DAY } from '@/const.js'; -import { shouldBlockInstance } from '@/misc/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 punycoded instance hosts that should be skipped (subset of hosts parameter) - */ -export async function skippedInstances(hosts: Array): 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 db.query( - `SELECT host FROM instance WHERE ("isSuspended" OR "latestStatus" = 410 OR "lastCommunicatedAt" < $1::date) AND host = ANY(string_to_array($2, ','))`, - [ - deadTime.toISOString(), - // don't check hosts again that we already know are suspended - // also avoids adding duplicates to the list - hosts.filter(host => !skipped.includes(host) && !host.includes(',')).join(','), - ], - ) - .then((res: Instance[]) => res.map(row => row.host)), - ); -} - -/** - * 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 cb120331a..000000000 --- a/packages/backend/src/misc/truncate.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { substring } from 'stringz'; - -export function truncate(input: string, size: number): string; -export function truncate(input: string | undefined, size: number): string | undefined; -export function truncate(input: string | undefined, size: number): string | undefined { - if (!input) { - return input; - } else { - return substring(input, 0, size); - } -} diff --git a/packages/backend/src/misc/webhook-cache.ts b/packages/backend/src/misc/webhook-cache.ts deleted file mode 100644 index 5e72dbd92..000000000 --- a/packages/backend/src/misc/webhook-cache.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Webhooks } from '@/models/index.js'; -import { 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 dc7f27377..000000000 --- a/packages/backend/src/models/entities/abuse-user-report.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public targetUser: User | null; - - @Index() - @Column(id()) - public reporterId: User['id']; - - @ManyToOne(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public reporter: User | null; - - @Column({ - ...id(), - nullable: true, - }) - public assigneeId: User['id'] | null; - - @ManyToOne(() => 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: 512, array: true, default: '{}', - }) - public urls: string[]; - - @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 b6dc8cebc..000000000 --- a/packages/backend/src/models/entities/access-token.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { App } from './app.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column({ - ...id(), - nullable: true, - }) - public appId: App['id'] | null; - - @ManyToOne(() => 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/announcement-read.ts b/packages/backend/src/models/entities/announcement-read.ts deleted file mode 100644 index 0102a2fa2..000000000 --- a/packages/backend/src/models/entities/announcement-read.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { Announcement } from './announcement.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public announcementId: Announcement['id']; - - @ManyToOne(() => 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 f2088455d..000000000 --- a/packages/backend/src/models/entities/announcement.ts +++ /dev/null @@ -1,35 +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; -} 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 1ff1c75fe..000000000 --- a/packages/backend/src/models/entities/antenna-note.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { Antenna } from './antenna.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(() => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The antenna ID.', - }) - public antennaId: Antenna['id']; - - @ManyToOne(() => 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 8fe47f1e4..000000000 --- a/packages/backend/src/models/entities/antenna.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, - comment: 'The name of the Antenna.', - }) - public name: string; - - @Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] }) - public src: 'home' | 'all' | 'users' | 'list' | 'group'; - - @Column({ - ...id(), - nullable: true, - }) - public userListId: UserList['id'] | null; - - @ManyToOne(() => UserList, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userList: UserList | null; - - @Column({ - ...id(), - nullable: true, - }) - public userGroupJoiningId: UserGroupJoining['id'] | null; - - @ManyToOne(() => UserGroupJoining, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroupJoining: UserGroupJoining | null; - - @Column('varchar', { - length: 1024, array: true, - default: '{}', - }) - public users: string[]; - - @Column('jsonb', { - default: [], - }) - public keywords: string[][]; - - @Column('jsonb', { - default: [], - }) - public excludeKeywords: string[][]; - - @Column('boolean', { - default: false, - }) - public caseSensitive: boolean; - - @Column('boolean', { - default: false, - }) - public withReplies: boolean; - - @Column('boolean') - public withFile: boolean; - - @Column('varchar', { - length: 2048, nullable: true, - }) - public expression: string | null; - - @Column('boolean') - public notify: boolean; -} diff --git a/packages/backend/src/models/entities/app.ts b/packages/backend/src/models/entities/app.ts deleted file mode 100644 index dcca3be4c..000000000 --- a/packages/backend/src/models/entities/app.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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 421a07fce..000000000 --- a/packages/backend/src/models/entities/attestation-challenge.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; - -@Entity() -export class AttestationChallenge { - @PrimaryColumn(id()) - public id: string; - - @Index() - @PrimaryColumn(id()) - public userId: User['id']; - - @ManyToOne(() => 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; -} 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 511a7fad6..000000000 --- a/packages/backend/src/models/entities/auth-session.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.js'; -import { AccessToken } from './access-token.js'; -import { App } from './app.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 accessTokenId: AccessToken['id'] | null; - - @ManyToOne(() => AccessToken, { - onDelete: 'CASCADE', - nullable: true, - }) - @JoinColumn() - public accessToken: AccessToken | null; - - @Column(id()) - public appId: App['id']; - - @ManyToOne(() => App, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public app: App | null; - - @Column('text', { - nullable: true, - comment: 'PKCE code_challenge value, if provided (OAuth only)', - }) - pkceChallenge: string | null; -} diff --git a/packages/backend/src/models/entities/blocking.ts b/packages/backend/src/models/entities/blocking.ts deleted file mode 100644 index ad45a0569..000000000 --- a/packages/backend/src/models/entities/blocking.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public blockee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The blocker user ID.', - }) - public blockerId: User['id']; - - @ManyToOne(() => 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 5a717c9e8..000000000 --- a/packages/backend/src/models/entities/channel-following.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => Channel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followee: Channel | null; - - @Index() - @Column({ - ...id(), - comment: 'The follower user ID.', - }) - public followerId: User['id']; - - @ManyToOne(() => 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 0be4f0dba..000000000 --- a/packages/backend/src/models/entities/channel-note-pining.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { Channel } from './channel.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(() => Channel, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public channel: Channel | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(() => 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 44219c37d..000000000 --- a/packages/backend/src/models/entities/channel.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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(() => 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 89c5f03d1..000000000 --- a/packages/backend/src/models/entities/clip-note.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { Clip } from './clip.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(() => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The clip ID.', - }) - public clipId: Clip['id']; - - @ManyToOne(() => 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 0580db129..000000000 --- a/packages/backend/src/models/entities/clip.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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 61f6cb2ec..000000000 --- a/packages/backend/src/models/entities/drive-file.ts +++ /dev/null @@ -1,169 +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'; - -@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(() => User, { - onDelete: 'RESTRICT', - }) - @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: 2048, - 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(() => DriveFolder, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public folder: DriveFolder | null; - - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the DriveFile is NSFW.', - }) - public isSensitive: boolean; - - /** - * 外部の(信頼されていない)URLへの直リンクか否か - */ - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the DriveFile is direct link to remote server.', - }) - public isLink: boolean; -} diff --git a/packages/backend/src/models/entities/drive-folder.ts b/packages/backend/src/models/entities/drive-folder.ts deleted file mode 100644 index d7e323e72..000000000 --- a/packages/backend/src/models/entities/drive-folder.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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(() => 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 7332dd185..000000000 --- a/packages/backend/src/models/entities/emoji.ts +++ /dev/null @@ -1,58 +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[]; -} 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 93d685c55..000000000 --- a/packages/backend/src/models/entities/follow-request.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The follower user ID.', - }) - public followerId: User['id']; - - @ManyToOne(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public follower: User | null; - - @Column('varchar', { - length: 2048, 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 c430cd388..000000000 --- a/packages/backend/src/models/entities/following.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The follower user ID.', - }) - public followerId: User['id']; - - @ManyToOne(() => 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 259981392..000000000 --- a/packages/backend/src/models/entities/gallery-like.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public postId: GalleryPost['id']; - - @ManyToOne(() => 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 315bcd371..000000000 --- a/packages/backend/src/models/entities/gallery-post.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { 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(() => 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 c830a3f34..000000000 --- a/packages/backend/src/models/entities/hashtag.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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 62c541950..000000000 --- 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; - - /** - * Date and time this instance was first seen. - */ - @Index() - @Column('timestamp with time zone', { - comment: 'The caught date of the Instance.', - }) - public caughtAt: Date; - - /** - * Hostname - */ - @Index({ unique: true }) - @Column('varchar', { - length: 128, - comment: 'The host of the Instance.', - }) - public host: string; - - /** - * Number of users on this instance. - */ - @Column('integer', { - default: 0, - comment: 'The count of the users of the Instance.', - }) - public usersCount: number; - - /** - * Number of notes on this instance. - */ - @Column('integer', { - default: 0, - comment: 'The count of the notes of the Instance.', - }) - public notesCount: number; - - /** - * Number of local users who are followed by users from this instance. - */ - @Column('integer', { - default: 0, - }) - public followingCount: number; - - /** - * Number of users from this instance who are followed by local users. - */ - @Column('integer', { - default: 0, - }) - public followersCount: number; - - /** - * Timestamp of the latest outgoing HTTP request. - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public latestRequestSentAt: Date | null; - - /** - * HTTP status code that was received for the last outgoing HTTP request. - */ - @Column('integer', { - nullable: true, - }) - public latestStatus: number | null; - - /** - * Timestamp of the latest incoming HTTP request. - */ - @Column('timestamp with time zone', { - nullable: true, - }) - public latestRequestReceivedAt: Date | null; - - /** - * Timestamp of last communication with this instance (incoming or outgoing). - */ - @Column('timestamp with time zone') - public lastCommunicatedAt: Date; - - /** - * Whether this instance seems unresponsive. - */ - @Column('boolean', { - default: false, - }) - public isNotResponding: boolean; - - /** - * Whether sending activities to this instance has been suspended. - */ - @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 95862aab4..000000000 --- a/packages/backend/src/models/entities/messaging-message.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { DriveFile } from './drive-file.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), nullable: true, - comment: 'The recipient user ID.', - }) - public recipientId: User['id'] | null; - - @ManyToOne(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public recipient: User | null; - - @Index() - @Column({ - ...id(), nullable: true, - comment: 'The recipient group ID.', - }) - public groupId: UserGroup['id'] | null; - - @ManyToOne(() => 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(() => 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 9c438bcd6..000000000 --- a/packages/backend/src/models/entities/meta.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { Clip } from './clip.js'; - -export enum TranslationService { - DeepL = 'deepl', - LibreTranslate = 'libretranslate', -} - -@Entity() -export class Meta { - @PrimaryColumn({ - type: 'varchar', - length: 32, - }) - public id: string; - - @Column('varchar', { - length: 128, nullable: true, - }) - public name: string | null; - - @Column('varchar', { - length: 1024, nullable: true, - }) - public description: string | null; - - /** - * メンテナの名前 - */ - @Column('varchar', { - length: 128, nullable: true, - }) - public maintainerName: string | null; - - /** - * メンテナの連絡先 - */ - @Column('varchar', { - length: 128, nullable: true, - }) - public maintainerEmail: string | null; - - @Column('boolean', { - default: false, - }) - public disableRegistration: boolean; - - @Column('boolean', { - default: false, - }) - public disableLocalTimeline: boolean; - - @Column('boolean', { - default: false, - }) - public disableGlobalTimeline: boolean; - - @Column('boolean', { - default: false, - }) - public useStarForReactionFallback: boolean; - - @Column('varchar', { - length: 64, array: true, default: '{}', - }) - public langs: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public pinnedUsers: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public hiddenTags: string[]; - - @Column('varchar', { - length: 256, array: true, default: '{}', - }) - public blockedHosts: string[]; - - @Column('varchar', { - length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-foundkey}', - }) - 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, - }) - 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, - }) - public iconUrl: string | null; - - @Column('boolean', { - default: true, - }) - public cacheRemoteFiles: boolean; - - @Column({ - ...id(), - nullable: true, - }) - public proxyAccountId: User['id'] | null; - - @ManyToOne(() => User, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public proxyAccount: User | null; - - @Column('boolean', { - default: false, - }) - public emailRequiredForSignup: boolean; - - @Column('boolean', { - default: false, - }) - public enableHcaptcha: boolean; - - @Column('varchar', { - length: 64, - nullable: true, - }) - public hcaptchaSiteKey: string | null; - - @Column('varchar', { - length: 64, - nullable: true, - }) - public hcaptchaSecretKey: string | null; - - @Column('boolean', { - default: false, - }) - public enableRecaptcha: boolean; - - @Column('varchar', { - length: 64, - nullable: true, - }) - public recaptchaSiteKey: string | null; - - @Column('varchar', { - length: 64, - nullable: true, - }) - public recaptchaSecretKey: string | null; - - @Column('integer', { - default: 1024, - comment: 'Drive capacity of a local user (MB)', - }) - public localDriveCapacityMb: number; - - @Column('integer', { - default: 32, - comment: 'Drive capacity of a remote user (MB)', - }) - public remoteDriveCapacityMb: number; - - @Column('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('varchar', { - length: 128, - }) - public swPublicKey: string; - - @Column('varchar', { - length: 128, - }) - public swPrivateKey: string; - - @Column('enum', { - enum: TranslationService, - nullable: true, - }) - public translationService: TranslationService | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public deeplAuthKey: string | null; - - @Column('varchar', { - length: 128, - nullable: true, - }) - public libreTranslateAuthKey: string | null; - - @Column('varchar', { - length: 2048, - nullable: true, - }) - public libreTranslateEndpoint: string | null; - - @Column('varchar', { - length: 512, - nullable: true, - }) - public ToSUrl: 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; -} 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 ad97db27f..000000000 --- a/packages/backend/src/models/entities/moderation-log.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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 3c034bec2..000000000 --- a/packages/backend/src/models/entities/muted-note.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm'; -import { mutedNoteReasons } from 'foundkey-js'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { User } from './user.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(() => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Index() - @Column({ - ...id(), - comment: 'The user ID.', - }) - public userId: User['id']; - - @ManyToOne(() => 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 02b37a78d..000000000 --- a/packages/backend/src/models/entities/muting.ts +++ /dev/null @@ -1,48 +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 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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public mutee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The muter user ID.', - }) - public muterId: User['id']; - - @ManyToOne(() => 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 8b4449c3e..000000000 --- a/packages/backend/src/models/entities/note-favorite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(() => 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 d86421671..000000000 --- a/packages/backend/src/models/entities/note-reaction.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { Note } from './note.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user?: User | null; - - @Index() - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(() => 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 709dcbaa6..000000000 --- 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 { noteNotificationTypes } from 'foundkey-js'; -import { id } from '../id.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column('varchar', { - length: 256, - }) - public threadId: string; - - @Column('enum', { - enum: noteNotificationTypes, - array: true, - default: [], - }) - public mutingNotificationTypes: typeof noteNotificationTypes[number][]; -} 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 dbcc7e2d8..000000000 --- a/packages/backend/src/models/entities/note-unread.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { Note } from './note.js'; -import { 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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(() => 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 7e9dfdb98..000000000 --- a/packages/backend/src/models/entities/note-watching.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { Note } from './note.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The target Note ID.', - }) - public noteId: Note['id']; - - @ManyToOne(() => 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 1e5f418c5..000000000 --- a/packages/backend/src/models/entities/note.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { noteVisibilities } from 'foundkey-js'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { DriveFile } from './drive-file.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(() => 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(() => 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(() => 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('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(() => 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; - } - } -} diff --git a/packages/backend/src/models/entities/notification.ts b/packages/backend/src/models/entities/notification.ts deleted file mode 100644 index cab6551f7..000000000 --- a/packages/backend/src/models/entities/notification.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm'; -import { notificationTypes } from 'foundkey-js'; -import { id } from '../id.js'; -import { User } from './user.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'; - -@Entity() -export class Notification { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column('timestamp with time zone', { - comment: 'The created date of the Notification.', - }) - public createdAt: Date; - - /** - * 通知の受信者 - */ - @Index() - @Column({ - ...id(), - comment: 'The ID of recipient user of the Notification.', - }) - public notifieeId: User['id']; - - @ManyToOne(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public notifiee: User | null; - - /** - * 通知の送信者(initiator) - */ - @Index() - @Column({ - ...id(), - nullable: true, - comment: 'The ID of sender user of the Notification.', - }) - public notifierId: User['id'] | null; - - @ManyToOne(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public notifier: User | null; - - /** - * 通知の種類。 - * follow - フォローされた - * mention - 投稿で自分が言及された - * reply - (自分または自分がWatchしている)投稿が返信された - * renote - (自分または自分がWatchしている)投稿がRenoteされた - * quote - (自分または自分がWatchしている)投稿が引用Renoteされた - * reaction - (自分または自分がWatchしている)投稿にリアクションされた - * pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された - * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した - * receiveFollowRequest - フォローリクエストされた - * followRequestAccepted - 自分の送ったフォローリクエストが承認された - * groupInvited - グループに招待された - * app - アプリ通知 - */ - @Index() - @Column('enum', { - enum: notificationTypes, - comment: 'The type of the Notification.', - }) - public type: typeof notificationTypes[number]; - - /** - * 通知が読まれたかどうか - */ - @Index() - @Column('boolean', { - default: false, - comment: 'Whether the Notification is read.', - }) - public isRead: boolean; - - @Column({ - ...id(), - nullable: true, - }) - public noteId: Note['id'] | null; - - @ManyToOne(() => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Column({ - ...id(), - nullable: true, - }) - public followRequestId: FollowRequest['id'] | null; - - @ManyToOne(() => FollowRequest, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public followRequest: FollowRequest | null; - - @Column({ - ...id(), - nullable: true, - }) - public userGroupInvitationId: UserGroupInvitation['id'] | null; - - @ManyToOne(() => UserGroupInvitation, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public userGroupInvitation: UserGroupInvitation | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public reaction: string | null; - - @Column('integer', { - nullable: true, - }) - public choice: number | null; - - /** - * アプリ通知のbody - */ - @Column('varchar', { - length: 2048, nullable: true, - }) - public customBody: string | null; - - /** - * アプリ通知のheader - * (省略時はアプリ名で表示されることを期待) - */ - @Column('varchar', { - length: 256, nullable: true, - }) - public customHeader: string | null; - - /** - * アプリ通知のicon(URL) - * (省略時はアプリアイコンで表示されることを期待) - */ - @Column('varchar', { - length: 1024, nullable: true, - }) - public customIcon: string | null; - - /** - * アプリ通知のアプリ(のトークン) - */ - @Index() - @Column({ - ...id(), - nullable: true, - }) - public appAccessTokenId: AccessToken['id'] | null; - - @ManyToOne(() => 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 149def459..000000000 --- a/packages/backend/src/models/entities/page-like.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public pageId: Page['id']; - - @ManyToOne(() => 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 b25e064e2..000000000 --- a/packages/backend/src/models/entities/page.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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', { - 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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column({ - ...id(), - nullable: true, - }) - public eyeCatchingImageId: DriveFile['id'] | null; - - @ManyToOne(() => DriveFile, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public eyeCatchingImage: DriveFile | null; - - @Column('text', { - default: '', - }) - public text: 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 4edd3d2a8..000000000 --- a/packages/backend/src/models/entities/password-reset-request.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { id } from '../id.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(() => 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 9824f5017..000000000 --- a/packages/backend/src/models/entities/poll-vote.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { Note } from './note.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(() => 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 2e90a24d8..000000000 --- a/packages/backend/src/models/entities/poll.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { noteVisibilities } from 'foundkey-js'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { User } from './user.js'; - -@Entity() -export class Poll { - @PrimaryColumn(id()) - public noteId: Note['id']; - - @OneToOne(() => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; - - @Column('timestamp with time zone', { - nullable: true, - }) - public expiresAt: Date | null; - - @Column('boolean') - public multiple: boolean; - - @Column('varchar', { - length: 128, array: true, default: '{}', - }) - public choices: string[]; - - @Column('integer', { - array: true, - }) - public votes: number[]; - - //#region Denormalized fields - @Column('enum', { - enum: noteVisibilities, - comment: '[Denormalized]', - }) - public noteVisibility: typeof noteVisibilities[number]; - - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public userId: User['id']; - - @Index() - @Column('varchar', { - length: 128, nullable: true, - comment: '[Denormalized]', - }) - public userHost: string | null; - //#endregion - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } -} - -export type IPoll = { - choices: string[]; - votes?: number[]; - multiple: boolean; - expiresAt: Date | null; -}; diff --git a/packages/backend/src/models/entities/registration-tickets.ts b/packages/backend/src/models/entities/registration-tickets.ts deleted file mode 100644 index 139e40f85..000000000 --- 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 bbafae4a3..000000000 --- a/packages/backend/src/models/entities/registry-item.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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 94d192957..000000000 --- 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 ee9bbb49b..000000000 --- a/packages/backend/src/models/entities/renote-muting.ts +++ /dev/null @@ -1,42 +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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public mutee: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The muter user ID.', - }) - public muterId: User['id']; - - @ManyToOne(() => 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 19587eee4..000000000 --- a/packages/backend/src/models/entities/signin.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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 2e723b76d..000000000 --- a/packages/backend/src/models/entities/sw-subscription.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 512, - }) - public endpoint: string; - - @Column('varchar', { - length: 256, - }) - public auth: string; - - @Column('varchar', { - length: 128, - }) - public publickey: string; -} diff --git a/packages/backend/src/models/entities/used-username.ts b/packages/backend/src/models/entities/used-username.ts deleted file mode 100644 index eb90bef6c..000000000 --- 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 52899f348..000000000 --- a/packages/backend/src/models/entities/user-group-invitation.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { UserGroup } from './user-group.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The group ID.', - }) - public userGroupId: UserGroup['id']; - - @ManyToOne(() => 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 836db73dd..000000000 --- a/packages/backend/src/models/entities/user-group-joining.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { UserGroup } from './user-group.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The group ID.', - }) - public userGroupId: UserGroup['id']; - - @ManyToOne(() => 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 7a488a625..000000000 --- a/packages/backend/src/models/entities/user-group.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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-keypair.ts b/packages/backend/src/models/entities/user-keypair.ts deleted file mode 100644 index 596cb64a3..000000000 --- a/packages/backend/src/models/entities/user-keypair.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; - -@Entity() -export class UserKeypair { - @PrimaryColumn(id()) - public userId: User['id']; - - @OneToOne(() => 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 466113d58..000000000 --- a/packages/backend/src/models/entities/user-list-joining.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; -import { UserList } from './user-list.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Index() - @Column({ - ...id(), - comment: 'The list ID.', - }) - public userListId: UserList['id']; - - @ManyToOne(() => 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 922565c59..000000000 --- a/packages/backend/src/models/entities/user-list.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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 2f0eadd04..000000000 --- a/packages/backend/src/models/entities/user-note-pining.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { User } from './user.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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(() => 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 763794884..000000000 --- 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 efa525f35..000000000 --- a/packages/backend/src/models/entities/user-profile.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { ffVisibility, notificationTypes } from 'foundkey-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(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'The location of the User.', - }) - public location: string | null; - - @Column('char', { - length: 10, nullable: true, - comment: 'The birthday (YYYY-MM-DD) of the User.', - }) - public birthday: string | null; - - @Column('varchar', { - length: 2048, nullable: true, - comment: 'The description (bio) of the User.', - }) - public description: string | null; - - @Column('jsonb', { - default: [], - }) - public fields: { - name: string; - value: string; - }[]; - - @Column('varchar', { - length: 32, nullable: true, - }) - public lang: string | null; - - @Column('varchar', { - length: 512, nullable: true, - comment: 'Remote URL of the user.', - }) - public url: string | null; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'The email address of the User.', - }) - public email: string | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public emailVerifyCode: string | null; - - @Column('boolean', { - default: false, - }) - public emailVerified: boolean; - - @Column('jsonb', { - default: ['follow', 'receiveFollowRequest', 'groupInvited'], - }) - public emailNotificationTypes: string[]; - - @Column('boolean', { - default: false, - }) - public publicReactions: boolean; - - @Column('enum', { - enum: ffVisibility, - default: 'public', - }) - public ffVisibility: typeof ffVisibility[number]; - - @Column('varchar', { - length: 128, nullable: true, - }) - public twoFactorTempSecret: string | null; - - @Column('varchar', { - length: 128, nullable: true, - }) - public twoFactorSecret: string | null; - - @Column('boolean', { - default: false, - }) - public twoFactorEnabled: boolean; - - @Column('boolean', { - default: false, - }) - public securityKeysAvailable: boolean; - - @Column('boolean', { - default: false, - }) - public usePasswordLessLogin: boolean; - - @Column('varchar', { - length: 128, nullable: true, - comment: 'The password hash of the User. It will be null if the origin of the user is local.', - }) - public password: string | null; - - // TODO: そのうち消す - @Column('jsonb', { - default: {}, - comment: 'The client-specific data of the User.', - }) - public clientData: Record; - - @Column('boolean', { - default: false, - }) - public autoAcceptFollowed: boolean; - - @Column('boolean', { - default: false, - comment: 'Whether reject index by crawler.', - }) - public noCrawle: boolean; - - @Column('boolean', { - default: false, - }) - public alwaysMarkNsfw: boolean; - - @Column('boolean', { - default: false, - }) - public carefulBot: boolean; - - @Column('boolean', { - default: true, - }) - public injectFeaturedNote: boolean; - - @Column('boolean', { - default: true, - }) - public receiveAnnouncementEmail: boolean; - - @Column({ - ...id(), - nullable: true, - }) - public pinnedPageId: Page['id'] | null; - - @OneToOne(() => Page, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public pinnedPage: Page | null; - - @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 ab686567d..000000000 --- a/packages/backend/src/models/entities/user-publickey.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.js'; - -@Entity() -export class UserPublickey { - @PrimaryColumn(id()) - public userId: User['id']; - - @OneToOne(() => 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 1f472f7e9..000000000 --- a/packages/backend/src/models/entities/user-security-key.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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 0aee4c90b..000000000 --- a/packages/backend/src/models/entities/user.ts +++ /dev/null @@ -1,256 +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('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(() => 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(() => 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: 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; - - // for local users: - // Indicates a deletion in progress. - // A hard delete of the record will follow after the deletion finishes. - // - // for remote users: - // Indicates the user was deleted by an admin. - // The users' data is not deleted from the database to keep them from reappearing. - // A hard delete of the record may follow if we receive a matching Delete activity. - @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 local users, or null.', - }) - public token: string | null; - - @Column('boolean', { - default: true, - }) - public federateBlocks: boolean; - - 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; - token: string; -} - -export interface IRemoteUser extends User { - host: string; - token: null; -} - -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 fe559af1c..000000000 --- a/packages/backend/src/models/entities/webhook.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { User } from './user.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(() => 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 d614fc504..000000000 --- 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 993f3e0f2..000000000 --- a/packages/backend/src/models/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { db } from '@/db/postgre.js'; - -import { Announcement } from './entities/announcement.js'; -import { AnnouncementRead } from './entities/announcement-read.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 { 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 { 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'; - -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 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 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 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 cf0c33e7c..000000000 --- a/packages/backend/src/models/repositories/abuse-user-report.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Users } from '../index.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, - urls: report.urls, - }); - }, - - 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 70180e2de..000000000 --- a/packages/backend/src/models/repositories/antenna.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Antenna } from '@/models/entities/antenna.js'; -import { 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, - 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 182347e74..000000000 --- a/packages/backend/src/models/repositories/app.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { App } from '@/models/entities/app.js'; -import { AccessTokens } from '../index.js'; -import { 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, - description: app.description, - 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 59aec7d9e..000000000 --- a/packages/backend/src/models/repositories/auth-session.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { AuthSession } from '@/models/entities/auth-session.js'; -import { User } from '@/models/entities/user.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Apps } from '../index.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 637e04bdb..000000000 --- a/packages/backend/src/models/repositories/blocking.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { Blocking } from '@/models/entities/blocking.js'; -import { User } from '@/models/entities/user.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Users } from '../index.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 f6c2088c2..000000000 --- a/packages/backend/src/models/repositories/channel.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { Channel } from '@/models/entities/channel.js'; -import { User } from '@/models/entities/user.js'; -import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index.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 80096a539..000000000 --- a/packages/backend/src/models/repositories/clip.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { Clip } from '@/models/entities/clip.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Users } from '../index.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 f75207134..000000000 --- a/packages/backend/src/models/repositories/drive-file.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { User } from '@/models/entities/user.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Packed } from '@/misc/schema.js'; -import config from '@/config/index.js'; -import { query, appendQuery } from '@/prelude/url.js'; -import { Users, DriveFolders } from '../index.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 = structuredClone(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'].includes(file.type); - - return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url); - }, - - calcDriveUsageOf(id: User['id']): Promise { - return db.query('SELECT SUM(size) AS sum FROM drive_file WHERE "userId" = $1 AND NOT "isLink"', [id]) - .then(res => res[0].sum as number ?? 0); - }, - - calcDriveUsageOfHost(host: string): Promise { - return db.query('SELECT SUM(size) AS sum FROM drive_file WHERE "userHost" = $1 AND NOT "isLink"', [toPuny(host)]) - .then(res => res[0].sum as number ?? 0); - }, - - calcDriveUsageOfLocal(): Promise { - return db.query('SELECT SUM(size) AS sum FROM drive_file WHERE "userHost" IS NULL AND NOT "isLink"') - .then(res => res[0].sum as number ?? 0); - }, - - calcDriveUsageOfRemote(): Promise { - return db.query('SELECT SUM(size) AS sum FROM drive_file WHERE "userHost" IS NOT NULL AND NOT "isLink"') - .then(res => res[0].sum as number ?? 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 this.pack(file, opts); - }, - - 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 bb744a4b7..000000000 --- a/packages/backend/src/models/repositories/drive-folder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { DriveFolder } from '@/models/entities/drive-folder.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { DriveFolders, DriveFiles } from '../index.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 a0d390d79..000000000 --- a/packages/backend/src/models/repositories/emoji.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { 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, - }; - }, - - 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 dc81c35ed..000000000 --- a/packages/backend/src/models/repositories/follow-request.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { FollowRequest } from '@/models/entities/follow-request.js'; -import { User } from '@/models/entities/user.js'; -import { Users } from '../index.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 5d4c57af1..000000000 --- a/packages/backend/src/models/repositories/following.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { Following } from '@/models/entities/following.js'; -import { User } from '@/models/entities/user.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Users } from '../index.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 }); - - 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 33f5b3ebb..000000000 --- a/packages/backend/src/models/repositories/gallery-like.ts +++ /dev/null @@ -1,24 +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 7c54001ec..000000000 --- a/packages/backend/src/models/repositories/gallery-post.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { GalleryPost } from '@/models/entities/gallery-post.js'; -import { User } from '@/models/entities/user.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Users, DriveFiles, GalleryLikes } from '../index.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 e6c0e36f0..000000000 --- a/packages/backend/src/models/repositories/hashtag.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Hashtag } from '@/models/entities/hashtag.js'; -import { 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 8e6a33a13..000000000 --- a/packages/backend/src/models/repositories/instance.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Instance } from '@/models/entities/instance.js'; -import { Packed } from '@/misc/schema.js'; -import { shouldBlockInstance } from '@/misc/should-block-instance.js'; - -export const InstanceRepository = db.getRepository(Instance).extend({ - async pack( - instance: Instance, - ): Promise> { - 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 8c63bd819..000000000 --- a/packages/backend/src/models/repositories/messaging-message.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { User } from '@/models/entities/user.js'; -import { Users, DriveFiles, UserGroups } from '../index.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 77f479425..000000000 --- a/packages/backend/src/models/repositories/moderation-logs.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { ModerationLog } from '@/models/entities/moderation-log.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Users } from '../index.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 02ee075d8..000000000 --- a/packages/backend/src/models/repositories/muting.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { Muting } from '@/models/entities/muting.js'; -import { User } from '@/models/entities/user.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Users } from '../index.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 47d549455..000000000 --- a/packages/backend/src/models/repositories/note-favorite.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { NoteFavorite } from '@/models/entities/note-favorite.js'; -import { User } from '@/models/entities/user.js'; -import { Notes } from '../index.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 c3440f125..000000000 --- a/packages/backend/src/models/repositories/note-reaction.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { convertLegacyReaction } from '@/misc/reaction-lib.js'; -import { Packed } from '@/misc/schema.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { User } from '@/models/entities/user.js'; -import { Notes, Users } from '../index.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: boolean; - }, - ): 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 03874d358..000000000 --- a/packages/backend/src/models/repositories/note.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { In } from 'typeorm'; -import * as mfm from 'mfm-js'; -import { db } from '@/db/postgre.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { nyaize } from '@/misc/nyaize.js'; -import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; -import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { User } from '@/models/entities/user.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '../index.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 && 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 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, - 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), - tags: note.tags.length > 0 ? note.tags : undefined, - emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host), - fileIds: note.fileIds, - files: DriveFiles.packMany(note.fileIds), - replyId: note.replyId, - renoteId: note.renoteId, - channelId: note.channelId || undefined, - channel: channel ? { - id: channel.id, - name: channel.name, - } : undefined, - mentions: note.mentions.length > 0 ? note.mentions : undefined, - uri: note.uri || undefined, - url: note.url || undefined, - - ...(opts.detail ? { - reply: note.replyId ? this.pack(note.reply || note.replyId, me, { - detail: false, - _hint_: options?._hint_, - }) : undefined, - - renote: note.renoteId ? this.pack(note.renote || note.renoteId, me, { - detail: true, - _hint_: options?._hint_, - }) : undefined, - - poll: note.hasPoll ? populatePoll(note, meId) : undefined, - - ...(meId ? { - myReaction: populateMyReaction(note, meId, options?._hint_), - } : {}), - } : {}), - }); - - if (packed.user.isCat && packed.text) { - const tokens = packed.text ? mfm.parse(packed.text) : []; - function nyaizeNode(node: mfm.Node) { - 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 c62a24959..000000000 --- a/packages/backend/src/models/repositories/notification.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { In } from 'typeorm'; -import { noteNotificationTypes } from 'foundkey-js'; -import { db } from '@/db/postgre.js'; -import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { Notification } from '@/models/entities/notification.js'; -import { User } from '@/models/entities/user.js'; -import { awaitAll } from '@/prelude/await-all.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, - ...(noteNotificationTypes.includes(notification.type) ? { - note: Notes.pack(notification.note || notification.noteId!, { id: notification.notifieeId }, { - detail: true, - _hint_: options._hintForEachNotes_, - }), - } : {}), - ...(notification.type === 'reaction' ? { - reaction: notification.reaction, - } : {}), - ...(notification.type === 'pollVote' ? { - choice: notification.choice, - } : {}), - ...(notification.type === 'groupInvited' ? { - invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!), - } : {}), - ...(notification.type === 'app' ? { - body: notification.customBody, - header: notification.customHeader || token?.name, - icon: notification.customIcon || token?.iconUrl, - } : {}), - }); - }, - - 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)); - - return await Promise.all(notifications.map(x => this.pack(x, { - _hintForEachNotes_: { - myReactions: myReactionsMap, - }, - }))); - }, -}); 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 e5d01a05a..000000000 --- a/packages/backend/src/models/repositories/page-like.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { PageLike } from '@/models/entities/page-like.js'; -import { 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: any[], - me: { id: User['id'] }, - ) { - return Promise.all(likes.map(x => this.pack(x, me))); - }, -}); diff --git a/packages/backend/src/models/repositories/page.ts b/packages/backend/src/models/repositories/page.ts deleted file mode 100644 index 53d70b70d..000000000 --- a/packages/backend/src/models/repositories/page.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Page } from '@/models/entities/page.js'; -import { Packed } from '@/misc/schema.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import { 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 }); - - 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 } すると無限ループするので注意 - text: page.text, - title: page.title, - name: page.name, - summary: page.summary, - hideTitleWhenPinned: page.hideTitleWhenPinned, - alignCenter: page.alignCenter, - font: page.font, - eyeCatchingImageId: page.eyeCatchingImageId, - eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : 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 fa1c8f4d8..000000000 --- a/packages/backend/src/models/repositories/relay.ts +++ /dev/null @@ -1,5 +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 bcc62482a..000000000 --- a/packages/backend/src/models/repositories/renote-muting.ts +++ /dev/null @@ -1,31 +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 94410ec58..000000000 --- a/packages/backend/src/models/repositories/signin.ts +++ /dev/null @@ -1,10 +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 79ad019c9..000000000 --- a/packages/backend/src/models/repositories/user-group-invitation.ts +++ /dev/null @@ -1,22 +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 01566c6f2..000000000 --- a/packages/backend/src/models/repositories/user-group.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { UserGroupJoinings } from '../index.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 fffa3ca74..000000000 --- a/packages/backend/src/models/repositories/user-list.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Packed } from '@/misc/schema.js'; -import { UserList } from '@/models/entities/user-list.js'; -import { UserListJoinings } from '../index.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 6a796b223..000000000 --- a/packages/backend/src/models/repositories/user.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { In, Not } from 'typeorm'; -import Ajv from 'ajv'; -import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import config from '@/config/index.js'; -import { Packed } from '@/misc/schema.js'; -import { awaitAll, Promiseable } from '@/prelude/await-all.js'; -import { populateEmojis } from '@/misc/populate-emojis.js'; -import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js'; -import { Cache } from '@/misc/cache.js'; -import { db } from '@/db/postgre.js'; -import { Instance } from '../entities/instance.js'; -import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, RenoteMutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js'; - -const userInstanceCache = new Cache( - 3 * HOUR, - (host) => Instances.findOneBy({ host }).then(x => x ?? undefined), -); - -type IsUserDetailed = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>; -type IsMeAndIsUserDetailed = - 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; token: string; }; -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; }; -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 { - return await db.query( - `SELECT EXISTS ( - SELECT 1 - FROM "messaging_message" - WHERE - "recipientId" = $1 - AND - NOT "isRead" - AND - "userId" NOT IN ( - SELECT "muteeId" - FROM "muting" - WHERE "muterId" = $1 - ) - - UNION - - SELECT 1 - FROM "messaging_message" - JOIN "user_group_joining" - ON "messaging_message"."groupId" = "user_group_joining"."userGroupId" - WHERE - "user_group_joining"."userId" = $1 - AND - "messaging_message"."userId" != $1 - AND - NOT $1 = ANY("messaging_message"."reads") - AND - "messaging_message"."createdAt" > "user_group_joining"."createdAt" - ) AS exists`, - [userId] - ).then(res => res[0].exists); - }, - - async getHasUnreadAnnouncement(userId: User['id']): Promise { - return await db.query( - `SELECT EXISTS (SELECT 1 FROM "announcement" WHERE "id" NOT IN (SELECT "announcementId" FROM "announcement_read" WHERE "userId" = $1)) AS exists`, - [userId] - ).then(res => res[0].exists); - }, - - async getHasUnreadAntenna(userId: User['id']): Promise { - return await db.query( - `SELECT EXISTS (SELECT 1 FROM "antenna_note" WHERE NOT "read" AND "antennaId" IN (SELECT "id" FROM "antenna" WHERE "userId" = $1)) AS exists`, - [userId] - ).then(res => res[0].exists); - }, - - async getHasUnreadChannel(userId: User['id']): Promise { - return await db.query( - `SELECT EXISTS (SELECT 1 FROM "note_unread" WHERE "noteChannelId" IN (SELECT "followeeId" FROM "channel_following" WHERE "followerId" = $1)) AS exists`, - [userId] - ).then(res => res[0].exists); - }, - - async getHasUnreadNotification(userId: User['id']): Promise { - return await db.query( - `SELECT EXISTS (SELECT 1 FROM "notification" WHERE NOT "isRead" AND "notifieeId" = $1 AND "notifierId" NOT IN (SELECT "muteeId" FROM "muting" WHERE "muterId" = $1)) AS exists`, - [userId] - ).then(res => res[0].exists); - }, - - async getHasPendingReceivedFollowRequest(userId: User['id']): Promise { - return await db.query( - `SELECT EXISTS (SELECT 1 FROM "follow_request" WHERE "followeeId" = $1) AS exists`, - [userId] - ).then(res => res[0].exists); - }, - - 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( - 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, - isAdmin: user.isAdmin || falsy, - isModerator: user.isModerator || falsy, - isBot: user.isBot || falsy, - isCat: user.isCat || falsy, - instance: !user.host ? undefined : userInstanceCache.fetch(user.host) - .then(instance => !instance ? undefined : { - name: instance.name, - softwareName: instance.softwareName, - softwareVersion: instance.softwareVersion, - iconUrl: instance.iconUrl, - faviconUrl: instance.faviconUrl, - themeColor: instance.themeColor, - }), - emojis: populateEmojis(user.emojis, user.host), - onlineStatus: this.getOnlineStatus(user), - - ...(opts.detail ? { - url: profile!.url, - uri: user.uri, - createdAt: user.createdAt.toISOString(), - updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, - lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, - bannerUrl: user.banner ? DriveFiles.getPublicUrl(user.banner, false) : null, - bannerBlurhash: user.banner?.blurhash || 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, - 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), - mutedWords: profile!.mutedWords, - mutedInstances: profile!.mutedInstances, - mutingNotificationTypes: profile!.mutingNotificationTypes, - emailNotificationTypes: profile!.emailNotificationTypes, - showTimelineReplies: user.showTimelineReplies || falsy, - federateBlocks: user!.federateBlocks, - } : {}), - - ...(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>; - - 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 9cf522802..000000000 --- a/packages/backend/src/models/schema/antenna.ts +++ /dev/null @@ -1,89 +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'], - }, - 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, - }, - }, - 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 c80dc81c3..000000000 --- a/packages/backend/src/models/schema/app.ts +++ /dev/null @@ -1,33 +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 553232242..000000000 --- a/packages/backend/src/models/schema/blocking.ts +++ /dev/null @@ -1,26 +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 7f4f2a48b..000000000 --- a/packages/backend/src/models/schema/channel.ts +++ /dev/null @@ -1,51 +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 f0ee2ce0c..000000000 --- a/packages/backend/src/models/schema/clip.ts +++ /dev/null @@ -1,38 +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 435907661..000000000 --- a/packages/backend/src/models/schema/drive-file.ts +++ /dev/null @@ -1,107 +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 88cb8ab4a..000000000 --- a/packages/backend/src/models/schema/drive-folder.ts +++ /dev/null @@ -1,39 +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 e97fdd5ef..000000000 --- a/packages/backend/src/models/schema/emoji.ts +++ /dev/null @@ -1,37 +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, - }, - }, -} 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 93327304f..000000000 --- a/packages/backend/src/models/schema/federation-instance.ts +++ /dev/null @@ -1,110 +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: 'misskey.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: 'misskey', - }, - 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 2bcffbfc4..000000000 --- a/packages/backend/src/models/schema/following.ts +++ /dev/null @@ -1,36 +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 fc503d4a6..000000000 --- a/packages/backend/src/models/schema/gallery-post.ts +++ /dev/null @@ -1,69 +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 98f882764..000000000 --- a/packages/backend/src/models/schema/hashtag.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const packedHashtagSchema = { - type: 'object', - properties: { - tag: { - type: 'string', - optional: false, nullable: false, - example: 'misskey', - }, - 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 b1ffa4595..000000000 --- a/packages/backend/src/models/schema/messaging-message.ts +++ /dev/null @@ -1,73 +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 3ab99e17e..000000000 --- a/packages/backend/src/models/schema/muting.ts +++ /dev/null @@ -1,31 +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 d133f7367..000000000 --- a/packages/backend/src/models/schema/note-favorite.ts +++ /dev/null @@ -1,26 +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 0d8fc5449..000000000 --- a/packages/backend/src/models/schema/note-reaction.ts +++ /dev/null @@ -1,25 +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 292bbb82f..000000000 --- a/packages/backend/src/models/schema/note.ts +++ /dev/null @@ -1,179 +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: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - name: { - type: 'string', - optional: false, nullable: false, - }, - url: { - type: 'string', - optional: false, 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 75634dc56..000000000 --- a/packages/backend/src/models/schema/notification.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { notificationTypes } from 'foundkey-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 658361bf5..000000000 --- a/packages/backend/src/models/schema/page.ts +++ /dev/null @@ -1,47 +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, - }, - text: { - type: 'string', - optional: false, nullable: false, - }, - userId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - user: { - type: 'object', - ref: 'UserLite', - 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 7ceeda26a..000000000 --- a/packages/backend/src/models/schema/queue.ts +++ /dev/null @@ -1,25 +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 69ed8510d..000000000 --- a/packages/backend/src/models/schema/renote-muting.ts +++ /dev/null @@ -1,26 +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 a73bf82bb..000000000 --- a/packages/backend/src/models/schema/user-group.ts +++ /dev/null @@ -1,34 +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 3ba5dc4a8..000000000 --- a/packages/backend/src/models/schema/user-list.ts +++ /dev/null @@ -1,29 +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 fcb61f918..000000000 --- a/packages/backend/src/models/schema/user.ts +++ /dev/null @@ -1,468 +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: 'ai', - }, - 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, - }, - 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, - }, - 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, - }, - 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, - }, - 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 891a65ba2..000000000 --- a/packages/backend/src/prelude/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Prelude -The code in this directory is intended to complement the expressive capabilities of JavaScript. -It is a collection of code that is independent of FoundKey-specific processing, but is intended to make FoundKey code easier to read. diff --git a/packages/backend/src/prelude/array.ts b/packages/backend/src/prelude/array.ts deleted file mode 100644 index 4e2088e72..000000000 --- a/packages/backend/src/prelude/array.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { 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): Record { - 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 fd9832d6f..000000000 --- a/packages/backend/src/prelude/await-all.ts +++ /dev/null @@ -1,21 +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 || !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/relation.ts b/packages/backend/src/prelude/relation.ts deleted file mode 100644 index 1f4703f52..000000000 --- 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/symbol.ts b/packages/backend/src/prelude/symbol.ts deleted file mode 100644 index 51e12f745..000000000 --- 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 4e2e320b0..000000000 --- a/packages/backend/src/prelude/time.ts +++ /dev/null @@ -1,25 +0,0 @@ -const dateTimeIntervals = { - 'day': 86400000, - 'hour': 3600000, - 'ms': 1, -}; - -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 a4f2f7f5a..000000000 --- a/packages/backend/src/prelude/url.ts +++ /dev/null @@ -1,13 +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 b4469a1d8..000000000 --- a/packages/backend/src/prelude/xml.ts +++ /dev/null @@ -1,41 +0,0 @@ -const map: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', -}; - -const beginingOfCDATA = ''; - -export function escapeValue(x: string): string { - let insideOfCDATA = false; - let builder = ''; - for ( - let i = 0; - i < x.length; - ) { - if (insideOfCDATA) { - if (x.slice(i, i + beginingOfCDATA.length) === beginingOfCDATA) { - insideOfCDATA = true; - i += beginingOfCDATA.length; - } else { - builder += x[i++]; - } - } else { - if (x.slice(i, i + endOfCDATA.length) === endOfCDATA) { - insideOfCDATA = false; - i += endOfCDATA.length; - } else { - const b = x[i++]; - builder += map[b] || b; - } - } - } - return builder; -} - -export function escapeAttribute(x: string): string { - return Object.entries(map).reduce((a, [k, v]) => a.replace(k, v), x); -} diff --git a/packages/backend/src/queue/get-job-info.ts b/packages/backend/src/queue/get-job-info.ts deleted file mode 100644 index d33e349c3..000000000 --- a/packages/backend/src/queue/get-job-info.ts +++ /dev/null @@ -1,15 +0,0 @@ -import 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 57de62a79..000000000 --- a/packages/backend/src/queue/index.ts +++ /dev/null @@ -1,338 +0,0 @@ -import httpSignature from '@peertube/http-signature'; -import { v4 as uuid } from 'uuid'; - -import config from '@/config/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js'; -import { IActivity } from '@/remote/activitypub/type.js'; -import { envOption } from '@/env.js'; -import { MINUTE } from '@/const.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 { 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 } from './queues.js'; -import { 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: MINUTE, - backoff: { - type: 'apBackoff', - }, - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { - const data = { - activity, - signature, - }; - - return inboxQueue.add(data, { - attempts: config.inboxJobMaxAttempts || 8, - timeout: 5 * MINUTE, - backoff: { - type: 'apBackoff', - }, - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createDeleteDriveFilesJob(user: ThinUser) { - return dbQueue.add('deleteDriveFiles', { - user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportCustomEmojisJob(user: ThinUser, ids: string[] | undefined) { - return dbQueue.add('exportCustomEmojis', { - user, - ids, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportNotesJob(user: ThinUser) { - return dbQueue.add('exportNotes', { - user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) { - return dbQueue.add('exportFollowing', { - user, - excludeMuting, - excludeInactive, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportMuteJob(user: ThinUser) { - return dbQueue.add('exportMute', { - user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportBlockingJob(user: ThinUser) { - return dbQueue.add('exportBlocking', { - user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createExportUserListsJob(user: ThinUser) { - return dbQueue.add('exportUserLists', { - user, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importFollowing', { - user, - fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importMuting', { - user, - fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importBlocking', { - user, - fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importUserLists', { - user, - fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) { - return dbQueue.add('importCustomEmojis', { - user, - fileId, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) { - return dbQueue.add('deleteAccount', { - user, - soft: opts.soft, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createDeleteObjectStorageFileJob(key: string) { - return objectStorageQueue.add('deleteFile', { - key, - }, { - removeOnComplete: true, - removeOnFail: true, - }); -} - -export function createCleanRemoteFilesJob() { - return objectStorageQueue.add('cleanRemoteFiles', {}, { - 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: MINUTE, - 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); - - 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('checkExpired', { - }, { - 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 d866ef61b..000000000 --- a/packages/backend/src/queue/initialize.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Bull from 'bull'; -import { SECOND, MINUTE, HOUR } from '@/const.js'; -import config from '@/config/index.js'; -import { getRedisOptions } from '@/config/redis.js'; - -export function initialize(name: string, limitPerSec = -1): Bull.Queue { - return new Bull(name, { - redis: getRedisOptions(), - prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue', - limiter: limitPerSec > 0 ? { - max: limitPerSec, - duration: SECOND, - } : undefined, - settings: { - backoffStrategies: { - apBackoff, - }, - }, - }); -} - -// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 -function apBackoff(attemptsMade: number /*, err: Error */): number { - const baseDelay = MINUTE; - const maxBackoff = 8 * HOUR; - 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 2843a3c26..000000000 --- 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/db/delete-account.ts b/packages/backend/src/queue/processors/db/delete-account.ts deleted file mode 100644 index 84e28f25d..000000000 --- a/packages/backend/src/queue/processors/db/delete-account.ts +++ /dev/null @@ -1,92 +0,0 @@ -import Bull from 'bull'; -import { MoreThan } from 'typeorm'; -import { DriveFiles, Notes, UserProfiles, Users } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { Note } from '@/models/entities/note.js'; -import { queueLogger } from '@/queue/logger.js'; -import { DbUserDeleteJobData } from '@/queue/types.js'; -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.'); - } - } - - // No physical deletion if soft is specified. - if (!job.data.soft) { - 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 503490657..000000000 --- a/packages/backend/src/queue/processors/db/delete-drive-files.ts +++ /dev/null @@ -1,55 +0,0 @@ -import Bull from 'bull'; -import { MoreThan } from 'typeorm'; -import { Users, DriveFiles } from '@/models/index.js'; -import { deleteFileSync } from '@/services/drive/delete-file.js'; -import { DbUserJobData } from '@/queue/types.js'; -import { queueLogger } from '@/queue/logger.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 29374e26f..000000000 --- a/packages/backend/src/queue/processors/db/export-blocking.ts +++ /dev/null @@ -1,93 +0,0 @@ -import * as fs from 'node:fs'; -import Bull from 'bull'; -import { format as dateFormat } from 'date-fns'; -import { MoreThan } from 'typeorm'; - -import { getFullApAccount } from '@/misc/convert-host.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { Users, Blockings } from '@/models/index.js'; -import { DbUserJobData } from '@/queue/types.js'; -import { queueLogger } from '@/queue/logger.js'; -import { addFile } from '@/services/drive/add-file.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 c7e2e825d..000000000 --- a/packages/backend/src/queue/processors/db/export-custom-emojis.ts +++ /dev/null @@ -1,112 +0,0 @@ -import * as fs from 'node:fs'; -import archiver from 'archiver'; -import Bull from 'bull'; -import { format as dateFormat } from 'date-fns'; -import mime from 'mime-types'; -import { In, IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import { createTemp, createTempDir } from '@/misc/create-temp.js'; -import { downloadUrl } from '@/misc/download-url.js'; -import { Users, Emojis } from '@/models/index.js'; -import { addFile } from '@/services/drive/add-file.js'; -import { queueLogger } from '@/queue/logger.js'; - -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(), - ...(job.data.ids ? { id: In(job.data.ids) } : {}), - }, - 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, - downloaded, - 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 6e115af0b..000000000 --- a/packages/backend/src/queue/processors/db/export-following.ts +++ /dev/null @@ -1,95 +0,0 @@ -import * as fs from 'node:fs'; -import Bull from 'bull'; -import { format as dateFormat } from 'date-fns'; -import { In, MoreThan, Not } from 'typeorm'; - -import { MONTH } from '@/const.js'; -import { getFullApAccount } from '@/misc/convert-host.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { Following } from '@/models/entities/following.js'; -import { Users, Followings, Mutings } from '@/models/index.js'; -import { DbUserJobData } from '@/queue/types.js'; -import { queueLogger } from '@/queue/logger.js'; -import { addFile } from '@/services/drive/add-file.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() > 3 * MONTH)) { - 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 8ff8851ce..000000000 --- a/packages/backend/src/queue/processors/db/export-mute.ts +++ /dev/null @@ -1,94 +0,0 @@ -import * as fs from 'node:fs'; -import Bull from 'bull'; -import { format as dateFormat } from 'date-fns'; -import { IsNull, MoreThan } from 'typeorm'; - -import { getFullApAccount } from '@/misc/convert-host.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { Users, Mutings } from '@/models/index.js'; -import { DbUserJobData } from '@/queue/types.js'; -import { queueLogger } from '@/queue/logger.js'; -import { addFile } from '@/services/drive/add-file.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 b04aed9fa..000000000 --- a/packages/backend/src/queue/processors/db/export-notes.ts +++ /dev/null @@ -1,118 +0,0 @@ -import * as fs from 'node:fs'; -import Bull from 'bull'; -import { format as dateFormat } from 'date-fns'; -import { MoreThan } from 'typeorm'; - -import { createTemp } from '@/misc/create-temp.js'; -import { Note } from '@/models/entities/note.js'; -import { Poll } from '@/models/entities/poll.js'; -import { Users, Notes, Polls } from '@/models/index.js'; -import { DbUserJobData } from '@/queue/types.js'; -import { addFile } from '@/services/drive/add-file.js'; -import { queueLogger } from '@/queue/logger.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, - 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 22521ca03..000000000 --- a/packages/backend/src/queue/processors/db/export-user-lists.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as fs from 'node:fs'; -import Bull from 'bull'; -import { format as dateFormat } from 'date-fns'; -import { In } from 'typeorm'; - -import { getFullApAccount } from '@/misc/convert-host.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { Users, UserLists, UserListJoinings } from '@/models/index.js'; -import { DbUserJobData } from '@/queue/types.js'; -import { queueLogger } from '@/queue/logger.js'; -import { addFile } from '@/services/drive/add-file.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 cc2ccc8e3..000000000 --- a/packages/backend/src/queue/processors/db/import-blocking.ts +++ /dev/null @@ -1,75 +0,0 @@ -import Bull from 'bull'; -import { IsNull } from 'typeorm'; - -import * as Acct from '@/misc/acct.js'; -import { isSelfHost, toPuny } from '@/misc/convert-host.js'; -import { downloadTextFile } from '@/misc/download-text-file.js'; -import { Users, DriveFiles } from '@/models/index.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import { queueLogger } from '@/queue/logger.js'; -import { resolveUser } from '@/remote/resolve-user.js'; -import block from '@/services/blocking/create.js'; - -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 1d06d5ff8..000000000 --- a/packages/backend/src/queue/processors/db/import-custom-emojis.ts +++ /dev/null @@ -1,81 +0,0 @@ -import * as fs from 'node:fs'; -import Bull from 'bull'; -import unzipper from 'unzipper'; - -import { db } from '@/db/postgre.js'; -import { createTempDir } from '@/misc/create-temp.js'; -import { downloadUrl } from '@/misc/download-url.js'; -import { genId } from '@/misc/gen-id.js'; -import { DriveFiles, Emojis } from '@/models/index.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import { queueLogger } from '@/queue/logger.js'; -import { addFile } from '@/services/drive/add-file.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 }); - 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, - }); - } - - 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 a63491d1d..000000000 --- a/packages/backend/src/queue/processors/db/import-following.ts +++ /dev/null @@ -1,74 +0,0 @@ -import Bull from 'bull'; -import { IsNull } from 'typeorm'; - -import * as Acct from '@/misc/acct.js'; -import { isSelfHost, toPuny } from '@/misc/convert-host.js'; -import { downloadTextFile } from '@/misc/download-text-file.js'; -import { Users, DriveFiles } from '@/models/index.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import { queueLogger } from '@/queue/logger.js'; -import { resolveUser } from '@/remote/resolve-user.js'; -import follow from '@/services/following/create.js'; - -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; - - 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 4dcd50397..000000000 --- a/packages/backend/src/queue/processors/db/import-muting.ts +++ /dev/null @@ -1,84 +0,0 @@ -import Bull from 'bull'; -import { IsNull } from 'typeorm'; - -import * as Acct from '@/misc/acct.js'; -import { isSelfHost, toPuny } from '@/misc/convert-host.js'; -import { downloadTextFile } from '@/misc/download-text-file.js'; -import { genId } from '@/misc/gen-id.js'; -import { Users, DriveFiles, Mutings } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import { queueLogger } from '@/queue/logger.js'; -import { resolveUser } from '@/remote/resolve-user.js'; - -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-user-lists.ts b/packages/backend/src/queue/processors/db/import-user-lists.ts deleted file mode 100644 index 69fe823d2..000000000 --- a/packages/backend/src/queue/processors/db/import-user-lists.ts +++ /dev/null @@ -1,80 +0,0 @@ -import Bull from 'bull'; -import { IsNull } from 'typeorm'; - -import * as Acct from '@/misc/acct.js'; -import { isSelfHost, toPuny } from '@/misc/convert-host.js'; -import { downloadTextFile } from '@/misc/download-text-file.js'; -import { genId } from '@/misc/gen-id.js'; -import { DriveFiles, Users, UserLists, UserListJoinings } from '@/models/index.js'; -import { DbUserImportJobData } from '@/queue/types.js'; -import { resolveUser } from '@/remote/resolve-user.js'; -import { pushUserToUserList } from '@/services/user-list/push.js'; -import { queueLogger } from '@/queue/logger.js'; - -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 e91d56977..000000000 --- a/packages/backend/src/queue/processors/db/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Bull from 'bull'; -import { 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 { 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, - importCustomEmojis, - deleteAccount, -} as Record | Bull.ProcessPromiseFunction>; - -export default function(dbQueue: Bull.Queue) { - for (const [k, v] of Object.entries(jobs)) { - dbQueue.process(k, v); - } -} diff --git a/packages/backend/src/queue/processors/deliver.ts b/packages/backend/src/queue/processors/deliver.ts deleted file mode 100644 index 60355e39e..000000000 --- a/packages/backend/src/queue/processors/deliver.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { URL } from 'node:url'; -import Bull from 'bull'; -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 { DeliverJobData } from '@/queue/types.js'; - -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) { - // A client error means that something is wrong with the request we are making, - // which means that retrying it makes no sense. - 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 bdbbdc72e..000000000 --- a/packages/backend/src/queue/processors/ended-poll-notification.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Bull from 'bull'; -import { Notes, PollVotes } from '@/models/index.js'; -import { EndedPollNotificationJobData } from '@/queue/types.js'; -import { createNotification } from '@/services/create-notification.js'; - -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 db2d87dce..000000000 --- a/packages/backend/src/queue/processors/inbox.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { URL } from 'node:url'; -import 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 { toPuny, extractDbHost } from '@/misc/convert-host.js'; -import { getApId } from '@/remote/activitypub/type.js'; -import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js'; -import { getAuthUser } from '@/remote/activitypub/misc/auth-user.js'; -import { StatusError } from '@/misc/fetch.js'; -import { InboxJobData } from '@/queue/types.js'; -import { shouldBlockInstance } from '@/misc/should-block-instance.js'; - -const logger = new Logger('inbox'); - -// ユーザーのinboxにアクティビティが届いた時の処理 -export default async (job: Bull.Job): Promise => { - const signature = job.data.signature; // HTTP-signature - const activity = job.data.activity; - - //#region Log - const info = Object.assign({}, activity) as any; - delete info['@context']; - logger.debug(JSON.stringify(info, null, 2)); - //#endregion - - const host = toPuny(new URL(signature.keyId).hostname); - - // Stop if the host is blocked. - if (await shouldBlockInstance(host)) { - return `Blocked request: ${host}`; - } - - const keyIdLower = signature.keyId.toLowerCase(); - if (keyIdLower.startsWith('acct:')) { - return `Old keyId is no longer supported. ${keyIdLower}`; - } - - const resolver = new Resolver(); - - let authUser; - try { - authUser = await getAuthUser(signature.keyId, getApId(activity.actor), resolver); - } catch (e) { - if (e instanceof StatusError) { - if (e.isClientError) { - return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`; - } else { - throw new Error(`Error in actor ${activity.actor} - ${e.statusCode || e}`); - } - } - } - - if (authUser == null) { - // Key not found? Unacceptable! - return 'skip: failed to resolve user'; - } else { - // Found key! - } - - // verify the HTTP Signature - const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); - - // The signature must be valid. - // The signature must also match the actor otherwise anyone could sign any activity. - if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { - // Last resort: LD-Signature - if (activity.signature) { - if (activity.signature.type !== 'RsaSignature2017') { - return `skip: unsupported LD-signature type ${activity.signature.type}`; - } - - // get user based on LD-Signature key id. - // lets assume that the creator has this common form: - // - // Then we can use it as the key id and (without fragment part) user id. - authUser = await getAuthUser(activity.signature.creator, activity.signature.creator.replace(/#.*$/, ''), resolver); - - if (authUser == null) { - return 'skip: failed to resolve LD-Signature user'; - } - - // LD-Signature verification - const ldSignature = new LdSignature(); - const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); - if (!verified) { - return 'skip: LD-Signatureの検証に失敗しました'; - } - - // Again, the actor must match. - if (authUser.user.uri !== activity.actor) { - return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`; - } - - // Stop if the host is blocked. - const ldHost = extractDbHost(authUser.user.uri); - if (await shouldBlockInstance(ldHost)) { - return `Blocked request: ${ldHost}`; - } - } else { - return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`; - } - } - - if (typeof activity.id === 'string') { - // Verify that activity and actor are from the same host. - const signerHost = extractDbHost(authUser.user.uri!); - const activityIdHost = extractDbHost(activity.id); - if (signerHost !== activityIdHost) { - return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`; - } - - // Verify that the id has a sane length - if (activity.id.length > 2048) { - return `skip: overly long id from ${signerHost}`; - } - } - - // 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, resolver); - 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 17d165bf2..000000000 --- a/packages/backend/src/queue/processors/object-storage/clean-remote-files.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Bull from 'bull'; -import { MoreThan, Not, IsNull } from 'typeorm'; - -import { DriveFiles } from '@/models/index.js'; -import { deleteFileSync } from '@/services/drive/delete-file.js'; -import { queueLogger } from '@/queue/logger.js'; - -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 399a9d150..000000000 --- a/packages/backend/src/queue/processors/object-storage/delete-file.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Bull from 'bull'; -import { ObjectStorageFileJobData } from '@/queue/types.js'; -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 ae6c481fe..000000000 --- a/packages/backend/src/queue/processors/object-storage/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Bull from 'bull'; -import { ObjectStorageJobData } from '@/queue/types.js'; -import deleteFile from './delete-file.js'; -import cleanRemoteFiles from './clean-remote-files.js'; - -const jobs = { - deleteFile, - cleanRemoteFiles, -} as Record | Bull.ProcessPromiseFunction>; - -export default function(q: Bull.Queue) { - for (const [k, v] of Object.entries(jobs)) { - q.process(k, 16, v); - } -} diff --git a/packages/backend/src/queue/processors/system/check-expired.ts b/packages/backend/src/queue/processors/system/check-expired.ts deleted file mode 100644 index eeb6149bb..000000000 --- a/packages/backend/src/queue/processors/system/check-expired.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Bull from 'bull'; -import { In, LessThan } from 'typeorm'; -import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins } from '@/models/index.js'; -import { publishUserEvent } from '@/services/stream.js'; -import { MINUTE, MONTH } from '@/const.js'; -import { queueLogger } from '@/queue/logger.js'; - -const logger = queueLogger.createSubLogger('check-expired'); - -export async function checkExpired(job: Bull.Job>, done: any): Promise { - logger.info('Checking expired data...'); - - const expiredMutings = await Mutings.createQueryBuilder('muting') - .where('muting.expiresAt IS NOT NULL') - .andWhere('muting.expiresAt < :now', { now: new Date() }) - .innerJoinAndSelect('muting.mutee', 'mutee') - .getMany(); - - if (expiredMutings.length > 0) { - await Mutings.delete({ - id: In(expiredMutings.map(m => m.id)), - }); - - for (const m of expiredMutings) { - publishUserEvent(m.muterId, 'unmute', m.mutee!); - } - } - - const OlderThan = (millis: number) => { - return LessThan(new Date(new Date().getTime() - millis)); - }; - - await Signins.delete({ - createdAt: OlderThan(2 * MONTH), - }); - - await AttestationChallenges.delete({ - createdAt: OlderThan(5 * MINUTE), - }); - - await PasswordResetRequests.delete({ - // this timing should be the same as in @/server/api/endpoints/reset-password.ts - createdAt: OlderThan(30 * MINUTE), - }); - - await AuthSessions.delete({ - createdAt: OlderThan(15 * MINUTE), - }); - - await Notifications.delete({ - isRead: true, - createdAt: OlderThan(3 * MONTH), - }); - - logger.succ('Deleted expired data.'); - - 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 b5e8ec09f..000000000 --- a/packages/backend/src/queue/processors/system/clean-charts.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Bull from 'bull'; - -import { activeUsersChart, driveChart, federationChart, hashtagChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js'; -import { queueLogger } from '@/queue/logger.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/index.ts b/packages/backend/src/queue/processors/system/index.ts deleted file mode 100644 index 3651f8790..000000000 --- a/packages/backend/src/queue/processors/system/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Bull from 'bull'; -import { tickCharts } from './tick-charts.js'; -import { resyncCharts } from './resync-charts.js'; -import { cleanCharts } from './clean-charts.js'; -import { checkExpired } from './check-expired.js'; - -const jobs = { - tickCharts, - resyncCharts, - cleanCharts, - checkExpired, -} as Record> | Bull.ProcessPromiseFunction>>; - -export default function(dbQueue: Bull.Queue>) { - for (const [k, v] of Object.entries(jobs)) { - dbQueue.process(k, v); - } -} diff --git a/packages/backend/src/queue/processors/system/resync-charts.ts b/packages/backend/src/queue/processors/system/resync-charts.ts deleted file mode 100644 index b0a0e933b..000000000 --- a/packages/backend/src/queue/processors/system/resync-charts.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Bull from 'bull'; - -import { driveChart, notesChart, usersChart } from '@/services/chart/index.js'; -import { queueLogger } from '@/queue/logger.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 451ec1079..000000000 --- a/packages/backend/src/queue/processors/system/tick-charts.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Bull from 'bull'; - -import { activeUsersChart, driveChart, federationChart, hashtagChart, instanceChart, notesChart, perUserDriveChart, perUserFollowingChart, perUserNotesChart, perUserReactionsChart, usersChart, apRequestChart } from '@/services/chart/index.js'; -import { queueLogger } from '@/queue/logger.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 00ba9ded2..000000000 --- a/packages/backend/src/queue/processors/webhook-deliver.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Bull from 'bull'; -import config from '@/config/index.js'; -import { getResponse, StatusError } from '@/misc/fetch.js'; -import { Webhooks } from '@/models/index.js'; -import Logger from '@/services/logger.js'; -import { WebhookDeliverJobData } from '@/queue/types.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': 'Misskey-Hooks', - 'X-Misskey-Host': config.host, - 'X-Misskey-Hook-Id': job.data.webhookId, - 'X-Misskey-Hook-Secret': job.data.secret, - }, - 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 f3a267790..000000000 --- a/packages/backend/src/queue/queues.ts +++ /dev/null @@ -1,21 +0,0 @@ -import config from '@/config/index.js'; -import { initialize as initializeQueue } from './initialize.js'; -import { 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 queues = [ - systemQueue, - endedPollNotificationQueue, - deliverQueue, - inboxQueue, - dbQueue, - objectStorageQueue, - webhookDeliverQueue, -]; diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts deleted file mode 100644 index 82bd28703..000000000 --- a/packages/backend/src/queue/types.ts +++ /dev/null @@ -1,63 +0,0 @@ -import httpSignature from '@peertube/http-signature'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; -import { Webhook } from '@/models/entities/webhook.js'; -import { IActivity } from '@/remote/activitypub/type.js'; - -export type DeliverJobData = { - /** Actor */ - user: ThinUser; - /** Activity */ - content: unknown; - /** inbox URL to deliver */ - to: string; -}; - -export type InboxJobData = { - activity: IActivity; - signature: httpSignature.IParsedSignature; -}; - -export type DbJobData = DbUserJobData | DbUserImportJobData | DbUserDeleteJobData; - -export type DbUserJobData = { - user: ThinUser; - excludeMuting: boolean; - excludeInactive: boolean; -}; - -export type DbUserDeleteJobData = { - user: ThinUser; - soft?: boolean; -}; - -export type DbUserImportJobData = { - user: ThinUser; - fileId: DriveFile['id']; -}; - -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 8b55f2247..000000000 --- a/packages/backend/src/remote/activitypub/ap-request.ts +++ /dev/null @@ -1,104 +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 9c04ecb6d..000000000 --- a/packages/backend/src/remote/activitypub/audience.ts +++ /dev/null @@ -1,92 +0,0 @@ -import promiseLimit from 'promise-limit'; -import { CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js'; -import { unique, concat } from '@/prelude/array.js'; -import { resolvePerson } from './models/person.js'; -import { Resolver } from './resolver.js'; -import { ApObject, getApIds } from './type.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/db-resolver.ts b/packages/backend/src/remote/activitypub/db-resolver.ts deleted file mode 100644 index 7f913de0b..000000000 --- a/packages/backend/src/remote/activitypub/db-resolver.ts +++ /dev/null @@ -1,101 +0,0 @@ -import escapeRegexp from 'escape-regexp'; -import config from '@/config/index.js'; -import { Note } from '@/models/entities/note.js'; -import { CacheableUser } from '@/models/entities/user.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { Notes, MessagingMessages } from '@/models/index.js'; -import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; -import { IObject, getApId } from './type.js'; - -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 class DbResolver { - constructor() { - } - - /** - * AP Note => FoundKey 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 => FoundKey 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.fetch(parsed.id) ?? null; - } else { - return await uriPersonCache.fetch(parsed.uri) ?? null; - } - } -} 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 4bc651c98..000000000 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { ILocalUser, IRemoteUser, User } from '@/models/entities/user.js'; -import { Users, Followings } from '@/models/index.js'; -import { deliver } from '@/queue/index.js'; -import { skippedInstances } from '@/misc/skipped-instances.js'; - -//#region types -interface IRecipe { - type: string; -} - -interface IEveryoneRecipe extends IRecipe { - type: 'Everyone'; -} - -interface IFollowersRecipe extends IRecipe { - type: 'Followers'; -} - -interface IDirectRecipe extends IRecipe { - type: 'Direct'; - to: IRemoteUser; -} - -const isEveryone = (recipe: any): recipe is IEveryoneRecipe => - recipe.type === 'Everyone'; - -const isFollowers = (recipe: any): recipe is IFollowersRecipe => - recipe.type === 'Followers'; - -const isDirect = (recipe: any): recipe is IDirectRecipe => - recipe.type === 'Direct'; -//#endregion - -export 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 to send this activity to all known sharedInboxes - */ - public addEveryone() { - this.addRecipe({ type: 'Everyone' } as IEveryoneRecipe); - } - - /** - * 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 - - Processing order matters to avoid duplication. - */ - - if (this.recipes.some(r => isEveryone(r))) { - // deliver to all of known network - const sharedInboxes = await Users.createQueryBuilder('users') - .select('users.sharedInbox', 'sharedInbox') - // so we don't have to make our inboxes Set work as hard - .distinct(true) - // can't deliver to unknown shared inbox - .where('users.sharedInbox IS NOT NULL') - // don't deliver to ourselves - .andWhere('users.host IS NOT NULL') - .getRawMany(); - - for (const inbox of sharedInboxes) { - inboxes.add(inbox.sharedInbox); - } - } - - if (this.recipes.some(r => isFollowers(r))) { - // followers deliver - const followers = await Followings.createQueryBuilder('followings') - // return either the shared inbox (if available) or the individual inbox - .select('COALESCE(followings.followerSharedInbox, followings.followerInbox)', 'inbox') - // so we don't have to make our inboxes Set work as hard - .distinct(true) - // ...for the specific actors followers - .where('followings.followeeId = :actorId', { actorId: this.actor.id }) - // don't deliver to ourselves - .andWhere('followings.followerHost IS NOT NULL') - .getRawMany(); - - followers.forEach(({ inbox }) => 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 037f660c6..000000000 --- a/packages/backend/src/remote/activitypub/kernel/accept/follow.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { acceptFollowRequest } from '@/services/following/requests/accept.js'; -import { relayAccepted } from '@/services/relay.js'; -import { IFollow } from '@/remote/activitypub/type.js'; -import { DbResolver } from '@/remote/activitypub/db-resolver.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 acceptFollowRequest(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 be9b80096..000000000 --- a/packages/backend/src/remote/activitypub/kernel/accept/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { apLogger } from '@/remote/activitypub/logger.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { IAccept, isFollow, getApType } from '@/remote/activitypub/type.js'; -import acceptFollow from './follow.js'; - -export default async (actor: CacheableRemoteUser, activity: IAccept, resolver: Resolver): Promise => { - const uri = activity.id || activity; - - apLogger.info(`Accept: ${uri}`); - - const object = await resolver.resolve(activity.object).catch(e => { - apLogger.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 3fd5f4723..000000000 --- a/packages/backend/src/remote/activitypub/kernel/add/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { addPinned } from '@/services/i/pin.js'; -import { resolveNote } from '@/remote/activitypub/models/note.js'; -import { IAdd } from '@/remote/activitypub/type.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; - -export default async (actor: CacheableRemoteUser, activity: IAdd, resolver: Resolver): 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, resolver); - 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 e4d77e1c5..000000000 --- a/packages/backend/src/remote/activitypub/kernel/announce/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { apLogger } from '@/remote/activitypub/logger.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; -import announceNote from './note.js'; - -export default async (actor: CacheableRemoteUser, activity: IAnnounce, resolver: Resolver): Promise => { - const uri = getApId(activity); - - apLogger.info(`Announce: ${uri}`); - - 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 254ef2727..000000000 --- a/packages/backend/src/remote/activitypub/kernel/announce/note.ts +++ /dev/null @@ -1,66 +0,0 @@ -import post from '@/services/note/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { extractDbHost } from '@/misc/convert-host.js'; -import { getApLock } from '@/misc/app-lock.js'; -import { StatusError } from '@/misc/fetch.js'; -import { Notes } from '@/models/index.js'; -import { parseAudience } from '@/remote/activitypub/audience.js'; -import { apLogger } from '@/remote/activitypub/logger.js'; -import { fetchNote, resolveNote } from '@/remote/activitypub/models/note.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { IAnnounce, getApId } from '@/remote/activitypub/type.js'; -import { shouldBlockInstance } from '@/misc/should-block-instance.js'; - -export default async function(resolver: Resolver, actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise { - const uri = getApId(activity); - - if (actor.isSuspended) { - return; - } - - // Cancel if the announced from host is blocked. - if (await shouldBlockInstance(extractDbHost(uri))) return; - - const unlock = await getApLock(uri); - - try { - // Check if this has already been announced. - const exist = await fetchNote(uri); - if (exist) { - return; - } - - // resolve the announce target - let renote; - try { - renote = await resolveNote(targetUri, resolver); - } catch (e) { - // skip if the target returns a HTTP client error - if (e instanceof StatusError) { - if (e.isClientError) { - apLogger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`); - return; - } - - apLogger.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'; - - apLogger.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 7095a36a5..000000000 --- a/packages/backend/src/remote/activitypub/kernel/block/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import block from '@/services/blocking/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { DbResolver } from '@/remote/activitypub/db-resolver.js'; -import { IBlock } from '@/remote/activitypub/type.js'; - -export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => { - // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず - - const dbResolver = new DbResolver(); - const blockee = await dbResolver.getUserFromApId(activity.object); - - if (blockee == null) { - return 'skip: blockee not found'; - } - - if (blockee.host != null) { - return 'skip: ブロックしようとしているユーザーはローカルユーザーではありません'; - } - - await block(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 f6e86d91d..000000000 --- a/packages/backend/src/remote/activitypub/kernel/create/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { toArray, concat, unique } from '@/prelude/array.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { ICreate, getApId, isPost, getApType } from '../../type.js'; -import { apLogger } from '../../logger.js'; -import createNote from './note.js'; - -export default async (actor: CacheableRemoteUser, activity: ICreate, resolver: Resolver): Promise => { - const uri = getApId(activity); - - apLogger.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 object = await resolver.resolve(activity.object).catch(e => { - apLogger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isPost(object)) { - createNote(resolver, actor, object, false, activity); - } else { - apLogger.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 892dbb26a..000000000 --- a/packages/backend/src/remote/activitypub/kernel/create/note.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { getApLock } from '@/misc/app-lock.js'; -import { extractDbHost } from '@/misc/convert-host.js'; -import { StatusError } from '@/misc/fetch.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { createNote, fetchNote } from '@/remote/activitypub/models/note.js'; -import { getApId, IObject } from '@/remote/activitypub/type.js'; - -/** - * 投稿作成アクティビティを捌きます - */ -export default async function(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false): 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 ea75a9739..000000000 --- a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { apLogger } from '@/remote/activitypub/logger.js'; -import { deleteAccount } from '@/services/delete-account.js'; - -export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise { - apLogger.info(`Deleting the Actor: ${uri}`); - - if (actor.uri !== uri) { - return `skip: delete actor ${actor.uri} !== ${uri}`; - } - - const user = await Users.findOneBy({ id: actor.id }); - if (!user) { - // maybe a race condition, relay or something else? - // anyway, the user is gone now so dont care - return 'ok: gone'; - } - if (user.isDeleted) { - // the actual deletion already happened by an admin, just delete the record - await Users.delete(actor.id); - } else { - await deleteAccount(actor); - } -} 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 ee05e5327..000000000 --- a/packages/backend/src/remote/activitypub/kernel/delete/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { toSingle } from '@/prelude/array.js'; -import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '@/remote/activitypub/type.js'; -import { deleteActor } from './actor.js'; -import deleteNote from './note.js'; - -/** - * 削除アクティビティを捌きます - */ -export default async (actor: CacheableRemoteUser, activity: IDelete): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - // 削除対象objectのtype - let formerType: string | undefined; - - if (typeof activity.object === 'string') { - // typeが不明だけど、どうせ消えてるので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); - - // type不明でもactorとobjectが同じならばそれはPersonに違いない - if (!formerType && actor.uri === uri) { - formerType = 'Person'; - } - - // それでもなかったらおそらく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 15c1cba8b..000000000 --- a/packages/backend/src/remote/activitypub/kernel/delete/note.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import deleteNode from '@/services/note/delete.js'; -import { getApLock } from '@/misc/app-lock.js'; -import { deleteMessage } from '@/services/messages/delete.js'; -import { DbResolver } from '@/remote/activitypub/db-resolver.js'; -import { apLogger } from '@/remote/activitypub/logger.js'; - -export default async function(actor: CacheableRemoteUser, uri: string): Promise { - apLogger.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 'skip: message not found'; - - if (message.userId !== actor.id) { - return 'skip: cant delete other actors message'; - } - - await deleteMessage(message); - return 'ok: message deleted'; - } else { - if (note.userId !== actor.id) { - return 'skip: cant delete other actors note'; - } - - 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 e50bcc2bd..000000000 --- a/packages/backend/src/remote/activitypub/kernel/flag/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { In } from 'typeorm'; -import config from '@/config/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { AbuseUserReports, Users } from '@/models/index.js'; -import { IFlag, getApIds } from '@/remote/activitypub/type.js'; - -export default async (actor: CacheableRemoteUser, activity: IFlag): Promise => { - // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので - // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する - const uris = getApIds(activity.object); - - const userIds = uris.filter(uri => uri.startsWith(config.url + '/users/')).map(uri => uri.split('/').pop()!); - const users = await Users.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, - urls: uris.filter(uri => !uri.startsWith(config.url + '/users/')), - }); - - 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 8125b4606..000000000 --- a/packages/backend/src/remote/activitypub/kernel/follow.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import follow from '@/services/following/create.js'; -import { 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: フォローしようとしているユーザーはローカルユーザーではありません'; - } - - 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 79e2dce75..000000000 --- a/packages/backend/src/remote/activitypub/kernel/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { toArray } from '@/prelude/array.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { extractDbHost } from '@/misc/convert-host.js'; -import { shouldBlockInstance } from '@/misc/should-block-instance.js'; -import { apLogger } from '../logger.js'; -import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, getApId } from '../type.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'; - -export async function performActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise { - if (isCollectionOrOrderedCollection(activity)) { - for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { - const act = await resolver.resolve(item); - try { - await performOneActivity(actor, act, resolver); - } catch (err) { - if (err instanceof Error || typeof err === 'string') { - apLogger.error(err); - } - } - } - } else { - await performOneActivity(actor, activity, resolver); - } -} - -async function performOneActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): 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, resolver); - } else if (isDelete(activity)) { - await performDeleteActivity(actor, activity); - } else if (isUpdate(activity)) { - await performUpdateActivity(actor, activity, resolver); - } else if (isRead(activity)) { - await performReadActivity(actor, activity); - } else if (isFollow(activity)) { - await follow(actor, activity); - } else if (isAccept(activity)) { - await accept(actor, activity, resolver); - } else if (isReject(activity)) { - await reject(actor, activity, resolver); - } else if (isAdd(activity)) { - await add(actor, activity, resolver).catch(err => apLogger.error(err)); - } else if (isRemove(activity)) { - await remove(actor, activity, resolver).catch(err => apLogger.error(err)); - } else if (isAnnounce(activity)) { - await announce(actor, activity, resolver); - } else if (isLike(activity)) { - await like(actor, activity); - } else if (isUndo(activity)) { - await undo(actor, activity, resolver); - } else if (isBlock(activity)) { - await block(actor, activity); - } else if (isFlag(activity)) { - await flag(actor, activity); - } else { - apLogger.warn(`unrecognized activity type: ${(activity as any).type}`); - } -} diff --git a/packages/backend/src/remote/activitypub/kernel/like.ts b/packages/backend/src/remote/activitypub/kernel/like.ts deleted file mode 100644 index 76272eea7..000000000 --- a/packages/backend/src/remote/activitypub/kernel/like.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { createReaction } from '@/services/note/reaction/create.js'; -import { ILike, getApId } from '../type.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 createReaction(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/read.ts b/packages/backend/src/remote/activitypub/kernel/read.ts deleted file mode 100644 index d367fb669..000000000 --- a/packages/backend/src/remote/activitypub/kernel/read.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.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'; -import { IRead, getApId } from '../type.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 bd3ad1660..000000000 --- a/packages/backend/src/remote/activitypub/kernel/reject/follow.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { remoteReject } from '@/services/following/reject.js'; -import { relayRejected } from '@/services/relay.js'; -import { Users } from '@/models/index.js'; -import { IFollow } from '../../type.js'; -import { DbResolver } from '../../db-resolver.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 (!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 3a91c8ec7..000000000 --- a/packages/backend/src/remote/activitypub/kernel/reject/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { apLogger } from '../../logger.js'; -import { IReject, isFollow, getApType } from '../../type.js'; -import rejectFollow from './follow.js'; - -export default async (actor: CacheableRemoteUser, activity: IReject, resolver: Resolver): Promise => { - const uri = activity.id || activity; - - apLogger.info(`Reject: ${uri}`); - - const object = await resolver.resolve(activity.object).catch(e => { - apLogger.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 6591f82b1..000000000 --- a/packages/backend/src/remote/activitypub/kernel/remove/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { removePinned } from '@/services/i/pin.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { IRemove } from '../../type.js'; -import { resolveNote } from '../../models/note.js'; - -export default async (actor: CacheableRemoteUser, activity: IRemove, resolver: Resolver): 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, resolver); - 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 fa4eea44c..000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/accept.ts +++ /dev/null @@ -1,26 +0,0 @@ -import unfollow from '@/services/following/delete.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { Followings } from '@/models/index.js'; -import { DbResolver } from '@/remote/activitypub/db-resolver.js'; -import { IAccept } from '@/remote/activitypub/type.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.countBy({ - followerId: follower.id, - followeeId: actor.id, - }); - - if (following) { - await unfollow(follower, actor); - return 'ok: unfollowed'; - } - - return '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 c06b21db3..000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/announce.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Notes } from '@/models/index.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import deleteNote from '@/services/note/delete.js'; -import { IAnnounce, getApId } from '@/remote/activitypub/type.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 ae1c9c0b6..000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/block.ts +++ /dev/null @@ -1,21 +0,0 @@ -import unblock from '@/services/blocking/delete.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { IBlock } from '@/remote/activitypub/type.js'; -import { DbResolver } from '@/remote/activitypub/db-resolver.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: ブロック解除しようとしているユーザーはローカルユーザーではありません'; - } - - 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 c7f99bcf2..000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/follow.ts +++ /dev/null @@ -1,40 +0,0 @@ -import unfollow from '@/services/following/delete.js'; -import { cancelFollowRequest } from '@/services/following/requests/cancel.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { FollowRequests, Followings } from '@/models/index.js'; -import { IFollow } from '@/remote/activitypub/type.js'; -import { DbResolver } from '@/remote/activitypub/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 unfollowed user is not local'; - } - - const [requested, following] = await Promise.all([ - FollowRequests.countBy({ - followerId: actor.id, - followeeId: followee.id, - }), - Followings.countBy({ - followerId: actor.id, - followeeId: followee.id, - }), - ]); - - if (requested) { - await cancelFollowRequest(followee, actor); - return 'ok: follow request canceled'; - } else if (following) { - await unfollow(actor, followee); - return 'ok: unfollowed'; - } else { - return 'skip: no such following or follow request'; - } -}; 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 05382f0f5..000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { apLogger } from '@/remote/activitypub/logger.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '@/remote/activitypub/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'; - -export default async (actor: CacheableRemoteUser, activity: IUndo, resolver: Resolver): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - throw new Error('invalid actor'); - } - - const uri = activity.id || activity; - - apLogger.info(`Undo: ${uri}`); - - const object = await resolver.resolve(activity.object).catch(e => { - apLogger.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 6c7b8d18b..000000000 --- a/packages/backend/src/remote/activitypub/kernel/undo/like.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { deleteReaction } from '@/services/note/reaction/delete.js'; -import { ILike, getApId } from '@/remote/activitypub/type.js'; -import { fetchNote } from '@/remote/activitypub/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 73085b181..000000000 --- a/packages/backend/src/remote/activitypub/kernel/update/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { getApId, getApType, IUpdate, isActor } from '@/remote/activitypub/type.js'; -import { apLogger } from '@/remote/activitypub/logger.js'; -import { updateQuestion } from '@/remote/activitypub/models/question.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { updatePerson } from '@/remote/activitypub/models/person.js'; - -/** - * Updateアクティビティを捌きます - */ -export default async (actor: CacheableRemoteUser, activity: IUpdate, resolver: Resolver): Promise => { - if ('actor' in activity && actor.uri !== activity.actor) { - return 'skip: invalid actor'; - } - - apLogger.debug('Update'); - - const object = await resolver.resolve(activity.object).catch(e => { - apLogger.error(`Resolution failed: ${e}`); - throw e; - }); - - if (isActor(object)) { - if (actor.uri !== getApId(object)) { - return 'skip: actor id !== updated actor id'; - } - - await updatePerson(object, resolver); - 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 cab51b3bf..000000000 --- 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/auth-user.ts b/packages/backend/src/remote/activitypub/misc/auth-user.ts deleted file mode 100644 index 4705bb791..000000000 --- a/packages/backend/src/remote/activitypub/misc/auth-user.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Cache } from '@/misc/cache.js'; -import { UserPublickeys } from '@/models/index.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { UserPublickey } from '@/models/entities/user-publickey.js'; -import { uriPersonCache, userByIdCache } from '@/services/user-cache.js'; -import { createPerson } from '@/remote/activitypub/models/person.js'; - -export type AuthUser = { - user: CacheableRemoteUser; - key: UserPublickey; -}; - -const publicKeyCache = new Cache( - Infinity, - (keyId) => UserPublickeys.findOneBy({ keyId }).then(x => x ?? undefined), -); -const publicKeyByUserIdCache = new Cache( - Infinity, - (userId) => UserPublickeys.findOneBy({ userId }).then(x => x ?? undefined), -); - -function authUserFromApId(uri: string): Promise { - return uriPersonCache.fetch(uri) - .then(async user => { - if (!user) return null; - const key = await publicKeyByUserIdCache.fetch(user.id); - if (!key) return null; - return { user, key }; - }); -} - -export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise { - let authUser = await publicKeyCache.fetch(keyId) - .then(async key => { - if (!key) return null; - else return { - user: await userByIdCache.fetch(key.userId), - key, - }; - }); - if (authUser != null) return authUser; - - authUser = await authUserFromApId(actorUri); - if (authUser != null) return authUser; - - // fetch from remote and then one last try - await createPerson(actorUri, resolver); - // if this one still returns null it seems this user really does not exist - return await authUserFromApId(actorUri); -} 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 aee0d3629..000000000 --- a/packages/backend/src/remote/activitypub/misc/contexts.ts +++ /dev/null @@ -1,526 +0,0 @@ -/* eslint:disable:quotemark indent */ -const id_v1 = { - '@context': { - 'id': '@id', - 'type': '@type', - - 'cred': 'https://w3id.org/credentials#', - 'dc': 'http://purl.org/dc/terms/', - 'identity': 'https://w3id.org/identity#', - 'perm': 'https://w3id.org/permissions#', - 'ps': 'https://w3id.org/payswarm#', - 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#', - 'sec': 'https://w3id.org/security#', - 'schema': 'http://schema.org/', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - - 'Group': 'https://www.w3.org/ns/activitystreams#Group', - - 'claim': { '@id': 'cred:claim', '@type': '@id' }, - 'credential': { '@id': 'cred:credential', '@type': '@id' }, - 'issued': { '@id': 'cred:issued', '@type': 'xsd:dateTime' }, - 'issuer': { '@id': 'cred:issuer', '@type': '@id' }, - 'recipient': { '@id': 'cred:recipient', '@type': '@id' }, - 'Credential': 'cred:Credential', - 'CryptographicKeyCredential': 'cred:CryptographicKeyCredential', - - 'about': { '@id': 'schema:about', '@type': '@id' }, - 'address': { '@id': 'schema:address', '@type': '@id' }, - 'addressCountry': 'schema:addressCountry', - 'addressLocality': 'schema:addressLocality', - 'addressRegion': 'schema:addressRegion', - 'comment': 'rdfs:comment', - 'created': { '@id': 'dc:created', '@type': 'xsd:dateTime' }, - 'creator': { '@id': 'dc:creator', '@type': '@id' }, - 'description': 'schema:description', - 'email': 'schema:email', - 'familyName': 'schema:familyName', - 'givenName': 'schema:givenName', - 'image': { '@id': 'schema:image', '@type': '@id' }, - 'label': 'rdfs:label', - 'name': 'schema:name', - 'postalCode': 'schema:postalCode', - 'streetAddress': 'schema:streetAddress', - 'title': 'dc:title', - 'url': { '@id': 'schema:url', '@type': '@id' }, - 'Person': 'schema:Person', - 'PostalAddress': 'schema:PostalAddress', - 'Organization': 'schema:Organization', - - 'identityService': { '@id': 'identity:identityService', '@type': '@id' }, - 'idp': { '@id': 'identity:idp', '@type': '@id' }, - 'Identity': 'identity:Identity', - - 'paymentProcessor': 'ps:processor', - 'preferences': { '@id': 'ps:preferences', '@type': '@vocab' }, - - 'cipherAlgorithm': 'sec:cipherAlgorithm', - 'cipherData': 'sec:cipherData', - 'cipherKey': 'sec:cipherKey', - 'digestAlgorithm': 'sec:digestAlgorithm', - 'digestValue': 'sec:digestValue', - 'domain': 'sec:domain', - 'expires': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, - 'initializationVector': 'sec:initializationVector', - 'member': { '@id': 'schema:member', '@type': '@id' }, - 'memberOf': { '@id': 'schema:memberOf', '@type': '@id' }, - 'nonce': 'sec:nonce', - 'normalizationAlgorithm': 'sec:normalizationAlgorithm', - 'owner': { '@id': 'sec:owner', '@type': '@id' }, - 'password': 'sec:password', - 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' }, - 'privateKeyPem': 'sec:privateKeyPem', - 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' }, - 'publicKeyPem': 'sec:publicKeyPem', - 'publicKeyService': { '@id': 'sec:publicKeyService', '@type': '@id' }, - 'revoked': { '@id': 'sec:revoked', '@type': 'xsd:dateTime' }, - 'signature': 'sec:signature', - 'signatureAlgorithm': 'sec:signatureAlgorithm', - 'signatureValue': 'sec:signatureValue', - 'CryptographicKey': 'sec:Key', - 'EncryptedMessage': 'sec:EncryptedMessage', - 'GraphSignature2012': 'sec:GraphSignature2012', - 'LinkedDataSignature2015': 'sec:LinkedDataSignature2015', - - 'accessControl': { '@id': 'perm:accessControl', '@type': '@id' }, - 'writePermission': { '@id': 'perm:writePermission', '@type': '@id' }, - }, -}; - -const security_v1 = { - '@context': { - 'id': '@id', - 'type': '@type', - - 'dc': 'http://purl.org/dc/terms/', - 'sec': 'https://w3id.org/security#', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - - 'EcdsaKoblitzSignature2016': 'sec:EcdsaKoblitzSignature2016', - 'Ed25519Signature2018': 'sec:Ed25519Signature2018', - 'EncryptedMessage': 'sec:EncryptedMessage', - 'GraphSignature2012': 'sec:GraphSignature2012', - 'LinkedDataSignature2015': 'sec:LinkedDataSignature2015', - 'LinkedDataSignature2016': 'sec:LinkedDataSignature2016', - 'CryptographicKey': 'sec:Key', - - 'authenticationTag': 'sec:authenticationTag', - 'canonicalizationAlgorithm': 'sec:canonicalizationAlgorithm', - 'cipherAlgorithm': 'sec:cipherAlgorithm', - 'cipherData': 'sec:cipherData', - 'cipherKey': 'sec:cipherKey', - 'created': { '@id': 'dc:created', '@type': 'xsd:dateTime' }, - 'creator': { '@id': 'dc:creator', '@type': '@id' }, - 'digestAlgorithm': 'sec:digestAlgorithm', - 'digestValue': 'sec:digestValue', - 'domain': 'sec:domain', - 'encryptionKey': 'sec:encryptionKey', - 'expiration': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, - 'expires': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, - 'initializationVector': 'sec:initializationVector', - 'iterationCount': 'sec:iterationCount', - 'nonce': 'sec:nonce', - 'normalizationAlgorithm': 'sec:normalizationAlgorithm', - 'owner': { '@id': 'sec:owner', '@type': '@id' }, - 'password': 'sec:password', - 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' }, - 'privateKeyPem': 'sec:privateKeyPem', - 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' }, - 'publicKeyBase58': 'sec:publicKeyBase58', - 'publicKeyPem': 'sec:publicKeyPem', - 'publicKeyWif': 'sec:publicKeyWif', - 'publicKeyService': { '@id': 'sec:publicKeyService', '@type': '@id' }, - 'revoked': { '@id': 'sec:revoked', '@type': 'xsd:dateTime' }, - 'salt': 'sec:salt', - 'signature': 'sec:signature', - 'signatureAlgorithm': 'sec:signingAlgorithm', - 'signatureValue': 'sec:signatureValue', - }, -}; - -const activitystreams = { - '@context': { - '@vocab': '_:', - 'xsd': 'http://www.w3.org/2001/XMLSchema#', - 'as': 'https://www.w3.org/ns/activitystreams#', - 'ldp': 'http://www.w3.org/ns/ldp#', - 'vcard': 'http://www.w3.org/2006/vcard/ns#', - 'id': '@id', - 'type': '@type', - 'Accept': 'as:Accept', - 'Activity': 'as:Activity', - 'IntransitiveActivity': 'as:IntransitiveActivity', - 'Add': 'as:Add', - 'Announce': 'as:Announce', - 'Application': 'as:Application', - 'Arrive': 'as:Arrive', - 'Article': 'as:Article', - 'Audio': 'as:Audio', - 'Block': 'as:Block', - 'Collection': 'as:Collection', - 'CollectionPage': 'as:CollectionPage', - 'Relationship': 'as:Relationship', - 'Create': 'as:Create', - 'Delete': 'as:Delete', - 'Dislike': 'as:Dislike', - 'Document': 'as:Document', - 'Event': 'as:Event', - 'Follow': 'as:Follow', - 'Flag': 'as:Flag', - 'Group': 'as:Group', - 'Ignore': 'as:Ignore', - 'Image': 'as:Image', - 'Invite': 'as:Invite', - 'Join': 'as:Join', - 'Leave': 'as:Leave', - 'Like': 'as:Like', - 'Link': 'as:Link', - 'Mention': 'as:Mention', - 'Note': 'as:Note', - 'Object': 'as:Object', - 'Offer': 'as:Offer', - 'OrderedCollection': 'as:OrderedCollection', - 'OrderedCollectionPage': 'as:OrderedCollectionPage', - 'Organization': 'as:Organization', - 'Page': 'as:Page', - 'Person': 'as:Person', - 'Place': 'as:Place', - 'Profile': 'as:Profile', - 'Question': 'as:Question', - 'Reject': 'as:Reject', - 'Remove': 'as:Remove', - 'Service': 'as:Service', - 'TentativeAccept': 'as:TentativeAccept', - 'TentativeReject': 'as:TentativeReject', - 'Tombstone': 'as:Tombstone', - 'Undo': 'as:Undo', - 'Update': 'as:Update', - 'Video': 'as:Video', - 'View': 'as:View', - 'Listen': 'as:Listen', - 'Read': 'as:Read', - 'Move': 'as:Move', - 'Travel': 'as:Travel', - 'IsFollowing': 'as:IsFollowing', - 'IsFollowedBy': 'as:IsFollowedBy', - 'IsContact': 'as:IsContact', - 'IsMember': 'as:IsMember', - 'subject': { - '@id': 'as:subject', - '@type': '@id', - }, - 'relationship': { - '@id': 'as:relationship', - '@type': '@id', - }, - 'actor': { - '@id': 'as:actor', - '@type': '@id', - }, - 'attributedTo': { - '@id': 'as:attributedTo', - '@type': '@id', - }, - 'attachment': { - '@id': 'as:attachment', - '@type': '@id', - }, - 'bcc': { - '@id': 'as:bcc', - '@type': '@id', - }, - 'bto': { - '@id': 'as:bto', - '@type': '@id', - }, - 'cc': { - '@id': 'as:cc', - '@type': '@id', - }, - 'context': { - '@id': 'as:context', - '@type': '@id', - }, - 'current': { - '@id': 'as:current', - '@type': '@id', - }, - 'first': { - '@id': 'as:first', - '@type': '@id', - }, - 'generator': { - '@id': 'as:generator', - '@type': '@id', - }, - 'icon': { - '@id': 'as:icon', - '@type': '@id', - }, - 'image': { - '@id': 'as:image', - '@type': '@id', - }, - 'inReplyTo': { - '@id': 'as:inReplyTo', - '@type': '@id', - }, - 'items': { - '@id': 'as:items', - '@type': '@id', - }, - 'instrument': { - '@id': 'as:instrument', - '@type': '@id', - }, - 'orderedItems': { - '@id': 'as:items', - '@type': '@id', - '@container': '@list', - }, - 'last': { - '@id': 'as:last', - '@type': '@id', - }, - 'location': { - '@id': 'as:location', - '@type': '@id', - }, - 'next': { - '@id': 'as:next', - '@type': '@id', - }, - 'object': { - '@id': 'as:object', - '@type': '@id', - }, - 'oneOf': { - '@id': 'as:oneOf', - '@type': '@id', - }, - 'anyOf': { - '@id': 'as:anyOf', - '@type': '@id', - }, - 'closed': { - '@id': 'as:closed', - '@type': 'xsd:dateTime', - }, - 'origin': { - '@id': 'as:origin', - '@type': '@id', - }, - 'accuracy': { - '@id': 'as:accuracy', - '@type': 'xsd:float', - }, - 'prev': { - '@id': 'as:prev', - '@type': '@id', - }, - 'preview': { - '@id': 'as:preview', - '@type': '@id', - }, - 'replies': { - '@id': 'as:replies', - '@type': '@id', - }, - 'result': { - '@id': 'as:result', - '@type': '@id', - }, - 'audience': { - '@id': 'as:audience', - '@type': '@id', - }, - 'partOf': { - '@id': 'as:partOf', - '@type': '@id', - }, - 'tag': { - '@id': 'as:tag', - '@type': '@id', - }, - 'target': { - '@id': 'as:target', - '@type': '@id', - }, - 'to': { - '@id': 'as:to', - '@type': '@id', - }, - 'url': { - '@id': 'as:url', - '@type': '@id', - }, - 'altitude': { - '@id': 'as:altitude', - '@type': 'xsd:float', - }, - 'content': 'as:content', - 'contentMap': { - '@id': 'as:content', - '@container': '@language', - }, - 'name': 'as:name', - 'nameMap': { - '@id': 'as:name', - '@container': '@language', - }, - 'duration': { - '@id': 'as:duration', - '@type': 'xsd:duration', - }, - 'endTime': { - '@id': 'as:endTime', - '@type': 'xsd:dateTime', - }, - 'height': { - '@id': 'as:height', - '@type': 'xsd:nonNegativeInteger', - }, - 'href': { - '@id': 'as:href', - '@type': '@id', - }, - 'hreflang': 'as:hreflang', - 'latitude': { - '@id': 'as:latitude', - '@type': 'xsd:float', - }, - 'longitude': { - '@id': 'as:longitude', - '@type': 'xsd:float', - }, - 'mediaType': 'as:mediaType', - 'published': { - '@id': 'as:published', - '@type': 'xsd:dateTime', - }, - 'radius': { - '@id': 'as:radius', - '@type': 'xsd:float', - }, - 'rel': 'as:rel', - 'startIndex': { - '@id': 'as:startIndex', - '@type': 'xsd:nonNegativeInteger', - }, - 'startTime': { - '@id': 'as:startTime', - '@type': 'xsd:dateTime', - }, - 'summary': 'as:summary', - 'summaryMap': { - '@id': 'as:summary', - '@container': '@language', - }, - 'totalItems': { - '@id': 'as:totalItems', - '@type': 'xsd:nonNegativeInteger', - }, - 'units': 'as:units', - 'updated': { - '@id': 'as:updated', - '@type': 'xsd:dateTime', - }, - 'width': { - '@id': 'as:width', - '@type': 'xsd:nonNegativeInteger', - }, - 'describes': { - '@id': 'as:describes', - '@type': '@id', - }, - 'formerType': { - '@id': 'as:formerType', - '@type': '@id', - }, - 'deleted': { - '@id': 'as:deleted', - '@type': 'xsd:dateTime', - }, - 'inbox': { - '@id': 'ldp:inbox', - '@type': '@id', - }, - 'outbox': { - '@id': 'as:outbox', - '@type': '@id', - }, - 'following': { - '@id': 'as:following', - '@type': '@id', - }, - 'followers': { - '@id': 'as:followers', - '@type': '@id', - }, - 'streams': { - '@id': 'as:streams', - '@type': '@id', - }, - 'preferredUsername': 'as:preferredUsername', - 'endpoints': { - '@id': 'as:endpoints', - '@type': '@id', - }, - 'uploadMedia': { - '@id': 'as:uploadMedia', - '@type': '@id', - }, - 'proxyUrl': { - '@id': 'as:proxyUrl', - '@type': '@id', - }, - 'liked': { - '@id': 'as:liked', - '@type': '@id', - }, - 'oauthAuthorizationEndpoint': { - '@id': 'as:oauthAuthorizationEndpoint', - '@type': '@id', - }, - 'oauthTokenEndpoint': { - '@id': 'as:oauthTokenEndpoint', - '@type': '@id', - }, - 'provideClientKey': { - '@id': 'as:provideClientKey', - '@type': '@id', - }, - 'signClientKey': { - '@id': 'as:signClientKey', - '@type': '@id', - }, - 'sharedInbox': { - '@id': 'as:sharedInbox', - '@type': '@id', - }, - 'Public': { - '@id': 'as:Public', - '@type': '@id', - }, - 'source': 'as:source', - 'likes': { - '@id': 'as:likes', - '@type': '@id', - }, - 'shares': { - '@id': 'as:shares', - '@type': '@id', - }, - 'alsoKnownAs': { - '@id': 'as:alsoKnownAs', - '@type': '@id', - }, - }, -}; - -export const CONTEXTS: Record = { - 'https://w3id.org/identity/v1': id_v1, - 'https://w3id.org/security/v1': security_v1, - 'https://www.w3.org/ns/activitystreams': activitystreams, -}; diff --git a/packages/backend/src/remote/activitypub/misc/ld-signature.ts b/packages/backend/src/remote/activitypub/misc/ld-signature.ts deleted file mode 100644 index 4e0940a72..000000000 --- a/packages/backend/src/remote/activitypub/misc/ld-signature.ts +++ /dev/null @@ -1,135 +0,0 @@ -import * as crypto from 'node:crypto'; -import jsonld from 'jsonld'; -import fetch from 'node-fetch'; -import { httpAgent, httpsAgent } from '@/misc/fetch.js'; -import { CONTEXTS } from './contexts.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) { - delete options.domain; - } - - const toBeSigned = await this.createVerifyData(data, options); - - const signer = crypto.createSign('sha256'); - signer.update(toBeSigned); - signer.end(); - - const signature = signer.sign(privateKey); - - return { - ...data, - signature: { - ...options, - signatureValue: signature.toString('base64'), - }, - }; - } - - public async verifyRsaSignature2017(data: any, publicKey: string): Promise { - const toBeSigned = await this.createVerifyData(data, data.signature); - const verifier = crypto.createVerify('sha256'); - verifier.update(toBeSigned); - return verifier.verify(publicKey, data.signature.signatureValue, 'base64'); - } - - public async createVerifyData(data: any, options: any) { - const transformedOptions = { - ...options, - '@context': 'https://w3id.org/identity/v1', - }; - delete transformedOptions['type']; - delete transformedOptions['id']; - delete transformedOptions['signatureValue']; - const canonizedOptions = await this.normalize(transformedOptions); - const optionsHash = this.sha256(canonizedOptions); - const transformedData = { ...data }; - delete transformedData['signature']; - const cannonidedData = await this.normalize(transformedData); - if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`); - const documentHash = this.sha256(cannonidedData); - const verifyData = `${optionsHash}${documentHash}`; - return verifyData; - } - - public async normalize(data: any) { - const customLoader = this.getLoader(); - return await jsonld.normalize(data, { - documentLoader: customLoader, - }); - } - - private getLoader() { - return async (url: string): Promise => { - if (!url.match('^https?\:\/\/')) throw 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, - 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 50794a937..000000000 --- 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 f6c3bb8c8..000000000 --- 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 281cbdf9a..000000000 --- a/packages/backend/src/remote/activitypub/models/image.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles } from '@/models/index.js'; -import { truncate } from '@/misc/truncate.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { apLogger } from '../logger.js'; - -/** - * Imageを作成します。 - */ -export async function createImage(actor: CacheableRemoteUser, value: any, resolver: Resolver): Promise { - // 投稿者が凍結されていたらスキップ - if (actor.isSuspended) { - throw new Error('actor has been suspended'); - } - - const image = await resolver.resolve(value) as any; - - if (image.url == null) { - throw new Error('invalid image: url not privided'); - } - - apLogger.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) { - // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 - // 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 FoundKey, return it; otherwise, fetch it from the remote server and return it. - * Fetch the image from the remote server, register it in FoundKey and return it. - */ -export async function resolveImage(actor: CacheableRemoteUser, value: any, resolver: Resolver): Promise { - // TODO - - // Fetch from remote server and register it. - return await createImage(actor, value, resolver); -} 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 183ab841a..000000000 --- a/packages/backend/src/remote/activitypub/models/mention.ts +++ /dev/null @@ -1,22 +0,0 @@ -import promiseLimit from 'promise-limit'; -import { toArray, unique } from '@/prelude/array.js'; -import { CacheableUser } from '@/models/entities/user.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { IObject, isMention, IApMention } from '../type.js'; -import { resolvePerson } from './person.js'; - -export async function extractApMentions(tags: IObject | IObject[] | null | undefined, resolver: Resolver): Promise { - const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); - - 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 f5426da3c..000000000 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ /dev/null @@ -1,359 +0,0 @@ -import promiseLimit from 'promise-limit'; - -import config from '@/config/index.js'; -import post from '@/services/note/create.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { unique, toArray, toSingle } from '@/prelude/array.js'; -import { vote } from '@/services/note/polls/vote.js'; -import { 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 { Note } from '@/models/entities/note.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { genId } from '@/misc/gen-id.js'; -import { getApLock } from '@/misc/app-lock.js'; -import { createMessage } from '@/services/messages/create.js'; -import { StatusError } from '@/misc/fetch.js'; -import { fromHtml } from '@/mfm/from-html.js'; -import { shouldBlockInstance } from '@/misc/should-block-instance.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { parseAudience } from '../audience.js'; -import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js'; -import { DbResolver } from '../db-resolver.js'; -import { apLogger } from '../logger.js'; -import { resolvePerson } from './person.js'; -import { resolveImage } from './image.js'; -import { extractApHashtags } from './tag.js'; -import { extractPollFromQuestion } from './question.js'; -import { extractApMentions } from './mention.js'; - -export function validateNote(object: IObject): Error | null { - 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)}`); - } - - const id = getApId(object); - if (id == null) { - // Only transient objects or anonymous objects may not have an id or an id that is explicitly null. - // We consider all Notes as not transient and not anonymous so require ids for them. - return new Error(`invalid Note: id required but not present`); - } - - // Check that the server is authorized to act on behalf of this author. - const expectHost = extractDbHost(id); - const attributedToHost = object.attributedTo - ? extractDbHost(getOneApId(object.attributedTo)) - : null; - if (attributedToHost !== expectHost) { - return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`); - } - - return null; -} - -/** - * Fetch Note. - * - * Returns the target Note if it is registered in FoundKey. - */ -export async function fetchNote(object: string | IObject): Promise { - const dbResolver = new DbResolver(); - return await dbResolver.getNoteFromApId(object); -} - -/** - * Noteを作成します。 - */ -export async function createNote(value: string | IObject, resolver: Resolver, silent = false): Promise { - const object: IObject = await resolver.resolve(value); - - const err = validateNote(object); - if (err) { - apLogger.error(`${err.message}`, { - resolver: { - history: resolver.getHistory(), - }, - value, - object, - }); - throw new Error('invalid note'); - } - - const note: IPost = object; - - apLogger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); - - apLogger.info(`Creating the Note: ${note.id}`); - - // 投稿者をフェッチ - const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser; - - // 投稿者が凍結されていたらスキップ - if (actor.isSuspended) { - throw new Error('actor has been suspended'); - } - - const noteAudience = await parseAudience(actor, note.to, note.cc); - let visibility = noteAudience.visibility; - const visibleUsers = noteAudience.visibleUsers; - - // Audience (to, cc) が指定されてなかった場合 - if (visibility === 'specified' && visibleUsers.length === 0) { - if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している - // こちらから匿名GET出来たものならばpublic - visibility = 'public'; - } - } - - let isTalk = note._misskey_talk && visibility === 'specified'; - - const apMentions = await extractApMentions(note.tag, resolver); - const apHashtags = await extractApHashtags(note.tag); - - // 添付ファイル - // TODO: attachmentは必ずしもImageではない - // TODO: attachmentは必ずしも配列ではない - // Noteがsensitiveなら添付もsensitiveにする - const limit = promiseLimit(2); - - note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; - const files = note.attachment - .map(attach => attach.sensitive = note.sensitive) - ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x, resolver)) as Promise))) - .filter(image => image != null) - : []; - - // リプライ - const reply: Note | null = note.inReplyTo - ? await resolveNote(note.inReplyTo, resolver).then(x => { - if (x == null) { - apLogger.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.countBy({ id }); - if (talk) { - isTalk = true; - return null; - } - } - - apLogger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`); - throw e; - }) - : null; - - // 引用 - let quote: Note | undefined | null; - - if (note._misskey_quote || 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, resolver); - 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.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 = fromHtml(note.content, quote?.uri); - } - - // vote - if (reply && 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()) { - apLogger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); - } else if (index >= 0) { - apLogger.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 => { - apLogger.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: getOneApHrefNullable(note.url), - }, silent); -} - -/** - * Resolve Note. - * - * If the target Note is registered in FoundKey, return it; otherwise, fetch it from a remote server and return it. - * Fetch the Note from the remote server, register it in FoundKey, 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'); - - // Interrupt if blocked. - if (await shouldBlockInstance(extractDbHost(uri))) throw new StatusError('host blocked', 451, `host ${extractDbHost(uri)} is blocked`); - - const unlock = await getApLock(uri); - - try { - //#region If already registered on this server, return it. - const exist = await fetchNote(uri); - - if (exist) { - return exist; - } - //#endregion - - if (uri.startsWith(config.url)) { - throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); - } - - // リモートサーバーからフェッチしてきて登録 - // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが - // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 - return await createNote(uri, resolver, true); - } finally { - unlock(); - } -} - -export async function extractEmojis(tags: IObject | IObject[], idnHost: string): Promise { - const host = toPuny(idnHost); - - 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; - } - - apLogger.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 803fd03c9..000000000 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ /dev/null @@ -1,450 +0,0 @@ -import promiseLimit from 'promise-limit'; -import { Not, IsNull } from 'typeorm'; - -import config from '@/config/index.js'; -import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js'; -import { Note } from '@/models/entities/note.js'; -import { updateUsertags } from '@/services/update-hashtag.js'; -import { Users, Instances, Followings, UserProfiles, UserPublickeys } from '@/models/index.js'; -import { User, IRemoteUser, CacheableUser } from '@/models/entities/user.js'; -import { 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 { extractDbHost } 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 { fromHtml } from '@/mfm/from-html.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { apLogger } from '../logger.js'; -import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, getApType, isActor } from '../type.js'; -import { extractApHashtags } from './tag.js'; -import { resolveNote, extractEmojis } from './note.js'; -import { resolveImage } from './image.js'; - -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): IActor { - if (x == null) { - throw new Error('invalid Actor: object is null'); - } - - if (!isActor(x)) { - throw new Error(`invalid Actor type '${x.type}'`); - } - - const uri = getApId(x); - if (uri == null) { - // Only transient objects or anonymous objects may not have an id or an id that is explicitly null. - // We consider all actors as not transient and not anonymous so require ids for them. - throw new Error('invalid Actor: wrong id'); - } - - // This check is security critical. - // Without this check, an entry could be inserted into UserPublickey for a local user. - if (extractDbHost(uri) === extractDbHost(config.url)) { - throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); - } - - 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); - } - - if (x.publicKey) { - if (typeof x.publicKey.id !== 'string') { - throw new Error('invalid Actor: publicKey.id is not a string'); - } - - // This is a security critical check to not insert or change an entry of - // UserPublickey to point to a local key id. - if (extractDbHost(uri) !== extractDbHost(x.publicKey.id)) { - throw new Error('invalid Actor: publicKey.id has different host'); - } - } - - return x; -} - -/** - * Fetches a person. - * - * If the target Person is registered in FoundKey, it is returned. - */ -export async function fetchPerson(uri: string): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); - - const cached = uriPersonCache.get(uri); - if (cached) return cached; - - // If the URI points to this server, fetch from database. - 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 このサーバーに既に登録されていたらそれを返す - const exist = await Users.findOneBy({ uri }); - - if (exist) { - uriPersonCache.set(uri, exist); - return exist; - } - //#endregion - - return null; -} - -/** - * Personを作成します。 - */ -export async function createPerson(value: string | IObject, resolver: Resolver): Promise { - const object = await resolver.resolve(value) as any; - - const person = validateActor(object); - - apLogger.info(`Creating the Person: ${person.id}`); - - const host = extractDbHost(object.id); - - const { fields } = analyzeAttachments(person.attachment || []); - - const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); - - const isBot = getApType(object) === 'Service'; - - const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); - - // Create user - let user: IRemoteUser; - try { - // Start transaction - await 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, - isExplorable: !!person.discoverable, - username: person.preferredUsername, - usernameLower: person.preferredUsername!.toLowerCase(), - host, - inbox: person.inbox, - sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), - followersUri: person.followers ? getApId(person.followers) : undefined, - featured: person.featured ? getApId(person.featured) : undefined, - uri: person.id, - tags, - isBot, - isCat: (person as any).isCat === true, - showTimelineReplies: false, - })) as IRemoteUser; - - await transactionalEntityManager.save(new UserProfile({ - userId: user.id, - description: person.summary ? fromHtml(truncate(person.summary, summaryLength)) : null, - url: getOneApHrefNullable(person.url), - fields, - birthday: bday ? bday[0] : null, - location: person['vcard:Address'] || null, - userHost: host, - })); - - if (person.publicKey) { - await transactionalEntityManager.save(new UserPublickey({ - userId: user.id, - keyId: person.publicKey.id, - keyPem: person.publicKey.publicKeyPem, - })); - } - }); - } catch (e) { - // duplicate key error - if (isDuplicateKeyValueError(e)) { - // /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応 - const u = await Users.findOneBy({ - uri: person.id, - }); - - if (u) { - user = u as IRemoteUser; - } else { - throw new Error('already registered'); - } - } else { - apLogger.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); - - // ハッシュタグ更新 - updateUsertags(user!, tags); - - //#region アバターとヘッダー画像をフェッチ - const [avatar, banner] = await Promise.all([ - person.icon, - person.image, - ].map(img => - img == null - ? Promise.resolve(null) - : resolveImage(user!, img, resolver).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 カスタム絵文字取得 - const emojis = await extractEmojis(person.tag || [], host).catch(e => { - apLogger.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 => apLogger.error(err)); - - return user!; -} - -/** - * Update Person information. - * If the target Person is not registered in FoundKey, it is ignored. - * @param value URI of Person or Person itself - * @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(value: IObject | string, resolver: Resolver): Promise { - const uri = getApId(value); - - // do we already know this user? - const exist = await Users.findOneBy({ uri, host: Not(IsNull()) }) as IRemoteUser; - - if (exist == null) { - return; - } - - const object = await resolver.resolve(value); - - const person = validateActor(object); - - apLogger.info(`Updating the Person: ${person.id}`); - - // Fetch avatar and banner image - const [avatar, banner] = await Promise.all([ - person.icon, - person.image, - ].map(img => - img == null - ? Promise.resolve(null) - : resolveImage(exist, img, resolver).catch(() => null), - )); - - // Get custom emoji - const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => { - apLogger.info(`extractEmojis: ${e}`); - return [] as Emoji[]; - }); - - const emojiNames = emojis.map(emoji => emoji.name); - - const { fields } = analyzeAttachments(person.attachment ?? []); - - const tags = extractApHashtags(person.tag).map(tag => normalizeForSearch(tag)).splice(0, 32); - - const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); - - const updates = { - lastFetchedAt: new Date(), - inbox: person.inbox, - sharedInbox: person.sharedInbox ?? (person.endpoints ? person.endpoints.sharedInbox : undefined), - followersUri: person.followers ? getApId(person.followers) : undefined, - featured: person.featured, - emojis: emojiNames, - name: truncate(person.name, nameLength), - tags, - isBot: getApType(object) === 'Service', - isCat: (person as any).isCat === true, - isLocked: !!person.manuallyApprovesFollowers, - isExplorable: !!person.discoverable, - } as Partial; - - if (avatar) { - updates.avatarId = avatar.id; - } - - 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: getOneApHrefNullable(person.url), - fields, - description: person.summary ? fromHtml(truncate(person.summary, summaryLength)) : null, - birthday: bday ? bday[0] : null, - location: person['vcard:Address'] || null, - }); - - publishInternalEvent('remoteUserUpdated', { id: exist.id }); - - updateUsertags(exist, tags); - - // If the user in question is already 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 => apLogger.error(err)); -} - -/** - * Resolve Person. - * - * If the target Person is registered in FoundKey, return it; otherwise, fetch it from a remote server and return it. - * Fetch the person from the remote server, register it in FoundKey, and return it. - */ -export async function resolvePerson(uri: string, resolver: Resolver): Promise { - if (typeof uri !== 'string') throw new Error('uri is not string'); - - //#region このサーバーに既に登録されていたらそれを返す - const exist = await fetchPerson(uri); - - if (exist) { - return exist; - } - //#endregion - - // リモートサーバーからフェッチしてきて登録 - return await createPerson(uri, resolver); -} - -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)) { - fields.push({ - name: attachment.name, - value: fromHtml(attachment.value), - }); - } - } - - return { fields, services }; -} - -async function updateFeatured(userId: User['id'], resolver: Resolver) { - const user = await Users.findOneByOrFail({ id: userId }); - if (!Users.isRemoteUser(user)) return; - if (!user.featured) return; - - apLogger.info(`Updating the featured: ${user.uri}`); - - // Resolve to (Ordered)Collection Object - const collection = await resolver.resolveCollection(user.featured); - if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection'); - - // Resolve to Object(may be Note) arrays - const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; - const items = await Promise.all(toArray(unresolvedItems).map(x => resolver.resolve(x))); - - // Resolve and regist Notes - const limit = promiseLimit(2); - const featuredNotes = await Promise.all(items - .filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも - .slice(0, 5) - .map(item => limit(() => resolveNote(item, resolver)))); - - await db.transaction(async transactionalEntityManager => { - await transactionalEntityManager.delete(UserNotePining, { userId: user.id }); - - // とりあえずidを別の時間で生成して順番を維持 - let td = 0; - for (const note of featuredNotes.filter(note => note != null)) { - td -= 1000; - transactionalEntityManager.insert(UserNotePining, { - id: genId(new Date(Date.now() + td)), - createdAt: new Date(), - userId: user.id, - noteId: note!.id, - }); - } - }); -} diff --git a/packages/backend/src/remote/activitypub/models/question.ts b/packages/backend/src/remote/activitypub/models/question.ts deleted file mode 100644 index 5d88e1d44..000000000 --- a/packages/backend/src/remote/activitypub/models/question.ts +++ /dev/null @@ -1,81 +0,0 @@ -import config from '@/config/index.js'; -import { Notes, Polls } from '@/models/index.js'; -import { IPoll } from '@/models/entities/poll.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { IObject, IQuestion, isQuestion } from '../type.js'; -import { apLogger } from '../logger.js'; - -export async function extractPollFromQuestion(source: string | IObject, resolver: Resolver): Promise { - 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 => x.name!); - - const votes = question[multiple ? 'anyOf' : 'oneOf']! - .map(x => x.replies && x.replies.totalItems || x._misskey_votes || 0); - - return { - choices, - votes, - multiple, - expiresAt, - }; -} - -/** - * Update votes of Question - * @param value AP Question object or its id - * @param resolver Resolver to use - * @returns true if updated - */ -export async function updateQuestion(value: string | IObject, resolver: Resolver) { - const uri = typeof value === 'string' ? value : value.id; - - // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(config.url + '/')) throw new Error('uri points local'); - - //#region このサーバーに既に登録されているか - const note = await Notes.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 - 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 964dabad0..000000000 --- a/packages/backend/src/remote/activitypub/models/tag.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { toArray } from '@/prelude/array.js'; -import { IObject, isHashtag, IApHashtag } 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 8622d43df..000000000 --- a/packages/backend/src/remote/activitypub/perform.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DAY } from '@/const.js'; -import { CacheableRemoteUser } from '@/models/entities/user.js'; -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { IObject } from './type.js'; -import { performActivity } from './kernel/index.js'; -import { updatePerson } from './models/person.js'; - -export async function perform(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise { - await performActivity(actor, activity, resolver); - - // And while I'm at it, I'll update the remote user information if it's out of date. - if (actor.uri) { - if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > DAY) { - setImmediate(() => { - updatePerson(actor.uri!, resolver); - }); - } - } -} 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 cb01f6a91..000000000 --- a/packages/backend/src/remote/activitypub/renderer/accept.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from '@/config/index.js'; -import { 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 ec4788429..000000000 --- a/packages/backend/src/remote/activitypub/renderer/add.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { 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 2709fea51..000000000 --- a/packages/backend/src/remote/activitypub/renderer/announce.ts +++ /dev/null @@ -1,29 +0,0 @@ -import config from '@/config/index.js'; -import { Note } from '@/models/entities/note.js'; - -export default (object: any, note: Note) => { - const attributedTo = `${config.url}/users/${note.userId}`; - - let to: string[] = []; - let cc: string[] = []; - - if (note.visibility === 'public') { - to = ['https://www.w3.org/ns/activitystreams#Public']; - cc = [`${attributedTo}/followers`]; - } else if (note.visibility === 'home') { - to = [`${attributedTo}/followers`]; - cc = ['https://www.w3.org/ns/activitystreams#Public']; - } else { - return null; - } - - return { - id: `${config.url}/notes/${note.id}/activity`, - actor: `${config.url}/users/${note.userId}`, - type: 'Announce', - published: note.createdAt.toISOString(), - to, - cc, - object, - }; -}; diff --git a/packages/backend/src/remote/activitypub/renderer/block.ts b/packages/backend/src/remote/activitypub/renderer/block.ts deleted file mode 100644 index 802d7280b..000000000 --- a/packages/backend/src/remote/activitypub/renderer/block.ts +++ /dev/null @@ -1,20 +0,0 @@ -import config from '@/config/index.js'; -import { 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 281a3cb2a..000000000 --- a/packages/backend/src/remote/activitypub/renderer/create.ts +++ /dev/null @@ -1,17 +0,0 @@ -import config from '@/config/index.js'; -import { 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 4edd3a880..000000000 --- a/packages/backend/src/remote/activitypub/renderer/delete.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { 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 c973de4c4..000000000 --- a/packages/backend/src/remote/activitypub/renderer/document.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { 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 0bf15eefd..000000000 --- a/packages/backend/src/remote/activitypub/renderer/emoji.ts +++ /dev/null @@ -1,14 +0,0 @@ -import config from '@/config/index.js'; -import { 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 7834c1f28..000000000 --- a/packages/backend/src/remote/activitypub/renderer/flag.ts +++ /dev/null @@ -1,12 +0,0 @@ -import config from '@/config/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; - -export const renderFlag = (user: ILocalUser, flag: AbuseUserReport) => { - return { - type: 'Flag', - actor: `${config.url}/users/${user.id}`, - content: flag.comment, - object: flag.urls, - }; -}; 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 b172f6ba9..000000000 --- a/packages/backend/src/remote/activitypub/renderer/follow-relay.ts +++ /dev/null @@ -1,21 +0,0 @@ -import config from '@/config/index.js'; -import { Relay } from '@/models/entities/relay.js'; -import { ILocalUser } from '@/models/entities/user.js'; - -export type FollowRelay = { - id: string; - type: 'Follow'; - actor: string; - object: 'https://www.w3.org/ns/activitystreams#Public'; -}; - -export function renderFollowRelay(relay: Relay, relayActor: ILocalUser): FollowRelay { - 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', - } as const; - - 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 bf7287c8f..000000000 --- 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 { 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 }); - 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 3052fd87e..000000000 --- a/packages/backend/src/remote/activitypub/renderer/follow.ts +++ /dev/null @@ -1,34 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; - -/** - * Renders a follow activity. - * @param follower user that is (trying to) follow someone - * @param followee the user that is (trying to) be followed - * @param requestId: ID of this follow. If undefined and follower is local, will be generated. - */ -export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => { - let id = requestId; - - if (id == null && follower.host == null) { - /* - Generate an id only if the follower is local. - Otherwise we may try to generate an ID for a remote activity, - in which case its better not to put an id at all. - - Why IDs must be generated: https://github.com/misskey-dev/misskey/issues/8655 - Why IDs must not be generated for remote activities: https://akkoma.dev/FoundKeyGang/FoundKey/issues/263 - */ - id = `${config.url}/follows/${follower.id}/${followee.id}`; - } - - const follow = { - 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 a7b441e00..000000000 --- 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 c7d5a31a2..000000000 --- a/packages/backend/src/remote/activitypub/renderer/image.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { 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 37e73d37b..000000000 --- a/packages/backend/src/remote/activitypub/renderer/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { v4 as uuid } from 'uuid'; -import config from '@/config/index.js'; -import { getUserKeypair } from '@/misc/keypair-store.js'; -import { User } from '@/models/entities/user.js'; -import { IActivity } from '../type.js'; -import { LdSignature } from '../misc/ld-signature.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', - sensitive: 'as:sensitive', - Hashtag: 'as:Hashtag', - // Mastodon - toot: 'http://joinmastodon.org/ns#', - Emoji: 'toot:Emoji', - featured: 'toot:featured', - discoverable: 'toot:discoverable', - // Fedibird - fedibird: 'http://fedibird.com/ns#', - quoteUri: 'fedibird:quoteUri', - // 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', - // 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; - return await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`); -}; 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 805d3cc68..000000000 --- a/packages/backend/src/remote/activitypub/renderer/key.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createPublicKey } from 'node:crypto'; -import config from '@/config/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { UserKeypair } from '@/models/entities/user-keypair.js'; - -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 3978bc961..000000000 --- a/packages/backend/src/remote/activitypub/renderer/like.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { Note } from '@/models/entities/note.js'; -import { Emojis } from '@/models/index.js'; -import renderEmoji from './emoji.js'; - -export const renderLike = async (noteReaction: NoteReaction, note: Note) => { - const reaction = noteReaction.reaction; - - const object = { - type: 'Like', - id: `${config.url}/likes/${noteReaction.id}`, - actor: `${config.url}/users/${noteReaction.userId}`, - object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`, - content: reaction, - _misskey_reaction: reaction, - } as any; - - if (reaction.startsWith(':')) { - const name = reaction.replace(/:/g, ''); - const emoji = await Emojis.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 c7e62e884..000000000 --- a/packages/backend/src/remote/activitypub/renderer/mention.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { 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 0d53cfe5e..000000000 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { In, IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import { Note } from '@/models/entities/note.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js'; -import { Emoji } from '@/models/entities/emoji.js'; -import { Poll } from '@/models/entities/poll.js'; -import { toHtml } from '@/mfm/to-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 inReplyToUserExists = await Users.countBy({ id: inReplyToNote.userId }); - - if (inReplyToUserExists) { - 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 mentionedUsers = note.mentions.length > 0 ? await Users.findBy({ - id: In(note.mentions), - }) : []; - - const mentionUris = mentionedUsers - // only remote users - .filter(user => Users.isRemoteUser(user)) - .map(user => user.uri); - - let to: string[] = []; - let cc: string[] = []; - - if (note.visibility === 'public') { - to = ['https://www.w3.org/ns/activitystreams#Public']; - cc = [`${attributedTo}/followers`].concat(mentionUris); - } else if (note.visibility === 'home') { - to = [`${attributedTo}/followers`]; - cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentionUris); - } else if (note.visibility === 'followers') { - to = [`${attributedTo}/followers`]; - cc = mentionUris; - } else { - to = mentionUris; - } - - 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 = await toHtml(apText, note.mentions); - - const emojis = await getEmojis(note.emojis); - const apemojis = emojis.map(emoji => renderEmoji(emoji)); - - const tag = [ - ...hashtagTags, - ...mentionTags, - ...apemojis, - ]; - - const asPoll = poll ? { - type: 'Question', - content: await toHtml(text, note.mentions), - [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, - 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 c5e25f577..000000000 --- a/packages/backend/src/remote/activitypub/renderer/ordered-collection-page.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Render OrderedCollectionPage - * @param id URL of self - * @param totalItems Number of total items - * @param orderedItems Items - * @param partOf URL of base - * @param prev URL of prev page (optional) - * @param next URL of next page (optional) - */ -export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { - const page = { - id, - partOf, - type: 'OrderedCollectionPage', - totalItems, - orderedItems, - } as any; - - if (prev) page.prev = prev; - if (next) page.next = next; - - return page; -} diff --git a/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts b/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts deleted file mode 100644 index ff9a77be3..000000000 --- a/packages/backend/src/remote/activitypub/renderer/ordered-collection.ts +++ /dev/null @@ -1,28 +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 7de957882..000000000 --- a/packages/backend/src/remote/activitypub/renderer/person.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { URL } from 'node:url'; -import config from '@/config/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { toHtml } from '@/mfm/to-html.js'; -import { DriveFiles, UserProfiles } from '@/models/index.js'; -import { getUserKeypair } from '@/misc/keypair-store.js'; -import { IIdentifier } from '../models/identifier.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'; - -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 != null && field.value.match(/^https?:/)) - ? `${new URL(field.value).href}` - : field.value, - }); - } - } - - const emojis = await getEmojis(user.emojis); - const apemojis = emojis.map(emoji => renderEmoji(emoji)); - - const hashtagTags = (user.tags || []).map(tag => renderHashtag(tag)); - - const tag = [ - ...apemojis, - ...hashtagTags, - ]; - - const keypair = await getUserKeypair(user.id); - - const person = { - type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', - id, - inbox: `${id}/inbox`, - outbox: `${id}/outbox`, - followers: `${id}/followers`, - following: `${id}/following`, - featured: `${id}/collections/featured`, - sharedInbox: `${config.url}/inbox`, - endpoints: { sharedInbox: `${config.url}/inbox` }, - url: `${config.url}/@${user.username}`, - preferredUsername: user.username, - name: user.name, - summary: profile.description ? await toHtml(profile.description) : null, - icon: avatar ? renderImage(avatar) : null, - image: banner ? renderImage(banner) : null, - tag, - manuallyApprovesFollowers: user.isLocked, - discoverable: !!user.isExplorable, - publicKey: renderKey(user, keypair, '#main-key'), - isCat: user.isCat, - attachment: attachment.length ? attachment : undefined, - } as any; - - if (profile.birthday) { - person['vcard:bday'] = profile.birthday; - } - - if (profile.location) { - person['vcard:Address'] = profile.location; - } - - return person; -} diff --git a/packages/backend/src/remote/activitypub/renderer/question.ts b/packages/backend/src/remote/activitypub/renderer/question.ts deleted file mode 100644 index d4d1b590a..000000000 --- a/packages/backend/src/remote/activitypub/renderer/question.ts +++ /dev/null @@ -1,23 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { 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 a30e649f6..000000000 --- a/packages/backend/src/remote/activitypub/renderer/read.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { 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 ab4cc1646..000000000 --- a/packages/backend/src/remote/activitypub/renderer/reject.ts +++ /dev/null @@ -1,8 +0,0 @@ -import config from '@/config/index.js'; -import { 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 1be3edc5d..000000000 --- a/packages/backend/src/remote/activitypub/renderer/remove.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@/config/index.js'; -import { 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 313ca74e9..000000000 --- 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 04e83189d..000000000 --- a/packages/backend/src/remote/activitypub/renderer/undo.ts +++ /dev/null @@ -1,15 +0,0 @@ -import config from '@/config/index.js'; -import { User } 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 cf880f03f..000000000 --- a/packages/backend/src/remote/activitypub/renderer/update.ts +++ /dev/null @@ -1,15 +0,0 @@ -import config from '@/config/index.js'; -import { 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 b6eb8e095..000000000 --- a/packages/backend/src/remote/activitypub/renderer/vote.ts +++ /dev/null @@ -1,23 +0,0 @@ -import config from '@/config/index.js'; -import { Note } from '@/models/entities/note.js'; -import { IRemoteUser, User } from '@/models/entities/user.js'; -import { PollVote } from '@/models/entities/poll-vote.js'; -import { 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 233bc025f..000000000 --- a/packages/backend/src/remote/activitypub/request.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { URL } from 'node:url'; -import config from '@/config/index.js'; -import { getUserKeypair } from '@/misc/keypair-store.js'; -import { User } from '@/models/entities/user.js'; -import { getResponse } from '@/misc/fetch.js'; -import { createSignedPost, createSignedGet } from './ap-request.js'; - -export async function request(user: { id: User['id'] }, url: string, object: any): Promise { - 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, - // don't allow redirects on the inbox - redirect: 'error', - }); -} - -/** - * 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'] }): Promise { - let url = _url; - const keypair = await getUserKeypair(user.id); - - for (let redirects = 0; redirects < 3; redirects++) { - 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, - redirect: 'manual', - }); - - if (res.status >= 300 && res.status < 400) { - // Have been redirected, need to make a new signature. - // Use Location header and fetched URL as the base URL. - url = new URL(res.headers.get('Location'), url).href; - } else { - return await res.json(); - } - } - - throw new Error('too many redirects'); -} diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts deleted file mode 100644 index 44e05b9e3..000000000 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { ILocalUser } from '@/models/entities/user.js'; -import { getInstanceActor } from '@/services/instance-actor.js'; -import { extractDbHost, isSelfHost } from '@/misc/convert-host.js'; -import { Notes, NoteReactions, Polls, Users } from '@/models/index.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'; -import { signedGet } from './request.js'; -import { getApId, IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js'; -import { parseUri } from './db-resolver.js'; - -/** - * Tries to resolve an ActivityPub URI into an AP object. - * - * As opposed to the DbResolver which will try to resolve an ActivityPub URI into a database object. - */ -export 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 | null, allowRedirect = false): 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); - } - - if (await shouldBlockInstance(host)) { - throw new Error('instance is blocked'); - } - - if (!this.user) { - this.user = await getInstanceActor(); - } - - const object = await signedGet(value, this.user); - - if ( - object == null - || // check that this is an activitypub object by looking at the @context - ( - Array.isArray(object['@context']) ? - !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : - object['@context'] !== 'https://www.w3.org/ns/activitystreams' - ) - // Did we actually get the object that corresponds to the canonical URL? - // Does the host we requested stuff from actually correspond to the host that owns the activity? - || !(getApId(object) == null || getApId(object) === value || allowRedirect) - ) { - 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 f5b411639..000000000 --- a/packages/backend/src/remote/activitypub/type.ts +++ /dev/null @@ -1,295 +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; - quoteUri?: string; - _misskey_talk: boolean; -} - -export interface IQuestion extends IObject { - type: 'Note' | 'Question'; - source?: { - content: string; - mediaType: string; - }; - _misskey_quote?: string; - quoteUri?: string; - oneOf?: IQuestionChoice[]; - anyOf?: IQuestionChoice[]; - endTime?: Date; - closed?: Date; -} - -export const isQuestion = (object: IObject): object is IQuestion => - getApType(object) === 'Note' || getApType(object) === 'Question'; - -interface IQuestionChoice { - name?: string; - replies?: ICollection; - _misskey_votes?: number; -} -export interface ITombstone extends IObject { - type: 'Tombstone'; - formerType?: string; - deleted?: Date; -} - -export const isTombstone = (object: IObject): object is ITombstone => - getApType(object) === 'Tombstone'; - -export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application']; - -export const isActor = (object: IObject): object is IActor => - validActor.includes(getApType(object)); - -export interface IActor extends IObject { - type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application'; - name?: string; - preferredUsername?: string; - manuallyApprovesFollowers?: boolean; - discoverable?: boolean; - inbox: string; - sharedInbox?: string; // 後方互換性のため - publicKey?: { - id: string; - publicKeyPem: string; - }; - followers?: string | ICollection | IOrderedCollection; - following?: string | ICollection | IOrderedCollection; - featured?: string | IOrderedCollection; - outbox: string | IOrderedCollection; - endpoints?: { - sharedInbox?: string; - }; - 'vcard:bday'?: string; - 'vcard:Address'?: string; -} - -export const isCollection = (object: IObject): object is ICollection => - getApType(object) === 'Collection'; - -export const isOrderedCollection = (object: IObject): object is IOrderedCollection => - getApType(object) === 'OrderedCollection'; - -export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => - isCollection(object) || isOrderedCollection(object); - -export interface IApPropertyValue extends IObject { - type: 'PropertyValue'; - identifier: IApPropertyValue; - name: string; - value: string; -} - -export const isPropertyValue = (object: IObject): object is IApPropertyValue => - object && - getApType(object) === 'PropertyValue' && - typeof object.name === 'string' && - typeof (object as any).value === 'string'; - -export interface IApMention extends IObject { - type: 'Mention'; - href: string; -} - -export const isMention = (object: IObject): object is IApMention => - getApType(object) === 'Mention' && - typeof object.href === 'string'; - -export interface IApHashtag extends IObject { - type: 'Hashtag'; - name: string; -} - -export const isHashtag = (object: IObject): object is IApHashtag => - getApType(object) === 'Hashtag' && - typeof object.name === 'string'; - -export interface IApEmoji extends IObject { - type: 'Emoji'; - updated: Date; -} - -export const isEmoji = (object: IObject): object is IApEmoji => - getApType(object) === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; - -export interface ICreate extends IActivity { - type: 'Create'; -} - -export interface IDelete extends IActivity { - type: 'Delete'; -} - -export interface IUpdate extends IActivity { - type: 'Update'; -} - -export interface IRead extends IActivity { - type: 'Read'; -} - -export interface IUndo extends IActivity { - type: 'Undo'; -} - -export interface IFollow extends IActivity { - type: 'Follow'; -} - -export interface IAccept extends IActivity { - type: 'Accept'; -} - -export interface IReject extends IActivity { - type: 'Reject'; -} - -export interface IAdd extends IActivity { - type: 'Add'; -} - -export interface IRemove extends IActivity { - type: 'Remove'; -} - -export interface ILike extends IActivity { - type: 'Like' | 'EmojiReaction' | 'EmojiReact'; - _misskey_reaction?: string; -} - -export interface IAnnounce extends IActivity { - type: 'Announce'; -} - -export interface IBlock extends IActivity { - type: 'Block'; -} - -export interface IFlag extends IActivity { - type: 'Flag'; -} - -export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; -export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; -export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; -export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read'; -export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo'; -export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow'; -export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept'; -export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject'; -export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add'; -export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove'; -export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact'; -export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; -export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; -export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; diff --git a/packages/backend/src/remote/logger.ts b/packages/backend/src/remote/logger.ts deleted file mode 100644 index 4921f53bd..000000000 --- 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 2b49afc40..000000000 --- a/packages/backend/src/remote/resolve-user.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { URL } from 'node:url'; -import chalk from 'chalk'; -import { IsNull } from 'typeorm'; -import { DAY } from '@/const.js'; -import { isSelfHost, toPuny } from '@/misc/convert-host.js'; -import { User, IRemoteUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { Resolver } from '@/remote/activitypub/resolver.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, idnHost: string | null, resolver: Resolver = new Resolver()): Promise { - const usernameLower = username.toLowerCase(); - - if (idnHost == 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; - } - }); - } - - if (isSelfHost(idnHost)) { - 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; - } - }); - } - - // `idnHost` can not be null here because that would have branched off with `isSelfHost`. - const host = toPuny(idnHost!); - 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, resolver); - } - - // If user information is out of date, start over with webfinger - if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > DAY) { - // Prevent race conditions - await Users.update(user.id, { - lastFetchedAt: new Date(), - }); - - logger.info(`try resync: ${acctLower}`); - const self = await resolveSelf(acctLower); - - if (user.uri !== self) { - // 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}`); - - // validate uri - const uri = new URL(self); - if (uri.hostname !== host) { - throw new Error('Invalid uri'); - } - - await Users.update({ - usernameLower, - host, - }, { - uri: self, - }); - } else { - logger.info(`uri is fine: ${acctLower}`); - } - - await updatePerson(self, resolver); - - logger.info(`return resynced remote user: ${acctLower}`); - return await Users.findOneBy({ uri: self }).then(u => { - if (u == null) { - throw new Error('user not found'); - } else { - return u; - } - }); - } - - logger.info(`return existing remote user: ${acctLower}`); - return user; -} - -/** - * Gets the Webfinger href matching rel="self". - */ -async function resolveSelf(acctLower: string): string { - logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); - // get webfinger response for user - 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 }`); - }); - // try to find the rel="self" link - const self = finger.links.find(link => link.rel?.toLowerCase() === 'self'); - if (!self?.href) { - logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`); - throw new Error('self link not found'); - } - return self.href; -} diff --git a/packages/backend/src/remote/webfinger.ts b/packages/backend/src/remote/webfinger.ts deleted file mode 100644 index 337df34c2..000000000 --- a/packages/backend/src/remote/webfinger.ts +++ /dev/null @@ -1,34 +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 2dc4ea1e7..000000000 --- a/packages/backend/src/server/activitypub.ts +++ /dev/null @@ -1,265 +0,0 @@ -import json from 'koa-json-body'; -import Router from '@koa/router'; -import { In, IsNull, Not } from 'typeorm'; -import httpSignature from '@peertube/http-signature'; - -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 } from '@/misc/convert-host.js'; -import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js'; -import { ILocalUser, User } from '@/models/entities/user.js'; -import { renderLike } from '@/remote/activitypub/renderer/like.js'; -import { getUserKeypair } from '@/misc/keypair-store.js'; -import renderFollow from '@/remote/activitypub/renderer/follow.js'; -import Outbox, { packActivity } from './activitypub/outbox.js'; -import Followers from './activitypub/followers.js'; -import Following from './activitypub/following.js'; -import Featured from './activitypub/featured.js'; - -// Init router -const router = new Router(); - -function inbox(ctx: Router.RouterContext): void { - 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): boolean { - ctx.response.vary('Accept'); - // if no accept header is supplied, koa returns the 1st, so html is used as a dummy - // i.e. activitypub requests must be explicit - const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON); - return typeof accepted === 'string' && !accepted.match(/html/); -} - -export function setResponseType(ctx: Router.RouterContext): void { - const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON); - if (accept === LD_JSON) { - ctx.response.type = LD_JSON; - } else { - ctx.response.type = ACTIVITY_JSON; - } -} - -// inbox -router.post('/inbox', json(), inbox); -router.post('/users/:user/inbox', json(), inbox); - -// note -router.get('/notes/:note', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - - const note = await Notes.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)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}); - -// note activity -router.get('/notes/:note/activity', async ctx => { - if (!isActivityPubReq(ctx)) { - /* - Redirect to the human readable page. in this case using next is not possible, - since there is no human readable page explicitly for the activity. - */ - ctx.redirect(`/notes/${ctx.params.note}`); - 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)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}); - -// outbox -router.get('/users/:user/outbox', Outbox); - -// followers -router.get('/users/:user/followers', Followers); - -// following -router.get('/users/:user/following', Following); - -// featured -router.get('/users/:user/collections/featured', Featured); - -// publickey -router.get('/users/:user/publickey', async ctx => { - const userId = ctx.params.user; - - const user = await Users.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)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } else { - ctx.status = 400; - } -}); - -// user -async function userInfo(ctx: Router.RouterContext, user: User | null): Promise { - if (user == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await renderPerson(user as ILocalUser)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -} - -router.get('/users/:user', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - - const userId = ctx.params.user; - - const user = await Users.findOneBy({ - id: userId, - host: IsNull(), - isSuspended: false, - }); - - await userInfo(ctx, user); -}); - -router.get('/@:user', async (ctx, next) => { - if (!isActivityPubReq(ctx)) return await next(); - - const user = await Users.findOneBy({ - usernameLower: ctx.params.user.toLowerCase(), - host: IsNull(), - isSuspended: false, - }); - - await userInfo(ctx, user); -}); - -// emoji -router.get('/emojis/:emoji', async ctx => { - const emoji = await Emojis.findOneBy({ - host: IsNull(), - name: ctx.params.emoji, - }); - - if (emoji == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await renderEmoji(emoji)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}); - -// like -router.get('/likes/:like', async ctx => { - const reaction = await NoteReactions.findOneBy({ id: ctx.params.like }); - - if (reaction == null) { - ctx.status = 404; - return; - } - - const note = await Notes.findOneBy({ - id: reaction.noteId, - visibility: In(['public' as const, 'home' as const]), - }); - - if (note == null) { - ctx.status = 404; - return; - } - - ctx.body = renderActivity(await renderLike(reaction, note)); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); -}); - -// follow -router.get('/follows/:follower/:followee', async ctx => { - // 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)); - 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 09906250f..000000000 --- a/packages/backend/src/server/activitypub/featured.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Router from '@koa/router'; -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 { setResponseType } from '../activitypub.js'; - -export default async (ctx: Router.RouterContext) => { - 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); - 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 beb48713a..000000000 --- a/packages/backend/src/server/activitypub/followers.ts +++ /dev/null @@ -1,95 +0,0 @@ -import Router from '@koa/router'; -import { FindOptionsWhere, 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 { Following } from '@/models/entities/following.js'; -import { setResponseType } from '../activitypub.js'; - -export default async (ctx: Router.RouterContext) => { - 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); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } -}; diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts deleted file mode 100644 index 3a25a6316..000000000 --- a/packages/backend/src/server/activitypub/following.ts +++ /dev/null @@ -1,95 +0,0 @@ -import Router from '@koa/router'; -import { LessThan, IsNull, FindOptionsWhere } 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 { Following } from '@/models/entities/following.js'; -import { setResponseType } from '../activitypub.js'; - -export default async (ctx: Router.RouterContext) => { - 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 (cursor) { - query.id = LessThan(cursor); - } - - // Get followings - const followings = await Followings.find({ - where: query, - take: limit + 1, - order: { id: -1 }, - }); - - // 「次のページ」があるかどうか - const inStock = followings.length === limit + 1; - if (inStock) followings.pop(); - - const renderedFollowees = await Promise.all(followings.map(following => renderFollowUser(following.followeeId))); - const rendered = renderOrderedCollectionPage( - `${partOf}?${url.query({ - page: 'true', - cursor, - })}`, - user.followingCount, renderedFollowees, partOf, - undefined, - inStock ? `${partOf}?${url.query({ - page: 'true', - cursor: followings[followings.length - 1].id, - })}` : undefined, - ); - - ctx.body = renderActivity(rendered); - setResponseType(ctx); - } else { - // index page - const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); - ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } -}; diff --git a/packages/backend/src/server/activitypub/outbox.ts b/packages/backend/src/server/activitypub/outbox.ts deleted file mode 100644 index 60e2ab711..000000000 --- a/packages/backend/src/server/activitypub/outbox.ts +++ /dev/null @@ -1,109 +0,0 @@ -import Router from '@koa/router'; -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 { Note } from '@/models/entities/note.js'; -import { isPureRenote } from '@/misc/renote.js'; -import { makePaginationQuery } from '../api/common/make-pagination-query.js'; -import { setResponseType } from '../activitypub.js'; - -export default async (ctx: Router.RouterContext) => { - 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('NOT note.localOnly'); - - const notes = await query.take(limit).getMany(); - - if (sinceId) notes.reverse(); - - const activities = await Promise.all(notes.map(note => packActivity(note))); - const rendered = renderOrderedCollectionPage( - `${partOf}?${url.query({ - page: 'true', - since_id: sinceId, - until_id: untilId, - })}`, - user.notesCount, activities, partOf, - notes.length ? `${partOf}?${url.query({ - page: 'true', - since_id: notes[0].id, - })}` : undefined, - notes.length ? `${partOf}?${url.query({ - page: 'true', - until_id: notes[notes.length - 1].id, - })}` : undefined, - ); - - ctx.body = renderActivity(rendered); - setResponseType(ctx); - } else { - // index page - const rendered = renderOrderedCollection(partOf, user.notesCount, - `${partOf}?page=true`, - `${partOf}?page=true&since_id=000000000000000000000000`, - ); - ctx.body = renderActivity(rendered); - ctx.set('Cache-Control', 'public, max-age=180'); - setResponseType(ctx); - } -}; - -/** - * Pack Create or Announce Activity - * @param note Note - */ -export async function packActivity(note: Note): Promise { - if (isPureRenote(note)) { - const renote = await Notes.findOneByOrFail({ id: note.renoteId }); - return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note); - } else { - 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 96b9316e4..000000000 --- a/packages/backend/src/server/api/2fa.ts +++ /dev/null @@ -1,422 +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 de9af0890..000000000 --- a/packages/backend/src/server/api/api-handler.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Koa from 'koa'; - -import { IEndpoint } from './endpoints.js'; -import authenticate, { AuthenticationError } from './authenticate.js'; -import call from './call.js'; -import { ApiError } from './error.js'; - -function getRequestArguments(ctx: Koa.Context): Record { - const args = { - ...(ctx.params || {}), - ...ctx.query, - ...(ctx.request.body || {}), - }; - - // For security reasons, we drop the i parameter if it's a GET request - if (ctx.method === 'GET') { - delete args['i']; - } - - return args; -} - -export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise { - const body = getRequestArguments(ctx); - - // Authentication - // for GET requests, do not even pass on the body parameter as it is considered unsafe - await authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(async ([user, app]) => { - // API invoking - await 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}`); - } - if (res == null) { - ctx.status = 204; - } else { - ctx.status = 200; - // If a string is returned, it must be passed through JSON.stringify to be recognized as JSON. - ctx.body = typeof res === 'string' ? JSON.stringify(res) : res; - } - }).catch((e: ApiError) => { - e.apply(ctx, endpoint.name); - }); - }).catch(e => { - if (e instanceof AuthenticationError) { - new ApiError('AUTHENTICATION_FAILED', e.message).apply(ctx, endpoint.name); - } else { - new ApiError().apply(ctx, endpoint.name); - } - }); -} diff --git a/packages/backend/src/server/api/authenticate.ts b/packages/backend/src/server/api/authenticate.ts deleted file mode 100644 index 25e87b75e..000000000 --- a/packages/backend/src/server/api/authenticate.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { CacheableLocalUser } from '@/models/entities/user.js'; -import { Users, AccessTokens } from '@/models/index.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { userByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js'; -import isNativeToken from './common/is-native-token.js'; - -export class AuthenticationError extends Error { - constructor(message: string) { - super(message); - this.name = 'AuthenticationError'; - } -} - -export default async (authorization: string | null | undefined, bodyToken: string | null | undefined): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => { - let maybeToken: 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 ') { - maybeToken = authorization.substring(7); - } else { - throw new AuthenticationError('unsupported authentication scheme'); - } - } else if (bodyToken != null) { - maybeToken = bodyToken; - } else { - return [null, null]; - } - const token: string = maybeToken; - - if (isNativeToken(token)) { - const user = await localUserByNativeTokenCache.fetch(token); - - if (user == null) { - throw new AuthenticationError('unknown token'); - } - - return [user, null]; - } else { - const accessToken = await AccessTokens.findOne({ - where: [{ - hash: token.toLowerCase(), // app - }, { - token, // miauth - }], - }); - - if (accessToken == null) { - throw new AuthenticationError('unknown token'); - } - - AccessTokens.update(accessToken.id, { - lastUsedAt: new Date(), - }); - - const user = await userByIdCache.fetch(accessToken.userId); - - // can't authorize remote users - if (!Users.isLocalUser(user)) return [null, null]; - - 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 89493ce96..000000000 --- a/packages/backend/src/server/api/call.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { performance } from 'perf_hooks'; -import Koa from 'koa'; -import { CacheableLocalUser } from '@/models/entities/user.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { getIpHash } from '@/misc/get-ip-hash.js'; -import { limiter } from './limiter.js'; -import endpoints, { IEndpointMeta } from './endpoints.js'; -import { ApiError } from './error.js'; -import { apiLogger } from './logger.js'; - -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); - - if (ep == null) throw new ApiError('NO_SUCH_ENDPOINT'); - - if (ep.meta.secure && !isSecure) { - throw new ApiError('ACCESS_DENIED', 'This operation can only be performed with a native token.'); - } - - if (ep.meta.limit && !isModerator) { - // 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(() => { - throw new ApiError('RATE_LIMIT_EXCEEDED'); - }); - } - - if (ep.meta.requireCredential && user == null) { - throw new ApiError('AUTHENTICATION_REQUIRED'); - } - - if (ep.meta.requireCredential && user!.isSuspended) { - throw new ApiError('SUSPENDED'); - } - - if (ep.meta.requireAdmin && !user!.isAdmin) { - throw new ApiError('ACCESS_DENIED', 'This operation requires administrator privileges.'); - } - - if (ep.meta.requireModerator && !isModerator) { - throw new ApiError('ACCESS_DENIED', 'This operation requires moderator privileges.'); - } - - if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { - throw new ApiError('ACCESS_DENIED', 'This operation requires privileges which this token does not grant.'); - } - - // 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('INVALID_PARAM', { - param: k, - reason: `cannot cast to ${param.type}`, - }); - } - } - } - } - - // API invoking - const before = performance.now(); - return await ep.exec(data, user, token, ctx?.file).catch((e: Error) => { - if (e instanceof ApiError) { - throw e; - } else { - apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, { - ep: ep.name, - ps: data, - e: { - message: e.message, - code: e.name, - stack: e.stack, - }, - }); - throw new ApiError('INTERNAL_ERROR', { - 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/compare-url.ts b/packages/backend/src/server/api/common/compare-url.ts deleted file mode 100644 index 2d35dbd97..000000000 --- a/packages/backend/src/server/api/common/compare-url.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { URL } from 'node:url'; - -/** - * Compares two URLs for OAuth. The first parameter is the trusted URL - * which decides how the comparison is conducted. - * - * Invalid URLs are never equal. - * - * Implements the current draft-ietf-oauth-security-topics-21 § 4.1.3 - * (published 2022-09-27) - */ -export function compareUrl(trusted: string, untrusted: string): boolean { - let trustedUrl, untrustedUrl; - - try { - trustedUrl = new URL(trusted); - untrustedUrl = new URL(untrusted); - } catch { - return false; - } - - // Excerpt from RFC 8252: - //> Loopback redirect URIs use the "http" scheme and are constructed with - //> the loopback IP literal and whatever port the client is listening on. - //> That is, "http://127.0.0.1:{port}/{path}" for IPv4, and - //> "http://[::1]:{port}/{path}" for IPv6. - // - // To be nice we also include the "localhost" name, since it is required - // to resolve to one of the other two. - if (trustedUrl.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(trustedUrl.host)) { - // localhost comparisons should ignore port number - trustedUrl.port = ''; - untrustedUrl.port = ''; - } - - // security recommendation is to just compare the (normalized) string - //> This document therefore advises to simplify the required logic and configuration - //> by using exact redirect URI matching. This means the authorization server MUST - //> compare the two URIs using simple string comparison as defined in [RFC3986], - //> Section 6.2.1. - return trustedUrl.href === untrustedUrl.href; -} 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 42db0c241..000000000 --- a/packages/backend/src/server/api/common/generate-block-query.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Brackets, SelectQueryBuilder } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { Blockings } from '@/models/index.js'; - -// ここでいう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 93003bfa0..000000000 --- a/packages/backend/src/server/api/common/generate-channel-query.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Brackets, SelectQueryBuilder } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { ChannelFollowings } from '@/models/index.js'; - -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 562dc4e3d..000000000 --- a/packages/backend/src/server/api/common/generate-muted-note-query.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { SelectQueryBuilder } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { MutedNotes } from '@/models/index.js'; - -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 681ec4eb2..000000000 --- a/packages/backend/src/server/api/common/generate-muted-note-thread-query.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Brackets, SelectQueryBuilder } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { NoteThreadMutings } from '@/models/index.js'; - -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 470ece1a6..000000000 --- a/packages/backend/src/server/api/common/generate-muted-user-query.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { SelectQueryBuilder, Brackets } from 'typeorm'; -import { 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 5d8a4c537..000000000 --- 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 31880382e..000000000 --- a/packages/backend/src/server/api/common/generate-replies-query.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Brackets, SelectQueryBuilder } from 'typeorm'; -import { User } from '@/models/entities/user.js'; - -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 12b610cde..000000000 --- a/packages/backend/src/server/api/common/generate-visibility-query.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Brackets, SelectQueryBuilder } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { Followings } from '@/models/index.js'; - -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 70159862a..000000000 --- a/packages/backend/src/server/api/common/generated-muted-renote-query.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Brackets, SelectQueryBuilder } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { RenoteMutings } from '@/models/index.js'; - -export function generateMutedRenotesQuery(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 f5fba27f6..000000000 --- a/packages/backend/src/server/api/common/getters.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { User } from '@/models/entities/user.js'; -import { 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 13bf156c1..000000000 --- a/packages/backend/src/server/api/common/inject-featured.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { DAY } from '@/const.js'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; -import { secureRndstr } from '@/misc/secure-rndstr.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 offset = 3 * DAY; - - 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() - offset) }) - .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_ = secureRndstr(8); - - // Inject featured - timeline.splice(3, 0, featured); -} 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 2833c570c..000000000 --- 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 91cdfec2c..000000000 --- a/packages/backend/src/server/api/common/make-pagination-query.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SelectQueryBuilder } from 'typeorm'; - -export function makePaginationQuery(q: SelectQueryBuilder, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number) { - if (sinceId && untilId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId }); - q.andWhere(`${q.alias}.id < :untilId`, { untilId }); - q.orderBy(`${q.alias}.id`, 'DESC'); - } else if (sinceId) { - q.andWhere(`${q.alias}.id > :sinceId`, { sinceId }); - q.orderBy(`${q.alias}.id`, 'ASC'); - } else if (untilId) { - q.andWhere(`${q.alias}.id < :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/oauth.ts b/packages/backend/src/server/api/common/oauth.ts deleted file mode 100644 index 3fec3ef11..000000000 --- a/packages/backend/src/server/api/common/oauth.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as crypto from 'node:crypto'; -import Koa from 'koa'; -import { IsNull, Not } from 'typeorm'; -import { Apps, AuthSessions } from '@/models/index.js'; -import { compareUrl } from './compare-url.js'; - -export async function oauth(ctx: Koa.Context): void { - const { - grant_type, - code, - redirect_uri, - code_verifier, - } = ctx.request.body; - - // check if any of the parameters are null or empty string - if ([grant_type, code].some(x => !x)) { - ctx.response.status = 400; - ctx.response.body = { - error: 'invalid_request', - }; - return; - } - - if (grant_type !== 'authorization_code') { - ctx.response.status = 400; - ctx.response.body = { - error: 'unsupported_grant_type', - error_description: 'only authorization_code grants are supported', - }; - return; - } - - const authHeader = ctx.headers.authorization; - if (!authHeader?.toLowerCase().startsWith('basic ')) { - ctx.response.status = 401; - ctx.response.set('WWW-Authenticate', 'Basic'); - ctx.response.body = { - error: 'invalid_client', - error_description: 'HTTP Basic Authentication required', - }; - return; - } - - const [client_id, client_secret] = new Buffer(authHeader.slice(6), 'base64') - .toString('ascii') - .split(':', 2); - - const [app, session] = await Promise.all([ - Apps.findOneBy({ - id: client_id, - secret: client_secret, - }), - AuthSessions.findOne({ - where: { - appId: client_id, - token: code, - // only check for approved auth sessions - accessTokenId: Not(IsNull()), - }, - relations: { - accessToken: true, - }, - }), - ]); - if (app == null) { - ctx.response.status = 401; - ctx.response.set('WWW-Authenticate', 'Basic'); - ctx.response.body = { - error: 'invalid_client', - error_description: 'authentication failed', - }; - return; - } - if (session == null) { - ctx.response.status = 400; - ctx.response.body = { - error: 'invalid_grant', - }; - return; - } - - // check PKCE challenge, if provided before - if (session.pkceChallenge) { - // Also checking the client's homework, the RFC says: - //> minimum length of 43 characters and a maximum length of 128 characters - if (!code_verifier || code_verifier.length < 43 || code_verifier.length > 128) { - ctx.response.status = 400; - ctx.response.body = { - error: 'invalid_grant', - error_description: 'invalid or missing PKCE code_verifier', - }; - return; - } else { - // verify that (from RFC 7636): - //> BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge - const hash = crypto.createHash('sha256'); - hash.update(code_verifier); - - if (hash.digest('base64url') !== code_challenge) { - ctx.response.status = 400; - ctx.response.body = { - error: 'invalid_grant', - error_description: 'invalid PKCE code_verifier', - }; - return; - } - } - } - - // check redirect URI - if (!compareUrl(app.callbackUrl, redirect_uri)) { - ctx.response.status = 400; - ctx.response.body = { - error: 'invalid_grant', - error_description: 'Mismatched redirect_uri', - }; - return; - } - - // session is single use - await AuthSessions.delete(session.id), - - ctx.response.status = 200; - ctx.response.body = { - access_token: session.accessToken.token, - token_type: 'bearer', - scope: session.accessToken.permission.join(' '), - }; -} 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 e8e16f319..000000000 --- a/packages/backend/src/server/api/common/read-messaging-message.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { In } from 'typeorm'; -import { publishMainStream, publishMessagingStream, publishMessagingIndexStream, publishGroupMessagingStream } from '@/services/stream.js'; -import { pushNotification } from '@/services/push-notification.js'; -import { User, IRemoteUser } from '@/models/entities/user.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { MessagingMessages, UserGroupJoinings, Users } from '@/models/index.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { 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, - 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, - }); - 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 }) - .andWhere('message.userId != :userId', { userId }) - .andWhere('NOT (:userId = ANY(message.reads))', { 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[]) { - const contents = toArray(messages) - .filter(x => x.uri) - .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 b0d38a9e3..000000000 --- a/packages/backend/src/server/api/common/read-notification.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { In } from 'typeorm'; -import { publishMainStream } from '@/services/stream.js'; -import { pushNotification } from '@/services/push-notification.js'; -import { User } from '@/models/entities/user.js'; -import { 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 038fd8d96..000000000 --- a/packages/backend/src/server/api/common/signin.ts +++ /dev/null @@ -1,44 +0,0 @@ -import Koa from 'koa'; - -import config from '@/config/index.js'; -import { 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 59a1c9538..000000000 --- a/packages/backend/src/server/api/common/signup.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { generateKeyPair } from 'node:crypto'; -import { IsNull } from 'typeorm'; -import { User } from '@/models/entities/user.js'; -import { Users, UsedUsernames } from '@/models/index.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { genId } from '@/misc/gen-id.js'; -import { toPunyNullable } from '@/misc/convert-host.js'; -import { hashPassword } from '@/misc/password.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 { ApiError } from '@/server/api/error.js'; -import generateUserToken from './generate-native-user-token.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; - - // Validate username - if (!Users.validateLocalUsername(username)) { - throw new ApiError('INVALID_USERNAME'); - } - - if (password != null && passwordHash == null) { - // Validate password - if (!Users.validatePassword(password)) { - throw new ApiError('INVALID_PASSWORD'); - } - - hash = await hashPassword(password); - } - - // Generate secret - const secret = generateUserToken(); - - // Check username duplication - if (await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) { - throw new ApiError('USED_USERNAME'); - } - - // Check deleted username duplication - if (await UsedUsernames.findOneBy({ username: username.toLowerCase() })) { - throw new ApiError('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.countBy(User, { - usernameLower: username.toLowerCase(), - host: IsNull(), - }); - - if (exist) throw new ApiError('USED_USERNAME'); - - account = await transactionalEntityManager.save(new User({ - id: genId(), - createdAt: new Date(), - username, - usernameLower: username.toLowerCase(), - host: toPunyNullable(host), - token: secret, - isAdmin: (await Users.countBy({ - host: IsNull(), - })) === 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/common/translator.ts b/packages/backend/src/server/api/common/translator.ts deleted file mode 100644 index dde4686b3..000000000 --- a/packages/backend/src/server/api/common/translator.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Meta, TranslationService } from '@/models/entities/meta.js'; - -export function translatorAvailable(instance: Meta): boolean { - switch (instance.translationService) { - case TranslationService.DeepL: - return instance.deeplAuthKey != null; - case TranslationService.LibreTranslate: - return instance.libreTranslateEndpoint != null; - default: - return false; - } -} diff --git a/packages/backend/src/server/api/define.ts b/packages/backend/src/server/api/define.ts deleted file mode 100644 index 243b105ae..000000000 --- a/packages/backend/src/server/api/define.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as fs from 'node:fs'; -import Ajv from 'ajv'; -import { CacheableLocalUser } from '@/models/entities/user.js'; -import { Schema, SchemaType } from '@/misc/schema.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { 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) => - Promise>>; - -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) => Promise { - const validate = ajv.compile(paramDef); - - return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => { - function cleanup() { - fs.unlink(file.path, () => {}); - } - - if (meta.requireFile && file == null) { - return Promise.reject(new ApiError('FILE_REQUIRED')); - } - - const valid = validate(params); - if (!valid) { - if (file) cleanup(); - - const errors = validate.errors!; - const err = new ApiError('INVALID_PARAM', { - param: errors[0].schemaPath, - reason: errors[0].message, - }); - return Promise.reject(err); - } - - return cb(params as SchemaType, user, token, file, cleanup); - }; -} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts deleted file mode 100644 index 59c4305c2..000000000 --- a/packages/backend/src/server/api/endpoints.ts +++ /dev/null @@ -1,739 +0,0 @@ -import { Schema } from '@/misc/schema.js'; -import { errors } from './error.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_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_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_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_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_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_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___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_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_deny from './endpoints/auth/deny.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_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___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_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_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___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_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___pinnedUsers from './endpoints/pinned-users.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_register from './endpoints/sw/register.js'; -import * as ep___sw_unregister from './endpoints/sw/unregister.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_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_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'; - -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/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/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/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/invite', ep___admin_invite], - ['admin/moderators/add', ep___admin_moderators_add], - ['admin/moderators/remove', ep___admin_moderators_remove], - ['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/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], - ['announcements', ep___announcements], - ['antennas/create', ep___antennas_create], - ['antennas/delete', ep___antennas_delete], - ['antennas/list', ep___antennas_list], - ['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/deny', ep___auth_deny], - ['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/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], - ['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/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/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', 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], - ['renote-mute/create', ep___renote_mute_create], - ['renote-mute/delete', ep___renote_mute_delete], - ['renote-mute/list', ep___renote_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/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], - ['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], - ['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/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/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], - ['fetch-rss', ep___fetchRss], -]; - -export interface IEndpointMeta { - readonly stability?: 'deprecated' | 'experimental' | 'stable'; - - readonly tags?: ReadonlyArray; - - readonly errors?: ReadonlyArray; - - 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; - - /** - * エンドポイントの種類 - * パーミッションの実現に利用されます。 - */ - readonly kind?: string; - - readonly description?: string; - - /** - * GETでのリクエストを許容するか否か - */ - readonly allowGet?: boolean; - - /** - * 正常応答をキャッシュ (Cache-Control: public) する秒数 - */ - readonly cacheSec?: number; - - /** - * API v2 options - */ - readonly v2?: { - - /** - * HTTP verb this endpoint supports - */ - readonly method: 'get' | 'put' | 'post' | 'patch' | 'delete'; - - /** - * Path alias for v2 endpoint - * - * @example (v0) /api/notes/create -> /api/v2/notes - */ - readonly alias?: string; - }; -} - -export interface IEndpoint { - name: string; - exec: any; - meta: IEndpointMeta; - params: Schema; -} - -const endpoints: IEndpoint[] = eps.map(([name, ep]) => { - return { - 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 e2e215ed8..000000000 --- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { AbuseUserReports } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import define from '../../define.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', - }, - forwarded: { - type: 'boolean', - nullable: false, optional: false, - example: 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' }, - 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' }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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'); break; - case 'unresolved': query.andWhere('NOT report.resolved'); 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 3012d2127..000000000 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { IsNull } from 'typeorm'; -import { Users } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.js'; -import { signup } from '../../../common/signup.js'; - -export const meta = { - tags: ['admin'], - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'User', - properties: { - token: { - type: 'string', - optional: false, nullable: false, - }, - }, - }, - - errors: ['ACCESS_DENIED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - username: Users.localUsernameSchema, - password: Users.passwordSchema, - }, - required: ['username', 'password'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, _me) => { - const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null; - if (me == null) { - // check if this is the initial setup - const noUsers = (await Users.countBy({ - host: IsNull(), - })) === 0; - if (!noUsers) { - throw new ApiError('ACCESS_DENIED'); - } - } else if (!me.isAdmin) { - throw new ApiError('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 4f5ea5568..000000000 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Users } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import { deleteAccount } from '@/services/delete-account.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireAdmin: true, - - errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'], -} 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) => { - const user = await Users.findOneBy({ - id: ps.userId, - isDeleted: false, - }); - - if (user == null) { - throw new ApiError('NO_SUCH_USER'); - } else if (user.isAdmin) { - throw new ApiError('IS_ADMIN'); - } else if (user.isModerator) { - throw new ApiError('IS_MODERATOR'); - } - - await deleteAccount(user); -}); 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 e1ea545ba..000000000 --- a/packages/backend/src/server/api/endpoints/admin/announcements/create.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Announcements } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -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 c2e5ccf24..000000000 --- a/packages/backend/src/server/api/endpoints/admin/announcements/delete.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Announcements } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_ANNOUNCEMENT'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { type: 'string', format: 'misskey:id' }, - }, - required: ['id'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const announcement = await Announcements.findOneBy({ id: ps.id }); - - if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); - - 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 1ffce9fbe..000000000 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Announcements, AnnouncementReads } from '@/models/index.js'; -import { 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; - -// eslint-disable-next-line import/no-default-export -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 341e5636b..000000000 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Announcements } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_ANNOUNCEMENT'], -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const announcement = await Announcements.findOneBy({ id: ps.id }); - - if (announcement == null) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); - - await Announcements.update(announcement.id, { - updatedAt: new Date(), - title: ps.title, - text: ps.text, - imageUrl: ps.imageUrl, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts deleted file mode 100644 index b6b2b7872..000000000 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { deleteFile } from '@/services/drive/delete-file.js'; -import { DriveFiles } 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' }, - }, - required: ['userId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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/clean-remote-files.ts b/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts deleted file mode 100644 index b36623293..000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createCleanRemoteFilesJob } from '@/queue/index.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - createCleanRemoteFilesJob(); -}); 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 5fcda7614..000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive/files.ts +++ /dev/null @@ -1,70 +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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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 f0c3ea7c0..000000000 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ /dev/null @@ -1,180 +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: ['NO_SUCH_FILE'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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('NO_SUCH_FILE'); - - 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 3d3ce1fe2..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { In } from 'typeorm'; -import { Emojis } from '@/models/index.js'; -import { db } from '@/db/postgre.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -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 a4871da27..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { publishBroadcastStream } from '@/services/stream.js'; -import { db } from '@/db/postgre.js'; -import { Emojis, DriveFiles } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_FILE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError('NO_SUCH_FILE'); - - const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${genId()}_`; - - const emoji = await Emojis.insert({ - id: genId(), - updatedAt: new Date(), - name, - category: null, - host: null, - aliases: [], - originalUrl: file.url, - publicUrl: file.webpublicUrl ?? file.url, - type: file.webpublicType ?? file.type, - }).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 a098217b4..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Emojis } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { 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'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_EMOJI', 'INTERNAL_ERROR'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const emoji = await Emojis.findOneBy({ id: ps.emojiId }); - - if (emoji == null) throw new ApiError('NO_SUCH_EMOJI'); - - let driveFile: DriveFile; - - try { - // Create file - driveFile = await uploadFromUrl({ url: emoji.originalUrl, user: null, force: true }); - } catch (e) { - throw new ApiError('INTERNAL_ERROR', e); - } - - 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, - }).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 4aef8b898..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { In } from 'typeorm'; -import { Emojis } from '@/models/index.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, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - ids: { type: 'array', items: { - type: 'string', format: 'misskey:id', - } }, - }, - required: ['ids'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 }); - } -}); 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 ab80e81bb..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Emojis } from '@/models/index.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { db } from '@/db/postgre.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_EMOJI'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - id: { type: 'string', format: 'misskey:id' }, - }, - required: ['id'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const emoji = await Emojis.findOneBy({ id: ps.id }); - - if (emoji == null) throw new ApiError('NO_SUCH_EMOJI'); - - await Emojis.delete(emoji.id); - - await db.queryResultCache!.remove(['meta_emojis']); - - insertModerationLog(me, 'deleteEmoji', { 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 48adef12b..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createImportCustomEmojisJob } from '@/queue/index.js'; -import define from '../../../define.js'; - -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; - -// eslint-disable-next-line import/no-default-export -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 b3134210d..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Emojis } from '@/models/index.js'; -import { toPuny } from '@/misc/convert-host.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', - }, - 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, - }, - }, - }, - }, -} 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; - -// eslint-disable-next-line import/no-default-export -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 51e987682..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Emoji } from '@/models/entities/emoji.js'; -import { Emojis } from '@/models/index.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', - }, - 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, - }, - }, - }, - }, -} 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; - -// eslint-disable-next-line import/no-default-export -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 cd948386e..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { In } from 'typeorm'; -import { db } from '@/db/postgre.js'; -import { Emojis } 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: { - ids: { type: 'array', items: { - type: 'string', format: 'misskey:id', - } }, - aliases: { type: 'array', items: { - type: 'string', - } }, - }, - required: ['ids', 'aliases'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 bc0cf3f04..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { In } from 'typeorm'; -import { Emojis } from '@/models/index.js'; -import { db } from '@/db/postgre.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -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 07e9cc7c8..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { In } from 'typeorm'; -import { db } from '@/db/postgre.js'; -import { Emojis } 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: { - 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; - -// eslint-disable-next-line import/no-default-export -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/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts deleted file mode 100644 index e451374d5..000000000 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { Emojis } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_EMOJI'], -} 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', - } }, - }, - required: ['id', 'name', 'aliases'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const emoji = await Emojis.findOneBy({ id: ps.id }); - - if (emoji == null) throw new ApiError('NO_SUCH_EMOJI'); - - await Emojis.update(emoji.id, { - updatedAt: new Date(), - name: ps.name, - category: ps.category, - aliases: ps.aliases, - }); - - 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 91a23e90f..000000000 --- a/packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { deleteFile } from '@/services/drive/delete-file.js'; -import { DriveFiles } 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: { - host: { type: 'string' }, - }, - required: ['host'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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 a043d4601..000000000 --- a/packages/backend/src/server/api/endpoints/admin/federation/refresh-remote-instance-metadata.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Instances } from '@/models/index.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { ApiError } from '@/server/api/error.js'; -import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_OBJECT'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - host: { type: 'string' }, - }, - required: ['host'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const instance = await Instances.findOneBy({ host: toPuny(ps.host) }); - - if (instance == null) { - throw new ApiError('NO_SUCH_OBJECT'); - } - - 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 34d894b34..000000000 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ /dev/null @@ -1,34 +0,0 @@ -import deleteFollowing from '@/services/following/delete.js'; -import { Followings, 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: { - host: { type: 'string' }, - }, - required: ['host'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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 9b2e86511..000000000 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Instances } from '@/models/index.js'; -import { toPuny } from '@/misc/convert-host.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_OBJECT'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - host: { type: 'string' }, - isSuspended: { type: 'boolean' }, - }, - required: ['host', 'isSuspended'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const instanceExists = await Instances.countBy({ host: toPuny(ps.host) }); - - if (!instanceExists) { - throw new ApiError('NO_SUCH_OBJECT'); - } - - 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 fb9b4e44f..000000000 --- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { db } from '@/db/postgre.js'; -import define from '../../define.js'; - -export const meta = { - requireCredential: true, - requireModerator: true, - - tags: ['admin'], -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 8a0e1350a..000000000 --- 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; - -// eslint-disable-next-line import/no-default-export -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/invite.ts b/packages/backend/src/server/api/endpoints/admin/invite.ts deleted file mode 100644 index 38ec85e51..000000000 --- a/packages/backend/src/server/api/endpoints/admin/invite.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { RegistrationTickets } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { secureRndstrCustom } from '@/misc/secure-rndstr.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - // omit visually ambiguous zero and letter O as well as one and letter I - const code = secureRndstrCustom(8, '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'); - - 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 ba3159112..000000000 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ /dev/null @@ -1,329 +0,0 @@ -import config from '@/config/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { TranslationService } from '@/models/entities/meta.js'; -import { translatorAvailable } from '../../common/translator.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: false, - }, - bannerUrl: { - type: 'string', - optional: false, nullable: false, - }, - iconUrl: { - type: 'string', - optional: false, nullable: true, - }, - maxNoteTextLength: { - 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', - }, - }, - }, - }, - enableEmail: { - type: 'boolean', - optional: false, nullable: false, - }, - translatorAvailable: { - type: 'boolean', - optional: false, nullable: false, - }, - proxyAccountName: { - type: 'string', - optional: false, nullable: true, - }, - userStarForReactionFallback: { - type: 'boolean', - optional: true, nullable: false, - }, - pinnedUsers: { - 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, - }, - }, - hcaptchaSecretKey: { - type: 'string', - optional: true, nullable: true, - }, - recaptchaSecretKey: { - type: 'string', - optional: true, nullable: true, - }, - proxyAccountId: { - type: 'string', - optional: true, nullable: true, - format: 'id', - }, - 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, - }, - 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, - }, - translatorService: { - type: 'string', - enum: [null, ...Object.values(TranslationService)], - optional: false, nullable: true, - }, - deeplAuthKey: { - type: 'string', - optional: true, nullable: true, - }, - libreTranslateEndpoint: { - type: 'string', - optional: true, nullable: true, - }, - libreTranslateAuthKey: { - type: 'string', - optional: true, nullable: true, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - 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, - disableRegistration: instance.disableRegistration, - disableLocalTimeline: instance.disableLocalTimeline, - disableGlobalTimeline: instance.disableGlobalTimeline, - driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, - driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, - emailRequiredForSignup: instance.emailRequiredForSignup, - enableHcaptcha: instance.enableHcaptcha, - hcaptchaSiteKey: instance.hcaptchaSiteKey, - enableRecaptcha: instance.enableRecaptcha, - recaptchaSiteKey: instance.recaptchaSiteKey, - swPublickey: instance.swPublicKey, - themeColor: instance.themeColor, - bannerUrl: instance.bannerUrl, - iconUrl: instance.iconUrl, - backgroundImageUrl: instance.backgroundImageUrl, - logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: config.maxNoteTextLength, - defaultLightTheme: instance.defaultLightTheme, - defaultDarkTheme: instance.defaultDarkTheme, - enableEmail: instance.enableEmail, - pinnedPages: instance.pinnedPages, - pinnedClipId: instance.pinnedClipId, - cacheRemoteFiles: instance.cacheRemoteFiles, - - useStarForReactionFallback: instance.useStarForReactionFallback, - pinnedUsers: instance.pinnedUsers, - hiddenTags: instance.hiddenTags, - blockedHosts: instance.blockedHosts, - hcaptchaSecretKey: instance.hcaptchaSecretKey, - recaptchaSecretKey: instance.recaptchaSecretKey, - proxyAccountId: instance.proxyAccountId, - summalyProxy: instance.summalyProxy, - email: instance.email, - smtpSecure: instance.smtpSecure, - smtpHost: instance.smtpHost, - smtpPort: instance.smtpPort, - smtpUser: instance.smtpUser, - smtpPass: instance.smtpPass, - 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, - - translatorAvailable: translatorAvailable(instance), - translationService: instance.translationService, - deeplAuthKey: instance.deeplAuthKey, - libreTranslateEndpoint: instance.libreTranslateEndpoint, - libreTranslateAuthKey: instance.libreTranslateAuthKey, - }; -}); 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 abea8c851..000000000 --- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Users } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['admin'], - - description: 'Grants a user moderator privileges. Administrators cannot be granted moderator privileges.', - - requireCredential: true, - requireAdmin: true, - - errors: ['NO_SUCH_USER', 'IS_ADMIN'], -} 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) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new ApiError('NO_SUCH_USER'); - } - - if (user.isAdmin) { - throw new ApiError('IS_ADMIN'); - } - - 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 0a988b820..000000000 --- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Users } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireAdmin: true, - - errors: ['NO_SUCH_USER'], -} 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) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new ApiError('NO_SUCH_USER'); - } - - await Users.update(user.id, { - isModerator: false, - }); - - publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: false }); -}); 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 cf38ed22d..000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/clear.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { destroy } from '@/queue/index.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 b89360720..000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { URL } from 'node:url'; -import { deliverQueue } from '@/queue/queues.js'; -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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - 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 274313f8a..000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { URL } from 'node:url'; -import { inboxQueue } from '@/queue/queues.js'; -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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - 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 7a0d6bdb7..000000000 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { deliverQueue, inboxQueue, dbQueue, objectStorageQueue } 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', - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - const deliverJobCounts = await deliverQueue.getJobCounts(); - const inboxJobCounts = await inboxQueue.getJobCounts(); - const dbJobCounts = await dbQueue.getJobCounts(); - const objectStorageJobCounts = await objectStorageQueue.getJobCounts(); - - return { - deliver: deliverJobCounts, - inbox: inboxJobCounts, - db: dbJobCounts, - objectStorage: objectStorageJobCounts, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts deleted file mode 100644 index 76a89ef3d..000000000 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { URL } from 'node:url'; -import { addRelay } from '@/services/relay.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['INVALID_URL'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - try { - if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError('INVALID_URL', 'https only'); - } catch (e) { - throw new ApiError('INVALID_URL', e); - } - - 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 2bc42f74a..000000000 --- a/packages/backend/src/server/api/endpoints/admin/relays/list.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { listRelay } from '@/services/relay.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - 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 5dcf241b1..000000000 --- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { removeRelay } from '@/services/relay.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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 5561b1507..000000000 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { hashPassword } from '@/misc/password.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { Users, UserProfiles } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../define.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, - }, - }, - }, - - errors: ['NO_SUCH_USER', 'IS_ADMIN'], -} 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) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new ApiError('NO_SUCH_USER'); - } - - if (user.isAdmin) { - throw new ApiError('IS_ADMIN'); - } - - const password = secureRndstr(8, true); - - await UserProfiles.update({ - userId: user.id, - }, { - password: await hashPassword(password), - }); - - return { - password, - }; -}); 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 fa674280c..000000000 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ /dev/null @@ -1,40 +0,0 @@ -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'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const report = await AbuseUserReports.findOneByOrFail({ id: ps.reportId }); - - if (ps.forward && report.targetUserHost != null) { - const actor = await getInstanceActor(); - const targetUser = await Users.findOneByOrFail({ id: report.targetUserId }); - - deliver(actor, renderActivity(renderFlag(actor, report)), 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/send-email.ts b/packages/backend/src/server/api/endpoints/admin/send-email.ts deleted file mode 100644 index bd0c740cd..000000000 --- a/packages/backend/src/server/api/endpoints/admin/send-email.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { sendEmail } from '@/services/send-email.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 61d80f5f2..000000000 --- a/packages/backend/src/server/api/endpoints/admin/server-info.ts +++ /dev/null @@ -1,127 +0,0 @@ -import * as os from 'node:os'; -import si from 'systeminformation'; -import { db } from '@/db/postgre.js'; -import { redisClient } from '@/db/redis.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 0dd0cbe1b..000000000 --- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ModerationLogs } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 604426470..000000000 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Signins, UserProfiles, Users } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - res: { - type: 'object', - nullable: false, optional: false, - }, - - errors: ['NO_SUCH_USER', 'IS_ADMIN'], -} 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, me) => { - const [user, profile] = await Promise.all([ - Users.findOneBy({ id: ps.userId }), - UserProfiles.findOneBy({ userId: ps.userId }), - ]); - - if (user == null || profile == null) { - throw new ApiError('NO_SUCH_USER'); - } - - const _me = await Users.findOneByOrFail({ id: me.id }); - if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) { - throw new ApiError('IS_ADMIN'); - } - - if (!_me.isAdmin) { - return { - isModerator: user.isModerator, - isSilenced: user.isSilenced, - isSuspended: user.isSuspended, - }; - } - - const signins = await Signins.findBy({ userId: user.id }); - - return { - email: profile.email, - emailVerified: profile.emailVerified, - autoAcceptFollowed: profile.autoAcceptFollowed, - noCrawle: profile.noCrawle, - alwaysMarkNsfw: profile.alwaysMarkNsfw, - carefulBot: profile.carefulBot, - injectFeaturedNote: profile.injectFeaturedNote, - receiveAnnouncementEmail: profile.receiveAnnouncementEmail, - mutedWords: profile.mutedWords, - mutedInstances: profile.mutedInstances, - mutingNotificationTypes: profile.mutingNotificationTypes, - isModerator: user.isModerator, - isSilenced: user.isSilenced, - isSuspended: user.isSuspended, - 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 254d76a89..000000000 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { DAY } from '@/const.js'; -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', - description: "To represent the local host, use `origin: 'local'` instead.", - }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Users.createQueryBuilder('user'); - - switch (ps.state) { - case 'available': query.where('NOT user.isSuspended'); break; - case 'admin': query.where('user.isAdmin'); break; - case 'moderator': query.where('user.isModerator'); break; - case 'adminOrModerator': query.where('user.isAdmin OR user.isModerator'); break; - case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 5 * DAY) }); break; - case 'silenced': query.where('user.isSilenced'); break; - case 'suspended': query.where('user.isSuspended'); break; - } - - switch (ps.origin) { - case 'local': query.andWhere('user.host IS NULL'); break; - case 'remote': query.andWhere('user.host IS NOT NULL'); break; - } - - if (ps.username) { - query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }); - } - - if (ps.hostname) { - query.andWhere('user.host like :hostname', { hostname: '%' + ps.hostname.toLowerCase() + '%' }); - } - - switch (ps.sort) { - case '+follower': query.orderBy('user.followersCount', 'DESC'); break; - case '-follower': query.orderBy('user.followersCount', 'ASC'); break; - case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; - case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; - case '+updatedAt': query.orderBy('user.updatedAt', 'DESC', 'NULLS LAST'); break; - case '-updatedAt': query.orderBy('user.updatedAt', 'ASC', 'NULLS FIRST'); break; - default: query.orderBy('user.id', 'ASC'); break; - } - - query.take(ps.limit); - query.skip(ps.offset); - - const users = await query.getMany(); - - return await Users.packMany(users, me, { detail: true }); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts deleted file mode 100644 index 7b0a1bbad..000000000 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Users } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_USER', 'IS_ADMIN'], -} 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, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new ApiError('NO_SUCH_USER'); - } - - if (user.isAdmin) { - throw new ApiError('IS_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 9527f7edf..000000000 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ /dev/null @@ -1,84 +0,0 @@ -import deleteFollowing from '@/services/following/delete.js'; -import { Users, Followings, Notifications } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { ApiError } from '@/server/api/error.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { doPostSuspend } from '@/services/suspend-user.js'; -import { publishUserEvent } from '@/services/stream.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'], -} 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, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new ApiError('NO_SUCH_USER'); - } else if (user.isAdmin) { - throw new ApiError('IS_ADMIN'); - } else if (user.isModerator) { - throw new ApiError('IS_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(() => {}); - await unFollowAll(user).catch(() => {}); - await readAllNotify(user).catch(() => {}); - })(); -}); - -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 7c088bd76..000000000 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Users } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_USER'], -} 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, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new ApiError('NO_SUCH_USER'); - } - - 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 17fad27c4..000000000 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Users } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { doPostUnsuspend } from '@/services/unsuspend-user.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['admin'], - - requireCredential: true, - requireModerator: true, - - errors: ['NO_SUCH_USER'], -} 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, me) => { - const user = await Users.findOneBy({ id: ps.userId }); - - if (user == null) { - throw new ApiError('NO_SUCH_USER'); - } - - 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 965140018..000000000 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { insertModerationLog } from '@/services/insert-moderation-log.js'; -import { fetchMeta, setMeta } from '@/misc/fetch-meta.js'; -import { TranslationService } from '@/models/entities/meta.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 }, - disableGlobalTimeline: { type: 'boolean', nullable: true }, - useStarForReactionFallback: { type: 'boolean', nullable: true }, - pinnedUsers: { type: 'array', nullable: true, items: { - type: 'string', - } }, - hiddenTags: { type: 'array', nullable: true, items: { - type: 'string', - } }, - blockedHosts: { type: 'array', nullable: true, items: { - type: 'string', - } }, - themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, - bannerUrl: { type: 'string', nullable: true }, - iconUrl: { type: 'string', nullable: true }, - backgroundImageUrl: { type: 'string', nullable: true }, - logoImageUrl: { 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 }, - 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 }, - translationService: { type: 'string', nullable: true, enum: [null, ...Object.values(TranslationService)] }, - deeplAuthKey: { type: 'string', nullable: true }, - libreTranslateAuthKey: { type: 'string', nullable: true }, - libreTranslateEndpoint: { 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 }, - tosUrl: { type: 'string', nullable: true }, - 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' }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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.disableGlobalTimeline === 'boolean') { - set.disableGlobalTimeline = ps.disableGlobalTimeline; - } - - if (typeof ps.useStarForReactionFallback === 'boolean') { - set.useStarForReactionFallback = ps.useStarForReactionFallback; - } - - if (Array.isArray(ps.pinnedUsers)) { - set.pinnedUsers = ps.pinnedUsers.filter(Boolean); - } - - if (Array.isArray(ps.hiddenTags)) { - set.hiddenTags = ps.hiddenTags.filter(Boolean); - } - - if (Array.isArray(ps.blockedHosts)) { - set.blockedHosts = ps.blockedHosts.filter(Boolean); - } - - if (ps.themeColor !== undefined) { - set.themeColor = ps.themeColor; - } - - if (ps.bannerUrl !== undefined) { - set.bannerUrl = ps.bannerUrl; - } - - if (ps.iconUrl !== undefined) { - set.iconUrl = ps.iconUrl; - } - - if (ps.backgroundImageUrl !== undefined) { - set.backgroundImageUrl = ps.backgroundImageUrl; - } - - if (ps.logoImageUrl !== undefined) { - set.logoImageUrl = ps.logoImageUrl; - } - - if (ps.name !== undefined) { - set.name = ps.name; - } - - if (ps.description !== undefined) { - set.description = ps.description; - } - - if (ps.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.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.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.tosUrl !== undefined) { - set.ToSUrl = ps.tosUrl; - } - - 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.translationService !== undefined) { - set.translationService = ps.translationService; - } - - if (ps.deeplAuthKey !== undefined) { - if (ps.deeplAuthKey === '') { - set.deeplAuthKey = null; - } else { - set.deeplAuthKey = ps.deeplAuthKey; - } - } - - if (ps.libreTranslateEndpoint !== undefined) { - if (ps.libreTranslateEndpoint === '') { - set.libreTranslateEndpoint = null; - } else { - set.libreTranslateEndpoint = ps.libreTranslateEndpoint; - } - } - - if (ps.libreTranslateAuthKey !== undefined) { - if (ps.libreTranslateAuthKey === '') { - set.libreTranslateAuthKey = null; - } else { - set.libreTranslateAuthKey = ps.libreTranslateAuthKey; - } - } - - const meta = await fetchMeta(); - await setMeta({ - ...meta, - ...set, - }); - - insertModerationLog(me, 'updateMeta'); -}); diff --git a/packages/backend/src/server/api/endpoints/admin/vacuum.ts b/packages/backend/src/server/api/endpoints/admin/vacuum.ts deleted file mode 100644 index 0250dc29e..000000000 --- a/packages/backend/src/server/api/endpoints/admin/vacuum.ts +++ /dev/null @@ -1,36 +0,0 @@ -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, - requireModerator: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - full: { type: 'boolean' }, - analyze: { type: 'boolean' }, - }, - required: ['full', 'analyze'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 23cb93c9a..000000000 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ /dev/null @@ -1,86 +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, - - 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; - -// eslint-disable-next-line import/no-default-export -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 5fae03808..000000000 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { genId } from '@/misc/gen-id.js'; -import { Antennas, UserLists, UserGroupJoinings } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['antennas'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'], - - 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'] }, - 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', - } }, - caseSensitive: { type: 'boolean' }, - withReplies: { type: 'boolean' }, - withFile: { type: 'boolean' }, - notify: { type: 'boolean' }, - }, - required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - 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('NO_SUCH_USER_LIST'); - } else if (ps.src === 'group' && ps.userGroupId) { - userGroupJoining = await UserGroupJoinings.findOneBy({ - userGroupId: ps.userGroupId, - userId: user.id, - }); - - if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP'); - } - - const antenna = await Antennas.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - src: ps.src, - userListId: userList ? userList.id : null, - userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, - keywords: ps.keywords, - excludeKeywords: ps.excludeKeywords, - users: ps.users, - caseSensitive: ps.caseSensitive, - withReplies: ps.withReplies, - withFile: ps.withFile, - notify: ps.notify, - }).then(x => Antennas.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 c3ad26dda..000000000 --- a/packages/backend/src/server/api/endpoints/antennas/delete.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Antennas } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['antennas'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['NO_SUCH_ANTENNA'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - antennaId: { type: 'string', format: 'misskey:id' }, - }, - required: ['antennaId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_ANTENNA'); - - 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 f4804c79d..000000000 --- a/packages/backend/src/server/api/endpoints/antennas/list.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Antennas } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts deleted file mode 100644 index 4dbc824a8..000000000 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ /dev/null @@ -1,81 +0,0 @@ -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 { generateBlockedUserQuery } from '../../common/generate-block-query.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['antennas', 'account', 'notes'], - - requireCredential: true, - - kind: 'read:account', - - errors: ['NO_SUCH_ANTENNA'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_ANTENNA'); - - 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 76278cd2c..000000000 --- a/packages/backend/src/server/api/endpoints/antennas/show.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Antennas } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['antennas', 'account'], - - requireCredential: true, - - kind: 'read:account', - - errors: ['NO_SUCH_ANTENNA'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_ANTENNA'); - - 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 1bdafec4c..000000000 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Antennas, UserLists, UserGroupJoinings } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['antennas'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['NO_SUCH_ANTENNA', 'NO_SUCH_USER_LIST', 'NO_SUCH_GROUP'], - - 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'] }, - 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', - } }, - caseSensitive: { type: 'boolean' }, - withReplies: { type: 'boolean' }, - withFile: { type: 'boolean' }, - notify: { type: 'boolean' }, - }, - required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_ANTENNA'); - - 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('NO_SUCH_USER_LIST'); - } else if (ps.src === 'group' && ps.userGroupId) { - userGroupJoining = await UserGroupJoinings.findOneBy({ - userGroupId: ps.userGroupId, - userId: user.id, - }); - - if (userGroupJoining == null) throw new ApiError('NO_SUCH_GROUP'); - } - - await Antennas.update(antenna.id, { - name: ps.name, - src: ps.src, - userListId: userList ? userList.id : null, - userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, - keywords: ps.keywords, - excludeKeywords: ps.excludeKeywords, - users: ps.users, - caseSensitive: ps.caseSensitive, - withReplies: ps.withReplies, - withFile: ps.withFile, - notify: ps.notify, - }); - - publishInternalEvent('antennaUpdated', await Antennas.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 3a051086a..000000000 --- a/packages/backend/src/server/api/endpoints/ap/get.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['federation'], - - requireCredential: true, - - limit: { - duration: HOUR, - max: 30, - }, - - res: { - type: 'object', - optional: false, nullable: false, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - uri: { type: 'string' }, - }, - required: ['uri'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const resolver = new Resolver(); - const object = await resolver.resolve(ps.uri, true); - 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 414f71d31..000000000 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ /dev/null @@ -1,143 +0,0 @@ -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 { extractDbHost } from '@/misc/convert-host.js'; -import { Users, Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import { CacheableLocalUser, User } from '@/models/entities/user.js'; -import { isActor, isPost } from '@/remote/activitypub/type.js'; -import { SchemaType } from '@/misc/schema.js'; -import { HOUR } from '@/const.js'; -import { shouldBlockInstance } from '@/misc/should-block-instance.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['federation'], - - requireCredential: true, - - limit: { - duration: HOUR, - max: 30, - }, - - errors: ['NO_SUCH_OBJECT'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const object = await fetchAny(ps.uri, me); - if (object) { - return object; - } else { - throw new ApiError('NO_SUCH_OBJECT'); - } -}); - -/*** - * URIからUserかNoteを解決する - */ -async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise | null> { - // Stop if the host is blocked. - const host = extractDbHost(uri); - if (await shouldBlockInstance(host)) { - 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; - - // fetch object from remote - const resolver = new Resolver(); - // allow redirect - const object = await resolver.resolve(uri, true) as any; - - // /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する - // これはDBに存在する可能性があるため再度DB検索 - 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(object, resolver) : null, - isPost(object) ? await createNote(object, resolver, 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 61de50d39..000000000 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Apps } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { kinds } from '@/misc/api-permissions.js'; -import define from '../../define.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', - enum: kinds, - }, - }, - callbackUrl: { type: 'string', nullable: true }, - }, - required: ['name', 'description', 'permission'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Generate secret - const secret = secureRndstr(32, true); - - // Create account - const app = await Apps.insert({ - id: genId(), - createdAt: new Date(), - userId: user ? user.id : null, - name: ps.name, - description: ps.description, - permission: ps.permission, - callbackUrl: ps.callbackUrl, - 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 048848437..000000000 --- a/packages/backend/src/server/api/endpoints/app/show.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Apps } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['app'], - - errors: ['NO_SUCH_APP'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user, token) => { - const isSecure = user != null && token == null; - - // Lookup app - const app = await Apps.findOneBy({ id: ps.appId }); - - if (app == null) throw new ApiError('NO_SUCH_APP'); - - return await Apps.pack(app, user, { - detail: true, - includeSecret: isSecure && (app.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 736c1e3f6..000000000 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ /dev/null @@ -1,80 +0,0 @@ -import * as crypto from 'node:crypto'; -import { AuthSessions, AccessTokens, Apps } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { kinds } from '@/misc/api-permissions.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['auth'], - - requireCredential: true, - - secure: true, - - errors: ['NO_SUCH_SESSION'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - token: { type: 'string' }, - permission: { - description: 'The permissions which the user wishes to grant in this token. ' - + 'Permissions that the app has not registered before will be removed. ' - + 'Defaults to all permissions the app was registered with if not provided.', - type: 'array', - uniqueItems: true, - items: { - type: 'string', - enum: kinds, - }, - }, - }, - required: ['token'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch token - const session = await AuthSessions - .findOneBy({ token: ps.token }); - - if (session == null) throw new ApiError('NO_SUCH_SESSION'); - - // Generate access token - const accessToken = secureRndstr(32, true); - - // Check for existing access token. - 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(); - - // Calculate the set intersection between requested permissions and - // permissions that the app registered with. If no specific permissions - // are given, grant all permissions the app registered with. - const permission = ps.permission?.filter(x => app.permission.includes(x)) ?? app.permission; - - const accessTokenId = genId(); - - // Insert access token doc - await AccessTokens.insert({ - id: accessTokenId, - createdAt: now, - lastUsedAt: now, - appId: session.appId, - userId: user.id, - token: accessToken, - hash, - permission, - }); - - // Update session - await AuthSessions.update(session.id, { accessTokenId }); -}); diff --git a/packages/backend/src/server/api/endpoints/auth/deny.ts b/packages/backend/src/server/api/endpoints/auth/deny.ts deleted file mode 100644 index 2c0f7abc8..000000000 --- a/packages/backend/src/server/api/endpoints/auth/deny.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AuthSessions } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const result = await AuthSessions.delete({ - token: ps.token, - }); - - if (result.affected === 0) { - throw new ApiError(meta.errors.noSuchSession); - } -}); 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 8fb133bdf..000000000 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { v4 as uuid } from 'uuid'; -import config from '@/config/index.js'; -import { Apps, AuthSessions } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { compareUrl } from '@/server/api/common/compare-url.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.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', - }, - // stuff that auth/session/show would respond with - id: { - type: 'string', - description: 'The ID of the authentication session. Same as returned by `auth/session/show`.', - optional: false, nullable: false, - format: 'id', - }, - app: { - type: 'object', - description: 'The App requesting permissions. Same as returned by `auth/session/show`.', - optional: false, nullable: false, - ref: 'App', - }, - }, - }, - - errors: ['NO_SUCH_APP'], -} as const; - -export const paramDef = { - type: 'object', - oneOf: [{ - properties: { - clientId: { type: 'string' }, - callbackUrl: { - type: 'string', - minLength: 1, - }, - pkceChallenge: { - type: 'string', - minLength: 1, - }, - }, - required: ['clientId'], - }, { - properties: { - appSecret: { type: 'string' }, - }, - required: ['appSecret'], - }], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - // Lookup app - const app = await Apps.findOneBy(ps.clientId ? { - id: ps.clientId, - } : { - secret: ps.appSecret, - }); - - if (app == null) { - throw new ApiError('NO_SUCH_APP'); - } - - // check URL if provided - // technically the OAuth specification says that the redirect URI has to be - // bound with the token request, but since an app may only register one - // redirect URI, we don't actually have to store that. - if (ps.callbackUrl && !compareUrl(app.callbackUrl, ps.callbackUrl)) { - throw new ApiError('NO_SUCH_APP', 'redirect URI mismatch'); - } - - // Generate token - const token = uuid(); - const id = genId(); - - // Create session token document - const doc = await AuthSessions.insert({ - id, - createdAt: new Date(), - appId: app.id, - token, - pkceChallenge: ps.pkceChallenge, - }).then(x => AuthSessions.findOneByOrFail(x.identifiers[0])); - - return { - token: doc.token, - url: `${config.authUrl}/${doc.token}`, - id, - app: await Apps.pack(app), - }; -}); diff --git a/packages/backend/src/server/api/endpoints/auth/session/oauth.ts b/packages/backend/src/server/api/endpoints/auth/session/oauth.ts deleted file mode 100644 index d6aa6caab..000000000 --- a/packages/backend/src/server/api/endpoints/auth/session/oauth.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* -This route is already in use, but the functionality is provided -by '@/server/api/common/oauth.ts'. The route is not here because -that route requires more deep level access to HTTP data. -*/ 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 cd30bfcca..000000000 --- a/packages/backend/src/server/api/endpoints/auth/session/show.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { AuthSessions } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['auth'], - - requireCredential: false, - - errors: ['NO_SUCH_SESSION'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Lookup session - const session = await AuthSessions.findOneBy({ - token: ps.token, - }); - - if (session == null) throw new ApiError('NO_SUCH_SESSION'); - - 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 a9e69c895..000000000 --- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Apps, AuthSessions, Users } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.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: ['NO_SUCH_APP', 'NO_SUCH_SESSION', 'PENDING_SESSION'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - appSecret: { type: 'string' }, - token: { type: 'string' }, - }, - required: ['appSecret', 'token'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - // Lookup app - const app = await Apps.findOneBy({ - secret: ps.appSecret, - }); - - if (app == null) throw new ApiError('NO_SUCH_APP'); - - // Fetch token - const session = await AuthSessions.findOne({ - where: { - token: ps.token, - appId: app.id, - }, - relations: { - accessToken: true, - }, - }); - - if (session == null) throw new ApiError('NO_SUCH_SESSION'); - - if (session.accessTokenId == null) throw new ApiError('PENDING_SESSION'); - - // Delete session - AuthSessions.delete(session.id); - - return { - accessToken: session.accessToken.token, - user: await Users.pack(session.accessToken.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 4a9a2db4a..000000000 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ /dev/null @@ -1,70 +0,0 @@ -import create from '@/services/blocking/create.js'; -import { Blockings, NoteWatchings, Users } from '@/models/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; - -export const meta = { - tags: ['account'], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: 'write:blocks', - - errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'ALREADY_BLOCKING'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const blocker = await Users.findOneByOrFail({ id: user.id }); - - // 自分自身 - if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF'); - - // Get blockee - const blockee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check if already blocking - const blocked = await Blockings.countBy({ - blockerId: blocker.id, - blockeeId: blockee.id, - }); - - if (blocked) throw new ApiError('ALREADY_BLOCKING'); - - await create(blocker, blockee); - - NoteWatchings.delete({ - userId: blocker.id, - noteUserId: blockee.id, - }); - - return await Users.pack(blockee.id, blocker, { - detail: true, - }); - - publishUserEvent(user.id, 'block', blockee); -}); 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 f2a206280..000000000 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ /dev/null @@ -1,66 +0,0 @@ -import deleteBlocking from '@/services/blocking/delete.js'; -import { Blockings, Users } from '@/models/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; - -export const meta = { - tags: ['account'], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: 'write:blocks', - - errors: ['NO_SUCH_USER', 'BLOCKEE_IS_YOURSELF', 'NOT_BLOCKING'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Check if the blockee is yourself - if (user.id === ps.userId) throw new ApiError('BLOCKEE_IS_YOURSELF'); - - const blocker = await Users.findOneByOrFail({ id: user.id }); - - // Get blockee - const blockee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check not blocking - const exist = await Blockings.countBy({ - blockerId: blocker.id, - blockeeId: blockee.id, - }); - - if (!exist) throw new ApiError('NOT_BLOCKING'); - - // Delete blocking - await deleteBlocking(blocker, blockee); - - return await Users.pack(blockee.id, blocker, { - detail: true, - }); - - publishUserEvent(user.id, 'unblock', blockee); -}); 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 6c064d7ec..000000000 --- a/packages/backend/src/server/api/endpoints/blocking/list.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Blockings } 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: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; - -// eslint-disable-next-line import/no-default-export -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 1a55cebe4..000000000 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Channels, DriveFiles } from '@/models/index.js'; -import { Channel } from '@/models/entities/channel.js'; -import { genId } from '@/misc/gen-id.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['channels'], - - description: 'Creates a new channel with the current user as its administrator.', - - requireCredential: true, - - kind: 'write:channels', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Channel', - }, - - errors: ['NO_SUCH_FILE'], -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - if (ps.bannerId != null) { - const bannerExists = await DriveFiles.countBy({ - id: ps.bannerId, - userId: user.id, - }); - - if (!bannerExists) throw new ApiError('NO_SUCH_FILE'); - } - - const channel = await Channels.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - name: ps.name, - description: ps.description, - bannerId: ps.bannerId, - } 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 9c8f63e6c..000000000 --- a/packages/backend/src/server/api/endpoints/channels/featured.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Channels } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['channels'], - - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -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 99bf508c8..000000000 --- a/packages/backend/src/server/api/endpoints/channels/follow.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Channels, ChannelFollowings } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { publishUserEvent } from '@/services/stream.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['channels'], - - requireCredential: true, - - kind: 'write:channels', - - errors: ['NO_SUCH_CHANNEL'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - channelId: { type: 'string', format: 'misskey:id' }, - }, - required: ['channelId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); - - 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 865f45850..000000000 --- a/packages/backend/src/server/api/endpoints/channels/followed.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Channels, ChannelFollowings } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery(ChannelFollowings.createQueryBuilder(), ps.sinceId, ps.untilId) - .andWhere({ followerId: me.id }); - - const followings = await query - .take(ps.limit) - .getMany(); - - return await Promise.all(followings.map(x => Channels.pack(x.followeeId, me))); -}); diff --git a/packages/backend/src/server/api/endpoints/channels/owned.ts b/packages/backend/src/server/api/endpoints/channels/owned.ts deleted file mode 100644 index 40c0928bb..000000000 --- a/packages/backend/src/server/api/endpoints/channels/owned.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Channels } from '@/models/index.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 c6a063746..000000000 --- a/packages/backend/src/server/api/endpoints/channels/show.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Channels } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['channels'], - - requireCredential: false, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Channel', - }, - - errors: ['NO_SUCH_CHANNEL'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - channelId: { type: 'string', format: 'misskey:id' }, - }, - required: ['channelId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); - - 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 99e13d5ab..000000000 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Notes, Channels } 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'; - -export const meta = { - tags: ['notes', 'channels'], - - requireCredential: false, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, - - errors: ['NO_SUCH_CHANNEL'], -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); - - //#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 47e5d6d9b..000000000 --- a/packages/backend/src/server/api/endpoints/channels/unfollow.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Channels, ChannelFollowings } from '@/models/index.js'; -import { publishUserEvent } from '@/services/stream.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['channels'], - - requireCredential: true, - - kind: 'write:channels', - - errors: ['NO_SUCH_CHANNEL'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - channelId: { type: 'string', format: 'misskey:id' }, - }, - required: ['channelId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); - - 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 33557df12..000000000 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Channels, DriveFiles } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['channels'], - - requireCredential: true, - - kind: 'write:channels', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Channel', - }, - - errors: ['ACCESS_DENIED', 'NO_SUCH_CHANNEL', 'NO_SUCH_FILE'], -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const channel = await Channels.findOneBy({ - id: ps.channelId, - }); - - if (channel == null) throw new ApiError('NO_SUCH_CHANNEL'); - - if (channel.userId !== me.id) throw new ApiError('ACCESS_DENIED', 'You are not the owner of this channel.'); - - // eslint:disable-next-line:no-unnecessary-initializer - let banner = undefined; - if (ps.bannerId != null) { - banner = await DriveFiles.findOneBy({ - id: ps.bannerId, - userId: me.id, - }); - - if (banner == null) throw new ApiError('NO_SUCH_FILE'); - } 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 ea2379429..000000000 --- a/packages/backend/src/server/api/endpoints/charts/active-users.ts +++ /dev/null @@ -1,27 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 06dee250e..000000000 --- a/packages/backend/src/server/api/endpoints/charts/ap-request.ts +++ /dev/null @@ -1,27 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 dd2c2d683..000000000 --- a/packages/backend/src/server/api/endpoints/charts/drive.ts +++ /dev/null @@ -1,27 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 8c35b3c46..000000000 --- a/packages/backend/src/server/api/endpoints/charts/federation.ts +++ /dev/null @@ -1,27 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 77e24a62c..000000000 --- a/packages/backend/src/server/api/endpoints/charts/hashtag.ts +++ /dev/null @@ -1,28 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 817d51ad0..000000000 --- a/packages/backend/src/server/api/endpoints/charts/instance.ts +++ /dev/null @@ -1,28 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 951adf540..000000000 --- a/packages/backend/src/server/api/endpoints/charts/notes.ts +++ /dev/null @@ -1,27 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 f165b4022..000000000 --- a/packages/backend/src/server/api/endpoints/charts/user/drive.ts +++ /dev/null @@ -1,28 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 d7f2334ea..000000000 --- a/packages/backend/src/server/api/endpoints/charts/user/following.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { getJsonSchema } from '@/services/chart/core.js'; -import { perUserFollowingChart } from '@/services/chart/index.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['charts', 'users', 'following'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 aefe550d4..000000000 --- a/packages/backend/src/server/api/endpoints/charts/user/notes.ts +++ /dev/null @@ -1,28 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 6bc6b56bf..000000000 --- a/packages/backend/src/server/api/endpoints/charts/user/reactions.ts +++ /dev/null @@ -1,28 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 338e8fd33..000000000 --- a/packages/backend/src/server/api/endpoints/charts/users.ts +++ /dev/null @@ -1,27 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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 6317db0f3..000000000 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ClipNotes, Clips } 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: ['account', 'notes', 'clips'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['ALREADY_CLIPPED', 'NO_SUCH_CLIP', 'NO_SUCH_NOTE'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_CLIP'); - - const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); - throw err; - }); - - const exist = await ClipNotes.countBy({ - noteId: note.id, - clipId: clip.id, - }); - - if (exist) throw new ApiError('ALREADY_CLIPPED'); - - 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 25408282c..000000000 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { genId } from '@/misc/gen-id.js'; -import { Clips } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 3fd9ee0b1..000000000 --- a/packages/backend/src/server/api/endpoints/clips/delete.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Clips } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['clips'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['NO_SUCH_CLIP'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - clipId: { type: 'string', format: 'misskey:id' }, - }, - required: ['clipId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_CLIP'); - - 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 53e1e3b00..000000000 --- a/packages/backend/src/server/api/endpoints/clips/list.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Clips } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 8af046d03..000000000 --- a/packages/backend/src/server/api/endpoints/clips/notes.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { ClipNotes, Clips, 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 { ApiError } from '../../error.js'; -import { generateBlockedUserQuery } from '../../common/generate-block-query.js'; - -export const meta = { - tags: ['account', 'notes', 'clips'], - - requireCredential: false, - - kind: 'read:account', - - errors: ['NO_SUCH_CLIP'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const clip = await Clips.findOneBy({ - id: ps.clipId, - }); - - if (clip == null) throw new ApiError('NO_SUCH_CLIP'); - - if (!clip.isPublic && (user == null || (clip.userId !== user.id))) { - throw new ApiError('NO_SUCH_CLIP'); - } - - 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 e6b60178e..000000000 --- a/packages/backend/src/server/api/endpoints/clips/remove-note.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ClipNotes, Clips } from '@/models/index.js'; -import define from '../../define.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: ['NO_SUCH_CLIP', 'NO_SUCH_NOTE', 'NOT_CLIPPED'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_CLIP'); - - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); - throw e; - }); - - const { affected } = await ClipNotes.delete({ - noteId: note.id, - clipId: clip.id, - }); - - if (affected === 0) throw new ApiError('NOT_CLIPPED'); -}); 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 942890dfe..000000000 --- a/packages/backend/src/server/api/endpoints/clips/show.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Clips } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['clips', 'account'], - - requireCredential: false, - - kind: 'read:account', - - errors: ['NO_SUCH_CLIP'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_CLIP'); - - if (!clip.isPublic && (me == null || (clip.userId !== me.id))) { - throw new ApiError('NO_SUCH_CLIP'); - } - - 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 6b26e2b20..000000000 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Clips } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['clips'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['NO_SUCH_CLIP'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_CLIP'); - - await Clips.update(clip.id, { - name: ps.name, - description: ps.description, - isPublic: ps.isPublic, - }); - - return await Clips.pack(clip.id); -}); diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts deleted file mode 100644 index f8e39fb57..000000000 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ /dev/null @@ -1,44 +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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const instance = await fetchMeta(true); - - const usage = await DriveFiles.calcDriveUsageOf(user.id); - - return { - capacity: 1024 * 1024 * instance.localDriveCapacityMb, - 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 862a5530d..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ /dev/null @@ -1,57 +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: ['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; - -// eslint-disable-next-line import/no-default-export -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.slice(0, -1) + '%' }); - } 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 bad04f188..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { DriveFiles, Notes } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.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: ['NO_SUCH_FILE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_FILE'); - - const notes = await Notes.createQueryBuilder('note') - .where(':file = ANY(note.fileIds)', { file: file.id }) - .getMany(); - - return await Notes.packMany(notes, user, { - detail: true, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts deleted file mode 100644 index 73d20450d..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DriveFiles } from '@/models/index.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - return 0 < await DriveFiles.countBy({ - md5: ps.md5, - userId: user.id, - }); -}); 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 8cbac7907..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ /dev/null @@ -1,75 +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 { 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: ['INTERNAL_ERROR', 'INVALID_FILE_NAME'], -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user, _, file, cleanup) => { - // Get 'name' parameter - let name = ps.name || file.originalname; - if (name !== undefined && name !== null) { - name = name.trim(); - if (name.length === 0) { - name = null; - } else if (name === 'blob') { - name = null; - } else if (!DriveFiles.validateFileName(name)) { - throw new ApiError('INVALID_FILE_NAME'); - } - } else { - name = null; - } - - try { - // Create file - const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive }); - return await DriveFiles.pack(driveFile, { self: true }); - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - apiLogger.error(e); - } - throw new ApiError('INTERNAL_ERROR'); - } 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 c5a8f94b6..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { deleteFile } from '@/services/drive/delete-file.js'; -import { publishDriveStream } from '@/services/stream.js'; -import { DriveFiles } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['drive'], - - requireCredential: true, - - kind: 'write:drive', - - description: 'Delete an existing drive file.', - - errors: ['ACCESS_DENIED', 'NO_SUCH_FILE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError('NO_SUCH_FILE'); - - if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - - // 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 f2bc7348c..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/find-by-hash.ts +++ /dev/null @@ -1,40 +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; - -// eslint-disable-next-line import/no-default-export -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 d9b76cdb0..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IsNull } from 'typeorm'; -import { DriveFiles } from '@/models/index.js'; -import define from '../../../define.js'; - -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; - -// eslint-disable-next-line import/no-default-export -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 08ac5ae2d..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/show.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles } 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: ['ACCESS_DENIED', 'NO_SUCH_FILE'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_FILE'); - - if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - - 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 3b5e57461..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { publishDriveStream } from '@/services/stream.js'; -import { DriveFiles, DriveFolders } from '@/models/index.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: ['ACCESS_DENIED', 'INVALID_FILE_NAME', 'NO_SUCH_FILE', 'NO_SUCH_FOLDER'], - - 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: 2048 }, - }, - required: ['fileId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError('NO_SUCH_FILE'); - - if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - - if (ps.name) file.name = ps.name; - if (!DriveFiles.validateFileName(file.name)) { - throw new ApiError('INVALID_FILE_NAME'); - } - - if (ps.comment !== undefined) file.comment = ps.comment; - - if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive; - - if (ps.folderId !== undefined) { - if (ps.folderId === null) { - file.folderId = null; - } else { - const folder = await DriveFolders.findOneBy({ - id: ps.folderId, - userId: user.id, - }); - - if (folder == null) throw new ApiError('NO_SUCH_FOLDER'); - - 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 db2911456..000000000 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; -import { DriveFiles } from '@/models/index.js'; -import { publishMainStream } from '@/services/stream.js'; -import { HOUR } from '@/const.js'; -import define from '../../../define.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: 2048, default: null }, - marker: { type: 'string', nullable: true, default: null }, - force: { type: 'boolean', default: false }, - }, - required: ['url'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 e243952c7..000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { DriveFolders } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 ea4e201ff..000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { publishDriveStream } from '@/services/stream.js'; -import { DriveFolders } 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: ['drive'], - - requireCredential: true, - - kind: 'write:drive', - - errors: ['NO_SUCH_FOLDER'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_FOLDER'); - } - - // Create folder - const folder = await DriveFolders.insert({ - id: genId(), - createdAt: new Date(), - name: ps.name, - parentId: parent?.id, - 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 76aeba323..000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/delete.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { publishDriveStream } from '@/services/stream.js'; -import { DriveFolders, DriveFiles } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['drive'], - - requireCredential: true, - - kind: 'write:drive', - - errors: ['HAS_CHILD_FILES_OR_FOLDERS', 'NO_SUCH_FOLDER'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - folderId: { type: 'string', format: 'misskey:id' }, - }, - required: ['folderId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_FOLDER'); - - const [childFoldersCount, childFilesCount] = await Promise.all([ - DriveFolders.countBy({ parentId: folder.id }), - DriveFiles.countBy({ folderId: folder.id }), - ]); - - if (childFoldersCount !== 0 || childFilesCount !== 0) { - throw new ApiError('HAS_CHILD_FILES_OR_FOLDERS'); - } - - 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 c3329b70c..000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/find.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { IsNull } from 'typeorm'; -import { DriveFolders } from '@/models/index.js'; -import define from '../../../define.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: { - name: { type: 'string' }, - parentId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, - }, - required: ['name'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 9c8734227..000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/show.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { DriveFolders } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['drive'], - - requireCredential: true, - - kind: 'read:drive', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'DriveFolder', - }, - - errors: ['NO_SUCH_FOLDER'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - folderId: { type: 'string', format: 'misskey:id' }, - }, - required: ['folderId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_FOLDER'); - - 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 8813456bb..000000000 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { publishDriveStream } from '@/services/stream.js'; -import { DriveFolders } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['drive'], - - requireCredential: true, - - kind: 'write:drive', - - errors: ['NO_SUCH_FOLDER', 'NO_SUCH_PARENT_FOLDER', 'RECURSIVE_FOLDER'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_FOLDER'); - - if (ps.name) folder.name = ps.name; - - if (ps.parentId !== undefined) { - if (ps.parentId === folder.id) { - throw new ApiError('RECURSIVE_FOLDER'); - } 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('NO_SUCH_PARENT_FOLDER'); - - // 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('RECURSIVE_FOLDER'); - } - } - - 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 26f7f5ee7..000000000 --- a/packages/backend/src/server/api/endpoints/drive/stream.ts +++ /dev/null @@ -1,50 +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: ['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; - -// eslint-disable-next-line import/no-default-export -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 9695fb6ba..000000000 --- a/packages/backend/src/server/api/endpoints/email-address/available.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { validateEmailForAccount } from '@/services/validate-email-for-account.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, - }, - reason: { - type: 'string', - optional: false, nullable: true, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - emailAddress: { type: 'string' }, - }, - required: ['emailAddress'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - return await validateEmailForAccount(ps.emailAddress); -}); diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts deleted file mode 100644 index c17412677..000000000 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ /dev/null @@ -1,28 +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; - -// eslint-disable-next-line import/no-default-export -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 b20da96eb..000000000 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ /dev/null @@ -1,34 +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; - -// eslint-disable-next-line import/no-default-export -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 104382c41..000000000 --- a/packages/backend/src/server/api/endpoints/export-custom-emojis.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createExportCustomEmojisJob } from '@/queue/index.js'; -import { HOUR } from '@/const.js'; -import define from '../define.js'; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - ids: { - description: 'Specific emoji IDs to be exported. Non-local emoji will be ignored. If not provided, all local emoji will be exported.', - type: 'array', - items: { type: 'string' }, - minItems: 1, - uniqueItems: true, - }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - createExportCustomEmojisJob(user, ps.ids); -}); 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 2bfd63ff8..000000000 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Followings } from '@/models/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; - -export const meta = { - tags: ['federation'], - - requireCredential: true, - requireAdmin: 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; - -// eslint-disable-next-line import/no-default-export -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 febcb9e90..000000000 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Followings } from '@/models/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; - -export const meta = { - tags: ['federation'], - - requireCredential: true, - requireAdmin: 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; - -// eslint-disable-next-line import/no-default-export -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 b9f4865fa..000000000 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Instances } from '@/models/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['federation'], - - requireCredential: 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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) { - query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts }); - } else { - query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts }); - } - } - - if (typeof ps.notResponding === 'boolean') { - if (ps.notResponding) { - query.andWhere('instance.isNotResponding'); - } else { - query.andWhere('NOT instance.isNotResponding'); - } - } - - if (typeof ps.suspended === 'boolean') { - if (ps.suspended) { - query.andWhere('instance.isSuspended'); - } else { - query.andWhere('NOT instance.isSuspended'); - } - } - - 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 a293bd586..000000000 --- a/packages/backend/src/server/api/endpoints/federation/show-instance.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Instances } from '@/models/index.js'; -import { toPuny } from '@/misc/convert-host.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['federation'], - - requireCredential: true, - - res: { - oneOf: [{ - type: 'object', - ref: 'FederationInstance', - }, { - type: 'null', - }], - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - host: { type: 'string' }, - }, - required: ['host'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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 f76e813b4..000000000 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { MoreThan } from 'typeorm'; -import { Instances } from '@/models/index.js'; -import { awaitAll } from '@/prelude/await-all.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['federation'], - - requireCredential: true, - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const [topSubInstances, topPubInstances] = 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, - }), - ]); - - return await awaitAll({ - topSubInstances: Instances.packMany(topSubInstances), - topPubInstances: Instances.packMany(topPubInstances), - }); -}); 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 65b505070..000000000 --- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Resolver } from '@/remote/activitypub/resolver.js'; -import { updatePerson } from '@/remote/activitypub/models/person.js'; -import define from '../../define.js'; -import { getRemoteUser } from '../../common/getters.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const user = await getRemoteUser(ps.userId); - await updatePerson(user.uri!, new Resolver()); -}); 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 b6f395e0e..000000000 --- a/packages/backend/src/server/api/endpoints/federation/users.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Users } from '@/models/index.js'; -import define from '../../define.js'; -import { makePaginationQuery } from '../../common/make-pagination-query.js'; - -export const meta = { - tags: ['federation'], - - requireCredential: 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; - -// eslint-disable-next-line import/no-default-export -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 7c32e2e4d..000000000 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ /dev/null @@ -1,39 +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: true, - allowGet: true, - cacheSec: 60 * 3, -} as const; - -export const paramDef = { - type: 'object', - properties: { - url: { type: 'string' }, - }, - required: ['url'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 9341024c0..000000000 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ /dev/null @@ -1,70 +0,0 @@ -import create from '@/services/following/create.js'; -import { Followings, Users } from '@/models/index.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; - -export const meta = { - tags: ['following', 'users'], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: 'write:following', - - errors: ['ALREADY_FOLLOWING', 'BLOCKING', 'BLOCKED', 'FOLLOWEE_IS_YOURSELF', 'NO_SUCH_USER'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const follower = user; - - // 自分自身 - if (user.id === ps.userId) throw new ApiError('FOLLOWEE_IS_YOURSELF'); - - // Get followee - const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check if already following - const exist = await Followings.countBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (exist) throw new ApiError('ALREADY_FOLLOWING'); - - try { - await create(follower, followee); - } catch (e) { - if (e instanceof IdentifiableError) { - if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError('BLOCKING'); - if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError('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 40fe718cc..000000000 --- a/packages/backend/src/server/api/endpoints/following/delete.ts +++ /dev/null @@ -1,61 +0,0 @@ -import deleteFollowing from '@/services/following/delete.js'; -import { Followings, Users } from '@/models/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; - -export const meta = { - tags: ['following', 'users'], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: 'write:following', - - errors: ['FOLLOWEE_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('FOLLOWEE_IS_YOURSELF'); - - // Get followee - const followee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check not following - const exist = await Followings.countBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (!exist) throw new ApiError('NOT_FOLLOWING'); - - 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 7bb794caa..000000000 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ /dev/null @@ -1,61 +0,0 @@ -import deleteFollowing from '@/services/following/delete.js'; -import { Followings, Users } from '@/models/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; -import { getUser } from '../../common/getters.js'; - -export const meta = { - tags: ['following', 'users'], - - limit: { - duration: HOUR, - max: 100, - }, - - requireCredential: true, - - kind: 'write:following', - - errors: ['FOLLOWER_IS_YOURSELF', 'NO_SUCH_USER', 'NOT_FOLLOWING'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('FOLLOWER_IS_YOURSELF'); - - // Get follower - const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check not following - const exist = await Followings.countBy({ - followerId: follower.id, - followeeId: followee.id, - }); - - if (!exist) throw new ApiError('NOT_FOLLOWING'); - - 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 f31aab855..000000000 --- a/packages/backend/src/server/api/endpoints/following/requests/accept.ts +++ /dev/null @@ -1,38 +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: ['NO_SUCH_USER', 'NO_SUCH_FOLLOW_REQUEST'], -} 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) => { - // Fetch follower - const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - await acceptFollowRequest(user, follower).catch(e => { - if (e.id === '8884c2dd-5795-4ac9-b27e-6a01d38190f9') throw new ApiError('NO_SUCH_FOLLOW_REQUEST'); - 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 c1c1e722e..000000000 --- a/packages/backend/src/server/api/endpoints/following/requests/cancel.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { cancelFollowRequest } from '@/services/following/requests/cancel.js'; -import { Users } from '@/models/index.js'; -import { IdentifiableError } from '@/misc/identifiable-error.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: ['NO_SUCH_USER', 'NO_SUCH_FOLLOW_REQUEST'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER'); - throw e; - }); - - try { - await cancelFollowRequest(followee, user); - } catch (e) { - if (e instanceof IdentifiableError) { - if (e.id === '17447091-ce07-46dd-b331-c1fd4f15b1e7') throw new ApiError('NO_SUCH_FOLLOW_REQUEST'); - } - 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 4e43c4948..000000000 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { FollowRequests } from '@/models/index.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -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 8de83d508..000000000 --- a/packages/backend/src/server/api/endpoints/following/requests/reject.ts +++ /dev/null @@ -1,35 +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: ['NO_SUCH_USER'], -} 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) => { - // Fetch follower - const follower = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - 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 baa70c14e..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/featured.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { DAY } from '@/const.js'; -import { GalleryPosts } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = GalleryPosts.createQueryBuilder('post') - .andWhere('post.createdAt > :date', { date: new Date(Date.now() - 3 * DAY) }) - .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 552810e54..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/popular.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { GalleryPosts } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -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 3a21afae1..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts.ts +++ /dev/null @@ -1,37 +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: ['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; - -// eslint-disable-next-line import/no-default-export -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 230b7bec3..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { DriveFiles, GalleryPosts } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { GalleryPost } from '@/models/entities/gallery-post.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { HOUR } from '@/const.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.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', - }, -} 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; - -// eslint-disable-next-line import/no-default-export -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 !== ps.fileIds.length) { - throw new ApiError( - 'INVALID_PARAM', - { - param: '#/properties/fileIds/items', - reason: 'contains invalid file IDs', - }, - ); - } - - 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 65c0e62d0..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { GalleryPosts } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true, - - kind: 'write:gallery', - - errors: ['NO_SUCH_POST'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - postId: { type: 'string', format: 'misskey:id' }, - }, - required: ['postId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_POST'); - - 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 1525d4dad..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { GalleryPosts, GalleryLikes } 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: ['gallery'], - - requireCredential: true, - - kind: 'write:gallery-likes', - - errors: ['NO_SUCH_POST', 'ALREADY_LIKED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - postId: { type: 'string', format: 'misskey:id' }, - }, - required: ['postId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ id: ps.postId }); - if (post == null) throw new ApiError('NO_SUCH_POST'); - - // if already liked - const exist = await GalleryLikes.countBy({ - postId: post.id, - userId: user.id, - }); - - if (exist) throw new ApiError('ALREADY_LIKED'); - - // 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 640048028..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/show.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { GalleryPosts } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: false, - - errors: ['NO_SUCH_POST'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const post = await GalleryPosts.findOneBy({ - id: ps.postId, - }); - - if (post == null) throw new ApiError('NO_SUCH_POST'); - - 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 61d71905e..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { GalleryPosts, GalleryLikes } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['gallery'], - - requireCredential: true, - - kind: 'write:gallery-likes', - - errors: ['NO_SUCH_POST', 'NOT_LIKED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - postId: { type: 'string', format: 'misskey:id' }, - }, - required: ['postId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const post = await GalleryPosts.findOneBy({ id: ps.postId }); - if (post == null) throw new ApiError('NO_SUCH_POST'); - - const exist = await GalleryLikes.findOneBy({ - postId: post.id, - userId: user.id, - }); - - if (exist == null) throw new ApiError('NOT_LIKED'); - - // 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 071229f89..000000000 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { DriveFiles, GalleryPosts } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { HOUR } from '@/const.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.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: ['INVALID_PARAM'], -} 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; - -// eslint-disable-next-line import/no-default-export -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 !== ps.fileIds.length) { - throw new ApiError( - 'INVALID_PARAM', - { - param: '#/properties/fileIds/items', - reason: 'contains invalid file IDs', - }, - ); - } - - 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 56c550297..000000000 --- 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, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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/hashtags/list.ts b/packages/backend/src/server/api/endpoints/hashtags/list.ts deleted file mode 100644 index 5f0d9152c..000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/list.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Hashtags } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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 bc31b0419..000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Hashtags } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -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 a3d8b5b98..000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/show.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Hashtags } from '@/models/index.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Hashtag', - }, - - errors: ['NO_SUCH_HASHTAG'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - tag: { type: 'string' }, - }, - required: ['tag'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const hashtag = await Hashtags.findOneBy({ name: normalizeForSearch(ps.tag) }); - if (hashtag == null) throw new ApiError('NO_SUCH_HASHTAG'); - - 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 b6943dd08..000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Brackets } from 'typeorm'; -import { MINUTE, HOUR } from '@/const.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import { safeForSql } from '@/misc/safe-for-sql.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import define from '../../define.js'; - -/* -トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 -ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる - -..が理想だけどPostgreSQLでどうするのか分からないので単に「直近Aの内に投稿されたユニーク投稿数が多いハッシュタグ」で妥協する -*/ - -const rangeA = HOUR; -//const rangeB = 2 * HOUR; -//const coefficient = 1.25; // 「n倍」の部分 -//const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか - -const max = 5; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -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; - - const interval = 10 * MINUTE; - - 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 cc51e5ee9..000000000 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { DAY } from '@/const.js'; -import { Users } from '@/models/index.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import define from '../../define.js'; - -export const meta = { - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -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() - 5 * DAY); - - 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 22aedfeee..000000000 --- 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; - -// eslint-disable-next-line import/no-default-export -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 e9fdd4c37..000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as speakeasy from 'speakeasy'; -import { UserProfiles } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['INTERNAL_ERROR', 'ACCESS_DENIED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - token: { type: 'string' }, - }, - required: ['token'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 ApiError('INTERNAL_ERROR', 'Two-step verification has not been initiated.'); - } - - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorTempSecret, - encoding: 'base32', - token, - }); - - if (!verified) { - throw new ApiError('ACCESS_DENIED', 'TOTP missmatch'); - } - - 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 73773c8c5..000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { promisify } from 'node:util'; -import * as cbor from 'cbor'; -import { MINUTE } from '@/const.js'; -import { comparePassword } from '@/misc/password.js'; -import { - UserProfiles, - UserSecurityKeys, - AttestationChallenges, - Users, -} from '@/models/index.js'; -import config from '@/config/index.js'; -import { ApiError } from '@/server/api/error.js'; -import { publishMainStream } from '@/services/stream.js'; -import define from '../../../define.js'; -import { procedures, hash } from '../../../2fa.js'; - -const cborDecodeFirst = promisify(cbor.decodeFirst) as any; -const rpIdHashReal = hash(Buffer.from(config.hostname, 'utf-8')); - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['ACCESS_DENIED', 'INTERNAL_ERROR', 'NO_SUCH_OBJECT'], -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (!(await comparePassword(ps.password, profile.password!))) { - throw new ApiError('ACCESS_DENIED'); - } - - if (!profile.twoFactorEnabled) { - throw new ApiError('INTERNAL_ERROR', '2fa not enabled'); - } - - const clientData = JSON.parse(ps.clientDataJSON); - - if (clientData.type !== 'webauthn.create') { - throw new ApiError('INTERNAL_ERROR', 'not a creation attestation'); - } - if (clientData.origin !== config.scheme + '://' + config.host) { - throw new ApiError('INTERNAL_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 ApiError('INTERNAL_ERROR', 'rpIdHash mismatch'); - } - - const flags = attestation.authData[32]; - - // eslint:disable-next-line:no-bitwise - if (!(flags & 1)) { - throw new ApiError('INTERNAL_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 ApiError('INTERNAL_ERROR', 'algorithm mismatch'); - } - - if (!(procedures as any)[attestation.fmt]) { - throw new ApiError('INTERNAL_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 ApiError('INTERNAL_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 ApiError('NO_SUCH_OBJECT', 'Attestation challenge not found.'); - } - - await AttestationChallenges.delete({ - userId: user.id, - id: ps.challengeId, - }); - - // Expired challenge - if ( - new Date().getTime() - attestationChallenge.createdAt.getTime() >= - 5 * MINUTE - ) { - throw new ApiError('NO_SUCH_OBJECT', 'Attestation challenge expired.'); - } - - 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 45e8b3ff3..000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { UserProfiles } from '@/models/index.js'; -import define from '../../../define.js'; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: 'object', - properties: { - value: { type: 'boolean' }, - }, - required: ['value'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 32e85463c..000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { promisify } from 'node:util'; -import * as crypto from 'node:crypto'; -import { UserProfiles, AttestationChallenges } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { comparePassword } from '@/misc/password.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.js'; -import { hash } from '../../../2fa.js'; - -const randomBytes = promisify(crypto.randomBytes); - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['ACCESS_DENIED', 'INTERNAL_ERROR'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - password: { type: 'string' }, - }, - required: ['password'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (!(await comparePassword(ps.password, profile.password!))) { - throw new ApiError('ACCESS_DENIED'); - } - - if (!profile.twoFactorEnabled) { - throw new ApiError('INTERNAL_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 59a7f5d8b..000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as speakeasy from 'speakeasy'; -import * as QRCode from 'qrcode'; -import config from '@/config/index.js'; -import { comparePassword } from '@/misc/password.js'; -import { UserProfiles } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['ACCESS_DENIED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - password: { type: 'string' }, - }, - required: ['password'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (!(await comparePassword(ps.password, profile.password!))) { - throw new ApiError('ACCESS_DENIED'); - } - - // 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 4164e114c..000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { comparePassword } from '@/misc/password.js'; -import { UserProfiles, UserSecurityKeys, Users } from '@/models/index.js'; -import { publishMainStream } from '@/services/stream.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['ACCESS_DENIED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - password: { type: 'string' }, - credentialId: { type: 'string' }, - }, - required: ['password', 'credentialId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (!(await comparePassword(ps.password, profile.password!))) { - throw new ApiError('ACCESS_DENIED'); - } - - // 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 c82fde0b3..000000000 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { comparePassword } from '@/misc/password.js'; -import { UserProfiles } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../../define.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['ACCESS_DENIED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - password: { type: 'string' }, - }, - required: ['password'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (!(await comparePassword(ps.password, profile.password!))) { - throw new ApiError('ACCESS_DENIED'); - } - - 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 3f8baf3dd..000000000 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AccessTokens } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 d54a76ddd..000000000 --- a/packages/backend/src/server/api/endpoints/i/authorized-apps.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { AccessTokens, Apps } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 e7fd70391..000000000 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { comparePassword, hashPassword } from '@/misc/password.js'; -import { UserProfiles } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../define.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['ACCESS_DENIED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - currentPassword: { type: 'string' }, - newPassword: { type: 'string', minLength: 1 }, - }, - required: ['currentPassword', 'newPassword'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (!(await comparePassword(ps.currentPassword, profile.password!))) { - throw new ApiError('ACCESS_DENIED'); - } - - await UserProfiles.update(user.id, { - password: await hashPassword(ps.newPassword), - }); -}); 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 5dae620ae..000000000 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { comparePassword } from '@/misc/password.js'; -import { UserProfiles, Users } from '@/models/index.js'; -import { deleteAccount } from '@/services/delete-account.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../../define.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['ACCESS_DENIED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - password: { type: 'string' }, - }, - required: ['password'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const [profile, userDetailed] = await Promise.all([ - UserProfiles.findOneByOrFail({ userId: user.id }), - Users.findOneByOrFail({ id: user.id }), - ]); - - if (userDetailed.isDeleted) { - return; - } - - if (!(await comparePassword(ps.password, profile.password!))) { - throw new ApiError('ACCESS_DENIED'); - } - - 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 07381fa90..000000000 --- a/packages/backend/src/server/api/endpoints/i/export-blocking.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createExportBlockingJob } from '@/queue/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 a1f2912dc..000000000 --- a/packages/backend/src/server/api/endpoints/i/export-following.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createExportFollowingJob } from '@/queue/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 92fd30f27..000000000 --- a/packages/backend/src/server/api/endpoints/i/export-mute.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createExportMuteJob } from '@/queue/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 6c9da1113..000000000 --- a/packages/backend/src/server/api/endpoints/i/export-notes.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createExportNotesJob } from '@/queue/index.js'; -import { DAY } from '@/const.js'; -import define from '../../define.js'; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: DAY, - max: 1, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 2295f0586..000000000 --- a/packages/backend/src/server/api/endpoints/i/export-user-lists.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createExportUserListsJob } from '@/queue/index.js'; -import { MINUTE } from '@/const.js'; -import define from '../../define.js'; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: MINUTE, - max: 1, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 25f5b4fa5..000000000 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NoteFavorites } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 f5ceddd43..000000000 --- a/packages/backend/src/server/api/endpoints/i/gallery/likes.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { GalleryLikes } 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-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; - -// eslint-disable-next-line import/no-default-export -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 2ecd47f1b..000000000 --- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts +++ /dev/null @@ -1,43 +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; - -// eslint-disable-next-line import/no-default-export -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 728e9289f..000000000 --- a/packages/backend/src/server/api/endpoints/i/get-word-muted-notes-count.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MutedNotes } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 ed5ae4259..000000000 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createImportBlockingJob } from '@/queue/index.js'; -import { DriveFiles } from '@/models/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - secure: true, - requireCredential: true, - - limit: { - duration: HOUR, - max: 1, - }, - - errors: ['EMPTY_FILE', 'FILE_TOO_BIG', 'NO_SUCH_FILE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError('EMPTY_FILE'); - if (file.size > 50000) throw new ApiError('FILE_TOO_BIG'); - if (file.size === 0) throw new ApiError('EMPTY_FILE'); - - 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 cbbb3a0ad..000000000 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createImportFollowingJob } from '@/queue/index.js'; -import { DriveFiles } from '@/models/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, - - errors: ['EMPTY_FILE', 'FILE_TOO_BIG', 'NO_SUCH_FILE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError('NO_SUCH_FILE'); - if (file.size > 50000) throw new ApiError('FILE_TOO_BIG'); - if (file.size === 0) throw new ApiError('EMPTY_FILE'); - - 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 37149408b..000000000 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createImportMutingJob } from '@/queue/index.js'; -import { DriveFiles } from '@/models/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - secure: true, - requireCredential: true, - - limit: { - duration: HOUR, - max: 1, - }, - - errors: ['EMPTY_FILE', 'FILE_TOO_BIG', 'NO_SUCH_FILE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError('NO_SUCH_FILE'); - if (file.size > 50000) throw new ApiError('FILE_TOO_BIG'); - if (file.size === 0) throw new ApiError('EMPTY_FILE'); - - createImportMutingJob(user, file.id); -}); diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts deleted file mode 100644 index 67d6afd0c..000000000 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createImportUserListsJob } from '@/queue/index.js'; -import { DriveFiles } from '@/models/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - secure: true, - requireCredential: true, - limit: { - duration: HOUR, - max: 1, - }, - - errors: ['EMPTY_FILE', 'FILE_TOO_BIG', 'NO_SUCH_FILE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - }, - required: ['fileId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const file = await DriveFiles.findOneBy({ id: ps.fileId }); - - if (file == null) throw new ApiError('NO_SUCH_FILE'); - if (file.size > 30000) throw new ApiError('FILE_TOO_BIG'); - if (file.size === 0) throw new ApiError('EMPTY_FILE'); - - createImportUserListsJob(user, file.id); -}); diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts deleted file mode 100644 index b0c4790c5..000000000 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Brackets } from 'typeorm'; -import { notificationTypes } from 'foundkey-js'; -import { Notifications, Followings, Mutings, Users, UserProfiles } from '@/models/index.js'; -import { readNote } 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: 10, - }, - - 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; - -// eslint-disable-next-line import/no-default-export -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'); - - 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) { - readNote(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 987387237..000000000 --- a/packages/backend/src/server/api/endpoints/i/page-likes.ts +++ /dev/null @@ -1,54 +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; - -// eslint-disable-next-line import/no-default-export -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 7e1820d45..000000000 --- a/packages/backend/src/server/api/endpoints/i/pages.ts +++ /dev/null @@ -1,43 +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; - -// eslint-disable-next-line import/no-default-export -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 e3a58bc70..000000000 --- a/packages/backend/src/server/api/endpoints/i/pin.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { addPinned } from '@/services/i/pin.js'; -import { Users } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['account', 'notes'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['ALREADY_PINNED', 'NO_SUCH_NOTE', 'PIN_LIMIT_EXCEEDED'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - if (e.id === '15a018eb-58e5-4da1-93be-330fcc5e4e1a') throw new ApiError('PIN_LIMIT_EXCEEDED'); - if (e.id === '23f0cf4e-59a3-4276-a91d-61a5891c1514') throw new ApiError('ALREADY_PINNED'); - 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 9b49dc9f7..000000000 --- a/packages/backend/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { publishMainStream } from '@/services/stream.js'; -import { MessagingMessages, UserGroupJoinings } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['account', 'messaging'], - - requireCredential: true, - - kind: 'write:account', -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 7a1c3f739..000000000 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { publishMainStream } from '@/services/stream.js'; -import { NoteUnreads } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['account'], - - requireCredential: true, - - kind: 'write:account', -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 bbb510f0d..000000000 --- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { genId } from '@/misc/gen-id.js'; -import { AnnouncementReads, Announcements, Users } from '@/models/index.js'; -import { publishMainStream } from '@/services/stream.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['account'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['NO_SUCH_ANNOUNCEMENT'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - announcementId: { type: 'string', format: 'misskey:id' }, - }, - required: ['announcementId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Check if announcement exists - const exists = await Announcements.countBy({ id: ps.announcementId }); - - if (!exists) throw new ApiError('NO_SUCH_ANNOUNCEMENT'); - - // Check if already read - const read = await AnnouncementReads.countBy({ - announcementId: ps.announcementId, - userId: user.id, - }); - - if (read) 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 84bfa6fd3..000000000 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { comparePassword } from '@/misc/password.js'; -import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js'; -import { Users, UserProfiles } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.js'; -import generateUserToken from '../../common/generate-native-user-token.js'; -import define from '../../define.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['ACCESS_DENIED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - password: { type: 'string' }, - }, - required: ['password'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 }); - - if (!(await comparePassword(ps.password, profile.password!))) { - throw new ApiError('ACCESS_DENIED'); - } - - 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 d92bc7af9..000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { RegistryItems } from '@/models/index.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -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 7addd0fab..000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { RegistryItems } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['NO_SUCH_KEY'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_KEY'); - - return { - updatedAt: item.updatedAt, - value: item.value, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts deleted file mode 100644 index a9ca60485..000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { RegistryItems } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['NO_SUCH_KEY'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_KEY'); - - 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 d9982b036..000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { RegistryItems } from '@/models/index.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -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 b625f50ca..000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { RegistryItems } from '@/models/index.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -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 f656f3d83..000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { RegistryItems } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - errors: ['NO_SUCH_KEY'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_KEY'); - - 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 d946fe609..000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { RegistryItems } from '@/models/index.js'; -import define from '../../../define.js'; - -export const meta = { - requireCredential: true, - - secure: true, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 03d81d8c4..000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { publishMainStream } from '@/services/stream.js'; -import { RegistryItems } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -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 ef85051f8..000000000 --- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AccessTokens } from '@/models/index.js'; -import { publishUserEvent } from '@/services/stream.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const exists = await AccessTokens.countBy({ id: ps.tokenId }); - - if (exists) { - 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 3ac1a2549..000000000 --- a/packages/backend/src/server/api/endpoints/i/signin-history.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Signins } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 88cb751b4..000000000 --- a/packages/backend/src/server/api/endpoints/i/unpin.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { removePinned } from '@/services/i/pin.js'; -import { Users } from '@/models/index.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['account', 'notes'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['NO_SUCH_NOTE'], - - 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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 2a2413730..000000000 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { publishMainStream } from '@/services/stream.js'; -import config from '@/config/index.js'; -import { comparePassword } from '@/misc/password.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { Users, UserProfiles } from '@/models/index.js'; -import { sendEmail } from '@/services/send-email.js'; -import { validateEmailForAccount } from '@/services/validate-email-for-account.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - requireCredential: true, - - secure: true, - - limit: { - duration: HOUR, - max: 3, - }, - - // FIXME: refactor to remove both of these errors? - // the password should not be passed as it is not compatible with using OAuth - errors: ['ACCESS_DENIED', 'INTERNAL_ERROR'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - password: { type: 'string' }, - email: { type: 'string', nullable: true }, - }, - required: ['password'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (!(await comparePassword(ps.password, profile.password!))) { - throw new ApiError('ACCESS_DENIED'); - } - - if (ps.email != null) { - const available = await validateEmailForAccount(ps.email); - if (!available) throw new ApiError('INTERNAL_ERROR'); - } - - 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 = secureRndstr(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 d73f2111a..000000000 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ /dev/null @@ -1,219 +0,0 @@ -import RE2 from 're2'; -import * as mfm from 'mfm-js'; -import { notificationTypes } from 'foundkey-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 { User } from '@/models/entities/user.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { langmap } from '@/misc/langmap.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['account'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['INVALID_REGEXP', 'NO_SUCH_FILE', 'NO_SUCH_PAGE', 'NOT_AN_IMAGE'], - - 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' }, - showTimelineReplies: { type: 'boolean' }, - injectFeaturedNote: { type: 'boolean' }, - receiveAnnouncementEmail: { type: 'boolean' }, - alwaysMarkNsfw: { type: 'boolean' }, - ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, - pinnedPageId: { type: 'array', items: { - type: 'string', format: 'misskey:id', - } }, - mutedWords: { type: 'array' }, - mutedInstances: { type: 'array', items: { - type: 'string', - } }, - mutingNotificationTypes: { type: 'array', items: { - type: 'string', enum: notificationTypes, - } }, - emailNotificationTypes: { type: 'array', items: { - type: 'string', - } }, - federateBlocks: { type: 'boolean' }, - }, -} as const; - -// eslint-disable-next-line import/no-default-export -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('INVALID_REGEXP'); - - try { - new RE2(regexp[1], regexp[2]); - } catch (err) { - throw new ApiError('INVALID_REGEXP'); - } - }); - - 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.federateBlocks === 'boolean') updates.federateBlocks = ps.federateBlocks; - if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; - if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; - if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; - if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; - if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; - - if (ps.avatarId) { - const avatar = await DriveFiles.findOneBy({ id: ps.avatarId }); - - if (avatar == null || avatar.userId !== user.id) throw new ApiError('NO_SUCH_FILE', 'Avatar file not found.'); - if (!avatar.type.startsWith('image/')) throw new ApiError('NOT_AN_IMAGE', 'Avatar file is not an image.'); - } - - if (ps.bannerId) { - const banner = await DriveFiles.findOneBy({ id: ps.bannerId }); - - if (banner == null || banner.userId !== user.id) throw new ApiError('NO_SUCH_FILE', 'Banner file not found.'); - if (!banner.type.startsWith('image/')) throw new ApiError('BANNER_NOT_AN_IMAGE', 'Banner file is not an image.'); - } - - if (ps.pinnedPageId) { - const page = await Pages.findOneBy({ id: ps.pinnedPageId }); - - if (page == null || page.userId !== user.id) throw new ApiError('NO_SUCH_PAGE'); - - profileUpdates.pinnedPageId = page.id; - } else if (ps.pinnedPageId === null) { - profileUpdates.pinnedPageId = null; - } - - if (ps.fields) { - profileUpdates.fields = ps.fields - .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '') - .map(x => { - return { name: x.name, value: x.value }; - }); - } - - //#region emojis/tags - - let emojis = [] as string[]; - let tags = [] as string[]; - - const newName = updates.name === undefined ? user.name : updates.name; - const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; - - if (newName != null) { - const tokens = mfm.parsePlain(newName); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); - } - - if (newDescription != null) { - const tokens = mfm.parse(newDescription); - emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); - tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32); - } - - updates.emojis = emojis; - updates.tags = tags; - - // ハッシュタグ更新 - updateUsertags(user, tags); - //#endregion - - if (Object.keys(updates).length > 0) await Users.update(user.id, updates); - if (Object.keys(profileUpdates).length > 0) await UserProfiles.update(user.id, profileUpdates); - - const iObj = await Users.pack(user.id, user, { - detail: true, - includeSecrets: isSecure, - }); - - // Publish meUpdated event - publishMainStream(user.id, 'meUpdated', iObj); - publishUserEvent(user.id, 'updateUserProfile', await UserProfiles.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 5bf20e67b..000000000 --- a/packages/backend/src/server/api/endpoints/i/user-group-invites.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { UserGroupInvitations } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 383eee433..000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ /dev/null @@ -1,43 +0,0 @@ -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'; -import define from '../../../define.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; - -// eslint-disable-next-line import/no-default-export -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 61778aa22..000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/delete.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Webhooks } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['webhooks'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['NO_SUCH_WEBHOOK'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - webhookId: { type: 'string', format: 'misskey:id' }, - }, - required: ['webhookId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_WEBHOOK'); - - 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 40e40f548..000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Webhooks } from '@/models/index.js'; -import define from '../../../define.js'; - -export const meta = { - tags: ['webhooks', 'account'], - - requireCredential: true, - - kind: 'read:account', -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 21baed811..000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Webhooks } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['webhooks'], - - requireCredential: true, - - kind: 'read:account', - - errors: ['NO_SUCH_WEBHOOK'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - webhookId: { type: 'string', format: 'misskey:id' }, - }, - required: ['webhookId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_WEBHOOK'); - - 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 d3076ba16..000000000 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Webhooks } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; -import { webhookEventTypes } from '@/models/entities/webhook.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['webhooks'], - - requireCredential: true, - - kind: 'write:account', - - errors: ['NO_SUCH_WEBHOOK'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_WEBHOOK'); - - 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/messaging/history.ts b/packages/backend/src/server/api/endpoints/messaging/history.ts deleted file mode 100644 index ff22828fa..000000000 --- a/packages/backend/src/server/api/endpoints/messaging/history.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Brackets } from 'typeorm'; -import { 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; - -// eslint-disable-next-line import/no-default-export -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 }); - - if (found.length > 0) { - query.andWhere('message.groupId NOT IN (:...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 }); - query.andWhere('message.recipientId NOT IN (:...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 b9fea1259..000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Brackets } from 'typeorm'; -import { MessagingMessages, UserGroups, UserGroupJoinings, Users } 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 { 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: ['ACCESS_DENIED', 'NO_SUCH_USER', 'NO_SUCH_GROUP'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER'); - 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('NO_SUCH_GROUP'); - - // check joined - const joined = await UserGroupJoinings.countBy({ - userId: user.id, - userGroupId: recipientGroup.id, - }); - - if (!joined) throw new ApiError('ACCESS_DENIED', 'You have to join a group to read messages in it.'); - - 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 cbca715c0..000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { DriveFiles, UserGroups, UserGroupJoinings, Blockings } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { createMessage } from '@/services/messages/create.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; -import { getUser } from '../../../common/getters.js'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true, - - kind: 'write:messaging', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'MessagingMessage', - }, - - errors: ['ACCESS_DENIED', 'BLOCKED', 'NO_SUCH_FILE', 'NO_SUCH_USER', 'NO_SUCH_GROUP', 'RECIPIENT_IS_YOURSELF'], -} as const; - -export const paramDef = { - type: 'object', - anyOf: [ - { - properties: { - text: { - type: 'string', - minLength: 1, - maxLength: 3000, - }, - fileId: { - type: 'string', - format: 'misskey:id', - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['text', 'userId'], - }, - { - properties: { - fileId: { - type: 'string', - format: 'misskey:id', - }, - userId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['fileId', 'userId'], - }, - { - properties: { - text: { - type: 'string', - minLength: 1, - maxLength: 3000, - }, - fileId: { - type: 'string', - format: 'misskey:id', - }, - groupId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['text', 'groupId'], - }, - { - properties: { - fileId: { - type: 'string', - format: 'misskey:id', - }, - groupId: { - type: 'string', - format: 'misskey:id', - }, - }, - required: ['fileId', 'groupId'], - }, - ], -} as const; - -// eslint-disable-next-line import/no-default-export -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('RECIPIENT_IS_YOURSELF'); - - // Fetch recipient (user) - recipientUser = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check blocking - const block = await Blockings.countBy({ - blockerId: recipientUser.id, - blockeeId: user.id, - }); - if (block) throw new ApiError('BLOCKED'); - } else if (ps.groupId != null) { - // Fetch recipient (group) - recipientGroup = await UserGroups.findOneBy({ id: ps.groupId! }); - - if (recipientGroup == null) throw new ApiError('NO_SUCH_GROUP'); - - // check joined - const joined = await UserGroupJoinings.countBy({ - userId: user.id, - userGroupId: recipientGroup.id, - }); - - if (!joined) throw new ApiError('ACCESS_DENIED', 'You have to join a group to send a message in it.'); - } - - let file = null; - if (ps.fileId != null) { - file = await DriveFiles.findOneBy({ - id: ps.fileId, - userId: user.id, - }); - - if (file == null) throw new ApiError('NO_SUCH_FILE'); - } - - 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 4e86fa530..000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages/delete.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MessagingMessages } from '@/models/index.js'; -import { deleteMessage } from '@/services/messages/delete.js'; -import { SECOND, HOUR } from '@/const.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true, - - kind: 'write:messaging', - - limit: { - duration: HOUR, - max: 300, - minInterval: SECOND, - }, - - errors: ['NO_SUCH_MESSAGE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - messageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['messageId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_MESSAGE'); - - 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 3a3372c83..000000000 --- a/packages/backend/src/server/api/endpoints/messaging/messages/read.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MessagingMessages } from '@/models/index.js'; -import define from '../../../define.js'; -import { ApiError } from '../../../error.js'; -import { readUserMessagingMessage, readGroupMessagingMessage } from '../../../common/read-messaging-message.js'; - -export const meta = { - tags: ['messaging'], - - requireCredential: true, - - kind: 'write:messaging', - - errors: ['NO_SUCH_MESSAGE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - messageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['messageId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const message = await MessagingMessages.findOneBy({ id: ps.messageId }); - - if (message == null) throw new ApiError('NO_SUCH_MESSAGE'); - - if (message.recipientId) { - await readUserMessagingMessage(user.id, message.userId, [message.id]).catch(e => { - if (e.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError('NO_SUCH_MESSAGE'); - 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('NO_SUCH_MESSAGE'); - 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 90d942811..000000000 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Emojis, Users } from '@/models/index.js'; -import define from '../define.js'; -import { translatorAvailable } from '../common/translator.js'; - -export const meta = { - tags: ['meta'], - - requireCredential: false, - - allowGet: true, - cacheSec: 60, - - 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://misskey.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, - }, - 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, - }, - 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, - }, - bannerUrl: { - type: 'string', - optional: false, nullable: false, - }, - iconUrl: { - type: 'string', - optional: false, nullable: true, - }, - maxNoteTextLength: { - 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', - }, - }, - }, - }, - requireSetup: { - type: 'boolean', - optional: false, nullable: false, - example: false, - }, - enableEmail: { - type: 'boolean', - optional: false, nullable: false, - }, - translatorAvailable: { - type: 'boolean', - optional: false, nullable: false, - }, - proxyAccountName: { - type: 'string', - optional: false, nullable: true, - }, - images: { - type: 'object', - optional: false, nullable: false, - properties: { - info: { type: 'string' }, - notFound: { type: 'string' }, - error: { type: 'string' }, - }, - }, - features: { - type: 'object', - optional: true, nullable: false, - properties: { - registration: { - type: 'boolean', - optional: false, nullable: false, - }, - localTimeLine: { - 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, - }, - serviceWorker: { - type: 'boolean', - optional: true, nullable: false, - default: true, - }, - miauth: { - type: 'boolean', - optional: true, nullable: false, - default: true, - }, - }, - }, - }, - }, - - v2: { - method: 'get', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - detail: { - deprecated: true, - description: 'This parameter is ignored. You will always get all details (as if it was `true`).', - type: 'boolean', - default: true, - }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - 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 - }, - }); - - 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, - disableRegistration: instance.disableRegistration, - disableLocalTimeline: instance.disableLocalTimeline, - disableGlobalTimeline: instance.disableGlobalTimeline, - driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, - driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, - emailRequiredForSignup: instance.emailRequiredForSignup, - enableHcaptcha: instance.enableHcaptcha, - hcaptchaSiteKey: instance.hcaptchaSiteKey, - enableRecaptcha: instance.enableRecaptcha, - recaptchaSiteKey: instance.recaptchaSiteKey, - swPublickey: instance.swPublicKey, - themeColor: instance.themeColor, - bannerUrl: instance.bannerUrl, - iconUrl: instance.iconUrl, - backgroundImageUrl: instance.backgroundImageUrl, - logoImageUrl: instance.logoImageUrl, - maxNoteTextLength: config.maxNoteTextLength, - emojis: await Emojis.packMany(emojis), - defaultLightTheme: instance.defaultLightTheme, - defaultDarkTheme: instance.defaultDarkTheme, - enableEmail: instance.enableEmail, - - translatorAvailable: translatorAvailable(instance), - - pinnedPages: instance.pinnedPages, - pinnedClipId: instance.pinnedClipId, - cacheRemoteFiles: instance.cacheRemoteFiles, - requireSetup: (await Users.countBy({ - host: IsNull(), - })) === 0, - - proxyAccountName: instance.proxyAccountId ? (await Users.pack(instance.proxyAccountId).catch(() => null))?.username : null, - - images: config.images, - - features: { - registration: !instance.disableRegistration, - localTimeLine: !instance.disableLocalTimeline, - globalTimeLine: !instance.disableGlobalTimeline, - emailRequiredForSignup: instance.emailRequiredForSignup, - elasticsearch: config.elasticsearch ? true : false, - hcaptcha: instance.enableHcaptcha, - recaptcha: instance.enableRecaptcha, - objectStorage: instance.useObjectStorage, - serviceWorker: true, - miauth: true, - }, - }; -}); 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 a295cf778..000000000 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { AccessTokens } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 ab013bade..000000000 --- a/packages/backend/src/server/api/endpoints/mute/create.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { genId } from '@/misc/gen-id.js'; -import { Mutings, NoteWatchings } from '@/models/index.js'; -import { Muting } from '@/models/entities/muting.js'; -import { publishUserEvent } from '@/services/stream.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: ['NO_SUCH_USER', 'MUTEE_IS_YOURSELF', 'ALREADY_MUTING'], -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const muter = user; - - // 自分自身 - if (user.id === ps.userId) throw new ApiError('MUTEE_IS_YOURSELF'); - - // Get mutee - const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check if already muting - const exist = await Mutings.countBy({ - muterId: muter.id, - muteeId: mutee.id, - }); - - if (exist) throw new ApiError('ALREADY_MUTING'); - - 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 7585aeacb..000000000 --- a/packages/backend/src/server/api/endpoints/mute/delete.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Mutings } from '@/models/index.js'; -import { publishUserEvent } from '@/services/stream.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: ['NO_SUCH_USER', 'MUTEE_IS_YOURSELF', 'NOT_MUTING'], -} 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; - - // Check if the mutee is yourself - if (user.id === ps.userId) throw new ApiError('MUTEE_IS_YOURSELF'); - - // Get mutee - const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check not muting - const exist = await Mutings.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, - }); - - if (exist == null) throw new ApiError('NOT_MUTING'); - - // 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 bea4877b7..000000000 --- a/packages/backend/src/server/api/endpoints/mute/list.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Mutings } 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: '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; - -// eslint-disable-next-line import/no-default-export -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 f5348fddd..000000000 --- a/packages/backend/src/server/api/endpoints/my/apps.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Apps } from '@/models/index.js'; -import define from '../../define.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; - -// eslint-disable-next-line import/no-default-export -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 b00032e27..000000000 --- a/packages/backend/src/server/api/endpoints/notes.ts +++ /dev/null @@ -1,79 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) - .andWhere('note.visibility = \'public\'') - .andWhere('NOT note.localOnly') - .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 ? '' : 'NOT') + 'note.hasPoll'); - } - - // 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 091bab766..000000000 --- a/packages/backend/src/server/api/endpoints/notes/children.ts +++ /dev/null @@ -1,70 +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, - - description: 'Get a list of children of a notes. Children includes replies as well as quote renotes that quote the respective post. A post will not be duplicated if it is a reply and a quote of a note in this thread. For depths larger than 1 the threading has to be computed by the client.', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, - - v2: { - method: 'get', - alias: 'notes/:noteId/children', - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - limit: { - description: 'The maximum number of replies/quotes to show per parent note, i.e. the maximum number of children each note may have.', - type: 'integer', - minimum: 1, - maximum: 100, - default: 10, - }, - depth: { - description: 'The number of layers of replies to fetch at once. Defaults to 1 for backward compatibility.', - type: 'integer', - minimum: 1, - maximum: 100, - default: 1, - }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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'); - - 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 3cd878800..000000000 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ /dev/null @@ -1,55 +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, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Clip', - }, - }, - - v2: { - method: 'get', - alias: 'notes/:noteId/clips', - }, - - errors: ['NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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 7e736c736..000000000 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { 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, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, - - v2: { - method: 'get', - alias: 'notes/:noteId/conversation', - }, - - errors: ['NO_SUCH_NOTE'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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 1b272e9dc..000000000 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { In } from 'typeorm'; -import { noteVisibilities } from 'foundkey-js'; -import create from '@/services/note/create.js'; -import { User } from '@/models/entities/user.js'; -import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { Note } from '@/models/entities/note.js'; -import { Channel } from '@/models/entities/channel.js'; -import { HOUR } from '@/const.js'; -import { isPureRenote } from '@/misc/renote.js'; -import config from '@/config/index.js'; -import { ApiError } from '../../error.js'; -import define from '../../define.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', - }, - }, - }, - - v2: { - method: 'post', - alias: 'notes', - }, - - errors: ['NO_SUCH_NOTE', 'PURE_RENOTE', 'EXPIRED_POLL', 'NO_SUCH_CHANNEL', 'BLOCKED', 'LESS_RESTRICTIVE_VISIBILITY'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - visibility: { - description: 'The visibility of the new note. Must be the same or more restrictive than a replied to or quoted note.', - type: 'string', - enum: noteVisibilities, - default: 'public', - }, - visibleUserIds: { type: 'array', uniqueItems: true, items: { - type: 'string', format: 'misskey:id', - } }, - text: { type: 'string', maxLength: config.maxNoteTextLength, 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: config.maxNoteTextLength, 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - 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('NO_SUCH_NOTE', 'Note to be renoted not found.'); - throw e; - }); - - if (isPureRenote(renote)) throw new ApiError('PURE_RENOTE', 'Cannot renote a pure renote.'); - - // check that the visibility is not less restrictive - if (noteVisibilities.indexOf(renote.visibility) > noteVisibilities.indexOf(ps.visibility)) { - throw new ApiError('LESS_RESTRICTIVE_VISIBILITY', `The renote has visibility ${renote.visibility}.`); - } - - // Check blocking - if (renote.userId !== user.id) { - const blocked = await Blockings.countBy({ - blockerId: renote.userId, - blockeeId: user.id, - }); - if (blocked) throw new ApiError('BLOCKED', 'Blocked by author of note to be renoted.'); - } - } - - 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('NO_SUCH_NOTE', 'Replied to note not found.'); - throw e; - }); - - if (isPureRenote(reply)) throw new ApiError('PURE_RENOTE', 'Cannot reply to a pure renote.'); - - // check that the visibility is not less restrictive - if (noteVisibilities.indexOf(reply.visibility) > noteVisibilities.indexOf(ps.visibility)) { - throw new ApiError('LESS_RESTRICTIVE_VISIBILITY', `The replied to note has visibility ${reply.visibility}.`); - } - - // Check blocking - if (reply.userId !== user.id) { - const blocked = await Blockings.countBy({ - blockerId: reply.userId, - blockeeId: user.id, - }); - if (blocked) throw new ApiError('BLOCKED', 'Blocked by author of replied to note.'); - } - } - - if (ps.poll) { - if (typeof ps.poll.expiresAt === 'number') { - if (ps.poll.expiresAt < Date.now()) { - throw new ApiError('EXPIRED_POLL'); - } - } 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('NO_SUCH_CHANNEL'); - } - - // 投稿を作成 - const note = await create(user, { - createdAt: new Date(), - files, - poll: ps.poll ? { - choices: ps.poll.choices, - multiple: ps.poll.multiple || false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - } : undefined, - text: ps.text || undefined, - reply, - renote, - cw: ps.cw, - 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 3d8af2c7c..000000000 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ /dev/null @@ -1,50 +0,0 @@ -import deleteNote from '@/services/note/delete.js'; -import { Users } from '@/models/index.js'; -import { SECOND, HOUR } from '@/const.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:notes', - - limit: { - duration: HOUR, - max: 300, - minInterval: SECOND, - }, - - v2: { - method: 'delete', - alias: 'notes/:noteId', - }, - - errors: ['ACCESS_DENIED', 'NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - throw err; - }); - - if ((!user.isAdmin && !user.isModerator) && (note.userId !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - - // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため - 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 4ae1f0830..000000000 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ /dev/null @@ -1,48 +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: ['NO_SUCH_NOTE', 'ALREADY_FAVORITED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - throw err; - }); - - // if already favorited - const exist = await NoteFavorites.countBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist) throw new ApiError('ALREADY_FAVORITED'); - - // 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 7c590c403..000000000 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ /dev/null @@ -1,42 +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: ['NO_SUCH_NOTE', 'NOT_FAVORITED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - throw err; - }); - - // if already favorited - const exist = await NoteFavorites.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist == null) throw new ApiError('NOT_FAVORITED'); - - // 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 112bc39a1..000000000 --- a/packages/backend/src/server/api/endpoints/notes/featured.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { DAY } from '@/const.js'; -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, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, - - v2: { - method: 'get', - }, -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const max = 30; - const day = 3 * DAY; - - const query = Notes.createQueryBuilder('note') - .addSelect('note.score') - .where('note.userHost IS NULL') - .andWhere('note.score > 0') - .andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) }) - .andWhere('note.visibility = \'public\'') - .innerJoinAndSelect('note.user', 'user') - .leftJoinAndSelect('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 (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 3cc8d292a..000000000 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ /dev/null @@ -1,94 +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 { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js'; - -export const meta = { - tags: ['notes'], - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, - - errors: ['TIMELINE_DISABLED'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('TIMELINE_DISABLED'); - } - } - - //#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); - generateMutedRenotesQuery(query, user); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.take(ps.limit).getMany(); - - process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } - }); - - return await Notes.packMany(timeline, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts deleted file mode 100644 index 18724c981..000000000 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ /dev/null @@ -1,135 +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 { generateMutedRenotesQuery } 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: ['TIMELINE_DISABLED'], -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const m = await fetchMeta(); - if (m.disableLocalTimeline && (!user.isAdmin && !user.isModerator)) { - throw new ApiError('TIMELINE_DISABLED'); - } - - //#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); - generateMutedRenotesQuery(query, user); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.take(ps.limit).getMany(); - - process.nextTick(() => { - activeUsersChart.read(user); - }); - - return await Notes.packMany(timeline, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts deleted file mode 100644 index f53d1788c..000000000 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ /dev/null @@ -1,115 +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 { generateMutedRenotesQuery } from '../../common/generated-muted-renote-query.js'; - -export const meta = { - tags: ['notes'], - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, - - errors: ['TIMELINE_DISABLED'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('TIMELINE_DISABLED'); - } - } - - //#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) generateMutedRenotesQuery(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")'); - } - } - //#endregion - - const timeline = await query.take(ps.limit).getMany(); - - process.nextTick(() => { - if (user) { - activeUsersChart.read(user); - } - }); - - return await Notes.packMany(timeline, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts deleted file mode 100644 index 10d329c9d..000000000 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Brackets } from 'typeorm'; -import { noteVisibilities } from 'foundkey-js'; -import { readNote } 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', - enum: noteVisibilities, - }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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()); - } - - const mentions = await query.take(ps.limit).getMany(); - - readNote(user.id, mentions); - - return await Notes.packMany(mentions, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts deleted file mode 100644 index 5a04d68f3..000000000 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ /dev/null @@ -1,83 +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; - -// eslint-disable-next-line import/no-default-export -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 4ffdcc4c4..000000000 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { ArrayOverlap, 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, NoteThreadMutings } from '@/models/index.js'; -import { 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: ['NO_SUCH_NOTE', 'INVALID_CHOICE', 'ALREADY_VOTED', 'EXPIRED_POLL', 'BLOCKED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - choice: { type: 'integer', minimum: 0 }, - }, - required: ['noteId', 'choice'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - throw err; - }); - - if (!note.hasPoll) { - throw new ApiError('NO_SUCH_NOTE', 'The note exists but does not have a poll attached.'); - } - - // Check blocking - if (note.userId !== user.id) { - const blocked = await Blockings.countBy({ - blockerId: note.userId, - blockeeId: user.id, - }); - if (blocked) throw new ApiError('BLOCKED'); - } - - const poll = await Polls.findOneByOrFail({ noteId: note.id }); - - if (poll.expiresAt && poll.expiresAt < createdAt) { - throw new ApiError('EXPIRED_POLL'); - } - - if (poll.choices[ps.choice] == null) { - throw new ApiError('INVALID_CHOICE', `There are only ${poll.choices.length} choices.`); - } - - // 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('ALREADY_VOTED', 'This is a multiple choice poll, but you already voted for that option.'); - } - } else { - throw new ApiError('ALREADY_VOTED', 'This is a single choice poll.'); - } - } - - // 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, - }); - - // check if this thread and notification type is muted - const threadMuted = await NoteThreadMutings.countBy({ - userId: note.userId, - threadId: note.threadId || note.id, - mutingNotificationTypes: ArrayOverlap(['pollVote']), - }); - // Notify - if (!threadMuted) { - createNotification(note.userId, 'pollVote', { - notifierId: user.id, - noteId: note.id, - choice: ps.choice, - }); - } - - // Fetch watchers - // checking for mutes is not necessary here, as note watchings will be - // deleted when a thread is muted - 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 6fd0e8f16..000000000 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { FindOptionsWhere } from 'typeorm'; -import { NoteReactions } from '@/models/index.js'; -import { 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, - - allowGet: true, - cacheSec: 60, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'NoteReaction', - }, - }, - - v2: { - method: 'get', - alias: 'notes/:noteId/reactions/:type?', - }, - - errors: ['NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - type: { - description: 'A Unicode emoji or custom emoji code. A custom emoji should look like `:name:` or `:name@example.com:`.', - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // check note visibility - await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); - 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 d99dc314e..000000000 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ /dev/null @@ -1,37 +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: ['NO_SUCH_NOTE', 'ALREADY_REACTED', 'BLOCKED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - reaction: { type: 'string' }, - }, - required: ['noteId', 'reaction'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - throw err; - }); - await createReaction(user, note, ps.reaction).catch(e => { - if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError('ALREADY_REACTED'); - if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError('BLOCKED'); - 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 b974fc3ef..000000000 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { deleteReaction } from '@/services/note/reaction/delete.js'; -import { SECOND, HOUR } from '@/const.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', - - limit: { - duration: HOUR, - max: 60, - minInterval: 3 * SECOND, - }, - - errors: ['NO_SUCH_NOTE', 'NOT_REACTED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - throw err; - }); - await deleteReaction(user, note).catch(e => { - if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError('NOT_REACTED'); - throw e; - }); -}); 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 55674f9ee..000000000 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ /dev/null @@ -1,72 +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, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, - - v2: { - method: 'get', - alias: 'notes/:noteId/renotes', - }, - - errors: ['NO_SUCH_NOTE'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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); - - const renotes = await query.take(ps.limit).getMany(); - - return await Notes.packMany(renotes, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/replies.ts b/packages/backend/src/server/api/endpoints/notes/replies.ts deleted file mode 100644 index 34b679ae4..000000000 --- a/packages/backend/src/server/api/endpoints/notes/replies.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Notes } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.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'; -import { getNote } from '../../common/getters.js'; - -export const meta = { - tags: ['notes'], - - requireCredential: false, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, - - v2: { - method: 'get', - alias: 'notes/:noteId/replies', - }, - - errors: ['NO_SUCH_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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); - throw err; - }); - - 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); - - const timeline = await query.take(ps.limit).getMany(); - - return await Notes.packMany(timeline, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts deleted file mode 100644 index 4f4711491..000000000 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ /dev/null @@ -1,140 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -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(ps.tag)) throw new Error('Injection'); - query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); - } else { - query.andWhere(new Brackets(qb => { - for (const tags of ps.query!) { - qb.orWhere(new Brackets(qb => { - for (const tag of tags) { - if (!safeForSql(tag)) throw new Error('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'); - } else { - query.andWhere('NOT note.hasPoll'); - } - } - - // Search notes - const notes = await query.take(ps.limit).getMany(); - - return await Notes.packMany(notes, me); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts deleted file mode 100644 index e75fe0ef6..000000000 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { In } from 'typeorm'; -import { Notes } from '@/models/index.js'; -import config from '@/config/index.js'; -import es from '@/db/elasticsearch.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, - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - }, -} 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - if (es == null) { - const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId); - - if (ps.userId) { - query.andWhere('note.userId = :userId', { userId: ps.userId }); - } else if (ps.host) { - query.andWhere('note.userHost = :host', { host: ps.host }); - } 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 = await query.take(ps.limit).getMany(); - - return await Notes.packMany(notes, me); - } else { - const userQuery = ps.userId != null ? [{ - term: { - userId: ps.userId, - }, - }] : []; - - const hostQuery = ps.userId == null ? - ps.host === null ? [{ - bool: { - must_not: { - exists: { - field: 'userHost', - }, - }, - }, - }] : ps.host !== undefined ? [{ - term: { - userHost: ps.host, - }, - }] : [] - : []; - - const result = await es.search({ - index: config.elasticsearch.index || 'misskey_note', - body: { - size: ps.limit, - from: ps.offset, - query: { - bool: { - must: [{ - simple_query_string: { - fields: ['text'], - query: ps.query.toLowerCase(), - default_operator: 'and', - }, - }, ...hostQuery, ...userQuery], - }, - }, - sort: [{ - _doc: 'desc', - }], - }, - }); - - const hits = result.body.hits.hits.map((hit: any) => hit._id); - - if (hits.length === 0) return []; - - // Fetch found notes - const notes = await Notes.find({ - where: { - id: In(hits), - }, - order: { - id: -1, - }, - }); - - return await Notes.packMany(notes, me); - } -}); diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts deleted file mode 100644 index 6c3f68407..000000000 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ /dev/null @@ -1,47 +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, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - - v2: { - method: 'get', - alias: 'notes/:noteId', - }, - - errors: ['NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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('NO_SUCH_NOTE'); - 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 c578e1419..000000000 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { NoteFavorites, NoteThreadMutings, NoteWatchings } from '@/models/index.js'; -import { ApiError } from '@/server/api/error.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, - }, - }, - }, - - v2: { - method: 'get', - alias: 'notes/:noteId/status', - }, - - errors: ['NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - throw err; - }); - - 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 dddb55fcb..000000000 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { noteNotificationTypes } from 'foundkey-js'; -import { Notes, NoteThreadMutings, NoteWatchings } 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: ['NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - mutingNotificationTypes: { - description: 'Defines which notification types from the thread should be muted. Replies are always muted. Applies in addition to the global settings, muting takes precedence.', - type: 'array', - items: { - type: 'string', enum: noteNotificationTypes, - }, - uniqueItems: true, - }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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, - mutingNotificationTypes: ps.mutingNotificationTypes, - }); - - // remove all note watchings in the muted thread - const notesThread = Notes.createQueryBuilder('notes') - .select('note.id') - .where({ - threadId: note.threadId ?? note.id, - }); - - await NoteWatchings.createQueryBuilder() - .delete() - .where(`"note_watching"."noteId" IN (${ notesThread.getQuery() })`) - .setParameters(notesThread.getParameters()) - .execute(); -}); 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 c1ea564eb..000000000 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ /dev/null @@ -1,35 +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: ['NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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 d042e555f..000000000 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ /dev/null @@ -1,133 +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 { generateMutedRenotesQuery } 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; - -// eslint-disable-next-line import/no-default-export -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); - generateMutedRenotesQuery(query, user); - - if (ps.includeMyRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.userId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeRenotedMyNotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserId != :meId', { meId: user.id }); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.includeLocalRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteUserHost IS NOT NULL'); - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); - })); - } - - if (ps.withFiles) { - query.andWhere('note.fileIds != \'{}\''); - } - //#endregion - - const timeline = await query.take(ps.limit).getMany(); - - process.nextTick(() => { - activeUsersChart.read(user); - }); - - return await Notes.packMany(timeline, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts deleted file mode 100644 index 1c4989354..000000000 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ /dev/null @@ -1,222 +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 { TranslationService } from '@/models/entities/meta.js'; -import { ApiError } from '../../error.js'; -import { getNote } from '../../common/getters.js'; -import define from '../../define.js'; - -const sourceLangs = [ - 'BG', - 'CS', - 'DA', - 'DE', - 'EL', - 'EN', - 'ES', - 'ET', - 'FI', - 'FR', - 'HU', - 'ID', - 'IT', - 'JA', - 'LT', - 'LV', - 'NL', - 'PL', - 'PT', - 'RO', - 'RU', - 'SK', - 'SL', - 'SV', - 'TR', - 'UK', - 'ZH', -]; - -export const meta = { - tags: ['notes'], - - requireCredential: false, - - res: { - type: 'object', - optional: false, nullable: false, - properties: { - sourceLang: { - type: 'string', - enum: sourceLangs, - }, - text: { type: 'string' }, - }, - }, - - v2: { - method: 'get', - alias: 'notes/:noteId/translate/:targetLang/:sourceLang?', - }, - - errors: ['NO_SUCH_NOTE'], -} as const; - -// List of permitted languages from https://www.deepl.com/docs-api/translate-text/translate-text/ -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - sourceLang: { - type: 'string', - enum: sourceLangs, - }, - targetLang: { - type: 'string', - enum: [ - 'BG', - 'CS', - 'DA', - 'DE', - 'EL', - 'EN', - 'EN-GB', - 'EN-US', - 'ES', - 'ET', - 'FI', - 'FR', - 'HU', - 'ID', - 'IT', - 'JA', - 'LT', - 'LV', - 'NL', - 'PL', - 'PT', - 'PT-BR', - 'PT-PT', - 'RO', - 'RU', - 'SK', - 'SL', - 'SV', - 'TR', - 'UK', - 'ZH', - ], - }, - }, - required: ['noteId', 'targetLang'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - throw err; - }); - const instance = await fetchMeta(); - - if (instance.translationService == null) { - return 204; - } - - type Translation = { - sourceLang: string; - text: string; - }; - - async function translateDeepL(): Promise { - if (note.text == null || instance.deeplAuthKey == null) { - return 204; // TODO: Return a better error - } - - const sourceLang = ps.sourceLang; - const targetLang = ps.targetLang; - - const params = new URLSearchParams(); - params.append('auth_key', instance.deeplAuthKey); - params.append('text', note.text); - params.append('target_lang', targetLang); - if (sourceLang) params.append('source_lang', sourceLang); - - // From the DeepL API docs: - //> DeepL API Free authentication keys can be identified easily by the suffix ":fx" - const endpoint = instance.deeplAuthKey.endsWith(':fx') - ? 'https://api-free.deepl.com/v2/translate' - : 'https://api.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, - }; - } - - async function translateLibreTranslate(): Promise { - if (note.text == null || instance.libreTranslateEndpoint == null) { - return 204; - } - - // LibteTranslate only understands 2-letter codes - const source = ps.sourceLang?.toLowerCase().split('-', 1)[0] ?? 'auto'; - const target = ps.targetLang.toLowerCase().split('-', 1)[0]; - const api_key = instance.libreTranslateAuthKey ?? undefined; - const endpoint = instance.libreTranslateEndpoint; - - const params = { - q: note.text, - source, - target, - api_key, - }; - - const res = await fetch(endpoint, { - method: 'POST', - body: JSON.stringify(params), - headers: { 'Content-Type': 'application/json' }, - }); - - const json = (await res.json()) as { - detectedLanguage?: { - confidence: number; - language: string; - }; - translatedText: string; - }; - - return { - sourceLang: (json.detectedLanguage?.language ?? source).toUpperCase(), - text: json.translatedText, - }; - } - - switch (instance.translationService) { - case TranslationService.DeepL: - return await translateDeepL(); - case TranslationService.LibreTranslate: - return await translateLibreTranslate(); - } -}); 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 20f3aa429..000000000 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ /dev/null @@ -1,52 +0,0 @@ -import deleteNote from '@/services/note/delete.js'; -import { Notes, Users } from '@/models/index.js'; -import { SECOND, HOUR } from '@/const.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:notes', - - limit: { - duration: HOUR, - max: 300, - minInterval: SECOND, - }, - - v2: { - method: 'delete', - alias: 'notes/:noteId/renotes', - }, - - errors: ['NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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 c95efd6b6..000000000 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ /dev/null @@ -1,115 +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: ['NO_SUCH_USER_LIST'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER_LIST'); - - //#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 - - const timeline = await query.take(ps.limit).getMany(); - - activeUsersChart.read(user); - - return await Notes.packMany(timeline, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts deleted file mode 100644 index 002e9b007..000000000 --- a/packages/backend/src/server/api/endpoints/notes/watching/create.ts +++ /dev/null @@ -1,32 +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: ['NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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 54f7163b9..000000000 --- a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts +++ /dev/null @@ -1,32 +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: ['NO_SUCH_NOTE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_NOTE'); - 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 e45ebdd1d..000000000 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createNotification } from '@/services/create-notification.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['notifications'], - - requireCredential: true, - - kind: 'write:notifications', -} 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; - -// eslint-disable-next-line import/no-default-export -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 d169afbb3..000000000 --- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ /dev/null @@ -1,33 +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; - -// eslint-disable-next-line import/no-default-export -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 f7b64fddb..000000000 --- a/packages/backend/src/server/api/endpoints/notifications/read.ts +++ /dev/null @@ -1,44 +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.', - - // FIXME: This error makes sense here but will never be thrown here. - // errors: ['NO_SUCH_NOTIFICATION'], -} 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; - -// eslint-disable-next-line import/no-default-export -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 e69b279ea..000000000 --- a/packages/backend/src/server/api/endpoints/page-push.ts +++ /dev/null @@ -1,37 +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: ['NO_SUCH_PAGE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - pageId: { type: 'string', format: 'misskey:id' }, - event: { type: 'string' }, - var: {}, - }, - required: ['pageId', 'event'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) throw new ApiError('NO_SUCH_PAGE'); - - 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 7649792cd..000000000 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Pages, DriveFiles } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { Page } from '@/models/entities/page.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.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: ['NO_SUCH_FILE', 'NAME_ALREADY_EXISTS'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - title: { type: 'string' }, - name: { type: 'string', minLength: 1 }, - summary: { type: 'string', nullable: true }, - text: { type: 'string', minLength: 1 }, - eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true }, - font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' }, - alignCenter: { type: 'boolean', default: false }, - hideTitleWhenPinned: { type: 'boolean', default: false }, - }, - required: ['title', 'name', 'text'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_FILE'); - } - - await Pages.findBy({ - userId: user.id, - name: ps.name, - }).then(result => { - if (result.length > 0) { - throw new ApiError('NAME_ALREADY_EXISTS'); - } - }); - - const page = await Pages.insert(new Page({ - id: genId(), - createdAt: new Date(), - updatedAt: new Date(), - title: ps.title, - name: ps.name, - summary: ps.summary, - text: ps.text, - eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null, - userId: user.id, - visibility: 'public', - alignCenter: ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned, - font: ps.font, - })).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 b8f8d43c7..000000000 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ /dev/null @@ -1,32 +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: ['NO_SUCH_PAGE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - pageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['pageId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ - id: ps.pageId, - userId: user.id, - }); - if (page == null) throw new ApiError('NO_SUCH_PAGE'); - - 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 5a149a626..000000000 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Pages } from '@/models/index.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['pages'], - - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -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 a3f8f3f3a..000000000 --- a/packages/backend/src/server/api/endpoints/pages/like.ts +++ /dev/null @@ -1,46 +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: ['ALREADY_LIKED', 'NO_SUCH_PAGE'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - pageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['pageId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) throw new ApiError('NO_SUCH_PAGE'); - - // if already liked - const exist = await PageLikes.countBy({ - pageId: page.id, - userId: user.id, - }); - - if (exist) throw new ApiError('ALREADY_LIKED'); - - // 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 9861efdaa..000000000 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { IsNull } from 'typeorm'; -import { Pages, Users } from '@/models/index.js'; -import { Page } from '@/models/entities/page.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['pages'], - - requireCredential: false, - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'Page', - }, - - errors: ['NO_SUCH_PAGE'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_PAGE'); - - 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 5c18312d3..000000000 --- a/packages/backend/src/server/api/endpoints/pages/unlike.ts +++ /dev/null @@ -1,39 +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: ['NO_SUCH_PAGE', 'NOT_LIKED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - pageId: { type: 'string', format: 'misskey:id' }, - }, - required: ['pageId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ id: ps.pageId }); - if (page == null) throw new ApiError('NO_SUCH_PAGE'); - - const exist = await PageLikes.findOneBy({ - pageId: page.id, - userId: user.id, - }); - - if (exist == null) throw new ApiError('NOT_LIKED'); - - // 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 f82f754d1..000000000 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Not } from 'typeorm'; -import { Pages, DriveFiles } from '@/models/index.js'; -import { HOUR } from '@/const.js'; -import define from '../../define.js'; -import { ApiError } from '../../error.js'; - -export const meta = { - tags: ['pages'], - - requireCredential: true, - - kind: 'write:pages', - - limit: { - duration: HOUR, - max: 300, - }, - - errors: ['NAME_ALREADY_EXISTS', 'NO_SUCH_FILE', 'NO_SUCH_PAGE'], -} 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 }, - text: { type: 'string', minLength: 1 }, - eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true }, - font: { type: 'string', enum: ['serif', 'sans-serif'] }, - alignCenter: { type: 'boolean' }, - hideTitleWhenPinned: { type: 'boolean' }, - }, - required: ['pageId', 'title', 'name', 'text'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const page = await Pages.findOneBy({ - id: ps.pageId, - userId: user.id, - }); - if (page == null) throw new ApiError('NO_SUCH_PAGE'); - - let eyeCatchingImage = null; - if (ps.eyeCatchingImageId != null) { - eyeCatchingImage = await DriveFiles.findOneBy({ - id: ps.eyeCatchingImageId, - userId: user.id, - }); - - if (eyeCatchingImage == null) throw new ApiError('NO_SUCH_FILE'); - } - - await Pages.findBy({ - id: Not(ps.pageId), - userId: user.id, - name: ps.name, - }).then(result => { - if (result.length > 0) throw new ApiError('NAME_ALREADY_EXISTS'); - }); - - 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, - text: ps.text, - alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, - hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, - font: ps.font === undefined ? page.font : ps.font, - eyeCatchingImageId: ps.eyeCatchingImageId === null - ? null - : ps.eyeCatchingImageId === undefined - ? page.eyeCatchingImageId - : eyeCatchingImage!.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/ping.ts b/packages/backend/src/server/api/endpoints/ping.ts deleted file mode 100644 index 2891a0860..000000000 --- a/packages/backend/src/server/api/endpoints/ping.ts +++ /dev/null @@ -1,31 +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; - -// eslint-disable-next-line import/no-default-export -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 41595b47d..000000000 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ /dev/null @@ -1,40 +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 { User } from '@/models/entities/user.js'; -import define from '../define.js'; - -export const meta = { - tags: ['users'], - - requireCredential: 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; - -// eslint-disable-next-line import/no-default-export -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/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts deleted file mode 100644 index 461312969..000000000 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { genId } from '@/misc/gen-id.js'; -import { RenoteMutings } from '@/models/index.js'; -import { RenoteMuting } from '@/models/entities/renote-muting.js'; -import { publishUserEvent } from '@/services/stream.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: ['NO_SUCH_USER', 'MUTEE_IS_YOURSELF', 'ALREADY_MUTING'], -} 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; - - // Check if the mutee is yourself - if (user.id === ps.userId) throw new ApiError('MUTEE_IS_YOURSELF'); - - // Get mutee - const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check if already muting - const exist = await RenoteMutings.countBy({ - muterId: muter.id, - muteeId: mutee.id, - }); - - if (exist) throw new ApiError('ALREADY_MUTING'); - - // Create mute - await RenoteMutings.insert({ - id: genId(), - createdAt: new Date(), - muterId: muter.id, - muteeId: mutee.id, - } as RenoteMuting); - - publishUserEvent(user.id, 'muteRenote', 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 0f7c4cde7..000000000 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { RenoteMutings } from '@/models/index.js'; -import { publishUserEvent } from '@/services/stream.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: ['NO_SUCH_USER', 'MUTEE_IS_YOURSELF', 'NOT_MUTING'], -} 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; - - // Check if the mutee is yourself - if (user.id === ps.userId) throw new ApiError('MUTEE_IS_YOURSELF'); - - // Get mutee - const mutee = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check not muting - const exist = await RenoteMutings.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, - }); - - if (exist == null) throw new ApiError('NOT_MUTING'); - - // Delete mute - await RenoteMutings.delete({ - id: exist.id, - }); - - publishUserEvent(user.id, 'unmuteRenote', 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 8d282243f..000000000 --- a/packages/backend/src/server/api/endpoints/renote-mute/list.ts +++ /dev/null @@ -1,43 +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 e97d9c4b2..000000000 --- a/packages/backend/src/server/api/endpoints/request-reset-password.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { IsNull } from 'typeorm'; -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 { secureRndstr } from '@/misc/secure-rndstr.js'; -import { DAY } from '@/const.js'; -import define from '../define.js'; - -export const meta = { - tags: ['reset password'], - - requireCredential: false, - - description: 'Request a users password to be reset.', - - limit: { - duration: DAY, - max: 1, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - username: { type: 'string' }, - email: { type: 'string' }, - }, - required: ['username', 'email'], -} as const; - -// eslint-disable-next-line import/no-default-export -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 = secureRndstr(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 b8f969bd5..000000000 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { resetDb } from '@/db/postgre.js'; -import { ApiError } from '@/server/api/error.js'; -import define from '../define.js'; - -export const meta = { - tags: ['non-productive'], - - requireCredential: false, - - description: 'Only available when running with NODE_ENV=testing. Reset the database and flush Redis.', -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async () => { - if (process.env.NODE_ENV !== 'test') throw new ApiError('ACCESS_DENIED'); - - 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 d45b20945..000000000 --- a/packages/backend/src/server/api/endpoints/reset-password.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { hashPassword } from '@/misc/password.js'; -import { UserProfiles, PasswordResetRequests } from '@/models/index.js'; -import { DAY, MINUTE } from '@/const.js'; -import define from '../define.js'; - -export const meta = { - tags: ['reset password'], - - requireCredential: false, - - description: 'Complete the password reset that was previously requested.', - - limit: { - duration: DAY, - max: 1, - }, - - errors: ['NO_SUCH_RESET_REQUEST'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - token: { type: 'string' }, - password: { type: 'string' }, - }, - required: ['token', 'password'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const req = await PasswordResetRequests.findOneBy({ - token: ps.token, - }); - if (req == null) throw new ApiError('NO_SUCH_RESET_REQUEST'); - - // expires after 30 minutes - // This is a secondary check just in case the expiry task is broken, - // the expiry task is badly aligned with this expiration or something - // else strange is going on. - if (Date.now() - req.createdAt.getTime() > 30 * MINUTE) { - await PasswordResetRequests.delete(req.id); - throw new ApiError('NO_SUCH_RESET_REQUEST'); - } - - await UserProfiles.update(req.userId, { - password: await hashPassword(ps.password), - }); - - await 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 99f3730e9..000000000 --- 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, - - tags: ['meta'], -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -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 2d263cae8..000000000 --- a/packages/backend/src/server/api/endpoints/stats.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { IsNull } from 'typeorm'; -import { Instances, NoteReactions, Notes, Users } from '@/models/index.js'; -import define from '../define.js'; - -export const meta = { - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -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 5ae858b99..000000000 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ /dev/null @@ -1,72 +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, - }, - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - endpoint: { type: 'string' }, - auth: { type: 'string' }, - publickey: { type: 'string' }, - }, - required: ['endpoint', 'auth', 'publickey'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // if already subscribed - const exist = await SwSubscriptions.countBy({ - userId: user.id, - endpoint: ps.endpoint, - auth: ps.auth, - publickey: ps.publickey, - }); - - const instance = await fetchMeta(true); - - if (exist) { - return { - state: 'already-subscribed' as const, - key: instance.swPublicKey, - }; - } - - await SwSubscriptions.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - endpoint: ps.endpoint, - auth: ps.auth, - publickey: ps.publickey, - }); - - return { - state: 'subscribed' as const, - key: instance.swPublicKey, - }; -}); diff --git a/packages/backend/src/server/api/endpoints/sw/unregister.ts b/packages/backend/src/server/api/endpoints/sw/unregister.ts deleted file mode 100644 index c19e06b87..000000000 --- a/packages/backend/src/server/api/endpoints/sw/unregister.ts +++ /dev/null @@ -1,26 +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' }, - }, - required: ['endpoint'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - await SwSubscriptions.delete({ - userId: user.id, - endpoint: ps.endpoint, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts deleted file mode 100644 index 3e41aeaed..000000000 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ /dev/null @@ -1,43 +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; - -// eslint-disable-next-line import/no-default-export -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 45708883d..000000000 --- a/packages/backend/src/server/api/endpoints/users.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { DAY } from '@/const.js'; -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: 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: { - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Users.createQueryBuilder('user'); - query.where('user.isExplorable'); - - switch (ps.state) { - case 'admin': query.andWhere('user.isAdmin'); break; - case 'moderator': query.andWhere('user.isModerator'); break; - case 'adminOrModerator': query.andWhere('user.isAdmin OR user.isModerator'); break; - case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 5 * DAY) }); 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 0d3706d7e..000000000 --- a/packages/backend/src/server/api/endpoints/users/clips.ts +++ /dev/null @@ -1,43 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - 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 15ecf42fe..000000000 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ /dev/null @@ -1,93 +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, - - description: 'Show everyone that follows this user.', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'Following', - }, - }, - - errors: ['NO_SUCH_USER', 'ACCESS_DENIED'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER'); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError('ACCESS_DENIED'); - } else if (me.id !== user.id) { - const following = await Followings.countBy({ - followeeId: user.id, - followerId: me.id, - }); - if (!following) { - throw new ApiError('ACCESS_DENIED'); - } - } - } - - 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 85dccccb7..000000000 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ /dev/null @@ -1,93 +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, - - 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: ['ACCESS_DENIED', 'NO_SUCH_USER'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER'); - - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - - if (profile.ffVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError('ACCESS_DENIED'); - } - } else if (profile.ffVisibility === 'followers') { - if (me == null) { - throw new ApiError('ACCESS_DENIED'); - } else if (me.id !== user.id) { - const following = await Followings.countBy({ - followeeId: user.id, - followerId: me.id, - }); - if (!following) { - throw new ApiError('ACCESS_DENIED'); - } - } - } - - 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 8184c4ce4..000000000 --- a/packages/backend/src/server/api/endpoints/users/gallery/posts.ts +++ /dev/null @@ -1,42 +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: ['users', 'gallery'], - - 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; - -// eslint-disable-next-line import/no-default-export -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/groups/create.ts b/packages/backend/src/server/api/endpoints/users/groups/create.ts deleted file mode 100644 index 4a6362a3c..000000000 --- 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 { UserGroup } from '@/models/entities/user-group.js'; -import { 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; - -// eslint-disable-next-line import/no-default-export -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 8c9ff7134..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/delete.ts +++ /dev/null @@ -1,35 +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: ['NO_SUCH_GROUP'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_GROUP'); - - 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 88086a084..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/accept.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { UserGroupJoinings, UserGroupInvitations } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { 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: ['NO_SUCH_INVITATION'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - invitationId: { type: 'string', format: 'misskey:id' }, - }, - required: ['invitationId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch the invitation - const invitation = await UserGroupInvitations.findOneBy({ - id: ps.invitationId, - userId: user.id, - }); - - if (invitation == null) throw new ApiError('NO_SUCH_INVITATION'); - - // 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 0f3712b02..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/invitations/reject.ts +++ /dev/null @@ -1,36 +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: ['NO_SUCH_INVITATION'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - invitationId: { type: 'string', format: 'misskey:id' }, - }, - required: ['invitationId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Fetch the invitation - const invitation = await UserGroupInvitations.findOneBy({ - id: ps.invitationId, - userId: user.id, - }); - - if (invitation == null) throw new ApiError('NO_SUCH_INVITATION'); - - 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 e8214e815..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/invite.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { 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: ['NO_SUCH_USER', 'NO_SUCH_GROUP', 'ALREADY_ADDED', 'ALREADY_INVITED'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_GROUP'); - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - const joined = await UserGroupJoinings.countBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (joined) throw new ApiError('ALREADY_ADDED'); - - const existInvitation = await UserGroupInvitations.countBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (existInvitation) throw new ApiError('ALREADY_INVITED'); - - 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 16c6e544e..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/joined.ts +++ /dev/null @@ -1,45 +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; - -// eslint-disable-next-line import/no-default-export -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 19a4982bb..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/leave.ts +++ /dev/null @@ -1,37 +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: ['NO_SUCH_GROUP', 'GROUP_OWNER'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_GROUP'); - - if (me.id === userGroup.userId) throw new ApiError('GROUP_OWNER'); - - 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 d77cf1a52..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/owned.ts +++ /dev/null @@ -1,37 +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; - -// eslint-disable-next-line import/no-default-export -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 30caceb27..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/pull.ts +++ /dev/null @@ -1,47 +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: ['NO_SUCH_USER', 'NO_SUCH_GROUP', 'GROUP_OWNER'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_GROUP'); - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - if (user.id === userGroup.userId) throw new ApiError('GROUP_OWNER'); - - // 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 2aa5abb5b..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/show.ts +++ /dev/null @@ -1,50 +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: ['NO_SUCH_GROUP'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - groupId: { type: 'string', format: 'misskey:id' }, - }, - required: ['groupId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_GROUP'); - - const joined = await UserGroupJoinings.countBy({ - userId: me.id, - userGroupId: userGroup.id, - }); - - if (!joined && userGroup.userId !== me.id) { - throw new ApiError('NO_SUCH_GROUP'); - } - - 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 b9020fbcc..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/transfer.ts +++ /dev/null @@ -1,61 +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: ['NO_SUCH_GROUP', 'NO_SUCH_USER'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_GROUP'); - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - const joined = await UserGroupJoinings.countBy({ - userGroupId: userGroup.id, - userId: user.id, - }); - - if (!joined) throw new ApiError('NO_SUCH_USER', 'The user exists but is not a member of the group.'); - - 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 81d5f26ec..000000000 --- a/packages/backend/src/server/api/endpoints/users/groups/update.ts +++ /dev/null @@ -1,47 +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: ['NO_SUCH_GROUP'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_GROUP'); - - 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 783e63f5d..000000000 --- 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 { 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; - -// eslint-disable-next-line import/no-default-export -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.ts b/packages/backend/src/server/api/endpoints/users/lists/delete.ts deleted file mode 100644 index e8f476794..000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/delete.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 an existing list of users.', - - errors: ['NO_SUCH_USER_LIST'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - listId: { type: 'string', format: 'misskey:id' }, - }, - required: ['listId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER_LIST'); - - 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 889052fa3..000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ /dev/null @@ -1,37 +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; - -// eslint-disable-next-line import/no-default-export -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 0860b97ef..000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ /dev/null @@ -1,48 +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: ['NO_SUCH_USER', 'NO_SUCH_USER_LIST'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER_LIST'); - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - 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 86f7aee9a..000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ /dev/null @@ -1,62 +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: ['ALREADY_ADDED', 'BLOCKED', 'NO_SUCH_USER', 'NO_SUCH_USER_LIST'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER_LIST'); - - // Fetch the user - const user = await getUser(ps.userId).catch(e => { - if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError('NO_SUCH_USER'); - throw e; - }); - - // Check blocking - if (user.id !== me.id) { - const blocked = await Blockings.countBy({ - blockerId: user.id, - blockeeId: me.id, - }); - if (blocked) throw new ApiError('BLOCKED'); - } - - const exist = await UserListJoinings.countBy({ - userListId: userList.id, - userId: user.id, - }); - - if (exist) throw new ApiError('ALREADY_ADDED'); - - // 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 64f280a61..000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/show.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', 'account'], - - requireCredential: true, - - kind: 'read:account', - - description: 'Show the properties of a list.', - - res: { - type: 'object', - optional: false, nullable: false, - ref: 'UserList', - }, - - errors: ['NO_SUCH_USER_LIST'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - listId: { type: 'string', format: 'misskey:id' }, - }, - required: ['listId'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER_LIST'); - - 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 f0df2d282..000000000 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ /dev/null @@ -1,47 +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: ['NO_SUCH_USER_LIST'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER_LIST'); - - 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 dddee1907..000000000 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ /dev/null @@ -1,116 +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'], - - 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: ['NO_SUCH_USER'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER'); - 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); - 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")'); - } - } - - 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 4bfafbdb4..000000000 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ /dev/null @@ -1,43 +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'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) - .andWhere('page.userId = :userId', { userId: ps.userId }) - .andWhere('page.visibility = \'public\''); - - const pages = await query - .take(ps.limit) - .getMany(); - - return await Pages.packMany(pages); -}); diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts deleted file mode 100644 index 67ef1271f..000000000 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ /dev/null @@ -1,60 +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, - - description: 'Show all reactions this user made.', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'NoteReaction', - }, - }, - - errors: ['ACCESS_DENIED'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('ACCESS_DENIED'); - } - - 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 5fe5b47b5..000000000 --- a/packages/backend/src/server/api/endpoints/users/recommendation.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Users, Followings } from '@/models/index.js'; -import { DAY } from '@/const.js'; -import define from '../../define.js'; -import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query.js'; -import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query.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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const query = Users.createQueryBuilder('user') - .where('NOT user.isLocked') - .andWhere('user.isExplorable') - .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 7984a8050..000000000 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ /dev/null @@ -1,129 +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; - -// eslint-disable-next-line import/no-default-export -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 7ad92293f..000000000 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ /dev/null @@ -1,87 +0,0 @@ -import sanitizeHtml from 'sanitize-html'; -import config from '@/config/index.js'; -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: ['NO_SUCH_USER', 'CANNOT_REPORT_ADMIN', 'CANNOT_REPORT_YOURSELF'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - urls: { type: 'array', items: { type: 'string' }, nullable: true, uniqueItems: true }, - comment: { type: 'string', minLength: 1, maxLength: 2048 }, - }, - required: ['userId', 'comment'], -} as const; - -// eslint-disable-next-line import/no-default-export -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('NO_SUCH_USER'); - throw e; - }); - - if (user.id === me.id) throw new ApiError('CANNOT_REPORT_YOURSELF'); - - if (user.isAdmin) throw new ApiError('CANNOT_REPORT_ADMIN'); - - const uri = user.host == null ? `${config.url}/users/${user.id}` : user.uri; - if (!ps.urls.includes(uri)) { - ps.urls.push(uri); - } - - const report = await AbuseUserReports.insert({ - id: genId(), - createdAt: new Date(), - targetUserId: user.id, - targetUserHost: user.host, - reporterId: me.id, - reporterHost: null, - comment: ps.comment, - urls: ps.urls, - }).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, - urls: report.urls, - }); - } - - 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 200e2b925..000000000 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Brackets } from 'typeorm'; -import { MONTH } from '@/const.js'; -import { Followings, Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['users'], - - requireCredential: false, - - 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したいけどエラーになる - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const activeThreshold = new Date(Date.now() - MONTH); - - if (ps.host) { - const q = Users.createQueryBuilder('user') - .where('NOT user.isSuspended') - .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('NOT user.isSuspended') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :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') - .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') - .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 18fa10401..000000000 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Brackets } from 'typeorm'; -import { MONTH } from '@/const.js'; -import { UserProfiles, Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import define from '../../define.js'; - -export const meta = { - tags: ['users'], - - requireCredential: false, - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, me) => { - const activeThreshold = new Date(Date.now() - MONTH); - - 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 }); - })) - .andWhere('NOT user.isSuspended'); - - 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 }); - })) - .andWhere('NOT user.isSuspended'); - - 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() })`) - // don't show users twice, but also make sure there is at least one value otherwise this is an invalid query - .andWhere('user.id NOT IN (:...ids)', { ids: users.length === 0 ? [''] : users.map(user => user.id) }) - .andWhere(new Brackets(qb => { qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold }); - })) - .andWhere('NOT user.isSuspended') - .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 33c0cfca3..000000000 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { FindOptionsWhere, In, IsNull } from 'typeorm'; -import { resolveUser } from '@/remote/resolve-user.js'; -import { Users } from '@/models/index.js'; -import { 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, - - 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: ['FAILED_TO_RESOLVE_REMOTE_USER', 'NO_SUCH_USER'], -} 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; - -// eslint-disable-next-line import/no-default-export -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('FAILED_TO_RESOLVE_REMOTE_USER'); - }); - } 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('NO_SUCH_USER'); - } - - 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 0f99e56b3..000000000 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ /dev/null @@ -1,187 +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, - - description: 'Show statistics about a user.', - - errors: ['NO_SUCH_USER'], - - 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; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps) => { - const user = await Users.findOneBy({ id: ps.userId }); - if (user == null) { - throw new ApiError('NO_SUCH_USER'); - } - - 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.id), - }); - - 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 c015c7608..000000000 --- a/packages/backend/src/server/api/error.ts +++ /dev/null @@ -1,387 +0,0 @@ -import Koa from 'koa'; - -export class ApiError extends Error { - public message: string; - public code: string; - public httpStatusCode: number; - public info?: any; - - constructor( - code: keyof errors = 'INTERNAL_ERROR', - info?: any | null, - ) { - let _info = info, _code = code; - if (!(code in errors)) { - _code = 'INTERNAL_ERROR'; - _info = `Unknown error "${code}" occurred.`; - } - - const { message, httpStatusCode } = errors[_code]; - super(message); - this.code = _code; - this.info = _info; - this.message = message; - this.httpStatusCode = httpStatusCode; - } - - /** - * Makes the response of ctx the current error, given the respective endpoint name. - */ - public apply(ctx: Koa.Context, endpoint: string): void { - ctx.status = this.httpStatusCode; - if (ctx.status === 401) { - ctx.response.set('WWW-Authenticate', 'Bearer'); - } - ctx.body = { - error: { - message: this.message, - code: this.code, - info: this.info ?? undefined, - endpoint, - }, - }; - } -} - -export const errors: Record = { - ACCESS_DENIED: { - message: 'Access denied.', - httpStatusCode: 403, - }, - ALREADY_ADDED: { - message: 'That user has already been added to that list or group.', - httpStatusCode: 409, - }, - ALREADY_BLOCKING: { - message: 'You are already blocking that user.', - httpStatusCode: 409, - }, - ALREADY_CLIPPED: { - message: 'That note is already added to that clip.', - httpStatusCode: 409, - }, - ALREADY_FAVORITED: { - message: 'That note is already favorited.', - httpStatusCode: 409, - }, - ALREADY_FOLLOWING: { - message: 'You are already following that user.', - httpStatusCode: 409, - }, - ALREADY_INVITED: { - message: 'That user has already been invited to that group.', - httpStatusCode: 409, - }, - ALREADY_LIKED: { - message: 'You already liked that page or gallery post.', - httpStatusCode: 409, - }, - ALREADY_MUTING: { - message: 'You are already muting that user.', - httpStatusCode: 409, - }, - ALREADY_PINNED: { - message: 'You already pinned that note.', - httpStatusCode: 409, - }, - ALREADY_REACTED: { - message: 'You already reacted to that note.', - httpStatusCode: 409, - }, - ALREADY_VOTED: { - message: 'You have already voted in that poll.', - httpStatusCode: 409, - }, - AUTHENTICATION_FAILED: { - message: 'Authentication failed.', - httpStatusCode: 401, - }, - AUTHENTICATION_REQUIRED: { - message: 'Authentication is required, but authenticating information was not or not appropriately provided.', - httpStatusCode: 401, - }, - BLOCKED: { - message: 'You are blocked by that user.', - httpStatusCode: 400, - }, - BLOCKEE_IS_YOURSELF: { - message: 'You cannot block yourself.', - httpStatusCode: 400, - }, - BLOCKING: { - message: 'You are blocking that user.', - httpStatusCode: 400, - }, - CANNOT_REPORT_ADMIN: { - message: 'You cannot report an administrator.', - httpStatusCode: 400, - }, - CANNOT_REPORT_YOURSELF: { - message: 'You cannot report yourself.', - httpStatusCode: 400, - }, - EMPTY_FILE: { - message: 'The provided file is empty.', - httpStatusCode: 400, - }, - EXPIRED_POLL: { - message: 'Poll is already expired.', - httpStatusCode: 400, - }, - FAILED_TO_RESOLVE_REMOTE_USER: { - message: 'Failed to resolve remote user.', - httpStatusCode: 502, - }, - FILE_TOO_BIG: { - message: 'The provided file is too big.', - httpStatusCode: 400, - }, - FILE_REQUIRED: { - message: 'This operation requires a file to be provided.', - httpStatusCode: 400, - }, - FOLLOWEE_IS_YOURSELF: { - message: 'You cannot follow yourself.', - httpStatusCode: 400, - }, - FOLLOWER_IS_YOURSELF: { - message: 'You cannot unfollow yourself.', - httpStatusCode: 400, - }, - GROUP_OWNER: { - message: 'The owner of a group may not leave. Instead, ownership can be transferred or the group deleted.', - httpStatusCode: 400, - }, - HAS_CHILD_FILES_OR_FOLDERS: { - message: 'That folder is not empty.', - httpStatusCode: 400, - }, - INTERNAL_ERROR: { - message: 'Internal error occurred. Please contact us if the error persists.', - httpStatusCode: 500, - }, - INVALID_CHOICE: { - message: 'Choice index is invalid.', - httpStatusCode: 400, - }, - INVALID_FILE_NAME: { - message: 'Invalid file name.', - httpStatusCode: 400, - }, - INVALID_PARAM: { - message: 'One or more parameters do not match the API definition.', - httpStatusCode: 400, - }, - INVALID_PASSWORD: { - message: 'The provided password is not suitable.', - httpStatusCode: 400, - }, - INVALID_REGEXP: { - message: 'Invalid Regular Expression', - httpStatusCode: 400, - }, - INVALID_URL: { - message: 'Invalid URL.', - httpStatusCode: 400, - }, - INVALID_USERNAME: { - message: 'Invalid username.', - httpStatusCode: 400, - }, - IS_ADMIN: { - message: 'This action cannot be done to an administrator account.', - httpStatusCode: 400, - }, - IS_MODERATOR: { - message: 'This action cannot be done to a moderator account.', - httpStatusCode: 400, - }, - LESS_RESTRICTIVE_VISIBILITY: { - message: 'The visibility cannot be less restrictive than the parent note.', - httpStatusCode: 400, - }, - MUTEE_IS_YOURSELF: { - message: 'You cannot mute yourself.', - httpStatusCode: 400, - }, - NAME_ALREADY_EXISTS: { - message: 'The specified name already exists.', - httpStatusCode: 409, - }, - NO_POLL: { - message: 'The note does not have an attached poll.', - httpStatusCode: 404, - }, - NO_SUCH_ANNOUNCEMENT: { - message: 'No such announcement.', - httpStatusCode: 404, - }, - NO_SUCH_ANTENNA: { - message: 'No such antenna.', - httpStatusCode: 404, - }, - NO_SUCH_APP: { - message: 'No such app.', - httpStatusCode: 404, - }, - NO_SUCH_CLIP: { - message: 'No such clip.', - httpStatusCode: 404, - }, - NO_SUCH_CHANNEL: { - message: 'No such channel.', - httpStatusCode: 404, - }, - NO_SUCH_EMOJI: { - message: 'No such emoji.', - httpStatusCode: 404, - }, - NO_SUCH_ENDPOINT: { - message: 'No such endpoint.', - httpStatusCode: 404, - }, - NO_SUCH_FILE: { - message: 'No such file.', - httpStatusCode: 404, - }, - NO_SUCH_FOLDER: { - message: 'No such folder.', - httpStatusCode: 404, - }, - NO_SUCH_FOLLOW_REQUEST: { - message: 'No such follow request.', - httpStatusCode: 404, - }, - NO_SUCH_GROUP: { - message: 'No such user group.', - httpStatusCode: 404, - }, - NO_SUCH_HASHTAG: { - message: 'No such hashtag.', - httpStatusCode: 404, - }, - NO_SUCH_INVITATION: { - message: 'No such group invitation.', - httpStatusCode: 404, - }, - NO_SUCH_KEY: { - message: 'No such key.', - httpStatusCode: 404, - }, - NO_SUCH_NOTE: { - message: 'No such note.', - httpStatusCode: 404, - }, - NO_SUCH_NOTIFICATION: { - message: 'No such notification.', - httpStatusCode: 404, - }, - NO_SUCH_MESSAGE: { - message: 'No such message.', - httpStatusCode: 404, - }, - NO_SUCH_OBJECT: { - message: 'No such object.', - httpStatusCode: 404, - }, - NO_SUCH_PAGE: { - message: 'No such page.', - httpStatusCode: 404, - }, - NO_SUCH_PARENT_FOLDER: { - message: 'No such parent folder.', - httpStatusCode: 404, - }, - NO_SUCH_POST: { - message: 'No such gallery post.', - httpStatusCode: 404, - }, - NO_SUCH_RESET_REQUEST: { - message: 'No such password reset request.', - httpStatusCode: 404, - }, - NO_SUCH_SESSION: { - message: 'No such session', - httpStatusCode: 404, - }, - NO_SUCH_USER: { - message: 'No such user.', - httpStatusCode: 404, - }, - NO_SUCH_USER_LIST: { - message: 'No such user list.', - httpStatusCode: 404, - }, - NO_SUCH_WEBHOOK: { - message: 'No such webhook.', - httpStatusCode: 404, - }, - NOT_AN_IMAGE: { - message: 'The file specified was expected to be an image, but it is not.', - httpStatusCode: 400, - }, - NOT_BLOCKING: { - message: 'You are not blocking that user.', - httpStatusCode: 409, - }, - NOT_CLIPPED: { - message: 'That note is not added to that clip.', - httpStatusCode: 409, - }, - NOT_FAVORITED: { - message: 'You have not favorited that note.', - httpStatusCode: 409, - }, - NOT_FOLLOWING: { - message: 'You are not following that user.', - httpStatusCode: 409, - }, - NOT_LIKED: { - message: 'You have not liked that page or gallery post.', - httpStatusCode: 409, - }, - NOT_MUTING: { - message: 'You are not muting that user.', - httpStatusCode: 409, - }, - NOT_REACTED: { - message: 'You have not reacted to that note.', - httpStatusCode: 409, - }, - PENDING_SESSION: { - message: 'That authorization process has not been completed yet.', - httpStatusCode: 400, - }, - PIN_LIMIT_EXCEEDED: { - message: 'You can not pin any more notes.', - httpStatusCode: 400, - }, - PURE_RENOTE: { - message: 'You cannot renote or reply to a pure renote.', - httpStatusCode: 400, - }, - RATE_LIMIT_EXCEEDED: { - message: 'Rate limit exceeded. Please try again later.', - httpStatusCode: 429, - }, - RECIPIENT_IS_YOURSELF: { - message: 'You cannot send a message to yourself.', - httpStatusCode: 400, - }, - RECURSIVE_FOLDER: { - message: 'Folder cannot be its own parent.', - httpStatusCode: 400, - }, - SUSPENDED: { - message: 'Your account has been suspended.', - httpStatusCode: 403, - }, - TIMELINE_DISABLED: { - message: 'This timeline is disabled by an administrator.', - httpStatusCode: 503, - }, - USED_USERNAME: { - message: 'That username is not available because it is being used or has been used before. Usernames cannot be reassigned.', - httpStatusCode: 409, - }, -}; diff --git a/packages/backend/src/server/api/index.ts b/packages/backend/src/server/api/index.ts deleted file mode 100644 index fe0c4450a..000000000 --- a/packages/backend/src/server/api/index.ts +++ /dev/null @@ -1,147 +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 { Instances, AccessTokens, Users } from '@/models/index.js'; -import config from '@/config/index.js'; -import endpoints from './endpoints.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 { oauth } from './common/oauth.js'; -import { ApiError } from './error.js'; - -// Init app -const app = new Koa(); - -app.use(cors({ - origin: '*', -})); - -// No caching -app.use(async (ctx, next) => { - ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); - await next(); -}); - -app.use(bodyParser()); - -// Init multer instance -const upload = multer({ - storage: multer.diskStorage({}), - limits: { - fileSize: config.maxFileSize || 262144000, - files: 1, - }, -}); -/** - * Wrap multer to return an appropriate API error when something goes wrong, e.g. the file is too big. - */ -type KoaMiddleware = (ctx: Koa.Context, next: () => Promise) => Promise; -const wrapped = upload.single('file'); -function uploadWrapper(endpoint: string): KoaMiddleware { - return (ctx: Koa.Context, next: () => Promise): Promise => { - // pass a fake "next" so we can separate multer errors from other API errors - return wrapped(ctx, () => {}) - .then( - () => next(), - (err) => { - let apiErr = new ApiError('INTERNAL_ERROR', err); - if (err?.code === 'LIMIT_FILE_SIZE') { - apiErr = new ApiError('FILE_TOO_BIG', { maxFileSize: config.maxFileSize || 262144000 }); - } - apiErr.apply(ctx, endpoint); - }, - ); - }; -} - -// Init router -const router = new Router(); - -/** - * Register endpoint handlers - */ -for (const endpoint of endpoints) { - if (endpoint.meta.requireFile) { - router.post(`/${endpoint.name}`, uploadWrapper(endpoint.name), 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; }); - } - - if (endpoint.meta.v2) { - const path = endpoint.meta.v2.alias ?? endpoint.name.replace(/-/g, '_'); - router[endpoint.meta.v2.method](`/v2/${path}`, handler.bind(null, endpoint)); - } - } -} - -// the OAuth endpoint does some shenanigans and can not use the normal API handler -router.post('/auth/session/oauth', oauth); - -router.post('/signup', signup); -router.post('/signin', signin); -router.post('/signup-pending', signupPending); - -router.get('/v1/instance/peers', async ctx => { - const instances = await Instances.find({ - select: ['host'], - }); - - ctx.body = instances.map(instance => instance.host); -}); - -router.post('/miauth/:session/check', async ctx => { - const token = await AccessTokens.findOneBy({ - session: ctx.params.session, - }); - - if (token && token.session != null && !token.fetched) { - AccessTokens.update(token.id, { - fetched: true, - }); - - ctx.body = { - ok: true, - token: token.token, - user: await Users.pack(token.userId, null, { detail: true }), - }; - } else { - ctx.body = { - ok: false, - }; - } -}); - -// Return 404 for unknown API -router.all('(.*)', async ctx => { - ctx.status = 404; -}); - -// Register router -app.use(router.routes()); - -export default app; diff --git a/packages/backend/src/server/api/limiter.ts b/packages/backend/src/server/api/limiter.ts deleted file mode 100644 index 2b835d446..000000000 --- a/packages/backend/src/server/api/limiter.ts +++ /dev/null @@ -1,76 +0,0 @@ -import Limiter from 'ratelimiter'; -import Logger from '@/services/logger.js'; -import { redisClient } from '@/db/redis.js'; -import { 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 ec22d6c3e..000000000 --- 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/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts deleted file mode 100644 index 3643887f7..000000000 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ /dev/null @@ -1,226 +0,0 @@ -import config from '@/config/index.js'; -import { kinds } from '@/misc/api-permissions.js'; -import { I18n } from '@/misc/i18n.js'; -import { errors as errorDefinitions } from '../error.js'; -import endpoints from '../endpoints.js'; -import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; -import { httpCodes } from './http-codes.js'; - -const i18n = new I18n('en-US'); - -export function genOpenapiSpec() { - const spec = { - openapi: '3.0.0', - - info: { - version: 'v1', - title: 'FoundKey API', - 'x-logo': { url: '/static-assets/api-doc.png' }, - }, - - externalDocs: { - description: 'Repository', - url: 'https://akkoma.dev/FoundKeyGang/FoundKey', - }, - - servers: [{ - url: config.apiUrl, - }], - - paths: {} as any, - - components: { - schemas, - - securitySchemes: { - ApiKeyAuth: { - type: 'apiKey', - in: 'body', - name: 'i', - }, - OAuth: { - type: 'oauth2', - flows: { - authorizationCode: { - authorizationUrl: `${config.url}/auth`, - tokenUrl: `${config.apiUrl}/auth/session/oauth`, - scopes: kinds.reduce((acc, kind) => { - acc[kind] = i18n.ts['_permissions'][kind]; - return acc; - }, {}), - }, - }, - }, - }, - }, - }; - - for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { - // generate possible responses, first starting with errors - const responses = [ - // general error codes that can always happen - 'INVALID_PARAM', - 'INTERNAL_ERROR', - // error codes that happen only if authentication is required - ...(!endpoint.meta.requireCredential ? [] : [ - 'ACCESS_DENIED', - 'AUTHENTICATION_REQUIRED', - 'AUTHENTICATION_FAILED', - 'SUSPENDED', - ]), - // error codes that happen only if a rate limit is defined - ...(!endpoint.meta.limit ? [] : [ - 'RATE_LIMIT_EXCEEDED', - ]), - // error codes that happen only if a file is required - ...(!endpoint.meta.requireFile ? [] : [ - 'FILE_REQUIRED', - ]), - // endpoint specific error codes - ...(endpoint.meta.errors ?? []), - ] - .reduce((acc, code) => { - const { message, httpStatusCode } = errorDefinitions[code]; - const httpCode = httpStatusCode.toString(); - - if (!(httpCode in acc)) { - acc[httpCode] = { - description: httpCodes[httpCode], - content: { - 'application/json': { - schema: { - '$ref': '#/components/schemas/Error', - }, - examples: {}, - }, - }, - }; - } - - acc[httpCode].content['application/json'].examples[code] = { - value: { - error: { - code, - message, - endpoint: endpoint.name, - }, - }, - }; - - return acc; - }, {}); - - // add successful response - if (endpoint.meta.res) { - responses['200'] = { - description: 'OK', - content: { - 'application/json': { - schema: convertSchemaToOpenApiSchema(endpoint.meta.res), - }, - }, - }; - } else { - responses['204'] = { - description: 'No Content', - }; - } - - 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: [], - }, - ]; - if (endpoint.meta.kind) { - security.push({ - OAuth: [endpoint.meta.kind], - }); - } else { - security.push({ - OAuth: [], - }); - } - 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://akkoma.dev/FoundKeyGang/FoundKey/src/branch/main/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`, - }, - tags: endpoint.meta.tags || undefined, - security, - requestBody: { - required: true, - content: { - [requestType]: { - schema, - }, - }, - }, - responses, - }; - - 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')); - - // fix the way parameters are passed - delete path.get.requestBody; - path.get.parameters = []; - for (const name in schema.properties) { - path.get.parameters.push({ - name, - in: 'query', - schema: schema.properties[name], - required: schema.required?.includes(name), - }); - } - } - - spec.paths['/' + endpoint.name] = path; - - if (endpoint.meta.v2) { - // we need a clone of the API endpoint info because otherwise we change it by reference - const infoClone = structuredClone(info); - const route = `/v2/${endpoint.meta.v2.alias ?? endpoint.name.replace(/-/g, '_')}`; - - infoClone['operationId'] = infoClone['summary'] = route; - - spec.paths[route] = { - ...spec.paths[route], - [endpoint.meta.v2.method]: infoClone, - }; - } - } - - return spec; -} diff --git a/packages/backend/src/server/api/openapi/http-codes.ts b/packages/backend/src/server/api/openapi/http-codes.ts deleted file mode 100644 index d08b10673..000000000 --- a/packages/backend/src/server/api/openapi/http-codes.ts +++ /dev/null @@ -1,67 +0,0 @@ -export const httpCodes: Record = { - '100': 'Continue', - '101': 'Switching Protocols', - '102': 'Processing', - '103': 'Early Hints', - '200': 'OK', - '201': 'Created', - '202': 'Accepted', - '203': 'Non-Authoritative Information', - '204': 'No Content', - '205': 'Reset Content', - '206': 'Partial Content', - '207': 'Multi-Status', - '208': 'Already Reported', - '226': 'IM Used', - '300': 'Multiple Choices', - '301': 'Moved Permanently', - '302': 'Found', - '303': 'See Other', - '304': 'Not Modified', - '305': 'Use Proxy', - '307': 'Temporary Redirect', - '308': 'Permanent Redirect', - '400': 'Bad Request', - '401': 'Unauthorized', - '402': 'Payment Required', - '403': 'Forbidden', - '404': 'Not Found', - '405': 'Method Not Allowed', - '406': 'Not Acceptable', - '407': 'Proxy Authentication Required', - '408': 'Request Timeout', - '409': 'Conflict', - '410': 'Gone', - '411': 'Length Required', - '412': 'Precondition Failed', - '413': 'Content Too Large', - '414': 'URI Too Long', - '415': 'Unsupported Media Type', - '416': 'Range Not Satisfiable', - '417': 'Expectation Failed', - '418': 'I\'m a Teapot', - '421': 'Misdirected Request', - '422': 'Unprocessable Content', - '423': 'Locked', - '424': 'Failed Dependency', - '425': 'Too Early', - '426': 'Upgrade Required', - '427': 'Unassigned', - '428': 'Precondition Required', - '429': 'Too Many Requests', - '430': 'Unassigned', - '431': 'Request Header Fields Too Large', - '451': 'Unavailable For Legal Reasons', - '500': 'Internal Server Error', - '501': 'Not Implemented', - '502': 'Bad Gateway', - '503': 'Service Unavailable', - '504': 'Gateway Timeout', - '505': 'HTTP Version Not Supported', - '506': 'Variant Also Negotiates', - '507': 'Insufficient Storage', - '508': 'Loop Detected', - '509': 'Unassigned', - '510': 'Not Extended', - '511': 'Network Authentication Required', -}; 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 f03da1c4d..000000000 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { refs, Schema } 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) - .flatMap(([k, v]) => v.optional ? [] : [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: 'A machine and human readable error code.', - }, - endpoint: { - type: 'string', - description: 'Name of the API endpoint the error happened in.', - }, - message: { - type: 'string', - description: 'A human readable error description in English.', - }, - info: { - description: 'Potentially more information, primarily intended for developers.', - }, - }, - required: ['code', 'endpoint', '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 b2bfabe52..000000000 --- a/packages/backend/src/server/api/private/signin.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { randomBytes } from 'node:crypto'; -import { IsNull } from 'typeorm'; -import Koa from 'koa'; -import * as speakeasy from 'speakeasy'; -import { SECOND, MINUTE, HOUR } from '@/const.js'; -import config from '@/config/index.js'; -import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js'; -import { ILocalUser } from '@/models/entities/user.js'; -import { genId } from '@/misc/gen-id.js'; -import { getIpHash } from '@/misc/get-ip-hash.js'; -import { comparePassword, hashPassword, isOldAlgorithm } from '@/misc/password.js'; -import signin from '../common/signin.js'; -import { verifyLogin, hash } from '../2fa.js'; -import { limiter } from '../limiter.js'; -import { ApiError, errors } from '../error.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, password, token } = body; - - function error(e: keyof errors, info?: Record): void { - new ApiError(e, info).apply(ctx, 'signin'); - } - - try { - // not more than 1 attempt per second and not more than 10 attempts per hour - await limiter({ key: 'signin', duration: HOUR, max: 10, minInterval: SECOND }, getIpHash(ctx.ip)); - } catch (err) { - error('RATE_LIMIT_EXCEEDED'); - return; - } - - if (typeof username !== 'string') { - error('INVALID_PARAM', { param: 'username', reason: 'not a string' }); - return; - } - - if (typeof password !== 'string') { - error('INVALID_PARAM', { param: 'password', reason: 'not a string' }); - return; - } - - if (token != null && typeof token !== 'string') { - error('INVALID_PARAM', { param: 'token', reason: 'provided but not a string' }); - return; - } - - // Fetch user - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: IsNull(), - }) as ILocalUser; - - if (user == null) { - error('NO_SUCH_USER'); - return; - } - - if (user.isSuspended) { - error('SUSPENDED'); - 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(): void { - // Append signin history - await Signins.insert({ - id: genId(), - createdAt: new Date(), - userId: user.id, - ip: ctx.ip, - headers: ctx.headers, - success: false, - }); - - error('ACCESS_DENIED'); - } - - if (!profile.twoFactorEnabled) { - if (same) { - signin(ctx, user); - return; - } else { - await fail(); - return; - } - } - - if (token) { - if (!same) { - await fail(); - return; - } - - const verified = (speakeasy as any).totp.verify({ - secret: profile.twoFactorSecret, - encoding: 'base32', - token, - window: 2, - }); - - if (verified) { - signin(ctx, user); - return; - } else { - await fail(); - return; - } - } else if (body.credentialId) { - if (!same && !profile.usePasswordLessLogin) { - await fail(); - 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(); - return; - } - - await AttestationChallenges.delete({ - userId: user.id, - id: body.challengeId, - }); - - if (new Date().getTime() - challenge.createdAt.getTime() >= 5 * MINUTE) { - await fail(); - return; - } - - const securityKey = await UserSecurityKeys.findOneBy({ - id: Buffer.from( - body.credentialId - .replace(/-/g, '+') - .replace(/_/g, '/'), - 'base64', - ).toString('hex'), - }); - - if (!securityKey) { - await fail(); - 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(); - return; - } - } else { - if (!same && !profile.usePasswordLessLogin) { - await fail(); - return; - } - - const keys = await UserSecurityKeys.findBy({ - userId: user.id, - }); - - if (keys.length === 0) { - await fail(); - 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 09505a895..000000000 --- a/packages/backend/src/server/api/private/signup-pending.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Koa from 'koa'; -import { 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 } = 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 fafa96362..000000000 --- a/packages/backend/src/server/api/private/signup.ts +++ /dev/null @@ -1,108 +0,0 @@ -import Koa from 'koa'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js'; -import { hashPassword } from '@/misc/password.js'; -import { Users, RegistrationTickets, UserPendings } from '@/models/index.js'; -import config from '@/config/index.js'; -import { sendEmail } from '@/services/send-email.js'; -import { secureRndstr } from '@/misc/secure-rndstr.js'; -import { genId } from '@/misc/gen-id.js'; -import { validateEmailForAccount } from '@/services/validate-email-for-account.js'; -import { signup } from '../common/signup.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 = secureRndstr(16); - - await UserPendings.insert({ - id: genId(), - createdAt: new Date(), - code, - email: emailAddress, - username, - password: await hashPassword(password), - }); - - 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/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts deleted file mode 100644 index 343c47ad8..000000000 --- a/packages/backend/src/server/api/stream/channel.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Note } from '@/models/entities/note.js'; -import { Notes } from '@/models/index.js'; -import { Packed } from '@/misc/schema.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { Connection } from './index.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, - body, - }); - } - - protected withPackedNote(callback: (note: Packed<'Note'>) => Promise): (note: Note) => Promise { - 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 = null; - note.renote = null; - note.user = null; - note.channel = null; - - const packed = await Notes.pack(note, this.user, { detail: true }); - - await 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 007b459df..000000000 --- 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() { - // 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 6d3871b20..000000000 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Notes } from '@/models/index.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { StreamMessages } from '../types.js'; -import Channel from '../channel.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 && this.renoteMuting.has(note.userId)) 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 f16a9f670..000000000 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Users } from '@/models/index.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { User } from '@/models/entities/user.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import { StreamMessages } from '../types.js'; -import Channel from '../channel.js'; - -export default class extends Channel { - public readonly chName = 'channel'; - public static shouldShare = false; - public static requireCredential = false; - private channelId: string; - private typers: Record = {}; - private emitTypersIntervalId: ReturnType; - private onNote: (note: Note) => Promise; - - constructor(id: string, connection: Channel['connection']) { - super(id, connection); - this.onNote = this.withPackedNote(this.onPackedNote.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 onPackedNote(note: Packed<'Note'>): Promise { - if (note.channelId !== this.channelId) return; - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - if (note.renote && this.renoteMuting.has(note.userId)) 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[id] == null; - this.typers[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) delete this.typers[userId]; - } - - const users = await Users.packMany(Object.keys(this.typers), 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 d7ccab5d8..000000000 --- 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() { - // 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 cefb80033..000000000 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; -import { isInstanceMuted } from '@/misc/is-instance-muted.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import Channel from '../channel.js'; - -export default class extends Channel { - public readonly chName = 'globalTimeline'; - public static shouldShare = true; - public static requireCredential = false; - private onNote: (note: Note) => Promise; - - constructor(id: string, connection: Channel['connection']) { - super(id, connection); - this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); - } - - public async init() { - 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 onPackedNote(note: Packed<'Note'>): Promise { - 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 && this.renoteMuting.has(note.userId)) return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - 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 3a4c4ad22..000000000 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import Channel from '../channel.js'; - -export default class extends Channel { - public readonly chName = 'hashtag'; - public static shouldShare = false; - public static requireCredential = false; - private q: string[][]; - private onNote: (note: Note) => Promise; - - constructor(id: string, connection: Channel['connection']) { - super(id, connection); - this.onNote = this.withPackedNote(this.onPackedNote.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 onPackedNote(note: Packed<'Note'>): Promise { - 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 && this.renoteMuting.has(note.userId)) 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 1dc81cdbc..000000000 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { checkWordMute } from '@/misc/check-word-mute.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { isInstanceMuted } from '@/misc/is-instance-muted.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import Channel from '../channel.js'; - -export default class extends Channel { - public readonly chName = 'homeTimeline'; - public static shouldShare = true; - public static requireCredential = true; - private onNote: (note: Note) => Promise; - - constructor(id: string, connection: Channel['connection']) { - super(id, connection); - this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); - } - - public async init() { - // Subscribe events - this.subscriber.on('notesStream', this.onNote); - } - - private async onPackedNote(note: Packed<'Note'>): Promise { - 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 && this.renoteMuting.has(note.userId)) return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - 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 e9e60a7c9..000000000 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { isInstanceMuted } from '@/misc/is-instance-muted.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import Channel from '../channel.js'; - -export default class extends Channel { - public readonly chName = 'hybridTimeline'; - public static shouldShare = true; - public static requireCredential = true; - private onNote: (note: Note) => Promise; - - constructor(id: string, connection: Channel['connection']) { - super(id, connection); - this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); - } - - public async init() { - const meta = await fetchMeta(); - if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return; - - // Subscribe events - this.subscriber.on('notesStream', this.onNote); - } - - private async onPackedNote(note: Packed<'Note'>): Promise { - // チャンネルの投稿ではなく、自分自身の投稿 または - // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または - // チャンネルの投稿ではなく、全体公開のローカルの投稿 または - // フォローしているチャンネルの投稿 の場合だけ - 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 && this.renoteMuting.has(note.userId)) return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - 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 d422edde8..000000000 --- a/packages/backend/src/server/api/stream/channels/index.ts +++ /dev/null @@ -1,33 +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 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, - 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 84524a154..000000000 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import Channel from '../channel.js'; - -export default class extends Channel { - public readonly chName = 'localTimeline'; - public static shouldShare = true; - public static requireCredential = false; - onNote: (note: Note) => Promise; - - constructor(id: string, connection: Channel['connection']) { - super(id, connection); - this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); - } - - public async init() { - 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 onPackedNote(note: Packed<'Note'>): Promise { - 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 && this.renoteMuting.has(note.userId)) return; - - // 流れてきたNoteがミュートすべきNoteだったら無視する - // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) - // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 - // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる - if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; - - this.connection.cacheNote(note); - - this.send('note', note); - } - - 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 f95ca62c8..000000000 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js'; -import Channel from '../channel.js'; - -export default class extends Channel { - public readonly chName = 'main'; - public static shouldShare = true; - public static requireCredential = true; - - public async init() { - // 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 1498e8649..000000000 --- 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() { - // 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 4f9f6a9c4..000000000 --- a/packages/backend/src/server/api/stream/channels/messaging.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index.js'; -import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivity } from '../../common/read-messaging-message.js'; -import Channel from '../channel.js'; -import { 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: Record = {}; - 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 joined = await UserGroupJoinings.countBy({ - userId: this.user!.id, - userGroupId: this.groupId, - }); - - if (!joined) { - 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[id] == null; - this.typers[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 Object.entries(this.typers)) { - if (now.getTime() - date.getTime() > 5000) delete this.typers[userId]; - } - - const users = await Users.packMany(Object.keys(this.typers), null, { detail: false }); - - this.send({ - type: 'typers', - body: users, - }); - } - - 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 f3dc7d668..000000000 --- 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() { - 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/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts deleted file mode 100644 index 7369bbefd..000000000 --- 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() { - 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 0c1a66a5b..000000000 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { UserListJoinings, UserLists } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { isUserRelated } from '@/misc/is-user-related.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import Channel from '../channel.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; - private onNote: (note: Note) => Promise; - - constructor(id: string, connection: Channel['connection']) { - super(id, connection); - this.updateListUsers = this.updateListUsers.bind(this); - this.onNote = this.withPackedNote(this.onPackedNote.bind(this)); - } - - public async init(params: any) { - this.listId = params.listId as string; - - // Check existence and owner - const exists = await UserLists.countBy({ - id: this.listId, - userId: this.user!.id, - }); - if (!exists) 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 onPackedNote(note: Packed<'Note'>): Promise { - if (!this.listUsers.includes(note.userId)) return; - - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; - if (note.renote && this.renoteMuting.has(note.userId)) 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 8ece8e0ff..000000000 --- a/packages/backend/src/server/api/stream/index.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { EventEmitter } from 'events'; -import { WebSocket } from 'ws'; -import { readNote } from '@/services/note/read.js'; -import { User } from '@/models/entities/user.js'; -import { Channel as ChannelModel } from '@/models/entities/channel.js'; -import { Followings, Mutings, RenoteMutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js'; -import { AccessToken } from '@/models/entities/access-token.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { Packed } from '@/misc/schema.js'; -import { readNotification } from '../common/read-notification.js'; -import channels from './channels/index.js'; -import Channel from './channel.js'; -import { StreamEventEmitter, StreamMessages } from './types.js'; -import Logger from '@/services/logger.js'; - -const logger = new Logger('streaming'); - -/** - * Main stream connection - */ -export 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 socket: WebSocket; - public subscriber: StreamEventEmitter; - private channels: Channel[] = []; - private subscribingNotes: any = {}; - private cachedNotes: Packed<'Note'>[] = []; - - constructor( - socket: WebSocket, - subscriber: EventEmitter, - user: User | null | undefined, - token: AccessToken | null | undefined, - ) { - this.socket = socket; - this.subscriber = subscriber; - if (user) this.user = user; - if (token) this.token = token; - - this.onMessage = this.onMessage.bind(this); - this.onUserEvent = this.onUserEvent.bind(this); - this.onNoteStreamMessage = this.onNoteStreamMessage.bind(this); - this.onBroadcastMessage = this.onBroadcastMessage.bind(this); - - this.socket.on('message', this.onMessage); - - 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); - } - } - - 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; - - case 'block': - this.blocking.add(data.body.id); - break; - - case 'unblock': - this.blocking.delete(data.body.id); - break; - - case 'muteRenote': - this.renoteMuting.add(data.body.id); - break; - - case 'unmuteRenote': - this.renoteMuting.delete(data.body.id); - break; - - 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.socket.close(); - this.dispose(); - break; - - default: - break; - } - } - - private async onMessage(data: WebSocket.RawData, isRaw: boolean) { - if (isRaw) { - logger.warn('received unexpected raw data from websocket'); - return; - } - - let obj: Record; - - try { - obj = JSON.parse(data); - } catch (err) { - logger.error(err); - return; - } - - const { type, body } = obj; - - switch (type) { - case 'readNotification': - this.onReadNotification(body); - break; - case 'subNote': case 's': - this.onSubscribeNote(body); - break; - case 'sr': - this.onSubscribeNote(body); - this.readNote(body); - break; - case 'unsubNote': case 'un': - this.onUnsubscribeNote(body); - break; - case 'connect': - this.onChannelConnectRequested(body); - break; - case 'disconnect': - this.onChannelDisconnectRequested(body); - break; - case 'channel': case 'ch': - this.onChannelMessageRequested(body); - break; - - // The reason for receiving these messages at the root level rather than in - // individual channels is that when considering the client's circumstances, the - // input form may be separate from the main components of the note channel or - // message, and it would be cumbersome to have each of those components connect to - // each channel. - 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; - - if (this.subscribingNotes[payload.id] == null) { - this.subscribingNotes[payload.id] = 0; - } - - this.subscribingNotes[payload.id]++; - - if (this.subscribingNotes[payload.id] === 1) { - this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); - } - } - - /** - * 投稿購読解除要求時 - */ - private onUnsubscribeNote(payload: any) { - if (!payload.id) return; - - this.subscribingNotes[payload.id]--; - if (this.subscribingNotes[payload.id] <= 0) { - delete this.subscribingNotes[payload.id]; - this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage); - } - } - - 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) { - this.socket.send(JSON.stringify({ - 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 }); - } - } - - /** - * チャンネルから切断 - * @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 != null && 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 6fab792ae..000000000 --- a/packages/backend/src/server/api/stream/types.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { EventEmitter } from 'events'; -import { Channel } from '@/models/entities/channel.js'; -import { User } from '@/models/entities/user.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -import { Note } from '@/models/entities/note.js'; -import { Antenna } from '@/models/entities/antenna.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFolder } from '@/models/entities/drive-folder.js'; -import { UserList } from '@/models/entities/user-list.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; -import { Signin } from '@/models/entities/signin.js'; -import { Page } from '@/models/entities/page.js'; -import { Packed } from '@/misc/schema.js'; -import { Webhook } from '@/models/entities/webhook.js'; -import type { StrictEventEmitter as Emitter } from 'strict-event-emitter-types'; - -//#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']; }; - 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; - muteRenote: User; - unmuteRenote: User; - block: User; - unblock: 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']; - }; -} -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; - urls: string[]; - }; -} -//#endregion - -// 辞書(interface or type)から{ type, body }ユニオンを定義 -// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type -// VS Codeの展開を防止するためにEvents型を定義 -type Events = { [K in keyof T]: { type: K; body: T[K]; } }; -type EventUnionFromDictionary< - T extends object, - U = Events -> = U[keyof U]; - -// name/messages(spec) pairs dictionary -export type StreamMessages = { - internal: { - name: 'internal'; - payload: EventUnionFromDictionary; - }; - broadcast: { - name: 'broadcast'; - payload: EventUnionFromDictionary; - }; - user: { - name: `user:${User['id']}`; - payload: EventUnionFromDictionary; - }; - main: { - name: `mainStream:${User['id']}`; - payload: EventUnionFromDictionary; - }; - drive: { - name: `driveStream:${User['id']}`; - payload: EventUnionFromDictionary; - }; - note: { - name: `noteStream:${Note['id']}`; - payload: EventUnionFromDictionary; - }; - channel: { - name: `channelStream:${Channel['id']}`; - payload: EventUnionFromDictionary; - }; - userList: { - name: `userListStream:${UserList['id']}`; - payload: EventUnionFromDictionary; - }; - antenna: { - name: `antennaStream:${Antenna['id']}`; - payload: EventUnionFromDictionary; - }; - messaging: { - name: `messagingStream:${User['id']}-${User['id']}`; - payload: EventUnionFromDictionary; - }; - groupMessaging: { - name: `messagingStream:${UserGroup['id']}`; - payload: EventUnionFromDictionary; - }; - messagingIndex: { - name: `messagingIndexStream:${User['id']}`; - payload: EventUnionFromDictionary; - }; - admin: { - name: `adminStream:${User['id']}`; - payload: EventUnionFromDictionary; - }; - notes: { - name: 'notesStream'; - payload: Note; - }; -}; - -// API event definitions -// ストリームごとのEmitterの辞書を用意 -type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter void }> }; -// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする -export type StreamEventEmitter = UnionToIntersection; -// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる - -// provide stream channels union -export type StreamChannels = StreamMessages[keyof StreamMessages]['name']; diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts deleted file mode 100644 index 825896b56..000000000 --- a/packages/backend/src/server/api/streaming.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { EventEmitter } from 'events'; -import * as http from 'node:http'; -import { WebSocketServer } from 'ws'; - -import { MINUTE } from '@/const.js'; -import { subscriber as redisClient } from '@/db/redis.js'; -import { Users } from '@/models/index.js'; -import { Connection } from './stream/index.js'; -import authenticate from './authenticate.js'; - -export const initializeStreamingServer = (server: http.Server): void => { - // Init websocket server - const ws = new WebSocketServer({ noServer: true }); - - server.on('upgrade', async (request, socket, head)=> { - if (!request.url.startsWith('/streaming?')) { - socket.write('HTTP/1.1 400 Bad Request\r\n\r\n', undefined, () => socket.destroy()); - return; - } - const q = new URLSearchParams(request.url.slice(11)); - - const [user, app] = await authenticate(request.headers.authorization, q.get('i')) - .catch(err => { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n', undefined, () => socket.destroy()); - return []; - }); - if (typeof user === 'undefined') return; - - if (user?.isSuspended) { - socket.write('HTTP/1.1 403 Forbidden\r\n\r\n', undefined, () => socket.destroy()); - return; - } - - ws.handleUpgrade(request, socket, head, (socket) => { - const ev = new EventEmitter(); - - async function onRedisMessage(_: string, data: string) { - const parsed = JSON.parse(data); - ev.emit(parsed.channel, parsed.message); - } - - redisClient.on('message', onRedisMessage); - - const main = new Connection(socket, ev, user, app); - - // keep user "online" while a stream is connected - const intervalId = user ? setInterval(() => { - Users.update(user.id, { - lastActiveDate: new Date(), - }); - }, 5 * MINUTE) : null; - if (user) { - Users.update(user.id, { - lastActiveDate: new Date(), - }); - } - socket.once('close', () => { - ev.removeAllListeners(); - main.dispose(); - redisClient.off('message', onRedisMessage); - if (intervalId) clearInterval(intervalId); - }); - - // ping/pong mechanism - // TODO: the websocket protocol already specifies a ping/pong mechanism, why is this necessary? - socket.on('message', async (data) => { - if (data.type === 'utf8' && data.utf8Data === 'ping') { - socket.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 4c4707e61..000000000 --- a/packages/backend/src/server/file/index.ts +++ /dev/null @@ -1,40 +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 0ae16d94c..000000000 --- a/packages/backend/src/server/file/send-drive-file.ts +++ /dev/null @@ -1,126 +0,0 @@ -import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import Koa from 'koa'; -import send from 'koa-send'; -import rename from 'rename'; -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, convertToPng } 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'; -import { serverLogger } from '../index.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'); -}; - -// eslint-disable-next-line import/no-default-export -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'].includes(mime)) { - return await convertToWebp(path, 498, 280); - } else if (mime.startsWith('video/')) { - return await GenerateVideoThumbnail(path); - } - } - - if (isWebpublic) { - if (['image/svg+xml'].includes(mime)) { - return await convertToPng(path, 2048, 2048); - } - } - - 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 9a897bc6d..000000000 --- a/packages/backend/src/server/index.ts +++ /dev/null @@ -1,168 +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 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'; - -export const serverLogger = new Logger('server', 'gray', false); - -// Init app -const app = new Koa(); -app.proxy = true; - -if (!['production', 'test'].includes(process.env.NODE_ENV || '')) { - // Logger - app.use(koaLogger(str => { - serverLogger.info(str); - })); - - // Delay - if (envOption.slow) { - app.use(slow({ - delay: 3000, - })); - } -} - -// HSTS -// 6months (15552000sec) -if (config.url.startsWith('https') && !config.disableHsts) { - app.use(async (ctx, next) => { - ctx.set('strict-transport-security', 'max-age=15552000; preload'); - await next(); - }); -} - -app.use(mount('/api', apiServer)); -app.use(mount('/files', fileServer)); -app.use(mount('/proxy', proxyServer)); - -// Init router -const router = new Router(); - -// Routing -router.use(activityPub.routes()); -router.use(nodeinfo.routes()); -router.use(wellKnown.routes()); - -router.get('/avatar/@:acct', async ctx => { - const { username, host } = Acct.parse(ctx.params.acct); - const user = await Users.findOne({ - 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; - } -}); - -// Register router -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 (): Promise => new Promise(resolve => { - const server = createServer(); - - initializeStreamingServer(server); - - server.on('error', e => { - switch ((e as NodeJS.ErrnoException).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); - } - }); - - 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 eb1769722..000000000 --- a/packages/backend/src/server/nodeinfo.ts +++ /dev/null @@ -1,129 +0,0 @@ -import Router from '@koa/router'; -import { IsNull, MoreThan } from 'typeorm'; -import config from '@/config/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import { Users, Notes } from '@/models/index.js'; -import { MONTH, DAY } from '@/const.js'; - -const router = new Router(); - -const nodeinfo2_1path = '/nodeinfo/2.1'; -const nodeinfo2_0path = '/nodeinfo/2.0'; - -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 repository = 'https://akkoma.dev/FoundKeyGang/FoundKey'; - -type NodeInfo2Base = { - software: { - name: string; - version: string; - repository?: string; - }; - protocols: string[]; - services: { - inbound: string[]; - outbound: string[]; - }; - openRegistrations: boolean; - usage: { - users: { - total: number; - activeHalfyear: number; - activeMonth: number; - }; - localPosts: number; - localComments: number; - }; - metadata: Record; -}; - -const nodeinfo2 = async (): Promise => { - 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 - 180 * DAY)) } }), - Users.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - MONTH)) } }), - Notes.count({ where: { userHost: IsNull() } }), - ]); - - const proxyAccount = meta.proxyAccountId ? await Users.pack(meta.proxyAccountId).catch(() => null) : null; - - return { - software: { - name: 'foundkey', - version: config.version, - repository, - }, - 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: repository, - feedbackUrl: 'ircs://irc.akkoma.dev/foundkey', - disableRegistration: meta.disableRegistration, - disableLocalTimeline: meta.disableLocalTimeline, - disableGlobalTimeline: meta.disableGlobalTimeline, - emailRequiredForSignup: meta.emailRequiredForSignup, - enableHcaptcha: meta.enableHcaptcha, - enableRecaptcha: meta.enableRecaptcha, - maxNoteTextLength: config.maxNoteTextLength, - enableEmail: meta.enableEmail, - proxyAccountName: proxyAccount?.username ?? null, - themeColor: meta.themeColor || '#86b300', - }, - }; -}; - -/* -Nodeinfo is cacheable for 1 day, the parts that change are the usage statistics -and those should not be time critical. -*/ -const cacheControl = 'public, max-age=86400'; - -router.get(nodeinfo2_1path, async ctx => { - const base = await nodeinfo2(); - - ctx.body = { version: '2.1', ...base }; - ctx.set('Cache-Control', cacheControl); -}); - -router.get(nodeinfo2_0path, async ctx => { - const base = await nodeinfo2(); - - delete base.software.repository; - - ctx.body = { version: '2.0', ...base }; - ctx.set('Cache-Control', cacheControl); -}); - -export default router; diff --git a/packages/backend/src/server/oauth.ts b/packages/backend/src/server/oauth.ts deleted file mode 100644 index 65261ccc9..000000000 --- a/packages/backend/src/server/oauth.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { kinds } from '@/misc/api-permissions.js'; -import config from '@/config/index.js'; - -// Since it cannot change while the server is running, we can serialize it once -// instead of having to serialize it every time it is requested. -export const oauthMeta = JSON.stringify({ - issuer: config.url, - authorization_endpoint: `${config.url}/auth`, - token_endpoint: `${config.apiUrl}/auth/session/oauth`, - scopes_supported: kinds, - response_types_supported: ['code'], - grant_types_supported: ['authorization_code'], - token_endpoint_auth_methods_supported: ['client_secret_basic'], - service_documentation: `${config.url}/api-doc`, - code_challenge_methods_supported: ['S256'], -}); diff --git a/packages/backend/src/server/proxy/index.ts b/packages/backend/src/server/proxy/index.ts deleted file mode 100644 index 0132817b4..000000000 --- a/packages/backend/src/server/proxy/index.ts +++ /dev/null @@ -1,26 +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 18bcd0ace..000000000 --- a/packages/backend/src/server/proxy/proxy-media.ts +++ /dev/null @@ -1,98 +0,0 @@ -import * as fs from 'node:fs'; -import Koa from 'koa'; -import sharp from 'sharp'; -import { IImage, 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 { isMimeImage } from '@/misc/is-mime-image.js'; -import { serverLogger } from '../index.js'; - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -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, 498, 280); - } else if ('preview' in ctx.query && isConvertibleImage) { - image = await convertToWebp(path, 200, 200); - } 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/boot.js b/packages/backend/src/server/web/boot.js index e82de8e14..341346296 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -14,10 +14,10 @@ // ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので (async () => { window.onerror = (e) => { - renderError('SOMETHING_HAPPENED', e); + throw e; }; window.onunhandledrejection = (e) => { - renderError('SOMETHING_HAPPENED_IN_PROMISE', e); + throw e; }; const v = localStorage.getItem('v') || VERSION; diff --git a/packages/backend/src/server/web/error.css b/packages/backend/src/server/web/error.css deleted file mode 100644 index 9f8f90501..000000000 --- a/packages/backend/src/server/web/error.css +++ /dev/null @@ -1,98 +0,0 @@ -* { - font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; -} - -body, -html { - background-color: #222; - color: #dfddcc; - justify-content: center; - margin: auto; - width: 80%; - 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(134, 179, 0), rgb(74, 179, 0)); - line-height: 50px; -} - -.button-big:hover { - background: rgb(153, 204, 0); -} - -.button-small { - background: #444; - line-height: 40px; -} - -.button-small:hover { - background: #555; -} - -.button-label-big { - color: #222; - font-weight: bold; - font-size: 20px; - padding: 12px; -} - -.button-label-small { - color: rgb(153, 204, 0); - font-size: 16px; - padding: 12px; -} - -a { - color: rgb(134, 179, 0); - text-decoration: none; -} - -p, -li { - font-size: 16px; -} - -.dont-worry, -#msg { - font-size: 18px; -} - -.icon-warning { - color: #dec340; - height: 4rem; -} - -h1 { - font-size: 32px; -} - -code { - font-family: Fira, FiraCode, monospace; -} - -details { - background: #333; - margin-bottom: 2rem; - padding: 0.5rem 1rem; - border-radius: 5px; - justify-content: center; - margin: auto; -} - -summary { - cursor: pointer; -} - -summary > * { - display: inline; -} diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts deleted file mode 100644 index a87433bd7..000000000 --- a/packages/backend/src/server/web/feed.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Feed } from 'feed'; -import { In, IsNull } from 'typeorm'; -import config from '@/config/index.js'; -import { 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: 'FoundKey', - 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 3776c24f2..000000000 --- a/packages/backend/src/server/web/index.ts +++ /dev/null @@ -1,515 +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 favicon from 'koa-favicon'; -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 { MINUTE, DAY } from '@/const.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'; - -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, - }, -})); - -// Serve favicon -app.use(favicon(`${_dirname}/../../../assets/favicon.ico`)); - -// Common request handler -app.use(async (ctx, next) => { - // IFrameの中に入れられないようにする - ctx.set('X-Frame-Options', 'DENY'); - await next(); -}); - -// Init router -const router = new Router(); - -//#region static assets - -router.get('/static-assets/(.*)', async ctx => { - await send(ctx as any, ctx.path.replace('/static-assets/', ''), { - root: staticAssets, - maxage: 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 { username, host } = Acct.parse(acct); - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: host ?? IsNull(), - isSuspended: false, - }); - - return user && await packFeed(user); -}; - -// Atom -router.get('/@:user.atom', async ctx => { - const feed = await getFeed(ctx.params.user); - - if (feed) { - ctx.set('Content-Type', 'application/atom+xml; charset=utf-8'); - ctx.body = feed.atom1(); - } else { - ctx.status = 404; - } -}); - -// RSS -router.get('/@:user.rss', async ctx => { - const feed = await getFeed(ctx.params.user); - - if (feed) { - ctx.set('Content-Type', 'application/rss+xml; charset=utf-8'); - ctx.body = feed.rss2(); - } else { - ctx.status = 404; - } -}); - -// JSON -router.get('/@:user.json', async ctx => { - const feed = await getFeed(ctx.params.user); - - if (feed) { - ctx.set('Content-Type', 'application/json; charset=utf-8'); - ctx.body = feed.json1(); - } else { - ctx.status = 404; - } -}); - -//#region SSR (for crawlers) -// User -router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { - const { username, host } = Acct.parse(ctx.params.user); - const user = await Users.findOneBy({ - usernameLower: username.toLowerCase(), - host: host ?? IsNull(), - isSuspended: false, - }); - - if (user != null) { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const meta = await fetchMeta(); - const me = profile.fields - ? profile.fields - .filter(filed => filed.value != null && filed.value.match(/^https?:/)) - .map(field => field.value) - : []; - - await ctx.render('user', { - user, profile, me, - avatarUrl: await Users.getAvatarUrl(user), - sub: ctx.params.sub, - instanceName: meta.name || 'FoundKey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - ctx.set('Cache-Control', 'public, max-age=15'); - } else { - // リモートユーザーなので - // モデレータがAPI経由で参照可能にするために404にはしない - await next(); - } -}); - -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) { - try { - // FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774) - 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 || 'FoundKey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - ctx.set('Cache-Control', 'public, max-age=15'); - - return; - } catch (err) { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') { - // note not visible to user - } else { - throw err; - } - } - } - - 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 || 'FoundKey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - 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: 非publicなclipのハンドリング -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 || 'FoundKey', - 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 || 'FoundKey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - 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 || 'FoundKey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - - ctx.set('Cache-Control', 'public, max-age=15'); - - return; - } - - await next(); -}); -//#endregion - -router.get('/_info_card_', async ctx => { - const meta = await fetchMeta(true); - - ctx.remove('X-Frame-Options'); - - await ctx.render('info-card', { - version: config.version, - host: config.host, - meta, - originalUsersCount: await Users.countBy({ host: IsNull() }), - originalNotesCount: await Notes.countBy({ userHost: IsNull() }), - }); -}); - -router.get('/flush', async ctx => { - await ctx.render('flush'); -}); - -// streamingに非WebSocketリクエストが来た場合にbase htmlをキャシュ付きで返すと、Proxy等でそのパスがキャッシュされておかしくなる -router.get('/streaming', async ctx => { - ctx.status = 503; - ctx.set('Cache-Control', 'private, max-age=0'); -}); - -// Render base html for all requests -router.get('(.*)', async ctx => { - const meta = await fetchMeta(); - await ctx.render('base', { - img: meta.bannerUrl, - title: meta.name || 'FoundKey', - instanceName: meta.name || 'FoundKey', - desc: meta.description, - icon: meta.iconUrl, - themeColor: meta.themeColor, - }); - ctx.set('Cache-Control', 'public, max-age=15'); -}); - -// 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 bc4f66bcc..000000000 --- a/packages/backend/src/server/web/manifest.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "short_name": "FoundKey", - "name": "FoundKey", - "start_url": "/", - "display": "standalone", - "background_color": "#313a42", - "theme_color": "#86b300", - "icons": [ - { - "src": "/static-assets/icons/192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/static-assets/icons/512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "share_target": { - "action": "/share/", - "params": { - "title": "title", - "text": "text", - "url": "url" - } - } -} diff --git a/packages/backend/src/server/web/manifest.ts b/packages/backend/src/server/web/manifest.ts deleted file mode 100644 index de7d43903..000000000 --- a/packages/backend/src/server/web/manifest.ts +++ /dev/null @@ -1,16 +0,0 @@ -import 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): Promise => { - const res = structuredClone(manifest); - - const instance = await fetchMeta(true); - - res.short_name = instance.name || 'FoundKey'; - res.name = instance.name || 'FoundKey'; - 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/url-preview.ts b/packages/backend/src/server/web/url-preview.ts deleted file mode 100644 index 7ff381443..000000000 --- a/packages/backend/src/server/web/url-preview.ts +++ /dev/null @@ -1,65 +0,0 @@ -import 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): Promise => { - 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, - lang: lang ?? 'en-US', - })}`) : await summaly.default(url, { - followRedirects: false, - lang: lang ?? 'en-US', - }); - - logger.succ(`Got preview of ${url}: ${summary.title}`); - - summary.icon = wrap(summary.icon); - summary.thumbnail = wrap(summary.thumbnail); - - // Cache 7days - ctx.set('Cache-Control', 'max-age=604800, immutable'); - - ctx.body = summary; - } catch (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 0904be396..000000000 --- a/packages/backend/src/server/web/views/base.pug +++ /dev/null @@ -1,90 +0,0 @@ -block vars - -block loadClientEntry - - const clientEntry = getClientEntry(); - -doctype html - -// - - - ___ _ _ __ - | __|__ _ _ _ _ __| | |/ /___ _ _ - | _/ _ \ || | ' \/ _` | ' - - - - - - - - - - block content - - script - include ../boot.js 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 486f0ecc4..000000000 --- a/packages/backend/src/server/web/views/channel.pug +++ /dev/null @@ -1,18 +0,0 @@ -extends ./base - -block vars - - const title = channel.name; - - const url = `${config.url}/channels/${channel.id}`; - -block title - = `${title} | ${instanceName}` - -block desc - meta(name='description' content= channel.description) - -block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content= channel.description) - meta(property='og:url' content= url) - meta(property='og:image' content= channel.bannerUrl) 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 4c692bf59..000000000 --- a/packages/backend/src/server/web/views/clip.pug +++ /dev/null @@ -1,31 +0,0 @@ -extends ./base - -block vars - - const user = clip.user; - - const title = clip.name; - - const url = `${config.url}/clips/${clip.id}`; - -block title - = `${title} | ${instanceName}` - -block desc - meta(name='description' content= clip.description) - -block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content= clip.description) - meta(property='og:url' content= url) - meta(property='og:image' content= avatarUrl) - -block meta - if profile.noCrawle - meta(name='robots' content='noindex') - - meta(name='misskey:user-username' content=user.username) - meta(name='misskey:user-id' content=user.id) - meta(name='misskey:clip-id' content=clip.id) - - // 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 d616e149b..000000000 --- a/packages/backend/src/server/web/views/flush.pug +++ /dev/null @@ -1,53 +0,0 @@ -doctype html - -html - head - meta(charset='utf-8') - meta(name='application-name' content='FoundKey') - title Flushing FoundKey - style - include ../error.css - #msg - script. - const msg = document.getElementById('msg'); - const successText = `\nSuccess Flush! Back to FoundKey\nSuccess, please reopen FoundKey.`; - - message('Start flushing.'); - - (async function() { - try { - localStorage.clear(); - message('localStorage cleared.'); - - const idbPromises = ['FoundKeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => { - const delidb = indexedDB.deleteDatabase(name); - delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`)); - delidb.onerror = e => rej(e) - })); - - await Promise.all(idbPromises); - - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage('clear'); - await navigator.serviceWorker.getRegistrations() - .then(registrations => { - return Promise.all(registrations.map(registration => registration.unregister())); - }) - .catch(e => { throw Error(e) }); - } - - message(successText); - } catch (e) { - message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`); - message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`) - - console.error(e); - setTimeout(() => { - location = '/'; - }, 10000) - } - })(); - - function message(text) { - msg.insertAdjacentHTML('beforeend', `

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

`) - } diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug deleted file mode 100644 index ca0663a48..000000000 --- a/packages/backend/src/server/web/views/gallery-post.pug +++ /dev/null @@ -1,33 +0,0 @@ -extends ./base - -block vars - - const user = post.user; - - const title = post.title; - - const url = `${config.url}/gallery/${post.id}`; - -block title - = `${title} | ${instanceName}` - -block desc - meta(name='description' content= post.description) - -block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content= post.description) - meta(property='og:url' content= url) - meta(property='og:image' content= post.files[0].thumbnailUrl) - -block meta - if user.host || profile.noCrawle - meta(name='robots' content='noindex') - - meta(name='misskey:user-username' content=user.username) - meta(name='misskey:user-id' content=user.id) - - // 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 c840e576b..000000000 --- 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='FoundKey') - 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 65696ea13..000000000 --- a/packages/backend/src/server/web/views/note.pug +++ /dev/null @@ -1,42 +0,0 @@ -extends ./base - -block vars - - const user = note.user; - - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - - const url = `${config.url}/notes/${note.id}`; - - const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null; - -block title - = `${title} | ${instanceName}` - -block desc - meta(name='description' content= summary) - -block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content= summary) - meta(property='og:url' content= url) - meta(property='og:image' content= avatarUrl) - -block meta - 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 4219e76a5..000000000 --- a/packages/backend/src/server/web/views/page.pug +++ /dev/null @@ -1,31 +0,0 @@ -extends ./base - -block vars - - const user = page.user; - - const title = page.title; - - const url = `${config.url}/@${user.username}/${page.name}`; - -block title - = `${title} | ${instanceName}` - -block desc - meta(name='description' content= page.summary) - -block og - meta(property='og:type' content='article') - meta(property='og:title' content= title) - meta(property='og:description' content= page.summary) - meta(property='og:url' content= url) - meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl) - -block meta - if profile.noCrawle - meta(name='robots' content='noindex') - - meta(name='misskey:user-username' content=user.username) - meta(name='misskey:user-id' content=user.id) - meta(name='misskey:page-id' content=page.id) - - // 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 119993fdb..000000000 --- a/packages/backend/src/server/web/views/user.pug +++ /dev/null @@ -1,39 +0,0 @@ -extends ./base - -block vars - - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - - const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`; - -block title - = `${title} | ${instanceName}` - -block desc - meta(name='description' content= profile.description) - -block og - meta(property='og:type' content='blog') - meta(property='og:title' content= title) - meta(property='og:description' content= profile.description) - meta(property='og:url' content= url) - meta(property='og:image' content= avatarUrl) - -block meta - if user.host || profile.noCrawle - meta(name='robots' content='noindex') - - meta(name='misskey:user-username' content=user.username) - meta(name='misskey:user-id' content=user.id) - - 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 527aa99bc..000000000 --- a/packages/backend/src/server/well-known.ts +++ /dev/null @@ -1,163 +0,0 @@ -import Router from '@koa/router'; -import { FindOptionsWhere, IsNull } from 'typeorm'; - -import config from '@/config/index.js'; -import * as Acct from '@/misc/acct.js'; -import { escapeAttribute, escapeValue } from '@/prelude/xml.js'; -import { Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import { links } from './nodeinfo.js'; -import { oauthMeta } from './oauth.js'; - -// 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}`, - }], - }; -}); - -router.get('/.well-known/nodeinfo', async ctx => { - ctx.body = { links }; -}); - -function oauth(ctx) { - ctx.body = oauthMeta; - ctx.type = 'application/json'; - ctx.set('Cache-Control', 'max-age=31536000, immutable'); -} - -// implements RFC 8414 -router.get('/.well-known/oauth-authorization-server', oauth); -// From the above RFC: -//> The identifiers "/.well-known/openid-configuration" [...] contain strings -//> referring to the OpenID Connect family of specifications [...]. Despite the reuse -//> of these identifiers that appear to be OpenID specific, their usage in this -//> specification is actually referring to general OAuth 2.0 features that are not -//> specific to OpenID Connect. -router.get('/.well-known/openid-configuration', oauth); - -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 e490eaf11..000000000 --- a/packages/backend/src/services/add-note-to-antenna.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Antenna } from '@/models/entities/antenna.js'; -import { 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 { User } from '@/models/entities/user.js'; -import { SECOND } from '@/const.js'; - -export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise { - // If it's set to not notify the user, or if it's the user's own post, read it. - const read = !antenna.notify || (antenna.userId === noteUser.id); - - AntennaNotes.insert({ - id: genId(), - antennaId: antenna.id, - noteId: note.id, - 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; - } - - // Notify if not read after 2 seconds - setTimeout(async () => { - const unread = await AntennaNotes.countBy({ antennaId: antenna.id, read: false }); - if (unread) { - publishMainStream(antenna.userId, 'unreadAntenna', antenna); - } - }, 2 * SECOND); - } -} diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts deleted file mode 100644 index 1550e3022..000000000 --- a/packages/backend/src/services/blocking/create.ts +++ /dev/null @@ -1,143 +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, webhookDeliver } from '@/queue/index.js'; -import renderReject from '@/remote/activitypub/renderer/reject.js'; -import { Blocking } from '@/models/entities/blocking.js'; -import { 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 { getActiveWebhooks } from '@/misc/webhook-cache.js'; - -export default async function(blocker: User, blockee: User): Promise { - 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) && blocker.federateBlocks) { - const content = renderActivity(renderBlock(blocking)); - deliver(blocker, content, blockee.inbox); - } -} - -async function cancelRequest(follower: User, followee: User): Promise { - 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, - }); - } - }); - } - - // Send Undo Follow if followee is remote - if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { - const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); - deliver(follower, content, followee.inbox); - } - - // Send Reject if follower is remote - 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): Promise { - 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, - }); - } - }); - } - - // Send Undo Follow if follower is remote - 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): Promise { - 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 82f92f05a..000000000 --- a/packages/backend/src/services/blocking/delete.ts +++ /dev/null @@ -1,34 +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 { CacheableUser } from '@/models/entities/user.js'; -import { Blockings, Users } from '@/models/index.js'; -import Logger from '../logger.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 b232548f2..000000000 --- a/packages/backend/src/services/chart/charts/active-users.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WEEK, MONTH, YEAR } from '@/const.js'; -import { User } from '@/models/entities/user.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/active-users.js'; - -/** - * Chart on Active Users - */ -// eslint-disable-next-line import/no-default-export -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 e9e42ade7..000000000 --- a/packages/backend/src/services/chart/charts/ap-request.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/ap-request.js'; - -/** - * Chart about ActivityPub requests - */ -// eslint-disable-next-line import/no-default-export -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 5e6e901b7..000000000 --- a/packages/backend/src/services/chart/charts/drive.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { DriveFile } from '@/models/entities/drive-file.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/drive.js'; - -/** - * ドライブに関するチャート - */ -// eslint-disable-next-line import/no-default-export -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 5767b76f8..000000000 --- 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 3a9f3dacf..000000000 --- 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 4bf5bb729..000000000 --- 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 a8466b0b4..000000000 --- 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 4d0403904..000000000 --- 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 06962120e..000000000 --- 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 9387dbfb2..000000000 --- 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 6111640ea..000000000 --- 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 4118daa47..000000000 --- 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 c1fa17445..000000000 --- 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 5e1a6c7b3..000000000 --- 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 66b6e8e86..000000000 --- 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 a3bdcb367..000000000 --- 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 b2cfb71b0..000000000 --- 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 7cba21e16..000000000 --- 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 c0b83094a..000000000 --- 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 922d8609a..000000000 --- a/packages/backend/src/services/chart/charts/federation.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { MONTH } from '@/const.js'; -import { Followings, Instances } from '@/models/index.js'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/federation.js'; - -/** - * フェデレーションに関するチャート - */ -// eslint-disable-next-line import/no-default-export -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() - MONTH) }) - .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() - MONTH) }) - .getRawOne() - .then(x => parseInt(x.count, 10)), - ]); - - return { - sub, - pub, - pubsub, - subActive, - 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 cc83f4b9f..000000000 --- a/packages/backend/src/services/chart/charts/hashtag.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/hashtag.js'; - -/** - * ハッシュタグに関するチャート - */ -// eslint-disable-next-line import/no-default-export -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 90538fb8b..000000000 --- a/packages/backend/src/services/chart/charts/instance.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { DriveFiles, Followings, Users, Notes } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { Note } from '@/models/entities/note.js'; -import { toPuny } from '@/misc/convert-host.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/instance.js'; - -/** - * インスタンスごとのチャート - */ -// eslint-disable-next-line import/no-default-export -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 64a1cdfe0..000000000 --- a/packages/backend/src/services/chart/charts/notes.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Not, IsNull } from 'typeorm'; -import { Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/notes.js'; - -/** - * ノートに関するチャート - */ -// eslint-disable-next-line import/no-default-export -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 db566c81c..000000000 --- a/packages/backend/src/services/chart/charts/per-user-drive.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { DriveFiles } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/per-user-drive.js'; - -/** - * ユーザーごとのドライブに関するチャート - */ -// eslint-disable-next-line import/no-default-export -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 3d77a4f64..000000000 --- a/packages/backend/src/services/chart/charts/per-user-following.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Not, IsNull } from 'typeorm'; -import { Followings, Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/per-user-following.js'; - -/** - * ユーザーごとのフォローに関するチャート - */ -// eslint-disable-next-line import/no-default-export -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 e67c91693..000000000 --- a/packages/backend/src/services/chart/charts/per-user-notes.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/per-user-notes.js'; - -/** - * ユーザーごとのノートに関するチャート - */ -// eslint-disable-next-line import/no-default-export -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 27345b99d..000000000 --- a/packages/backend/src/services/chart/charts/per-user-reactions.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { Users } from '@/models/index.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/per-user-reactions.js'; - -/** - * ユーザーごとのリアクションに関するチャート - */ -// eslint-disable-next-line import/no-default-export -export default class PerUserReactionsChart extends Chart { - constructor() { - super(name, schema, true); - } - - protected async tickMajor(): 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 d01c9fcbd..000000000 --- a/packages/backend/src/services/chart/charts/test-grouped.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/test-grouped.js'; - -/** - * For testing - */ -// eslint-disable-next-line import/no-default-export -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 88b5a715c..000000000 --- a/packages/backend/src/services/chart/charts/test-intersection.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/test-intersection.js'; - -/** - * For testing - */ -// eslint-disable-next-line import/no-default-export -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 d714f1d40..000000000 --- a/packages/backend/src/services/chart/charts/test-unique.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/test-unique.js'; - -/** - * For testing - */ -// eslint-disable-next-line import/no-default-export -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 adb2b18c8..000000000 --- a/packages/backend/src/services/chart/charts/test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/test.js'; - -/** - * For testing - */ -// eslint-disable-next-line import/no-default-export -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 4888571d2..000000000 --- a/packages/backend/src/services/chart/charts/users.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Not, IsNull } from 'typeorm'; -import { Users } from '@/models/index.js'; -import { User } from '@/models/entities/user.js'; -import Chart, { KVs } from '../core.js'; -import { name, schema } from './entities/users.js'; - -/** - * ユーザー数に関するチャート - */ -// eslint-disable-next-line import/no-default-export -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 cfc02d11a..000000000 --- a/packages/backend/src/services/chart/core.ts +++ /dev/null @@ -1,675 +0,0 @@ -/** - * chart engine - * - * Tests located in test/chart - */ - -import * as nestedProperty from 'nested-property'; -import { EntitySchema, Repository, LessThan, Between } from 'typeorm'; -import { isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time.js'; -import { unique } from '@/prelude/array.js'; -import { getChartInsertLock } from '@/misc/app-lock.js'; -import { db } from '@/db/postgre.js'; -import Logger from '../logger.js'; - -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; - - range?: 'big' | 'small' | 'medium'; - - // Whether or not to accumulate with previous values. - 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}`]: S[K]['uniqueIncrement'] extends true ? string[] : never; -}; - -type RawRecord = { - id: number; - - /** - * aggregation group - */ - group?: string | null; - - /** - * Unix epoch timestamp (seconds) of aggregation - */ - date: number; -} & TempColumnsForUnique & Columns; - -const camelToSnake = (str: string): string => { - return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase()); -}; - -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>>; -} - -/** - * Parent class of all charts that governs how they run in general. - */ -// eslint-disable-next-line import/no-default-export -export default abstract class Chart { - public schema: T; - - private name: string; - private buffer: { - diff: Commit; - group: string | null; - }[] = []; - - /* - * The following would be nice but it gives a type error when used with 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; }>; - - /** - * Computation to run once a day. Intended to fix discrepancies e.g. due to cascaded deletes or other changes that were missed. - */ - protected abstract tickMajor(group: string | null): Promise>>; - - /** - * A smaller computation that should be run once per lowest time interval. - */ - protected abstract tickMinor(group: string | null): Promise>>; - - private static convertSchemaToColumnDefinitions(schema: Schema): Record { - const columns = {} as Record; - 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 } : {}, - order: { - date: -1, - }, - }).then(x => x ?? null) as Promise | null>; - } - - /** - * Search the database for the current (=current Hour or Day) log and return it if available, otherwise create and return it. - */ - private async claimCurrentLog(group: string | null, span: 'hour' | 'day'): Promise> { - const [y, m, d, h] = Chart.getCurrentDate(); - - const current = new Date(Date.UTC(...( - 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; - - // current hour or day log entry - const currentLog = await repository.findOneBy({ - date: Chart.dateToTimestamp(current), - ...(group ? { group } : {}), - }) as RawRecord | undefined; - - // If logs are available, return them and exit. - if (currentLog != null) { - return currentLog; - } - - let log: RawRecord; - let data: KVs; - - // If this is the first chart update since the start of the aggregation period, - // use the most recent log entry. - // - // For example, if the aggregation period is "day", if nothing happened yesterday - // to change the chart, the log entry is not created in the first place. So "most - // recent" is used instead of "yesterdays" because there might be missing log - // entries. - const latest = await this.getLatestLog(group, span); - - if (latest != null) { - // Create empty log data - data = this.getNewLog(this.convertRawRecord(latest)); - } else { - // if the log did not exist. - // (e.g., when updating a chart for the first time after building a FoundKey instance) - - // Create initial log data - 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 { - // check once more now that we're holding the lock - const currentLog = await repository.findOneBy({ - date, - ...(group ? { group } : {}), - }) as RawRecord | undefined; - - // if log entries are available now, return them and exit - 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; - } - - // insert new entries - log = await repository.insert({ - date, - ...(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)) 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: handling of previous time logs in buffer - - // For example, suppose that a save is performed every 20 minutes, and the last - // save was performed at 01:50. If a new log is added to the buffer at 01:55, the - // next save will take place at 02:10, and if the new log is added to the buffer - // at 01:55, then If a new log is added to the buffer at 01:55, the log is - // treated as a 02:00~ log, even though it should be saved as a 01:00~ log. The - // implementation to work around this is pending, as it would be complicated. - - 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) { // unique increment - const tempColumnName = uniqueTempColumnPrefix + k.replaceAll('.', columnDot) as keyof TempColumnsForUnique; - // TODO: SQL escape for item - 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) { - 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: what to do if the column specified for intersection is an intersection itself - 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; - } - } - - // update log - 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: do not delete anything new in the buffer since this round of processing began - this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group)); - }; - - const groups = unique(this.buffer.map(log => log.group)); - - await Promise.all( - groups.map(group => - Promise.all([ - this.claimCurrentLog(group, 'hour'), - this.claimCurrentLog(group, 'day'), - ]).then(([logHour, logDay]) => - update(logHour, logDay)))); - } - - 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 = new Date(Date.UTC(...Chart.getCurrentDate())); - - // more than 1 day and less than 3 days - 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 = new Date(Date.UTC(y, m, d, h, _m, _s, _ms)); - - const gt = - span === 'day' ? subtractTime(cursor ? new Date(Date.UTC(y2, m2, d2, 0)) : new Date(Date.UTC(y, m, d, 0)), amount - 1, 'day') : - span === 'hour' ? subtractTime(cursor ? new Date(Date.UTC(y2, m2, d2, h2)) : new Date(Date.UTC(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; - - // gathering logs - let logs = await repository.find({ - where: { - date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt)), - ...(group ? { group } : {}), - }, - order: { - date: -1, - }, - }) as RawRecord[]; - - // If there is no log entry in the requested range - if (logs.length === 0) { - // Use the most recent logs instead. - // (At least 1 log entry is needed to fill the gap.) - const recentLog = await repository.findOne({ - where: group ? { group } : {}, - order: { - date: -1, - }, - }) as RawRecord | undefined; - - if (recentLog) { - logs = [recentLog]; - } - - // If there is no log located at the earliest point in the requested range - } else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) { - // Bring the most recent log as of the earliest point in the requested range and append it to the end. - // (Due to inability to fill gaps) - const outdatedLog = await repository.findOne({ - where: { - date: LessThan(Chart.dateToTimestamp(gt)), - ...(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(new Date(Date.UTC(y, m, d, h)), i, 'hour') : - span === 'day' ? subtractTime(new Date(Date.UTC(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 { - // fill in gaps - 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; - - // Turn array of objects into object of arrays. - 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 a9eeabd63..000000000 --- a/packages/backend/src/services/chart/entities.ts +++ /dev/null @@ -1,39 +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 0fc2e7ff2..000000000 --- a/packages/backend/src/services/chart/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { beforeShutdown } from '@/misc/before-shutdown.js'; - -import { MINUTE } from '@/const.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, -]; - -// Write memory information to DB every 20 minutes -setInterval(() => { - for (const chart of charts) { - chart.save(); - } -}, 20 * MINUTE); - -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 30499ce4c..000000000 --- a/packages/backend/src/services/create-notification.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { publishMainStream } from '@/services/stream.js'; -import { pushNotification } from '@/services/push-notification.js'; -import { Notifications, Mutings, UserProfiles } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { User } from '@/models/entities/user.js'; -import { Notification } from '@/models/entities/notification.js'; - -export async function createNotification( - notifieeId: User['id'], - type: Notification['type'], - data: Partial, -): Promise { - if (data.notifierId && (notifieeId === data.notifierId)) { - return null; - } - - const profile = await UserProfiles.findOneBy({ userId: notifieeId }); - - const isMuted = profile?.mutingNotificationTypes.includes(type); - - // Create notification - const notification = await Notifications.insert({ - id: genId(), - createdAt: new Date(), - notifieeId, - type, - // If the other party seems to have muted this notification, pre-read it. - 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); - - // If the notification (created this time) has not been read after 2 seconds, issue a "You have unread notifications" event. - setTimeout(async () => { - const fresh = await Notifications.findOneBy({ id: notification.id }); - if (fresh == null) return; // It may have already been deleted. - if (fresh.isRead) return; - - //#region However, if the notification comes from a muted user, ignore it. - 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); - }, 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 8e39b00fb..000000000 --- a/packages/backend/src/services/create-system-user.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { v4 as uuid } from 'uuid'; -import { IsNull } from 'typeorm'; -import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; -import { hashPassword } from '@/misc/password.js'; -import { User } from '@/models/entities/user.js'; -import { UserProfile } from '@/models/entities/user-profile.js'; -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 generateNativeUserToken from '@/server/api/common/generate-native-user-token.js'; - -export async function createSystemUser(username: string): Promise { - const password = await hashPassword(uuid()); - - // Generate secret - const secret = generateNativeUserToken(); - - const keyPair = await genRsaKeyPair(4096); - - let account!: User; - - // Start transaction - await db.transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.countBy(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, - 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, - }); - - 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 9c8d4c277..000000000 --- a/packages/backend/src/services/delete-account.ts +++ /dev/null @@ -1,27 +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 { - await Users.update(user.id, { - isDeleted: true, - }); - - if (Users.isLocalUser(user)) { - // Terminate streaming - publishUserEvent(user.id, 'terminate', {}); - } - - // Send Delete activity before physical deletion - await doPostSuspend(user).catch(() => {}); - - createDeleteAccountJob(user, { - // Deleting remote users is specified as SOFT, because if they are physically deleted - // from the DB completely, they may be reassociated and their accounts may be reinstated. - soft: Users.isLocalUser(user), - }); -} 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 81da49df8..000000000 --- a/packages/backend/src/services/drive/add-file.ts +++ /dev/null @@ -1,504 +0,0 @@ -import * as fs from 'node:fs'; - -import { v4 as uuid } from 'uuid'; -import S3 from 'aws-sdk/clients/s3.js'; -import { IsNull } from 'typeorm'; -import sharp from 'sharp'; - -import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; -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, UserProfiles } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { 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 { DriveFolder } from '@/models/entities/drive-folder.js'; -import { deleteFile } from './delete-file.js'; -import { GenerateVideoThumbnail } from './generate-video-thumbnail.js'; -import { driveLogger } from './logger.js'; -import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js'; -import { InternalStorage } from './internal-storage.js'; -import { getS3 } from './s3.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/vnd.mozilla.apng') ext = '.apng'; - } - - // 拡張子からContent-Typeを設定してそうな挙動を示すオブジェクトストレージ (upcloud?) も存在するので、 - // 許可されているファイル形式でしか拡張子をつけない - 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): Promise<{ - webpublic: IImage | null; - thumbnail: IImage | null; -}> { - 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'].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', 'image/webp'].includes(type)) { - webpublic = await convertSharpToJpeg(img, 2048, 2048); - } else if (['image/png'].includes(type)) { - webpublic = await convertSharpToPng(img, 2048, 2048); - } else if (['image/svg+xml'].includes(type)) { - webpublic = await convertSharpToPng(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'].includes(type)) { - thumbnail = await convertSharpToWebp(img, 498, 280); - } 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): Promise { - const type = (_type === 'image/apng') - ? 'image/png' - : (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): Promise { - const q = DriveFiles.createQueryBuilder('file') - .where('file.userId = :userId', { userId: user.id }) - .andWhere('NOT file.isLink'); - - 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'] } | 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; -}; - -/** - * 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, -}: AddFileArgs): Promise { - const info = await getFileInfo(path); - logger.info(`${JSON.stringify(info)}`); - - // detect name - const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); - - if (user && !force) { - // Check if there is a file with the same hash - const much = await DriveFiles.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.id); - - const instance = await fetchMeta(); - const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); - - logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); - - // If usage limit exceeded - if (usage + info.size > driveCapacity) { - if (Users.isLocalUser(user)) { - throw new Error('no-free-space'); - } else { - // delete oldest file (excluding banner and avatar) - deleteOldFile(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser); - } - } - } - //#endregion - - const fetchFolder = async (): Promise => { - 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?.id ?? null; - file.comment = comment; - file.properties = properties; - file.blurhash = info.blurhash || null; - file.isLink = isLink; - file.isSensitive = user - ? Users.isLocalUser(user) && profile!.alwaysMarkNsfw - ? true - : sensitive ?? false - : false; - - if (url !== null) { - file.src = url; - - if (isLink) { - file.url = url; - // ローカルプロキシ用 - file.accessKey = uuid(); - file.thumbnailAccessKey = 'thumbnail-' + uuid(); - file.webpublicAccessKey = 'webpublic-' + uuid(); - } - } - - if (uri !== null) { - file.uri = uri; - } - - if (isLink) { - try { - file.size = 0; - file.md5 = info.md5; - file.name = detectedName; - file.type = info.type.mime; - file.storedInternal = false; - - file = await DriveFiles.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 2fe8993a8..000000000 --- a/packages/backend/src/services/drive/delete-file.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { v4 as uuid } from 'uuid'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles } 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 { InternalStorage } from './internal-storage.js'; -import { getS3 } from './s3.js'; - -export async function deleteFile(file: DriveFile, isExpired = false): Promise { - 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): Promise { - 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): Promise { - // Turn into a direct link after expiring a remote file. - if (isExpired && file.userHost != null && file.uri != null) { - const id = uuid(); - DriveFiles.update(file.id, { - isLink: true, - url: file.uri, - thumbnailUrl: null, - webpublicUrl: null, - storedInternal: false, - accessKey: id, - thumbnailAccessKey: 'thumbnail-' + id, - webpublicAccessKey: 'webpublic-' + id, - }); - } else { - DriveFiles.delete(file.id); - } - - // update statistics - driveChart.update(file, false); - perUserDriveChart.update(file, false); - if (file.userHost != null) { - instanceChart.updateDrive(file, false); - } -} - -export async function deleteObjectStorageFile(key: string): Promise { - 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 116ca7ff6..000000000 --- a/packages/backend/src/services/drive/generate-video-thumbnail.ts +++ /dev/null @@ -1,28 +0,0 @@ -import FFmpeg from 'fluent-ffmpeg'; -import { createTempDir } from '@/misc/create-temp.js'; -import { IImage, convertToJpeg } from './image-processor.js'; - -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%'], - }); - }); - - // JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる) - return await convertToJpeg(`${dir}/out.png`, 498, 280); - } 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 a85e0fbde..000000000 --- a/packages/backend/src/services/drive/image-processor.ts +++ /dev/null @@ -1,87 +0,0 @@ -import sharp from 'sharp'; - -export type IImage = { - data: Buffer; - ext: string | null; - type: string; -}; - -/** - * Convert to JPEG - * with resize, remove metadata, resolve orientation, stop animation - */ -export async function convertToJpeg(path: string, width: number, height: number): Promise { - return convertSharpToJpeg(sharp(path), width, height); -} - -export async function convertSharpToJpeg(sharp: sharp.Sharp, width: number, height: number): Promise { - const data = await sharp - .resize(width, height, { - fit: 'inside', - withoutEnlargement: true, - }) - .rotate() - .jpeg({ - quality: 85, - progressive: true, - }) - .toBuffer(); - - return { - data, - ext: 'jpg', - type: 'image/jpeg', - }; -} - -/** - * Convert to WebP - * with resize, remove metadata, resolve orientation, stop animation - */ -export async function convertToWebp(path: string, width: number, height: number, quality = 85): Promise { - return convertSharpToWebp(sharp(path), width, height, quality); -} - -export async function convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality = 85): Promise { - const data = await sharp - .resize(width, height, { - fit: 'inside', - withoutEnlargement: true, - }) - .rotate() - .webp({ - quality, - }) - .toBuffer(); - - return { - data, - ext: 'webp', - type: 'image/webp', - }; -} - -/** - * Convert to PNG - * with resize, remove metadata, resolve orientation, stop animation - */ -export async function convertToPng(path: string, width: number, height: number): Promise { - return convertSharpToPng(sharp(path), width, height); -} - -export async function convertSharpToPng(sharp: sharp.Sharp, width: number, height: number): Promise { - const data = await sharp - .resize(width, height, { - fit: 'inside', - withoutEnlargement: true, - }) - .rotate() - .png() - .toBuffer(); - - return { - data, - ext: 'png', - type: 'image/png', - }; -} 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 ee067844f..000000000 --- a/packages/backend/src/services/drive/internal-storage.ts +++ /dev/null @@ -1,36 +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 = config.internalStoragePath || Path.resolve(_dirname, '../../../../../files'); - - public static resolvePath = (key: string): string => Path.resolve(InternalStorage.path, key); - - public static read(key: string): fs.ReadStream { - return fs.createReadStream(InternalStorage.resolvePath(key)); - } - - public static saveFromPath(key: string, srcPath: string): string { - fs.mkdirSync(InternalStorage.path, { recursive: true }); - const target = InternalStorage.resolvePath(key); - fs.copyFileSync(srcPath, target); - fs.chmodSync(target, 0o644); - return `${config.url}/files/${key}`; - } - - public static saveFromBuffer(key: string, data: Buffer): string { - fs.mkdirSync(InternalStorage.path, { recursive: true }); - fs.writeFileSync(InternalStorage.resolvePath(key), data); - return `${config.url}/files/${key}`; - } - - public static del(key: string): void { - 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 917a8317e..000000000 --- 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 f698d9869..000000000 --- a/packages/backend/src/services/drive/s3.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { URL } from 'node:url'; -import S3 from 'aws-sdk/clients/s3.js'; -import { Meta } from '@/models/entities/meta.js'; -import { getAgentByUrl } from '@/misc/fetch.js'; - -export function getS3(meta: Meta): S3 { - 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 a815d8374..000000000 --- a/packages/backend/src/services/drive/upload-from-url.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { URL } from 'node:url'; -import { User } from '@/models/entities/user.js'; -import { createTemp } from '@/misc/create-temp.js'; -import { downloadUrl } from '@/misc/download-url.js'; -import { DriveFolder } from '@/models/entities/drive-folder.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { DriveFiles } from '@/models/index.js'; -import { addFile } from './add-file.js'; -import { driveLogger } from './logger.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; -}; - -export async function uploadFromUrl({ - url, - user, - folderId = null, - uri = null, - sensitive = false, - force = false, - isLink = false, - comment = null, -}: Args): Promise { - let name = new URL(url).pathname.split('/').pop() || null; - if (name == null || !DriveFiles.validateFileName(name)) { - name = 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, - // If the comment is same as the name, skip comment - // (image.name is passed in when receiving attachment) - comment: name === comment ? null : comment, - folderId, - force, - isLink, - url, - uri, - sensitive, - }); - logger.succ(`Got: ${driveFile.id}`); - return driveFile; - } catch (e) { - logger.error(`Failed to create drive file: ${e}`, { - url, - 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 ad312c3c8..000000000 --- a/packages/backend/src/services/fetch-instance-metadata.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { URL } from 'node:url'; -import { DOMWindow, JSDOM } from 'jsdom'; -import fetch from 'node-fetch'; -import tinycolor from 'tinycolor2'; -import { DAY } from '@/const.js'; -import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js'; -import { Instance } from '@/models/entities/instance.js'; -import { Instances } from '@/models/index.js'; -import { getFetchInstanceMetadataLock } from '@/misc/app-lock.js'; -import Logger from './logger.js'; - -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 && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < DAY)) { - 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; - themeColor?: 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) { - const message = e instanceof Error ? e.message : e; - logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${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 && 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 && info.metadata) { - if (info.metadata.nodeName || info.metadata.name) { - return info.metadata.nodeName || info.metadata.name; - } - } - - if (doc) { - const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); - - if (og) { - return og; - } - } - - if (manifest) { - return manifest.name || manifest.short_name; - } - - return null; -} - -async function getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { - if (info && info.metadata) { - if (info.metadata.nodeDescription || info.metadata.description) { - return info.metadata.nodeDescription || info.metadata.description; - } - } - - if (doc) { - const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); - if (meta) { - return meta; - } - - const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); - if (og) { - return og; - } - } - - if (manifest) { - return manifest.name || manifest.short_name; - } - - return null; -} diff --git a/packages/backend/src/services/following/create.ts b/packages/backend/src/services/following/create.ts deleted file mode 100644 index 7ad85dd99..000000000 --- a/packages/backend/src/services/following/create.ts +++ /dev/null @@ -1,200 +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, webhookDeliver } from '@/queue/index.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { 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 { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { Packed } from '@/misc/schema.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; -import Logger from '../logger.js'; -import { createNotification } from '../create-notification.js'; -import { createFollowRequest } from './requests/create.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 requested = await FollowRequests.countBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (requested) { - await FollowRequests.delete({ - followeeId: followee.id, - followerId: follower.id, - }); - - // 通知を作成 - 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 != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); - if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); - } - - const followeeProfile = await UserProfiles.findOneByOrFail({ userId: followee.id }); - - // フォロー対象が鍵アカウントである or - // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or - // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである - // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく - if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) { - let autoAccept = false; - - // Even for locked accounts, if they are already following, go through immediately. - const following = await Followings.countBy({ - followerId: follower.id, - followeeId: followee.id, - }); - if (following) { - autoAccept = true; - } - - // handle automatic approval for users that follow you, if enabled - if (!autoAccept && (Users.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { - const followed = await Followings.countBy({ - 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 9a29d1921..000000000 --- a/packages/backend/src/services/following/delete.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { db } from '@/db/postgre.js'; -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 { Followings, Users, Instances } from '@/models/index.js'; -import { instanceChart, perUserFollowingChart } from '@/services/chart/index.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; -import { User } from '@/models/entities/user.js'; -import Logger from '../logger.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.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('unfollow requested, but did not follow'); - return; - } - - await Promise.all([ - Followings.delete(following.id), - // delete notifications that the ex-follower can now no longer see - db.query('DELETE FROM "notification" WHERE "noteId" IS NOT NULL AND "notifieeId" = $1 AND NOT note_visible("noteId", "notifieeId")', [follower.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 ec98da635..000000000 --- a/packages/backend/src/services/following/reject.ts +++ /dev/null @@ -1,122 +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 { ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { Users, FollowRequests, Followings } from '@/models/index.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; -import { decrementFollowing } from './delete.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): Promise { - 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): Promise { - 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): Promise { - await removeFollowRequest(actor, follower); - await removeFollow(actor, follower); - publishUnfollow(actor, follower); -} - -/** - * Remove follow request record - */ -async function removeFollowRequest(followee: Both, follower: Both): Promise { - 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): Promise { - 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): Promise { - 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): Promise { - 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 ccc608c28..000000000 --- a/packages/backend/src/services/following/requests/accept-all.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { FollowRequests, Users } from '@/models/index.js'; -import { acceptFollowRequest } from './accept.js'; - -/** - * Approve all follow requests addressed to the specified user. - * @param user The user whom to accept all follow requests to - */ -export async function acceptAllFollowRequests(user: User): Promise { - const requests = await FollowRequests.findBy({ - followeeId: user.id, - }); - - for (const request of requests) { - const follower = await Users.findOneByOrFail({ id: request.followerId }); - acceptFollowRequest(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 231075100..000000000 --- a/packages/backend/src/services/following/requests/accept.ts +++ /dev/null @@ -1,36 +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 { User } from '@/models/entities/user.js'; -import { FollowRequests, Users } from '@/models/index.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { insertFollowingDoc } from '../create.js'; - -/** - * Accept a follow request from user `followee` to follow `follower`. - * @param followee User who is being followed - * @param follower User making the follow request - */ -export async function acceptFollowRequest(followee: User, follower: User): Promise { - 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 4ceac19cb..000000000 --- a/packages/backend/src/services/following/requests/cancel.ts +++ /dev/null @@ -1,41 +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 { User } from '@/models/entities/user.js'; -import { Users, FollowRequests } from '@/models/index.js'; - -/** - * Cancel a follow request from `follower` to `followee`. - * @param followee User that was going to be followed - * @param follower User who is making the follow request - */ -export async function cancelFollowRequest(followee: User, follower: User): Promise { - if (Users.isRemoteUser(followee)) { - const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); - - if (Users.isLocalUser(follower)) { - deliver(follower, content, followee.inbox); - } - } - - const requested = await FollowRequests.countBy({ - followeeId: followee.id, - followerId: follower.id, - }); - - if (!requested) { - 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 446f2de40..000000000 --- a/packages/backend/src/services/following/requests/create.ts +++ /dev/null @@ -1,69 +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 { User } from '@/models/entities/user.js'; -import { Blockings, FollowRequests, Users } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { createNotification } from '@/services/create-notification.js'; - -/** - * Make a follow request from `follower` to `followee`. - * @param follower User making the follow request - * @param followee User to make the follow request to - * @param requestId Follow request ID - */ -export async function createFollowRequest(follower: User, followee: User, requestId?: string): Promise { - if (follower.id === followee.id) return; - - // check blocking - const [blocking, blocked] = await Promise.all([ - Blockings.countBy({ - blockerId: follower.id, - blockeeId: followee.id, - }), - Blockings.countBy({ - 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 89616813a..000000000 --- a/packages/backend/src/services/i/pin.ts +++ /dev/null @@ -1,98 +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 { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { Notes, UserNotePinings, Users } from '@/models/index.js'; -import { 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'; - -/** - * Pin a given post to a user profile. - * @param user the user to pin the note to - * @param noteId ID of note to pin - */ -export async function addPinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']): Promise { - // 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); - } -} - -/** - * Unpin a given post from a user profile. - * @param user the user to unpin a note from - * @param noteId ID of note to unpin - */ -export async function removePinned(user: { id: User['id']; host: User['host']; }, noteId: Note['id']): Promise { - // 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); - } -} - -/** - * Notify followers and relays when a note is pinned/unpinned. - * @param userId ID of user - * @param noteId ID of note - * @param isAddition whether the note was pinned or unpinned - */ -export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean): Promise { - 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 4bfbb0032..000000000 --- a/packages/backend/src/services/i/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 { Users } from '@/models/index.js'; -import { 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'; - -/** - * Send an Update activity to a user's followers. - * @param userId ID of user - */ -export async function publishToFollowers(userId: User['id']): Promise { - const user = await Users.findOneBy({ id: userId }); - if (user == null) throw new Error('user not found'); - - // Deliver Update if the follower is a remote user and the poster is a local user - 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 6c225eb5b..000000000 --- a/packages/backend/src/services/insert-moderation-log.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ModerationLogs } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { User } from '@/models/entities/user.js'; - -export async function insertModerationLog(moderator: { id: User['id'] }, type: string, info?: Record): Promise { - await ModerationLogs.insert({ - id: genId(), - createdAt: new Date(), - userId: moderator.id, - 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 279142b32..000000000 --- a/packages/backend/src/services/instance-actor.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IsNull } from 'typeorm'; -import { ILocalUser } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { createSystemUser } from './create-system-user.js'; - -const ACTOR_USERNAME = 'instance.actor' as const; - -let instanceActor = await Users.findOneBy({ - host: IsNull(), - username: ACTOR_USERNAME, -}) as ILocalUser | undefined; - -export async function getInstanceActor(): Promise { - if (!instanceActor) { - instanceActor = await createSystemUser(ACTOR_USERNAME) as ILocalUser; - } - - return instanceActor; -} diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts deleted file mode 100644 index a3bfc8a0a..000000000 --- a/packages/backend/src/services/logger.ts +++ /dev/null @@ -1,172 +0,0 @@ -import cluster from 'node:cluster'; -import chalk from 'chalk'; -import convertColor from 'color-convert'; -import { format as dateFormat } from 'date-fns'; -import * as SyslogPro from 'syslog-pro'; -import config from '@/config/index.js'; -import { envOption } from '@/env.js'; -import type { KEYWORD } from 'color-convert/conversions.js'; - -type Domain = { - name: string; - color?: KEYWORD; -}; - -type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; - -/** - * Class that facilitates recording log messages to the console and optionally a syslog server. - */ -export default class Logger { - private domain: Domain; - private parentLogger: Logger | null = null; - private store: boolean; - private syslogClient: SyslogPro.RFC5424 | null = null; - - /** - * Create a logger instance. - * @param domain Logging domain - * @param color Log message color - * @param store Whether to store messages - */ - constructor(domain: string, color?: KEYWORD, store = true) { - this.domain = { - name: domain, - color, - }; - this.store = store; - - if (config.syslog) { - this.syslogClient = new SyslogPro.RFC5424({ - applicationName: 'FoundKey', - timestamp: true, - includeStructuredData: true, - color: true, - extendedColor: true, - server: { - target: config.syslog.host, - port: config.syslog.port, - }, - }); - } - } - - /** - * Create a child logger instance. - * @param domain Logging domain - * @param color Log message color - * @param store Whether to store messages - * @returns A Logger instance whose parent logger is this instance. - */ - public createSubLogger(domain: string, color?: KEYWORD, 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; - const store = _store && this.store && (level !== 'debug'); - - 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') : - chalk.blue('INFO'); - 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) : - message; - - 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 : - this.syslogClient.info; - - send.bind(this.syslogClient)(message).catch(() => {}); - } - } - } - - /** - * Log an error message. - * Use in situations where execution cannot be continued. - * @param err Error or string containing an error message - * @param data Data relating to the error - * @param important Whether this error is important - */ - public error(err: string | Error, data: Record = {}, important = false): void { - if (err instanceof Error) { - data.e = err; - this.log('error', err.toString(), data, important); - } else if (typeof err === 'object') { - this.log('error', `${(err as any).message || (err as any).name || err}`, data, important); - } else { - this.log('error', `${err}`, data, important); - } - } - - /** - * Log a warning message. - * Use in situations where execution can continue but needs to be improved. - * @param message Warning message - * @param data Data relating to the warning - * @param important Whether this warning is important - */ - public warn(message: string, data?: Record | null, important = false): void { - this.log('warning', message, data, important); - } - - /** - * Log a success message. - * Use in situations where something has been successfully done. - * @param message Success message - * @param data Data relating to the success - * @param important Whether this success message is important - */ - public succ(message: string, data?: Record | null, important = false): void { - this.log('success', message, data, important); - } - - /** - * Log a debug message. - * Use for debugging (information needed by developers but not required by users). - * @param message Debug message - * @param data Data relating to the debug message - * @param important Whether this debug message is 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); - } - } - - /** - * Log an informational message. - * Use when something needs to be logged but doesn't fit into other levels. - * @param message Info message - * @param data Data relating to the info message - * @param important Whether this info message is 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 4a0ea53a8..000000000 --- a/packages/backend/src/services/messages/create.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Not } from 'typeorm'; -import { CacheableUser, User } from '@/models/entities/user.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { MessagingMessage } from '@/models/entities/messaging-message.js'; -import { publishMessagingStream, publishMessagingIndexStream, publishMainStream, publishGroupMessagingStream } from '@/services/stream.js'; -import { pushNotification } from '@/services/push-notification.js'; -import { 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.id ], - } 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 25f49ae50..000000000 --- a/packages/backend/src/services/messages/delete.ts +++ /dev/null @@ -1,30 +0,0 @@ -import config from '@/config/index.js'; -import { MessagingMessages, Users } from '@/models/index.js'; -import { 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): Promise { - await MessagingMessages.delete(message.id); - await postDeleteMessage(message); -} - -async function postDeleteMessage(message: MessagingMessage): Promise { - 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 67f5e0d57..000000000 --- a/packages/backend/src/services/note/create.ts +++ /dev/null @@ -1,688 +0,0 @@ -import { ArrayOverlap, Not, In } from 'typeorm'; -import * as mfm from 'mfm-js'; -import { db } from '@/db/postgre.js'; -import es from '@/db/elasticsearch.js'; -import { publishMainStream, publishNotesStream } 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 { concat } from '@/prelude/array.js'; -import { insertNoteUnread } from '@/services/note/unread.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 { Note } from '@/models/entities/note.js'; -import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js'; -import { DriveFile } from '@/models/entities/drive-file.js'; -import { App } from '@/models/entities/app.js'; -import { 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 { Poll, IPoll } from '@/models/entities/poll.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { checkHitAntenna } from '@/misc/check-hit-antenna.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; -import { countSameRenotes } from '@/misc/count-same-renotes.js'; -import { 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 { UserProfile } from '@/models/entities/user-profile.js'; -import { getActiveWebhooks } from '@/misc/webhook-cache.js'; -import { IActivity } from '@/remote/activitypub/type.js'; -import { MINUTE } from '@/const.js'; -import { updateHashtags } from '../update-hashtag.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; -import { createNotification } from '../create-notification.js'; -import { addNoteToAntenna } from '../add-note-to-antenna.js'; -import { deliverToRelays } from '../relay.js'; - -const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>( - 5 * MINUTE, - () => UserProfiles.find({ - where: { - enableWordMute: true, - }, - select: ['userId', 'mutedWords'], - }), -); - -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): void { - // No notification to yourself. - if (this.notifier.id === notifiee) return; - - const exist = this.queue.find(x => x.target === notifiee); - - if (exist) { - // If you have been "mentioned and replied to," make the notification as a reply, not as a mention. - if (reason !== 'mention') { - exist.reason = reason; - } - } else { - this.queue.push({ - reason, - target: notifiee, - }); - } - } - - public async deliver(): Promise { - for (const x of this.queue) { - // check if the sender or thread are muted - const userMuted = await Mutings.countBy({ - muterId: x.target, - muteeId: this.notifier.id, - }); - - const threadMuted = await NoteThreadMutings.countBy({ - userId: x.target, - threadId: In([ - // replies - this.note.threadId ?? this.note.id, - // renotes - this.note.renoteId ?? undefined, - ]), - mutingNotificationTypes: ArrayOverlap([x.reason]), - }); - - if (!userMuted && !threadMuted) { - createNotification(x.target, x.reason, { - notifierId: this.notifier.id, - noteId: this.note.id, - }); - } - } - } -} - -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?: 'home' | 'public' | 'followers' | 'specified'; - 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): Promise => new Promise(async (res, rej) => { - // If you reply outside the channel, adjust to the scope of the target - // (I think this could be done client-side, but 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 to a channel, adjust the scope to that of the target. - // (I think this could be done client-side, but server-side for now) - if (data.reply?.channelId && (data.channel == null)) { - 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; - - // silence - if (user.isSilenced && data.visibility === 'public' && data.channel == null) { - data.visibility = 'home'; - } - - // Reject if the target of the renote is not Home or Public. - 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'; - } - - // Ff the original note is local-only, make the renote also local-only. - if (data.renote && data.renote.localOnly && data.channel == null) { - data.localOnly = true; - } - - // If you reply to local only, make it local only. - if (data.reply && 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); - - // Update Statistics - 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); - }); - } - - // Hashtag Update - if (data.visibility === 'public' || data.visibility === 'home') { - updateHashtags(user, tags); - } - - // Increment notes count (user) - incNotesCountOfUser(user); - - // Word mute - mutedWordsCache.fetch('').then(us => { - for (const u of us) { - checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => { - if (shouldMute) { - MutedNotes.insert({ - id: genId(), - userId: u.userId, - noteId: note.id, - reason: 'word', - }); - } - }); - } - }); - - // Antenna - 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); - } - - // When there is no re-note of the specified note by the specified user except for this post - if (data.renote && (await countSameRenotes(user.id, data.renote.id, note.id) === 0)) { - incRenoteCount(data.renote); - } - - if (data.poll && 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); - - // Create unread notifications - if (data.visibility === 'specified') { - if (data.visibleUsers == null) throw new Error('invalid param'); - - for (const u of data.visibleUsers) { - // Local users only - if (!Users.isLocalUser(u)) continue; - - insertNoteUnread(u.id, note, { - isSpecified: true, - isMentioned: false, - }); - } - } else { - for (const u of mentionedUsers) { - // Local users only - if (!Users.isLocalUser(u)) continue; - - insertNoteUnread(u.id, note, { - isSpecified: false, - isMentioned: true, - }); - } - } - - publishNotesStream(note); - - 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.countBy({ - 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) { - 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); - - // Delivered to remote users who have been mentioned - for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) { - dm.addDirectRecipe(u as IRemoteUser); - } - - // If the post is a reply and the poster is a local user and the poster of the post to which you are replying is a remote user, deliver - if (data.reply && data.reply.userHost !== null) { - const u = await Users.findOneBy({ id: data.reply.userId }); - if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); - } - - // If the post is a Renote and the poster is a local user and the poster of the original Renote post is a remote user, deliver - if (data.renote && data.renote.userHost !== null) { - const u = await Users.findOneBy({ id: data.renote.userId }); - if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u); - } - - // Deliver to followers - 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, - }); - - // This process takes place after the note is created, so if there is only one note, you can determine that it is the first submission. - // TODO: but there's also the messiness of deleting a note and posting it multiple times, which is incremented by the number of times it's posted, so I'd like to do something about that. - if (count === 1) { - Channels.increment({ id: data.channel.id }, 'usersCount', 1); - } - } - - // Register to search database - index(note); -}); - -async function renderNoteOrRenoteActivity(data: Option, note: Note): Promise { - 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): void { - 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[]): Promise { - const createdAt = data.createdAt ?? new Date(); - - const insert = new Note({ - id: genId(createdAt), - createdAt, - fileIds: data.files?.map(file => file.id) ?? [], - replyId: data.reply?.id ?? null, - renoteId: data.renote?.id ?? null, - channelId: data.channel?.id ?? null, - threadId: data.reply?.threadId ?? data.reply?.id ?? null, - name: data.name, - text: data.text, - hasPoll: data.poll != null, - cw: data.cw ?? null, - tags: tags.map(tag => normalizeForSearch(tag)), - emojis, - userId: user.id, - localOnly: data.localOnly ?? false, - visibility: data.visibility, - visibleUserIds: data.visibility === 'specified' - ? data.visibleUsers?.map(u => u.id) ?? [] - : [], - - attachedFileTypes: data.files?.map(file => file.type) ?? [], - - // denormalized data below - replyUserId: data.reply?.userId, - replyUserHost: data.reply?.userHost, - renoteUserId: data.renote?.userId, - renoteUserHost: data.renote?.userHost, - 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); - } - - // Create a post - try { - // Start transaction - await db.transaction(async transactionalEntityManager => { - await transactionalEntityManager.insert(Note, insert); - - if (data.poll != null) { - 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); - } - }); - - return insert; - } catch (e) { - // duplicate key error - if (isDuplicateKeyValueError(e)) { - const err = new Error('Duplicated note'); - err.name = 'duplicated'; - throw err; - } - - console.error(e); - - throw e; - } -} - -function index(note: Note): void { - if (note.text == null || config.elasticsearch == null) return; - - es.index({ - index: config.elasticsearch.index || 'misskey_note', - id: note.id.toString(), - body: { - text: normalizeForSearch(note.text), - userId: note.userId, - userHost: note.userHost, - }, - }); -} - -async function notifyToWatchersOfRenotee(renote: Note, user: { id: User['id']; }, nm: NotificationManager, type: NotificationType): Promise { - 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): Promise { - 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): Promise { - for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) { - const threadMuted = await NoteThreadMutings.countBy({ - 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): void { - Notes.increment({ id: reply.id }, 'repliesCount', 1); -} - -function incNotesCountOfUser(user: { id: User['id']; }): void { - 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.length === 0) 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 6075f15b1..000000000 --- a/packages/backend/src/services/note/delete.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { FindOptionsWhere, In, IsNull, Not } 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 { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { Notes, Users, Instances } from '@/models/index.js'; -import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js'; -import { DeliverManager } from '@/remote/activitypub/deliver-manager.js'; -import { countSameRenotes } from '@/misc/count-same-renotes.js'; -import { isPureRenote } from '@/misc/renote.js'; -import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js'; -import { deliverToRelays } from '../relay.js'; - -/** - * Delete your note. - * @param user author - * @param note note to be deleted - */ -export default async function(user: { id: User['id']; uri: User['uri']; host: User['host']; }, note: Note, quiet = false): Promise { - const deletedAt = new Date(); - - // If this is the only renote of this note by this user - 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 }); - - // deliver delete activity of note itself for local posts - if (Users.isLocalUser(user) && !note.localOnly) { - let renote: Note | null = null; - - // if deleted note is renote - if (isPureRenote(note)) { - 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 deliver delete activity to cascaded notes - const cascadingNotes = await findCascadingNotes(note); - for (const cascadingNote of cascadingNotes) { - const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); - deliverToConcerned(cascadingNote.user, cascadingNote, content); - } - - // update statistics - 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, - }); -} - -/** - * Search for notes that will be affected by ON CASCADE DELETE. - * However, only notes for which it is relevant to deliver delete activities are searched. - * This means only local notes that are not local-only are searched. - */ -async function findCascadingNotes(note: Note): Promise { - const cascadingNotes: Note[] = []; - - const recursive = async (noteId: string): Promise => { - // FIXME: use note_replies SQL function? Unclear what to do with 2nd and 3rd parameter, maybe rewrite the function. - const replies = await Notes.find({ - where: [{ - replyId: noteId, - localOnly: false, - userHost: IsNull(), - }, { - renoteId: noteId, - text: Not(IsNull()), - localOnly: false, - userHost: IsNull(), - }], - relations: { - user: true, - }, - }); - - await Promise.all(replies.map(reply => { - // only add unique notes - if (cascadingNotes.find((x) => x.id === reply.id) != null) return; - - cascadingNotes.push(reply); - return recursive(reply.id); - })); - }; - await recursive(note.id); - - return cascadingNotes; -} - -async function getMentionedRemoteUsers(note: Note): Promise { - const where: FindOptionsWhere[] = []; - - // mention / reply / dm - if (note.mentions.length > 0) { - where.push({ - id: In(note.mentions), - // only remote users, local users are on the server and do not need to be notified - host: Not(IsNull()), - }); - } - - // 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): Promise { - const manager = new DeliverManager(user, content); - - const remoteUsers = await getMentionedRemoteUsers(note); - for (const remoteUser of remoteUsers) { - manager.addDirectRecipe(remoteUser); - } - - if (['public', 'home', 'followers'].includes(note.visibility)) { - manager.addFollowersRecipe(); - } - - if (['public', 'home'].includes(note.visibility)) { - manager.addEveryone(); - } - - await manager.execute(); - - deliverToRelays(user, content); -} 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 2d8050b68..000000000 --- a/packages/backend/src/services/note/polls/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 renderNote from '@/remote/activitypub/renderer/note.js'; -import { Users, Notes } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; -import { deliverToFollowers } from '@/remote/activitypub/deliver-manager.js'; -import { deliverToRelays } from '@/services/relay.js'; - -export async function deliverQuestionUpdate(noteId: Note['id']): Promise { - 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 b86e7107d..000000000 --- a/packages/backend/src/services/note/polls/vote.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { ArrayOverlap, Not } from 'typeorm'; -import { publishNoteStream } from '@/services/stream.js'; -import { CacheableUser } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { PollVotes, NoteWatchings, Polls, Blockings, NoteThreadMutings } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { createNotification } from '@/services/create-notification.js'; - -export async function vote(user: CacheableUser, note: Note, choice: number): Promise { - 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.countBy({ - 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, - }); - - // 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, - userId: user.id, - }); - - // check if this thread and notification type is muted - const muted = await NoteThreadMutings.countBy({ - userId: note.userId, - threadId: note.threadId || note.id, - mutingNotificationTypes: ArrayOverlap(['pollVote']), - }); - // Notify - if (!muted) { - createNotification(note.userId, 'pollVote', { - notifierId: user.id, - noteId: note.id, - 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, - }); - } - }); -} 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 d50761a29..000000000 --- a/packages/backend/src/services/note/reaction/create.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { ArrayOverlap, IsNull, Not } from 'typeorm'; -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 { User, IRemoteUser } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteReactions, Users, NoteWatchings, Notes, Emojis, Blockings, NoteThreadMutings } from '@/models/index.js'; -import { perUserReactionsChart } from '@/services/chart/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { createNotification } from '@/services/create-notification.js'; -import { deleteReaction } from './delete.js'; - -export async function createReaction(user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string): Promise { - // Check blocking - if (note.userId !== user.id) { - const block = await Blockings.countBy({ - 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 - const dbReaction = await toDbReaction(reaction, user.host); - - const record: NoteReaction = { - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - reaction: dbReaction, - }; - - // 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 !== dbReaction) { - // 別のリアクションがすでにされていたら置き換える - 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", '{${dbReaction}}', (COALESCE("reactions"->>'${dbReaction}', '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(dbReaction); - - 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, - }); - - // check if this thread is muted - const threadMuted = await NoteThreadMutings.countBy({ - userId: note.userId, - threadId: note.threadId || note.id, - mutingNotificationTypes: ArrayOverlap(['reaction']), - }); - // リアクションされたユーザーがローカルユーザーなら通知を作成 - if (note.userHost === null && !threadMuted) { - createNotification(note.userId, 'reaction', { - notifierId: user.id, - noteId: note.id, - reaction: dbReaction, - }); - } - - // 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, - noteId: note.id, - reaction: dbReaction, - }); - } - }); - - //#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 616a30015..000000000 --- a/packages/backend/src/services/note/reaction/delete.ts +++ /dev/null @@ -1,58 +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 { User, IRemoteUser } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteReactions, Users, Notes } from '@/models/index.js'; -import { decodeReaction } from '@/misc/reaction-lib.js'; - -export async function deleteReaction(user: { id: User['id']; host: User['host']; }, note: Note): Promise { - // 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 6899eccc5..000000000 --- a/packages/backend/src/services/note/read.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Not, IsNull, In } from 'typeorm'; -import { publishMainStream } from '@/services/stream.js'; -import { Note } from '@/models/entities/note.js'; -import { User } from '@/models/entities/user.js'; -import { NoteUnreads, AntennaNotes, Users, Followings, ChannelFollowings } from '@/models/index.js'; -import { 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 { Packed } from '@/misc/schema.js'; - -/** - * Mark notes as read - */ -export async function readNote( - userId: User['id'], - notes: (Note | Packed<'Note'>)[], - info?: { - following: Set; - followingChannels: Set; - }, -): Promise { - const following = info?.following ? info.following : new Set((await Followings.find({ - where: { - followerId: userId, - }, - select: ['followeeId'], - })).map(x => x.followeeId)); - const followingChannels = info?.followingChannels ? info.followingChannels : new Set((await ChannelFollowings.find({ - where: { - followerId: userId, - }, - select: ['followeeId'], - })).map(x => x.followeeId)); - - const myAntennas = (await getAntennas()).filter(a => a.userId === userId); - const readMentions: (Note | Packed<'Note'>)[] = []; - const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; - const readChannelNotes: (Note | Packed<'Note'>)[] = []; - const readAntennaNotes: (Note | Packed<'Note'>)[] = []; - - for (const note of notes) { - if (note.mentions && note.mentions.includes(userId)) { - readMentions.push(note); - } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { - readSpecifiedNotes.push(note); - } - - if (note.channelId && followingChannels.has(note.channelId)) { - readChannelNotes.push(note); - } - - if (note.user != null) { // たぶんnullになることは無いはずだけど一応 - for (const antenna of myAntennas) { - if (await checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) { - readAntennaNotes.push(note); - } - } - } - } - - if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { - // Remove the record - await NoteUnreads.delete({ - userId, - noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]), - }); - - // TODO: ↓まとめてクエリしたい - - NoteUnreads.countBy({ - userId, - isMentioned: true, - }).then(mentionsCount => { - if (mentionsCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadMentions'); - } - }); - - NoteUnreads.countBy({ - userId, - isSpecified: true, - }).then(specifiedCount => { - if (specifiedCount === 0) { - // 全て既読になったイベントを発行 - publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); - } - }); - - NoteUnreads.countBy({ - 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 4ef257c21..000000000 --- a/packages/backend/src/services/note/unread.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Note } from '@/models/entities/note.js'; -import { publishMainStream } from '@/services/stream.js'; -import { User } from '@/models/entities/user.js'; -import { Mutings, NoteThreadMutings, NoteUnreads } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { SECOND } from '@/const.js'; - -export async function insertNoteUnread(userId: User['id'], note: Note, params: { - // NOTE: if isSpecified is true, isMentioned is always false - isSpecified: boolean; - isMentioned: boolean; -}): Promise { - //#region ignore if muted - // TODO: The current design does not apply mutes to channels. - const muted = await Mutings.countBy({ - muterId: userId, - muteeId: note.userId, - }); - if (muted) return; - - const threadMuted = await NoteThreadMutings.countBy({ - userId, - threadId: note.threadId || note.id, - }); - if (threadMuted) return; - //#endregion - - const unread = { - id: genId(), - noteId: note.id, - userId, - isSpecified: params.isSpecified, - isMentioned: params.isMentioned, - noteChannelId: note.channelId, - noteUserId: note.userId, - }; - - await NoteUnreads.insert(unread); - - // Issue the events for unread messages if it hasn't been read after 2 seconds. - setTimeout(async () => { - const exist = await NoteUnreads.countBy({ id: unread.id }); - - if (!exist) 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); - } - }, 2 * SECOND); -} diff --git a/packages/backend/src/services/note/unwatch.ts b/packages/backend/src/services/note/unwatch.ts deleted file mode 100644 index 2f779586d..000000000 --- a/packages/backend/src/services/note/unwatch.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { NoteWatchings } from '@/models/index.js'; -import { Note } from '@/models/entities/note.js'; - -export async function unwatch(me: User['id'], note: Note): Promise { - 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 634870c75..000000000 --- a/packages/backend/src/services/note/watch.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteWatchings } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import { NoteWatching } from '@/models/entities/note-watching.js'; - -export async function watch(me: User['id'], note: Note): Promise { - // User can't watch their own posts. - 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 6607c05d9..000000000 --- a/packages/backend/src/services/push-notification.ts +++ /dev/null @@ -1,77 +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 { 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 }; -}; - -// Reduce the content of the push message because of the character limit -function truncateNotification(notification: Packed<'Notification'>): Record { - if (notification.note) { - return { - ...notification, - note: { - ...notification.note, - // replace text with getNoteSummary - text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note), - - cw: undefined, - reply: undefined, - renote: undefined, - // unnecessary, since usually the user who is receiving the notification knows who they are - user: undefined as any, - }, - }; - } - - return notification; -} - -export async function pushNotification(userId: string, type: T, body: pushNotificationsTypes[T]): Promise { - const meta = await fetchMeta(); - - // Register key pair information - push.setVapidDetails(config.url, - meta.swPublicKey, - meta.swPrivateKey); - - // Fetch - const subscriptions = await SwSubscriptions.findBy({ userId }); - - for (const subscription of subscriptions) { - const pushSubscription = { - endpoint: subscription.endpoint, - keys: { - auth: subscription.auth, - p256dh: subscription.publickey, - }, - }; - - push.sendNotification(pushSubscription, JSON.stringify({ - type, - body: type === 'notification' ? truncateNotification(body as Packed<'Notification'>) : body, - userId, - }), { - proxy: config.proxy, - }).catch((err: any) => { - if (err.statusCode === 410) { - SwSubscriptions.delete({ - 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 3d41fafa1..000000000 --- a/packages/backend/src/services/register-or-fetch-instance-doc.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { 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'; -import { HOUR } from '@/const.js'; - -const cache = new Cache( - HOUR, - async (host) => { - if (host == null) return undefined; - const res = await Instances.findOneBy({ host }); - return res ?? undefined; - } , -); - -export async function registerOrFetchInstanceDoc(idnHost: string): Promise { - const host = toPuny(idnHost); - - const cached = await cache.fetch(host); - if (cached != null) return cached; - - // apparently a new instance - 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; -} diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts deleted file mode 100644 index b60bbd425..000000000 --- a/packages/backend/src/services/relay.ts +++ /dev/null @@ -1,107 +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 { 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 { Relay } from '@/models/entities/relay.js'; -import { MINUTE } from '@/const.js'; -import { createSystemUser } from './create-system-user.js'; - -const ACTOR_USERNAME = 'relay.actor' as const; - -/** - * There is only one cache key: null. - * A cache is only used here to have expiring storage. - */ -const relaysCache = new Cache( - 10 * MINUTE, - () => Relays.findBy({ - status: 'accepted', - }), -); - -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): Promise { - const relay = await Relays.insert({ - id: genId(), - inbox, - status: 'requesting', - }).then(x => Relays.findOneByOrFail(x.identifiers[0])); - - const relayActor = await getRelayActor(); - const follow = renderFollowRelay(relay, relayActor); - const activity = renderActivity(follow); - deliver(relayActor, activity, relay.inbox); - - return relay; -} - -export async function removeRelay(inbox: string): Promise { - 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(): Promise { - const relays = await Relays.find(); - return relays; -} - -export async function relayAccepted(id: string): Promise { - const result = await Relays.update(id, { - status: 'accepted', - }); - - return JSON.stringify(result); -} - -export async function relayRejected(id: string): Promise { - const result = await Relays.update(id, { - status: 'rejected', - }); - - return JSON.stringify(result); -} - -export async function deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise { - if (activity == null) return; - - const relays = await relaysCache.fetch(''); - if (relays == null || relays.length === 0) return; - - const copy = structuredClone(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.ts b/packages/backend/src/services/send-email.ts deleted file mode 100644 index b4dd0d828..000000000 --- a/packages/backend/src/services/send-email.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as nodemailer from 'nodemailer'; -import { fetchMeta } from '@/misc/fetch-meta.js'; -import config from '@/config/index.js'; -import Logger from './logger.js'; - -export const logger = new Logger('email'); - -export async function sendEmail(to: string, subject: string, html: string, text: string): Promise { - 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, - subject, - text, - html: ` - - - - ${ subject } - - - -
-
- -
-
-

${ 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 0119d7fdf..000000000 --- a/packages/backend/src/services/stream.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { redisClient } from '@/db/redis.js'; -import { User } from '@/models/entities/user.js'; -import { Note } from '@/models/entities/note.js'; -import { UserList } from '@/models/entities/user-list.js'; -import { UserGroup } from '@/models/entities/user-group.js'; -import config from '@/config/index.js'; -import { Antenna } from '@/models/entities/antenna.js'; -import { Channel } from '@/models/entities/channel.js'; -import { - 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, body: null } : - { type, body: value }; - - redisClient.publish(config.host, JSON.stringify({ - channel, - message, - })); - }; - - public publishInternalEvent = (type: K, value?: InternalStreamTypes[K]): void => { - this.publish('internal', type, typeof value === 'undefined' ? null : value); - }; - - public publishUserEvent = (userId: User['id'], type: K, value?: UserStreamTypes[K]): void => { - this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishBroadcastStream = (type: K, value?: BroadcastTypes[K]): void => { - this.publish('broadcast', type, typeof value === 'undefined' ? null : value); - }; - - public publishMainStream = (userId: User['id'], type: K, value?: MainStreamTypes[K]): void => { - this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishDriveStream = (userId: User['id'], type: K, value?: DriveStreamTypes[K]): void => { - this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishNoteStream = (noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void => { - this.publish(`noteStream:${noteId}`, type, { - id: noteId, - body: value, - }); - }; - - public publishChannelStream = (channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void => { - this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishUserListStream = (listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void => { - this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishAntennaStream = (antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void => { - this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void => { - this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishGroupMessagingStream = (groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void => { - this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value); - }; - - public publishMessagingIndexStream = (userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void => { - this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); - }; - - public 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 11e6266a0..000000000 --- a/packages/backend/src/services/suspend-user.ts +++ /dev/null @@ -1,20 +0,0 @@ -import renderDelete from '@/remote/activitypub/renderer/delete.js'; -import { renderActivity } from '@/remote/activitypub/renderer/index.js'; -import { DeliverManager } from '@/remote/activitypub/deliver-manager.js'; -import config from '@/config/index.js'; -import { User } from '@/models/entities/user.js'; -import { Users } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; - -export async function doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise { - publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); - - if (Users.isLocalUser(user)) { - const content = renderActivity(renderDelete(`${config.url}/users/${user.id}`, user)); - - // deliver to all of known network - const dm = new DeliverManager(user, content); - dm.addEveryone(); - await dm.execute(); - } -} diff --git a/packages/backend/src/services/unsuspend-user.ts b/packages/backend/src/services/unsuspend-user.ts deleted file mode 100644 index 766b10f21..000000000 --- a/packages/backend/src/services/unsuspend-user.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Not, IsNull } from 'typeorm'; -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 { User } from '@/models/entities/user.js'; -import { Users, Followings } from '@/models/index.js'; -import { publishInternalEvent } from '@/services/stream.js'; - -export async function doPostUnsuspend(user: User): Promise { - 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 7a77cb124..000000000 --- a/packages/backend/src/services/update-hashtag.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { 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 { 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[]): Promise { - for (const tag of tags) { - await updateHashtag(user, tag); - } -} - -export async function updateUsertags(user: User, tags: string[]): Promise { - 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): Promise { - const 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 1266ef5f0..000000000 --- a/packages/backend/src/services/user-cache.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { IsNull } from 'typeorm'; -import { CacheableLocalUser, ILocalUser, 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, - async (id) => await Users.findOneBy({ id }) ?? undefined, -); -export const localUserByNativeTokenCache = new Cache( - Infinity, - async (token) => await Users.findOneBy({ token, host: IsNull() }) as ILocalUser | null ?? undefined, -); -export const uriPersonCache = new Cache( - Infinity, - async (uri) => await Users.findOneBy({ uri }) ?? undefined, -); - -subscriber.on('message', async (_, data) => { - const obj = JSON.parse(data); - - if (obj.channel === 'internal') { - const { type, body } = obj.message; - switch (type) { - 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); - } - 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 527aa7bf5..000000000 --- a/packages/backend/src/services/user-list/push.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { publishUserListStream } from '@/services/stream.js'; -import { User } from '@/models/entities/user.js'; -import { UserList } from '@/models/entities/user-list.js'; -import { UserListJoinings, Users } from '@/models/index.js'; -import { 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): Promise { - 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 3f863fb71..000000000 --- a/packages/backend/src/services/validate-email-for-account.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { validate as validateEmail } from 'deep-email-validator'; -import { UserProfiles } from '@/models/index.js'; - -export async function validateEmailForAccount(emailAddress: string): Promise<{ - available: boolean; - reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp'; -}> { - const exist = await UserProfiles.countBy({ - emailVerified: true, - email: emailAddress, - }); - - const validated = await validateEmail({ - email: emailAddress, - validateRegex: true, - validateMx: true, - validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので - validateDisposable: true, // 捨てアドかどうかチェック - validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので - }); - - const available = exist === 0 && validated.valid; - - return { - available, - reason: available ? null : - exist !== 0 ? 'used' : - validated.reason === 'regex' ? 'format' : - validated.reason === 'disposable' ? 'disposable' : - validated.reason === 'mx' ? 'mx' : - validated.reason === 'smtp' ? 'smtp' : - null, - }; -} diff --git a/packages/backend/test/.eslintrc.cjs b/packages/backend/test/.eslintrc.cjs deleted file mode 100644 index d83dc37d2..000000000 --- a/packages/backend/test/.eslintrc.cjs +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: ['../.eslintrc.cjs'], - env: { - node: true, - mocha: true, - }, -}; diff --git a/packages/backend/test/activitypub.ts b/packages/backend/test/activitypub.ts deleted file mode 100644 index 6ce9c7c8e..000000000 --- a/packages/backend/test/activitypub.ts +++ /dev/null @@ -1,113 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import { initDb } from '../src/db/postgre.js'; -import { initTestDb } from './utils.js'; - - -function rndstr(length): string { - const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - const chars_len = 62; - - let str = ''; - - for (let i = 0; i < length; i++) { - let rand = Math.floor(Math.random() * chars_len); - if (rand === chars_len) { - rand = chars_len - 1; - } - str += chars.charAt(rand); - } - - return str; -} - -describe('ActivityPub', () => { - before(async () => { - //await initTestDb(); - await initDb(); - }); - - describe('Parse minimum object', () => { - const host = 'https://host1.test'; - const preferredUsername = `${rndstr(8)}`; - 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(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(8)}`; - const actorId = `${host}/users/${preferredUsername.toLowerCase()}`; - - const name = rndstr(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 744b2f2c9..000000000 --- a/packages/backend/test/ap-request.ts +++ /dev/null @@ -1,55 +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 cde3cd2d0..000000000 --- a/packages/backend/test/api-visibility.ts +++ /dev/null @@ -1,476 +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 b1b2ecafc..000000000 --- a/packages/backend/test/api.ts +++ /dev/null @@ -1,83 +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 b3343813c..000000000 --- a/packages/backend/test/block.ts +++ /dev/null @@ -1,85 +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 ac0844679..000000000 --- a/packages/backend/test/chart.ts +++ /dev/null @@ -1,531 +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 5f95bec4c..000000000 --- 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 2aedc25f2..000000000 --- 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 85afb098d..000000000 --- a/packages/backend/test/extract-mentions.ts +++ /dev/null @@ -1,42 +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 ddb0e94b8..000000000 --- a/packages/backend/test/fetch-resource.ts +++ /dev/null @@ -1,205 +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 4f6847be6..000000000 --- a/packages/backend/test/ff-visibility.ts +++ /dev/null @@ -1,167 +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 7ce98db50..000000000 --- a/packages/backend/test/get-file-info.ts +++ /dev/null @@ -1,173 +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) as any; - delete info.warnings; - delete info.blurhash; - 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) as any; - delete info.warnings; - delete info.blurhash; - 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) as any; - delete info.warnings; - delete info.blurhash; - 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) as any; - delete info.warnings; - delete info.blurhash; - 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) as any; - delete info.warnings; - delete info.blurhash; - 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) as any; - delete info.warnings; - delete info.blurhash; - 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) as any; - delete info.warnings; - delete info.blurhash; - 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) as any; - delete info.warnings; - delete info.blurhash; - 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) as any; - delete info.warnings; - delete info.blurhash; - 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 6b21587e3..000000000 --- a/packages/backend/test/loader.js +++ /dev/null @@ -1,34 +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 5b9e47414..000000000 --- a/packages/backend/test/mfm.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as assert from 'assert'; - -import { toHtml } from '../src/mfm/to-html.js'; -import { fromHtml } from '../src/mfm/from-html.js'; - -describe('toHtml', () => { - it('br', async () => { - const input = 'foo\nbar\nbaz'; - const output = '

foo
bar
baz

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

foo
bar
baz

'; - assert.equal(await toHtml(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 ba89ac329..000000000 --- a/packages/backend/test/misc/mock-resolver.ts +++ /dev/null @@ -1,35 +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 465633973..000000000 --- a/packages/backend/test/mute.ts +++ /dev/null @@ -1,123 +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 b495d8b7b..000000000 --- a/packages/backend/test/note.ts +++ /dev/null @@ -1,370 +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/url.ts b/packages/backend/test/prelude/url.ts deleted file mode 100644 index df102c8df..000000000 --- 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 7c61dc76c..000000000 --- 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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
+ + + diff --git a/packages/client/package.json b/packages/client/package.json index 44aa9a9c9..325379eb3 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "version": "13.0.0-preview3", "private": true, "scripts": { - "watch": "vite build --watch --mode development", + "watch": "vite", "build": "vite build", "lint": "tsc --noEmit && eslint src --ext .ts,.vue" }, diff --git a/packages/client/src/components/form/textarea.vue b/packages/client/src/components/form/textarea.vue index b41ddf9e0..055858544 100644 --- a/packages/client/src/components/form/textarea.vue +++ b/packages/client/src/components/form/textarea.vue @@ -67,7 +67,7 @@ const props = withDefaults(defineProps<{ const { modelValue } = toRefs(props); // modelValue is read only, so a separate ref is needed. -const v = $ref(modelValue.value); +let v = $ref(modelValue.value); watch(modelValue, () => v = modelValue.value); diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts index 8fa26aa2e..cb7c4e2de 100644 --- a/packages/client/src/config.ts +++ b/packages/client/src/config.ts @@ -1,5 +1,6 @@ -const address = new URL(location.href); -const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement).content; +import localeJson from '../../../locales/en-US.json'; + +const address = new URL('https://ak.kawen.space'); export const host = address.host; export const hostname = address.hostname; @@ -7,9 +8,9 @@ export const url = address.origin; export const apiUrl = url + '/api'; export const lang = localStorage.getItem('lang'); export const langs = _LANGS_; -export const locale = JSON.parse(localStorage.getItem('locale')); +export const locale = localeJson; export const version = _VERSION_; -export const software = _SOFTWARE_; -export const instanceName = siteName === 'FoundKey' ? host : siteName; +export const software = "lost-fe"; +export const instanceName = host; export const ui = localStorage.getItem('ui'); export const debug = localStorage.getItem('debug') === 'true'; diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index 692617abd..830b4a471 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -50,27 +50,27 @@ import { getAccountFromId } from '@/scripts/get-account-from-id'; (window as any).$i = $i; (window as any).$store = defaultStore; - window.addEventListener('error', event => { - console.error(event); - /* - alert({ - type: 'error', - title: 'DEV: Unhandled error', - text: event.message - }); - */ - }); + // window.addEventListener('error', event => { + // console.error(event); + // /* + // alert({ + // type: 'error', + // title: 'DEV: Unhandled error', + // text: event.message + // }); + // */ + // }); - window.addEventListener('unhandledrejection', event => { - console.error(event); - /* - alert({ - type: 'error', - title: 'DEV: Unhandled promise rejection', - text: event.reason - }); - */ - }); + // window.addEventListener('unhandledrejection', event => { + // console.error(event); + // /* + // alert({ + // type: 'error', + // title: 'DEV: Unhandled promise rejection', + // text: event.reason + // }); + // */ + // }); } // タッチデバイスでCSSの:hoverを機能させる diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 24f2659de..7f61187e2 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -4,7 +4,8 @@ import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue'; import { EventEmitter } from 'eventemitter3'; import insertTextAtCursor from 'insert-text-at-cursor'; import * as foundkey from 'foundkey-js'; -import { apiUrl, url } from '@/config'; +import { getBuiltinThemes } from './scripts/theme'; +import { url } from '@/config'; import MkPostFormDialog from '@/components/post-form-dialog.vue'; import MkWaitingDialog from '@/components/waiting-dialog.vue'; import { MenuItem } from '@/types/menu'; @@ -16,94 +17,210 @@ const apiClient = new foundkey.api.APIClient({ origin: url, }); -export const api = ((endpoint: string, data: Record = {}, token?: string | null | undefined) => { +function convertUser(user) { + return { + id: user.id, + username: user.username, + host: user.fqn.split('@')[1], + avatarUrl: user.avatar, + avatarBlurhash: '', + isAdmin: user.pleroma?.is_admin || false, + isModerator: user.pleroma?.is_moderator || false, + isBot: user.bot, + isCat: true, // TODO lmao + instance: { + name: user.akkoma.instance.name, + softwareName: user.akkoma.instance.nodeinfo.software.name, + softwareVersion: user.akkoma.instance.nodeinfo.software.version, + iconUrl: user.akkoma.instance.favicon, + faviconUrl: user.akkoma.instance.favicon, + themeColor: 'transparent', + }, + emojis: [], // TODO + onlineStatus: 'unknown', + url: user.url, + uri: user.url, + createdAt: user.created_at, + updatedAt: user.last_status_at, + lastFetchedAt: user.last_status_at, + bannerUrl: user.header, + bannerBlurhash: '', + isLocked: user.locked, + isSilenced: false, + isSuspended: false, + description: user.source.note, + location: null, + birthday: null, + lang: null, + fields: [], // TODO + followersCount: user.followers_count, + followingCount: user.following_count, + notesCount: user.statuses_count, + pinnedNoteIds: [], + pinnedNotes: [], + pinnedPageId: null, + pinnedPage: null, + publicReactions: false, + ffVisibility: 'public', + twoFactorEnabled: false, + usePasswordLessLogin: false, + securityKeys: false, + }; +} + +function convertVisibility(visibility: string): string { + return { public: 'public', unlisted: 'home', private: 'followers', direct: 'specified' }[visibility]; +} + +function convertNote(note) { + return { + id: note.id, + createdAt: note.created_at, + userId: note.account.id, + user: convertUser(note.account), + text: note.akkoma?.source?.content, + cw: note.spoiler_text ? note.spoiler_text : null, + visibility: convertVisibility(note.visibility), + renoteCount: note.reblogs_count, + repliesCount: note.replies_count, + reactions: {}, // TODO, + emojis: [], // TODO, + fileIds: [], // TODO + files: [], // TODO + replyId: note.in_reply_to_id, + renoteId: note.reblog?.id, + mentions: note.mentions.map(mention => mention.id), + uri: note.url, + reply: null, + }; +} + +export const api = (async (endpoint: string, args: Record = {}, token?: string | null | undefined) => { pendingApiRequestsCount.value++; - const onFinally = () => { - pendingApiRequestsCount.value--; - }; + try { + const authorizationToken = token ?? $i?.token ?? undefined; + const authorization = authorizationToken ? `Bearer ${authorizationToken}` : undefined; - const authorizationToken = token ?? $i?.token ?? undefined; - const authorization = authorizationToken ? `Bearer ${authorizationToken}` : undefined; + switch (endpoint) { + // not implemented area + case 'get-online-users-count': + return { count: 0 }; + case 'hashtags/list': + case 'notes/featured': + case 'announcements': + return []; + case 'charts/user/notes': + return { total: [], inc: [], dec: [], diffs: { normal: [], "reply": [], "renote": [], "withFile": [] } }; - const promise = new Promise((resolve, reject) => { - fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: 'omit', - cache: 'no-cache', - headers: { - 'content-type': 'application/json', - ...(authorization ? { authorization } : {}), - }, - }).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); + // implemented area + case 'users/notes': { + const params = new URLSearchParams({}); + if (args.limit) params.set("limit", args.limit); + const resp = await fetch(`${url}/api/v1/accounts/${args.userId}/statuses?${params.toString()}`); + if (resp.status !== 200) { + throw await resp.json(); + } + const notes = await resp.json(); + return notes.map(convertNote); } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; + case 'users/show': { + const acct = args.username + (args.host !== null ? `@${args.host}` : ''); + const params = new URLSearchParams({ acct }); + const resp = await fetch(`${url}/api/v1/accounts/lookup?${params.toString()}`); + if (resp.status !== 200) { + throw await resp.json(); + } + const user = await resp.json(); + return convertUser(user); + } + case 'meta': { + const resp = await fetch(`${url}/api/v1/instance`); + if (resp.status !== 200) { + throw await resp.json(); + } + const meta = await resp.json(); + const builtinThemes = await getBuiltinThemes(); + return { + 'maintainerName': meta.email, + 'maintainerEmail': meta.email, + 'version': meta.version, + 'name': meta.title, + 'uri': meta.uri, + 'description': meta.description, + 'langs': meta.languages, + 'tosUrl': '', + 'defaultDarkTheme': builtinThemes.find(th => th.name === 'Mi Cherry Dark'), + 'defaultLightTheme': builtinThemes.find(th => th.name === 'Mi Cherry Light'), + 'disableRegistration': !meta.registrations, + 'disableLocalTimeline': false, + 'disableGlobalTimeline': false, + 'driveCapacityPerLocalUserMb': 0, + 'driveCapacityPerRemoteUserMb': 0, + 'cacheRemoteFiles': true, + 'emailRequiredForSignup': true, + 'enableHcaptcha': false, + // "hcaptchaSiteKey": "string", + 'enableRecaptcha': false, + // "recaptchaSiteKey": "string", + // "swPublickey": "string", + 'bannerUrl': 'string', + 'iconUrl': meta.thumbnail, + 'maxNoteTextLength': meta.max_toot_chars || 500, + // "emojis": [ + // { + // "id": "string", + // "aliases": [ + // "string" + // ], + // "category": "string", + // "host": "string", + // "url": "string" + // } + // ], + 'requireSetup': false, + 'enableEmail': true, + 'translatorAvailable': false, + // "proxyAccountName": "string", + // "images": { + // "info": "string", + // "notFound": "string", + // "error": "string" + // }, + 'features': { + 'registration': meta.registrations, + 'localTimeLine': true, + 'globalTimeLine': true, + 'elasticsearch': false, + 'hcaptcha': false, + 'recaptcha': false, + 'objectStorage': false, + 'serviceWorker': false, + 'miauth': false, + }, + }; + } + default: + console.error('unhandled api', { endpoint, args }); + } + } finally { + pendingApiRequestsCount.value--; + } }) as typeof apiClient.request; -export const apiGet = ((endpoint: string, data: Record = {}, token?: string | null | undefined) => { - pendingApiRequestsCount.value++; - - const onFinally = () => { - pendingApiRequestsCount.value--; - }; - - const query = new URLSearchParams(data); - - const authorizationToken = token ?? $i?.token ?? undefined; - const authorization = authorizationToken ? `Bearer ${authorizationToken}` : undefined; - - const promise = new Promise((resolve, reject) => { - // Send request - fetch(`${apiUrl}/${endpoint}?${query}`, { - method: 'GET', - credentials: 'omit', - cache: 'default', - headers: { - 'content-type': 'application/json', - ...(authorization ? { authorization } : {}), - }, - }).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); - }); - - promise.then(onFinally, onFinally); - - return promise; -}) as typeof apiClient.request; +export const apiGet = api; export const apiWithDialog = (( endpoint: string, data: Record = {}, token?: string | null | undefined, ) => { + debugger; const promise = api(endpoint, data, token); promiseDialog(promise, null, (err) => { alert({ type: 'error', - text: (err.message + '\n' + (err?.endpoint ?? '') + ' ' + (err?.code ?? '')).trim(), + text: (err.message + '\n' + (err.endpoint ?? '') + ' ' + (err.code ?? '')).trim(), }); }); diff --git a/packages/client/src/scripts/twemoji-base.ts b/packages/client/src/scripts/twemoji-base.ts index 638aae328..18c18ead9 100644 --- a/packages/client/src/scripts/twemoji-base.ts +++ b/packages/client/src/scripts/twemoji-base.ts @@ -1,4 +1,4 @@ -export const twemojiSvgBase = '/twemoji'; +export const twemojiSvgBase = '/dist/twemoji'; export function char2fileName(char: string): string { let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16)); diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts index 443582a8d..66c877897 100644 --- a/packages/client/vite.config.ts +++ b/packages/client/vite.config.ts @@ -1,4 +1,3 @@ -import * as fs from 'fs'; import pluginVue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; @@ -9,12 +8,7 @@ import pluginJson5 from './vite.json5'; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; export default defineConfig(({ command, mode }) => { - fs.mkdirSync(__dirname + '/../../built', { recursive: true }); - fs.writeFileSync(__dirname + '/../../built/meta.json', JSON.stringify({ version: meta.version }), 'utf-8'); - return { - base: '/assets/', - plugins: [ pluginVue({ reactivityTransform: true, @@ -64,7 +58,6 @@ export default defineConfig(({ command, mode }) => { }, }, cssCodeSplit: true, - outDir: __dirname + '/../../built/_client_dist_', assetsDir: '.', emptyOutDir: false, sourcemap: process.env.NODE_ENV !== 'production', diff --git a/packages/foundkey-js/src/streaming.ts b/packages/foundkey-js/src/streaming.ts index 0892db9d9..3c98ef85c 100644 --- a/packages/foundkey-js/src/streaming.ts +++ b/packages/foundkey-js/src/streaming.ts @@ -25,7 +25,7 @@ type StreamEvents = { * Misskey stream connection */ export default class Stream extends EventEmitter { - private stream: ReconnectingWebSocket; + //private stream: ReconnectingWebSocket; public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing'; private sharedConnectionPools: Pool[] = []; private sharedConnections: SharedConnection[] = []; @@ -47,13 +47,13 @@ export default class Stream extends EventEmitter { const wsOrigin = origin.replace('http://', 'ws://').replace('https://', 'wss://'); - this.stream = new ReconnectingWebSocket(`${wsOrigin}/streaming?${query}`, '', { - minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91 - WebSocket: options.WebSocket, - }); - this.stream.addEventListener('open', this.onOpen); - this.stream.addEventListener('close', this.onClose); - this.stream.addEventListener('message', this.onMessage); + // this.stream = new ReconnectingWebSocket(`${wsOrigin}/streaming?${query}`, '', { + // minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91 + // WebSocket: options.WebSocket, + // }); + // this.stream.addEventListener('open', this.onOpen); + // this.stream.addEventListener('close', this.onClose); + // this.stream.addEventListener('message', this.onMessage); } @boundMethod @@ -174,7 +174,7 @@ export default class Stream extends EventEmitter { body: payload, }; - this.stream.send(JSON.stringify(data)); + // this.stream.send(JSON.stringify(data)); } /** @@ -182,7 +182,7 @@ export default class Stream extends EventEmitter { */ @boundMethod public close(): void { - this.stream.close(); + // this.stream.close(); } } diff --git a/scripts/dev.mjs b/scripts/dev.mjs index cb1eb2615..4cdb061dd 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -19,18 +19,6 @@ execa('npx', ['gulp', 'watch'], { stderr: process.stderr, }); -execa('npm', ['run', 'watch'], { - cwd: __dirname + '/../packages/backend', - stdout: process.stdout, - stderr: process.stderr, -}); - -execa('npm', ['run', 'watch'], { - cwd: __dirname + '/../packages/client', - stdout: process.stdout, - stderr: process.stderr, -}); - execa('npm', ['run', 'watch'], { cwd: __dirname + '/../packages/sw', stdout: process.stdout, @@ -39,8 +27,8 @@ execa('npm', ['run', 'watch'], { const start = async () => { try { - await execa('npm', ['run', 'start'], { - cwd: __dirname + '/../', + await execa('npm', ['run', 'watch'], { + cwd: __dirname + '/../packages/client', stdout: process.stdout, stderr: process.stderr, }); diff --git a/yarn.lock b/yarn.lock index ae981623b..345d629b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -198,7 +198,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.4, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.18.13, @babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.4, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.18.13": version: 7.18.13 resolution: "@babel/parser@npm:7.18.13" bin: @@ -388,7 +388,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.13, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.6.1, @babel/types@npm:^7.8.3, @babel/types@npm:^7.9.6": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.13, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.8.3": version: 7.18.13 resolution: "@babel/types@npm:7.18.13" dependencies: @@ -406,40 +406,6 @@ __metadata: languageName: node linkType: hard -"@bull-board/api@npm:4.3.1, @bull-board/api@npm:^4.3.1": - version: 4.3.1 - resolution: "@bull-board/api@npm:4.3.1" - dependencies: - redis-info: ^3.0.8 - checksum: 05113b1e888e79f8efecdffdc1043455fa6f8714c55a1e973d8a0a7f60cf574b00487b5b86324523ff91641784a55ff14c469edc8dd985295dcfc27cf55b4c4a - languageName: node - linkType: hard - -"@bull-board/koa@npm:^4.3.1": - version: 4.3.1 - resolution: "@bull-board/koa@npm:4.3.1" - dependencies: - "@bull-board/api": 4.3.1 - "@bull-board/ui": 4.3.1 - ejs: ^3.1.7 - koa: ^2.13.1 - koa-mount: ^4.0.0 - koa-router: ^10.0.0 - koa-static: ^5.0.0 - koa-views: ^7.0.1 - checksum: 08f198cdaaa28fe8e254288a0d4c13e9cd481a97e40e5e9152fb9094cbac54459e86901da5d90c46fe2dccf310f78a50ef8763bf5980b98d33180299c64fbc3f - languageName: node - linkType: hard - -"@bull-board/ui@npm:4.3.1": - version: 4.3.1 - resolution: "@bull-board/ui@npm:4.3.1" - dependencies: - "@bull-board/api": 4.3.1 - checksum: 7bc4787ba8f9e3dda5cb580b4374872bc7b0870a08a504cfc2f380a39dda164ae71518e7b1921e53ff8abc4224c0861504b26b63510b6c9c9d23d647bdab54b2 - languageName: node - linkType: hard - "@cropper/element-canvas@npm:^2.0.0-beta.1": version: 2.0.0-beta.1 resolution: "@cropper/element-canvas@npm:2.0.0-beta.1" @@ -606,18 +572,7 @@ __metadata: languageName: node linkType: hard -"@digitalbazaar/http-client@npm:^3.2.0": - version: 3.2.0 - resolution: "@digitalbazaar/http-client@npm:3.2.0" - dependencies: - ky: ^0.30.0 - ky-universal: ^0.10.1 - undici: ^5.2.0 - checksum: f8409a66977623a8f946b80169a4008d13af097f422ba9aa17beafe9a3fb243d274512b46bd08c0f9821d32fc12708f4237950bef1d0a37b1c1c86f85c9d2990 - languageName: node - linkType: hard - -"@discordapp/twemoji@npm:14.0.2": +"@discordapp/twemoji@npm:14.0.2, @discordapp/twemoji@npm:^14.0.2": version: 14.0.2 resolution: "@discordapp/twemoji@npm:14.0.2" dependencies: @@ -629,19 +584,6 @@ __metadata: languageName: node linkType: hard -"@elastic/elasticsearch@npm:7.11.0": - version: 7.11.0 - resolution: "@elastic/elasticsearch@npm:7.11.0" - dependencies: - debug: ^4.1.1 - hpagent: ^0.1.1 - ms: ^2.1.1 - pump: ^3.0.0 - secure-json-parse: ^2.1.0 - checksum: 58aee93f1b0d216ecf7fd936c6310123fa05b50a8419207820a483e31f2e5110ae54747111ecf57a2237a99765eb7bf1b78aa405f538132c551cf7959cc2cd99 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.14.54": version: 0.14.54 resolution: "@esbuild/linux-loong64@npm:0.14.54" @@ -1020,37 +962,6 @@ __metadata: languageName: node linkType: hard -"@koa/cors@npm:3.1.0": - version: 3.1.0 - resolution: "@koa/cors@npm:3.1.0" - dependencies: - vary: ^1.1.2 - checksum: b6f18de404a967696fe19c9a8d35816e2a845be51f9aa49f1a8d31bbea095eaa20c35eceafe1b764a7e3a912052ca6eaf1965ac34b00020b1ec2823cb60a9b5d - languageName: node - linkType: hard - -"@koa/multer@npm:3.0.0": - version: 3.0.0 - resolution: "@koa/multer@npm:3.0.0" - peerDependencies: - multer: "*" - checksum: 7671ffed2ab23224b30b4378b44b2db1749d36f82216b0b67ae349fd6fe86b40c4a72c98412499e059f69916b454fba152451f140adf288866462ba765eb6c32 - languageName: node - linkType: hard - -"@koa/router@npm:9.0.1": - version: 9.0.1 - resolution: "@koa/router@npm:9.0.1" - dependencies: - debug: ^4.1.1 - http-errors: ^1.7.3 - koa-compose: ^4.1.0 - methods: ^1.1.2 - path-to-regexp: ^6.1.0 - checksum: 0013bfd26c1acd44c772a08adae5edac8168c662fd2a06c7e174e2a899806908e73e077234c26888fad3ddc54478b27e5b8c7f5db41cd2e27f3b837c54b2f9b8 - languageName: node - linkType: hard - "@mapbox/node-pre-gyp@npm:^1.0.10": version: 1.0.10 resolution: "@mapbox/node-pre-gyp@npm:1.0.10" @@ -1122,48 +1033,6 @@ __metadata: languageName: node linkType: hard -"@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:2.1.2": - version: 2.1.2 - resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:2.1.2" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@msgpackr-extract/msgpackr-extract-darwin-x64@npm:2.1.2": - version: 2.1.2 - resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:2.1.2" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@msgpackr-extract/msgpackr-extract-linux-arm64@npm:2.1.2": - version: 2.1.2 - resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:2.1.2" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"@msgpackr-extract/msgpackr-extract-linux-arm@npm:2.1.2": - version: 2.1.2 - resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:2.1.2" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@msgpackr-extract/msgpackr-extract-linux-x64@npm:2.1.2": - version: 2.1.2 - resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:2.1.2" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"@msgpackr-extract/msgpackr-extract-win32-x64@npm:2.1.2": - version: 2.1.2 - resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:2.1.2" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1211,17 +1080,6 @@ __metadata: languageName: node linkType: hard -"@peertube/http-signature@npm:1.7.0": - version: 1.7.0 - resolution: "@peertube/http-signature@npm:1.7.0" - dependencies: - assert-plus: ^1.0.0 - jsprim: ^1.2.2 - sshpk: ^1.14.1 - checksum: a6a3d344327c454fad624dcbd8be2dac0444751656139f54d744ce4445c41dd08d266f4f64b6633a20c00fcea0341fa694e92424de6602444334c9ba5191a1d0 - languageName: node - linkType: hard - "@phc/format@npm:^1.0.0": version: 1.0.0 resolution: "@phc/format@npm:1.0.0" @@ -1229,92 +1087,6 @@ __metadata: languageName: node linkType: hard -"@redis/bloom@npm:1.0.2": - version: 1.0.2 - resolution: "@redis/bloom@npm:1.0.2" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 4872e7e5e4ff03d63349ca88d3164d487f62805651ada91924de2592995993401c98a01cb93bff8d71e9a2e54985b2485b6cb0e084a7e8b1283e2ebb8bc4b833 - languageName: node - linkType: hard - -"@redis/client@npm:1.3.0": - version: 1.3.0 - resolution: "@redis/client@npm:1.3.0" - dependencies: - cluster-key-slot: 1.1.0 - generic-pool: 3.8.2 - yallist: 4.0.0 - checksum: 3f935cdffeecbf999ea9e03115af7d95421078a58b3a5aa74afb2d0f90b6e203726753b4aeea5d2f1483c42bf3f64a6794375b2e10873829c2b7641cf179ad84 - languageName: node - linkType: hard - -"@redis/graph@npm:1.0.1": - version: 1.0.1 - resolution: "@redis/graph@npm:1.0.1" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 72e485efa416bdff10420f6e13c9cb4e1e5c70752e5172717adf62fc1d4d9ba12e708229fd87876f3a93270ff74c4bcd4d916987438dc36a94f7f12c9785fa44 - languageName: node - linkType: hard - -"@redis/json@npm:1.0.3": - version: 1.0.3 - resolution: "@redis/json@npm:1.0.3" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 26a7003c2fbacfa5998671e3a301cb2285432bf90f237adedcf76c0be0d379528e6710d469a8ea93c04bbd22951f9c2f41d460dbd79e85856f199248c4a250d5 - languageName: node - linkType: hard - -"@redis/search@npm:1.1.0": - version: 1.1.0 - resolution: "@redis/search@npm:1.1.0" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: dc0f1c8234580fb839e1135f76a70654461cb03be8918799a2a0cd79dd9815e0978feb99bcc01f5be37dabd95460f6cdba9f61a90a4d39183205883e12855afd - languageName: node - linkType: hard - -"@redis/time-series@npm:1.0.3": - version: 1.0.3 - resolution: "@redis/time-series@npm:1.0.3" - peerDependencies: - "@redis/client": ^1.0.0 - checksum: 4d11518185dd15f31c5b4a433902e53a3ebc24614a0221080ab12abf4f6fc60b3db00a71a83de7b4b10f11077de611dc1c273274573646d63481d40ca246f82d - languageName: node - linkType: hard - -"@redocly/ajv@npm:^8.6.4": - version: 8.6.5 - resolution: "@redocly/ajv@npm:8.6.5" - dependencies: - fast-deep-equal: ^3.1.1 - json-schema-traverse: ^1.0.0 - require-from-string: ^2.0.2 - uri-js: ^4.2.2 - checksum: 332af8581d3455bc45acf0806a1d90812946fa213888b7a1da1a573d903f361b69da71a128a936ed4911fe00e0eb41e57e633cfed4cbaf55a93eef86da4bb866 - languageName: node - linkType: hard - -"@redocly/openapi-core@npm:1.0.0-beta.97": - version: 1.0.0-beta.97 - resolution: "@redocly/openapi-core@npm:1.0.0-beta.97" - dependencies: - "@redocly/ajv": ^8.6.4 - "@types/node": ^14.11.8 - colorette: ^1.2.0 - js-levenshtein: ^1.1.6 - js-yaml: ^4.1.0 - lodash.isequal: ^4.5.0 - minimatch: ^5.0.1 - node-fetch: ^2.6.1 - pluralize: ^8.0.0 - yaml-ast-parser: 0.0.43 - checksum: 10abe89b7d95c8b4964e9b53467561c5782559b1c773581ab8a60bbe51989a906a0164a7c14ccab83dd60e776d3251ec0417b7de7b912cc78ed0f96957358a0b - languageName: node - linkType: hard - "@rollup/plugin-alias@npm:3.1.9": version: 3.1.9 resolution: "@rollup/plugin-alias@npm:3.1.9" @@ -1428,20 +1200,6 @@ __metadata: languageName: node linkType: hard -"@sindresorhus/is@npm:^3.0.0": - version: 3.1.2 - resolution: "@sindresorhus/is@npm:3.1.2" - checksum: 6b68b2c0bc36beda9442c64e40e2e971999b0814af610a52d5c0bda2213061ff63d158912bd494dc8b8fa5c027ed13ec5947e4902dd9a315b2f2337221dbcb7f - languageName: node - linkType: hard - -"@sindresorhus/is@npm:^5.2.0": - version: 5.3.0 - resolution: "@sindresorhus/is@npm:5.3.0" - checksum: b31cebabcdece3d5322de2a4dbc8c0f004e04147a00f2606787bcaf5655ad4b1954f6727fc6914c524009b2b9a2cc01c42835b55f651ce69fd2a0083b60bb852 - languageName: node - linkType: hard - "@sinonjs/commons@npm:^1.7.0": version: 1.8.3 resolution: "@sinonjs/commons@npm:1.8.3" @@ -1451,33 +1209,6 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^2.0.0": - version: 2.0.0 - resolution: "@sinonjs/commons@npm:2.0.0" - dependencies: - type-detect: 4.0.8 - checksum: 5023ba17edf2b85ed58262313b8e9b59e23c6860681a9af0200f239fe939e2b79736d04a260e8270ddd57196851dde3ba754d7230be5c5234e777ae2ca8af137 - languageName: node - linkType: hard - -"@sinonjs/fake-timers@npm:9.1.2, @sinonjs/fake-timers@npm:^9.1.2": - version: 9.1.2 - resolution: "@sinonjs/fake-timers@npm:9.1.2" - dependencies: - "@sinonjs/commons": ^1.7.0 - checksum: 7d3aef54e17c1073101cb64d953157c19d62a40e261a30923fa1ee337b049c5f29cc47b1f0c477880f42b5659848ba9ab897607ac8ea4acd5c30ddcfac57fca6 - languageName: node - linkType: hard - -"@sinonjs/fake-timers@npm:^7.0.4": - version: 7.1.2 - resolution: "@sinonjs/fake-timers@npm:7.1.2" - dependencies: - "@sinonjs/commons": ^1.7.0 - checksum: c84773d7973edad5511a31d2cc75023447b5cf714a84de9bb50eda45dda88a0d3bd2c30bf6e6e936da50a048d5352e2151c694e13e59b97d187ba1f329e9a00c - languageName: node - linkType: hard - "@sinonjs/fake-timers@npm:^8.0.1": version: 8.1.0 resolution: "@sinonjs/fake-timers@npm:8.1.0" @@ -1487,31 +1218,6 @@ __metadata: languageName: node linkType: hard -"@sinonjs/samsam@npm:^7.0.1": - version: 7.0.1 - resolution: "@sinonjs/samsam@npm:7.0.1" - dependencies: - "@sinonjs/commons": ^2.0.0 - lodash.get: ^4.4.2 - type-detect: ^4.0.8 - checksum: 291efb158d54c67dee23ddabcb28873d22063449b692aaa3b2a4f1826d2f79d38695574063c92e9c17573cc805cd6acbf0ab0c66c9f3aed7afd0f12a2b905615 - languageName: node - linkType: hard - -"@sinonjs/text-encoding@npm:^0.7.1": - version: 0.7.2 - resolution: "@sinonjs/text-encoding@npm:0.7.2" - checksum: fe690002a32ba06906cf87e2e8fe84d1590294586f2a7fd180a65355b53660c155c3273d8011a5f2b77209b819aa7306678ae6e4aea0df014bd7ffd4bbbcf1ab - languageName: node - linkType: hard - -"@sqltools/formatter@npm:^1.2.2": - version: 1.2.3 - resolution: "@sqltools/formatter@npm:1.2.3" - checksum: 5d80554b84ed15747fcfa6e488ef794c610c08152a53ebac0f270574ad938cdf39a02de7dfba4e9d9c33a790368f819945d315ee6dae360b220c29e092cba930 - languageName: node - linkType: hard - "@syuilo/aiscript@npm:0.11.1": version: 0.11.1 resolution: "@syuilo/aiscript@npm:0.11.1" @@ -1525,31 +1231,6 @@ __metadata: languageName: node linkType: hard -"@szmarczak/http-timer@npm:^4.0.5": - version: 4.0.6 - resolution: "@szmarczak/http-timer@npm:4.0.6" - dependencies: - defer-to-connect: ^2.0.0 - checksum: c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95 - languageName: node - linkType: hard - -"@szmarczak/http-timer@npm:^5.0.1": - version: 5.0.1 - resolution: "@szmarczak/http-timer@npm:5.0.1" - dependencies: - defer-to-connect: ^2.0.1 - checksum: fc9cb993e808806692e4a3337c90ece0ec00c89f4b67e3652a356b89730da98bc824273a6d67ca84d5f33cd85f317dcd5ce39d8cc0a2f060145a608a7cb8ce92 - languageName: node - linkType: hard - -"@tokenizer/token@npm:^0.3.0": - version: 0.3.0 - resolution: "@tokenizer/token@npm:0.3.0" - checksum: 1d575d02d2a9f0c5a4ca5180635ebd2ad59e0f18b42a65f3d04844148b49b3db35cf00b6012a1af2d59c2ab3caca59451c5689f747ba8667ee586ad717ee58e1 - languageName: node - linkType: hard - "@tootallnate/once@npm:1": version: 1.1.2 resolution: "@tootallnate/once@npm:1.1.2" @@ -1599,15 +1280,6 @@ __metadata: languageName: node linkType: hard -"@types/accepts@npm:*": - version: 1.3.5 - resolution: "@types/accepts@npm:1.3.5" - dependencies: - "@types/node": "*" - checksum: 590b7580570534a640510c071e09074cf63b5958b237a728f94322567350aea4d239f8a9d897a12b15c856b992ee4d7907e9812bb079886af2c00714e7fb3f60 - languageName: node - linkType: hard - "@types/argparse@npm:1.0.38": version: 1.0.38 resolution: "@types/argparse@npm:1.0.38" @@ -1656,112 +1328,13 @@ __metadata: languageName: node linkType: hard -"@types/bcryptjs@npm:2.4.2": - version: 2.4.2 - resolution: "@types/bcryptjs@npm:2.4.2" - checksum: 220dade7b0312b41e23ccfb15f2ddde7804eb3c7ef41db41a6c49054be1e19a15eb3dd8c8ef196494f0866307cce22ad6f3f272941387124707d81dc66155bbc - languageName: node - linkType: hard - -"@types/body-parser@npm:*": - version: 1.19.2 - resolution: "@types/body-parser@npm:1.19.2" - dependencies: - "@types/connect": "*" - "@types/node": "*" - checksum: e17840c7d747a549f00aebe72c89313d09fbc4b632b949b2470c5cb3b1cb73863901ae84d9335b567a79ec5efcfb8a28ff8e3f36bc8748a9686756b6d5681f40 - languageName: node - linkType: hard - -"@types/bull@npm:3.15.8": - version: 3.15.8 - resolution: "@types/bull@npm:3.15.8" - dependencies: - "@types/ioredis": "*" - "@types/redis": ^2.8.0 - checksum: 9a081a67d563545269dd2467419d43e833d3511afa5ef16ffa97f18ed178199088b810c8e5951dfad15929fccb712d67351f7c6d0640aee6b66fd726615ba01b - languageName: node - linkType: hard - -"@types/cacheable-request@npm:^6.0.1, @types/cacheable-request@npm:^6.0.2": - version: 6.0.2 - resolution: "@types/cacheable-request@npm:6.0.2" - dependencies: - "@types/http-cache-semantics": "*" - "@types/keyv": "*" - "@types/node": "*" - "@types/responselike": "*" - checksum: 667d25808dbf46fe104d6f029e0281ff56058d50c7c1b9182774b3e38bb9c1124f56e4c367ba54f92dbde2d1cc573f26eb0e9748710b2822bc0fd1e5498859c6 - languageName: node - linkType: hard - -"@types/cbor@npm:6.0.0": - version: 6.0.0 - resolution: "@types/cbor@npm:6.0.0" - dependencies: - cbor: "*" - checksum: 1100c9bc4043218100ac82948827b47c0a02927fc7283abac3aa9f6e1c2a69c4314f9e56a1b122cb0ac580e646db8f93f5d5f2f9d6392dbaf1ddc724943509d3 - languageName: node - linkType: hard - -"@types/color-convert@npm:^2.0.0": - version: 2.0.0 - resolution: "@types/color-convert@npm:2.0.0" - dependencies: - "@types/color-name": "*" - checksum: 027b68665dc2278cc2d83e796ada0a05a08aa5a11297e227c48c7f9f6eac518dec98578ab0072bd211963d3e4b431da70b20ea28d6c3136d0badfd3f9913baee - languageName: node - linkType: hard - -"@types/color-name@npm:*, @types/color-name@npm:^1.1.1": +"@types/color-name@npm:^1.1.1": version: 1.1.1 resolution: "@types/color-name@npm:1.1.1" checksum: b71fcad728cc68abcba1d405742134410c8f8eb3c2ef18113b047afca158ad23a4f2c229bcf71a38f4a818dead375c45b20db121d0e69259c2d81e97a740daa6 languageName: node linkType: hard -"@types/connect@npm:*": - version: 3.4.35 - resolution: "@types/connect@npm:3.4.35" - dependencies: - "@types/node": "*" - checksum: fe81351470f2d3165e8b12ce33542eef89ea893e36dd62e8f7d72566dfb7e448376ae962f9f3ea888547ce8b55a40020ca0e01d637fab5d99567673084542641 - languageName: node - linkType: hard - -"@types/content-disposition@npm:*": - version: 0.5.5 - resolution: "@types/content-disposition@npm:0.5.5" - checksum: fdf7379db1d509990bcf9a21d85f05aad878596f28b1418f9179f6436cb22513262c670ce88c6055054a7f5804a9303eeacb70aa59a5e11ffdc1434559db9692 - languageName: node - linkType: hard - -"@types/cookies@npm:*": - version: 0.7.7 - resolution: "@types/cookies@npm:0.7.7" - dependencies: - "@types/connect": "*" - "@types/express": "*" - "@types/keygrip": "*" - "@types/node": "*" - checksum: d3759efc1182cb0651808570ae13638677b67b0ea724eef7b174e58ffe6ea044b62c7c2715e532f76f88fce4dd8101ed32ac6fbb73226db654017924e8a2a1e6 - languageName: node - linkType: hard - -"@types/disposable-email-domains@npm:^1.0.1": - version: 1.0.2 - resolution: "@types/disposable-email-domains@npm:1.0.2" - checksum: ddff079e5644c65d6684f56ae0bdb10ee75d3fcbbd19f6a69e0ea7c6eebdc7de540defe5a3f5988e5ec010a47fec981ec1e10de13cbd570c654ac8a34d3e776b - languageName: node - linkType: hard - -"@types/escape-regexp@npm:0.0.1": - version: 0.0.1 - resolution: "@types/escape-regexp@npm:0.0.1" - checksum: 4a57cab3d8d4cb62007e0a579abc1667324a58a794fe73b46b7279d93b0031bbfdbb9a56a5cfcbe0b6bdc5fe149e037b7fc84ae167188ea8b23cebb8b28cbe4c - languageName: node - linkType: hard - "@types/eslint@npm:^7.2.13": version: 7.29.0 resolution: "@types/eslint@npm:7.29.0" @@ -1800,38 +1373,6 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:^4.17.18": - version: 4.17.30 - resolution: "@types/express-serve-static-core@npm:4.17.30" - dependencies: - "@types/node": "*" - "@types/qs": "*" - "@types/range-parser": "*" - checksum: c40d9027884ab9e97fa29d9d41d1b75a5966109312e26594cf03c61b278b5bf8e095f53589e47899b34a2e224291a44043617695c3e8bd22284f988e48582ee6 - languageName: node - linkType: hard - -"@types/express@npm:*": - version: 4.17.13 - resolution: "@types/express@npm:4.17.13" - dependencies: - "@types/body-parser": "*" - "@types/express-serve-static-core": ^4.17.18 - "@types/qs": "*" - "@types/serve-static": "*" - checksum: 12a2a0e6c4b993fc0854bec665906788aea0d8ee4392389d7a98a5de1eefdd33c9e1e40a91f3afd274011119c506f7b4126acb97fae62ae20b654974d44cba12 - languageName: node - linkType: hard - -"@types/fluent-ffmpeg@npm:2.1.20": - version: 2.1.20 - resolution: "@types/fluent-ffmpeg@npm:2.1.20" - dependencies: - "@types/node": "*" - checksum: f810a12a427f8e6568cec2ef250693e533112b66b7cd9ca9e10fe32b2aecb85623b8bb4ae5e51e47f79e27cdb969d3115720588b9f6fc7e4d17b4d4dc5e0d677 - languageName: node - linkType: hard - "@types/glob-stream@npm:*": version: 6.1.0 resolution: "@types/glob-stream@npm:6.1.0" @@ -1883,43 +1424,6 @@ __metadata: languageName: node linkType: hard -"@types/http-assert@npm:*": - version: 1.5.3 - resolution: "@types/http-assert@npm:1.5.3" - checksum: 9553e5a0b8bcfdac4b51d3fa3b89a91b5450171861a667a5b4c47204e0f4a1ca865d97396e6ceaf220e87b64d06b7a8bad7bfba15ef97acb41a87507c9940dbc - languageName: node - linkType: hard - -"@types/http-cache-semantics@npm:*": - version: 4.0.1 - resolution: "@types/http-cache-semantics@npm:4.0.1" - checksum: 1048aacf627829f0d5f00184e16548205cd9f964bf0841c29b36bc504509230c40bc57c39778703a1c965a6f5b416ae2cbf4c1d4589c889d2838dd9dbfccf6e9 - languageName: node - linkType: hard - -"@types/http-errors@npm:*": - version: 1.8.2 - resolution: "@types/http-errors@npm:1.8.2" - checksum: ecc365eea98d7eca650d593e742571acc3003742f0dd0fbbb15b8fce286e0f7421644b4140fb9bf701bbb7f1b744aea3967ebe025f0f0811aa5ab2c3d40fe111 - languageName: node - linkType: hard - -"@types/ioredis@npm:*": - version: 4.28.10 - resolution: "@types/ioredis@npm:4.28.10" - dependencies: - "@types/node": "*" - checksum: 0f2788cf25f490d3b345db8c5f8b8ce3f6c92cc99abcf744c8f974f02b9b3875233b3d22098614c462a0d6c41c523bd655509418ea88eb6249db6652290ce7cf - languageName: node - linkType: hard - -"@types/is-url@npm:1.2.30": - version: 1.2.30 - resolution: "@types/is-url@npm:1.2.30" - checksum: 81f337a7442f56d160ec0f63b2847cff368c5701f46a32c3c4fff5a1424b6afb4ad3eb7206febd26760a869680d61b7626e41f0ad652003f5783bcfda5b41d9e - languageName: node - linkType: hard - "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -1955,32 +1459,7 @@ __metadata: languageName: node linkType: hard -"@types/js-yaml@npm:4.0.5": - version: 4.0.5 - resolution: "@types/js-yaml@npm:4.0.5" - checksum: 7dcac8c50fec31643cc9d6444b5503239a861414cdfaa7ae9a38bc22597c4d850c4b8cec3d82d73b3fbca408348ce223b0408d598b32e094470dfffc6d486b4d - languageName: node - linkType: hard - -"@types/jsdom@npm:16.2.14": - version: 16.2.14 - resolution: "@types/jsdom@npm:16.2.14" - dependencies: - "@types/node": "*" - "@types/parse5": "*" - "@types/tough-cookie": "*" - checksum: 12bb926fa74ea07c0ba0bfd5bf185ac0fd771b28666a5e8784b9af4bb96bb0c51fc5f494eff7da1d3cd804e4757f640a23c344c1cd5d188f95ab0ab51770d88b - languageName: node - linkType: hard - -"@types/json-buffer@npm:~3.0.0": - version: 3.0.0 - resolution: "@types/json-buffer@npm:3.0.0" - checksum: 6b0a371dd603f0eec9d00874574bae195382570e832560dadf2193ee0d1062b8e0694bbae9798bc758632361c227b1e3b19e3bd914043b498640470a2da38b77 - languageName: node - linkType: hard - -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.9": version: 7.0.11 resolution: "@types/json-schema@npm:7.0.11" checksum: 527bddfe62db9012fccd7627794bd4c71beb77601861055d87e3ee464f2217c85fca7a4b56ae677478367bbd248dbde13553312b7d4dbc702a2f2bbf60c4018d @@ -1994,20 +1473,6 @@ __metadata: languageName: node linkType: hard -"@types/jsonld@npm:1.5.6": - version: 1.5.6 - resolution: "@types/jsonld@npm:1.5.6" - checksum: 66cf4586b6336c3d7024bcf3d07e2d31b9eee8dd0f57af1caa991ce0c70fcdf7ea011496d2c0ccf15ce5d40f88cf3218eb5cb1c011ab512564d0b2694314d4af - languageName: node - linkType: hard - -"@types/jsrsasign@npm:10.5.1": - version: 10.5.1 - resolution: "@types/jsrsasign@npm:10.5.1" - checksum: 358937dceec9e97b547062ab05b2994a3b5c9c96ab9c1d8a6c6120279a02940924ad52594fbb5b7f2068923a2755df2298dde863f6fd2b8e295d97f86166dbcb - languageName: node - linkType: hard - "@types/katex@npm:0.14.0": version: 0.14.0 resolution: "@types/katex@npm:0.14.0" @@ -2015,137 +1480,6 @@ __metadata: languageName: node linkType: hard -"@types/keygrip@npm:*": - version: 1.0.2 - resolution: "@types/keygrip@npm:1.0.2" - checksum: 60bc2738a4f107070ee3d96f44709cb38f3a96c7ccabab09f56c1b2b4d85f869fd8fb9f1f2937e863d0e9e781f005c2223b823bf32b859185b4f52370c352669 - languageName: node - linkType: hard - -"@types/keyv@npm:*": - version: 3.1.4 - resolution: "@types/keyv@npm:3.1.4" - dependencies: - "@types/node": "*" - checksum: e009a2bfb50e90ca9b7c6e8f648f8464067271fd99116f881073fa6fa76dc8d0133181dd65e6614d5fb1220d671d67b0124aef7d97dc02d7e342ab143a47779d - languageName: node - linkType: hard - -"@types/koa-bodyparser@npm:4.3.7": - version: 4.3.7 - resolution: "@types/koa-bodyparser@npm:4.3.7" - dependencies: - "@types/koa": "*" - checksum: 3a2cac14cb4a720d017d7708fbe9e8a310b5ecebbe62703a2606bb48c775fbeaf9fd601ba9cb7add03c7059f90327d8bcad3d70443eaea383b117b5c020054ec - languageName: node - linkType: hard - -"@types/koa-compose@npm:*": - version: 3.2.5 - resolution: "@types/koa-compose@npm:3.2.5" - dependencies: - "@types/koa": "*" - checksum: 5d1147c4b057eb158195f442f0384f06503f3e69dba99fb517b30a05261a9f92928945c12bb1cfc17a5b7d60db003f38b455a3a9b125f12e4fc81fffa396b3cf - languageName: node - linkType: hard - -"@types/koa-cors@npm:0.0.2": - version: 0.0.2 - resolution: "@types/koa-cors@npm:0.0.2" - dependencies: - "@types/koa": "*" - checksum: 7218bd8f4600fede227626e01fabe2022c691ee8721945792eb3dba3b348b10ddc438c3a679734de783172be512eb6b780d0600ed7052c3f881ed234a601656e - languageName: node - linkType: hard - -"@types/koa-favicon@npm:2.0.21": - version: 2.0.21 - resolution: "@types/koa-favicon@npm:2.0.21" - dependencies: - "@types/koa": "*" - checksum: 7e3da0dd430a96a007845be4d0d918281d5177ee36511c182f23048179168aac9b8b1fcd2a3de0b6e0f89b70fcb2a23c7b278de735e6daf71ef5cdea50761593 - languageName: node - linkType: hard - -"@types/koa-logger@npm:3.1.2": - version: 3.1.2 - resolution: "@types/koa-logger@npm:3.1.2" - dependencies: - "@types/koa": "*" - checksum: 8e4cfcdb2491052bbff35c8f0a1f0bf839e966c3903afcf39656bf21bd3089b1a50945ce6a92bea430a83c9341d714c968360953d3a52a5cc10cdb3fb0af4218 - languageName: node - linkType: hard - -"@types/koa-mount@npm:4.0.1": - version: 4.0.1 - resolution: "@types/koa-mount@npm:4.0.1" - dependencies: - "@types/koa": "*" - checksum: c010bfe6b2d81e6b1ca163b7699a5a0c90414079fcbc1d44c4005c896486db0d22b8220bd5f68a1cca7be481dba6a3b4f1c05b6affd80954c724d81170532f33 - languageName: node - linkType: hard - -"@types/koa-send@npm:4.1.3": - version: 4.1.3 - resolution: "@types/koa-send@npm:4.1.3" - dependencies: - "@types/koa": "*" - checksum: f20f6a0dcccd0d090348c7cce3635220cc82420b9579fa521dc6deae23c242aa8adb760a5a3fc84d7590a7f393b41b71b18312f9519c1c4a0b16ee24aae2e104 - languageName: node - linkType: hard - -"@types/koa-views@npm:7.0.0": - version: 7.0.0 - resolution: "@types/koa-views@npm:7.0.0" - dependencies: - koa-views: "*" - checksum: 03253380413e82806ef14c8f3ecadd659e0b00e4e8483fa9d8fe52f369edbfff02a779a5e09fca11eeaabf555dcc139378c8b123d1e4fc49125cb14ed27b3cab - languageName: node - linkType: hard - -"@types/koa@npm:*, @types/koa@npm:2.13.5": - version: 2.13.5 - resolution: "@types/koa@npm:2.13.5" - dependencies: - "@types/accepts": "*" - "@types/content-disposition": "*" - "@types/cookies": "*" - "@types/http-assert": "*" - "@types/http-errors": "*" - "@types/keygrip": "*" - "@types/koa-compose": "*" - "@types/node": "*" - checksum: e3b634d934b79ce8f394bf4130511596081f9c073dbfb4309aa32e4c421c47049a002b65111f8d9687eabec55d5a27b1b9ae0699afa83894cb7032c3536bfa17 - languageName: node - linkType: hard - -"@types/koa__cors@npm:3.1.1": - version: 3.1.1 - resolution: "@types/koa__cors@npm:3.1.1" - dependencies: - "@types/koa": "*" - checksum: 7ef2c09a453383f7813c25657633a3fe1c3321fe5cdd30a5806aa8be61d02f475deeb44a472bae046c48849d4a60165410b3ea0a7b931ed720da02e3f26515f7 - languageName: node - linkType: hard - -"@types/koa__multer@npm:2.0.4": - version: 2.0.4 - resolution: "@types/koa__multer@npm:2.0.4" - dependencies: - "@types/koa": "*" - checksum: 4a945061a6a44ef981081132e85ce9d0c171c4a895439b511313d628f0567d2624635166ac3e2455bdc216b15da83819ab52537f9609bfa1540ffc0d1b09519b - languageName: node - linkType: hard - -"@types/koa__router@npm:8.0.11": - version: 8.0.11 - resolution: "@types/koa__router@npm:8.0.11" - dependencies: - "@types/koa": "*" - checksum: 81f55ed77273871728c81a20fa546ee906bebfe72fd72e3723d983a19504eb7d9578908a0fb8ef230764c5495031412df4245eee93479161bd7bd5135ca1ea04 - languageName: node - linkType: hard - "@types/matter-js@npm:0.17.7": version: 0.17.7 resolution: "@types/matter-js@npm:0.17.7" @@ -2153,13 +1487,6 @@ __metadata: languageName: node linkType: hard -"@types/mime@npm:*": - version: 3.0.1 - resolution: "@types/mime@npm:3.0.1" - checksum: 4040fac73fd0cea2460e29b348c1a6173da747f3a87da0dbce80dd7a9355a3d0e51d6d9a401654f3e5550620e3718b5a899b2ec1debf18424e298a2c605346e7 - languageName: node - linkType: hard - "@types/minimatch@npm:*": version: 3.0.3 resolution: "@types/minimatch@npm:3.0.3" @@ -2174,22 +1501,6 @@ __metadata: languageName: node linkType: hard -"@types/mocha@npm:9.1.1": - version: 9.1.1 - resolution: "@types/mocha@npm:9.1.1" - checksum: 516077c0acd9806dc78317f88aaac0df5aaf0bdc2f63dfdadeabdf0b0137953b6ca65472e6ff7c30bc93ce4e0ae76eae70e8d46764b9a8eae4877a928b6ef49a - languageName: node - linkType: hard - -"@types/node-fetch@npm:3.0.3": - version: 3.0.3 - resolution: "@types/node-fetch@npm:3.0.3" - dependencies: - node-fetch: "*" - checksum: 1d46abfda354d7d423f5873f6bd07550f170c869ca33740b0e170265d3b65f76f9cac5f83fa0c4aa7afee66a6e1b14d4c4edd0297133fd186fef21fb53727413 - languageName: node - linkType: hard - "@types/node@npm:*": version: 16.6.2 resolution: "@types/node@npm:16.6.2" @@ -2211,13 +1522,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^14.11.8": - version: 14.18.26 - resolution: "@types/node@npm:14.18.26" - checksum: c6ac3f9d4f6f77c0fa5ac17757779ad4d9835abfe3af5708b7552597cb5c7c1103e83499ccaf767a1cfa70040990bcf3f58761c547051dc8d5077f3c419091b1 - languageName: node - linkType: hard - "@types/node@npm:^14.14.31, @types/node@npm:^14.14.41": version: 14.17.9 resolution: "@types/node@npm:14.17.9" @@ -2225,15 +1529,6 @@ __metadata: languageName: node linkType: hard -"@types/nodemailer@npm:6.4.5": - version: 6.4.5 - resolution: "@types/nodemailer@npm:6.4.5" - dependencies: - "@types/node": "*" - checksum: ecbe34a6eb5559bdca58115b5c03ff048896e14dc7ea6426b3fdfc03484764eb9e4100fcff37b96d012543ca47238c973d0f5bbfb1ab5a2d1f1f25d494714c59 - languageName: node - linkType: hard - "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -2241,26 +1536,6 @@ __metadata: languageName: node linkType: hard -"@types/parse5@npm:*": - version: 7.0.0 - resolution: "@types/parse5@npm:7.0.0" - dependencies: - parse5: "*" - checksum: ae9ffc515ac3130e125922ef4cc1cd98ec29f2b841b5c5da4b3ca47b7d580b38ffbc0f82554739ffb3cf620876d662434af8ffbcec229687512263fdeb183bed - languageName: node - linkType: hard - -"@types/pg@npm:^8.6.5": - version: 8.6.5 - resolution: "@types/pg@npm:8.6.5" - dependencies: - "@types/node": "*" - pg-protocol: "*" - pg-types: ^2.2.0 - checksum: 49ef858d95e84a6c1be193d367996581c991cf383d8628449507cc4ce67ef43b75acb18a0f9f49f870594cdec3501ddf4d2ac3fbe0dcdaab35c295c038d47199 - languageName: node - linkType: hard - "@types/prettier@npm:^2.1.5": version: 2.7.0 resolution: "@types/prettier@npm:2.7.0" @@ -2268,13 +1543,6 @@ __metadata: languageName: node linkType: hard -"@types/pug@npm:2.0.6": - version: 2.0.6 - resolution: "@types/pug@npm:2.0.6" - checksum: e8d09c3ddc7e6b87050a16d73694518f259a8ed74a0ab79b81b847baf89d92d44959ed68199966ac6f4a218c715c9bb3e4c86c8800d4868a4a674f4b21d2f01d - languageName: node - linkType: hard - "@types/punycode@npm:2.1.0": version: 2.1.0 resolution: "@types/punycode@npm:2.1.0" @@ -2282,95 +1550,6 @@ __metadata: languageName: node linkType: hard -"@types/qrcode@npm:1.5.0": - version: 1.5.0 - resolution: "@types/qrcode@npm:1.5.0" - dependencies: - "@types/node": "*" - checksum: b0ece3834ca5ba6171132928fd1ef764772dc619b45cb4123461ee05e377ad15553a330d234c69db0d0028c6639a99429e88d99192fbba9c5ee97c23f278c48b - languageName: node - linkType: hard - -"@types/qs@npm:*": - version: 6.9.7 - resolution: "@types/qs@npm:6.9.7" - checksum: 7fd6f9c25053e9b5bb6bc9f9f76c1d89e6c04f7707a7ba0e44cc01f17ef5284adb82f230f542c2d5557d69407c9a40f0f3515e8319afd14e1e16b5543ac6cdba - languageName: node - linkType: hard - -"@types/random-seed@npm:0.3.3": - version: 0.3.3 - resolution: "@types/random-seed@npm:0.3.3" - checksum: de0f4ce70d47af68424b9bfdb27fd07e2be31cbbafee1ef7f4e661b94576b2cf6a2f1f479697d5b79186bf70bad0988485a7d2ce76084fe7c0544d32b2ec0a67 - languageName: node - linkType: hard - -"@types/range-parser@npm:*": - version: 1.2.4 - resolution: "@types/range-parser@npm:1.2.4" - checksum: b7c0dfd5080a989d6c8bb0b6750fc0933d9acabeb476da6fe71d8bdf1ab65e37c136169d84148034802f48378ab94e3c37bb4ef7656b2bec2cb9c0f8d4146a95 - languageName: node - linkType: hard - -"@types/ratelimiter@npm:3.4.3": - version: 3.4.3 - resolution: "@types/ratelimiter@npm:3.4.3" - dependencies: - "@types/redis": ^2.8.0 - checksum: 1a571b64c15a0b0be61e86c34ce1c57adad9bcf8777528658051ddc92ec29811029a81327d6095d95486babcdd354c93adfbb5036f577ab44c71c344bb5cda5d - languageName: node - linkType: hard - -"@types/redis@npm:4.0.11": - version: 4.0.11 - resolution: "@types/redis@npm:4.0.11" - dependencies: - redis: "*" - checksum: 4b2d252368a7dc78738a98a8bcc817f92ca097511a901427628566f24f53998a2b1b524a85dea6a19307d4a95b55bd7d5a6ad7cc944ae8f39e54ca2f28ddf893 - languageName: node - linkType: hard - -"@types/redis@npm:^2.8.0": - version: 2.8.32 - resolution: "@types/redis@npm:2.8.32" - dependencies: - "@types/node": "*" - checksum: 2b12103e05977941870c9a248f6ea51f4b7ad7e0f16a7403799c2ed1b3e63b60f693c39f9186be0ea02776934c4595ddcd2a5bde41e530aaad42d26449f6a669 - languageName: node - linkType: hard - -"@types/rename@npm:1.0.4": - version: 1.0.4 - resolution: "@types/rename@npm:1.0.4" - checksum: 259539f8795ce056de02ef937a04711bd373e07bf46b522fdb77b39497aa01407fe1c942e00d7988ae58d7d112c46eae9b9c47aace65a2894e818efebd74baf2 - languageName: node - linkType: hard - -"@types/responselike@npm:*, @types/responselike@npm:^1.0.0": - version: 1.0.0 - resolution: "@types/responselike@npm:1.0.0" - dependencies: - "@types/node": "*" - checksum: e99fc7cc6265407987b30deda54c1c24bb1478803faf6037557a774b2f034c5b097ffd65847daa87e82a61a250d919f35c3588654b0fdaa816906650f596d1b0 - languageName: node - linkType: hard - -"@types/sanitize-html@npm:2.6.2": - version: 2.6.2 - resolution: "@types/sanitize-html@npm:2.6.2" - dependencies: - htmlparser2: ^6.0.0 - checksum: 08b43427427cbd8acd2843bbf9e00576c06e3916fc523d27fd9016f39563f7999f78b632ff473ef83a77f86bdea9286de2f81e3a8f8a05af6721687651c84f1c - languageName: node - linkType: hard - -"@types/semver@npm:7.3.12": - version: 7.3.12 - resolution: "@types/semver@npm:7.3.12" - checksum: 35536b2fc5602904f21cae681f6c9498e177dab3f54ae37c92f9a1b7e43c35f18bcd81e1c98c1cf0d33ee046bb06c771e9928c1c00a401d56a03f56549252a15 - languageName: node - linkType: hard - "@types/semver@npm:^7.3.12": version: 7.3.13 resolution: "@types/semver@npm:7.3.13" @@ -2378,41 +1557,6 @@ __metadata: languageName: node linkType: hard -"@types/serve-static@npm:*": - version: 1.15.0 - resolution: "@types/serve-static@npm:1.15.0" - dependencies: - "@types/mime": "*" - "@types/node": "*" - checksum: b6ac93d471fb0f53ddcac1f9b67572a09cd62806f7db5855244b28f6f421139626f24799392566e97d1ffc61b12f9de7f30380c39fcae3c8a161fe161d44edf2 - languageName: node - linkType: hard - -"@types/sharp@npm:0.30.5": - version: 0.30.5 - resolution: "@types/sharp@npm:0.30.5" - dependencies: - "@types/node": "*" - checksum: 8aa458d4c4187ae9a69894904832ecfe7533e0c405d1a7971a9984b0996eb6eb2ced103854b71199cf8df1350540bb4d3625c671a1946bd417b2bf4405c7292a - languageName: node - linkType: hard - -"@types/sinon@npm:^10.0.13": - version: 10.0.13 - resolution: "@types/sinon@npm:10.0.13" - dependencies: - "@types/sinonjs__fake-timers": "*" - checksum: 46a14c888db50f0098ec53d451877e0111d878ec4a653b9e9ed7f8e54de386d6beb0e528ddc3e95cd3361a8ab9ad54e4cca33cd88d45b9227b83e9fc8fb6688a - languageName: node - linkType: hard - -"@types/sinonjs__fake-timers@npm:*, @types/sinonjs__fake-timers@npm:8.1.2": - version: 8.1.2 - resolution: "@types/sinonjs__fake-timers@npm:8.1.2" - checksum: bbc73a5ab6c0ec974929392f3d6e1e8db4ebad97ec506d785301e1c3d8a4f98a35b1aa95b97035daef02886fd8efd7788a2fa3ced2ec7105988bfd8dce61eedd - languageName: node - linkType: hard - "@types/sinonjs__fake-timers@npm:8.1.1": version: 8.1.1 resolution: "@types/sinonjs__fake-timers@npm:8.1.1" @@ -2427,15 +1571,6 @@ __metadata: languageName: node linkType: hard -"@types/speakeasy@npm:2.0.7": - version: 2.0.7 - resolution: "@types/speakeasy@npm:2.0.7" - dependencies: - "@types/node": "*" - checksum: 30152d950ea23654060ef596ea459935a9ea80ba4d9803b13fc9b02c7a27a7b5c96742f2cb00db51b19ba0e13ef9a16c1fd977042f61c9019b10c4191e2f1b97 - languageName: node - linkType: hard - "@types/stack-utils@npm:^2.0.0": version: 2.0.1 resolution: "@types/stack-utils@npm:2.0.1" @@ -2443,13 +1578,6 @@ __metadata: languageName: node linkType: hard -"@types/syslog-pro@npm:^1.0.0": - version: 1.0.0 - resolution: "@types/syslog-pro@npm:1.0.0" - checksum: d0dcd87efad8a629bba449f86a617605a3fbffa5c18a8b309c82e7b85036ac21cfd34711fd522f50528dd0f0d07bdb66261a6f9ef20f2a9133e847b2e717c1bc - languageName: node - linkType: hard - "@types/throttle-debounce@npm:5.0.0": version: 5.0.0 resolution: "@types/throttle-debounce@npm:5.0.0" @@ -2464,20 +1592,6 @@ __metadata: languageName: node linkType: hard -"@types/tmp@npm:0.2.3": - version: 0.2.3 - resolution: "@types/tmp@npm:0.2.3" - checksum: 0ca45e99b3b3c6959d5c4f4555f73c8007db540cfb0fbbb9373217f9ab85e67eef75193f51a1d6564b0ee6c6f5ffa259d1034d7f7530a5b7ce80acb94cfc4daa - languageName: node - linkType: hard - -"@types/tough-cookie@npm:*": - version: 4.0.2 - resolution: "@types/tough-cookie@npm:4.0.2" - checksum: e055556ffdaa39ad85ede0af192c93f93f986f4bd9e9426efdc2948e3e2632db3a4a584d4937dbf6d7620527419bc99e6182d3daf2b08685e710f2eda5291905 - languageName: node - linkType: hard - "@types/undertaker-registry@npm:*": version: 1.0.1 resolution: "@types/undertaker-registry@npm:1.0.1" @@ -2522,24 +1636,6 @@ __metadata: languageName: node linkType: hard -"@types/web-push@npm:3.3.2": - version: 3.3.2 - resolution: "@types/web-push@npm:3.3.2" - dependencies: - "@types/node": "*" - checksum: 0b635e63779b1b03af536373ea7a43cfadc993d941be45a7bccd6b891b5e361ecb0f10e5375a58c0f9d355697281bc366c6a5908110afd57361fef6f2a5017d7 - languageName: node - linkType: hard - -"@types/ws@npm:8.5.3": - version: 8.5.3 - resolution: "@types/ws@npm:8.5.3" - dependencies: - "@types/node": "*" - checksum: 0ce46f850d41383fcdc2149bcacc86d7232fa7a233f903d2246dff86e31701a02f8566f40af5f8b56d1834779255c04ec6ec78660fe0f9b2a69cf3d71937e4ae - languageName: node - linkType: hard - "@types/yargs-parser@npm:*": version: 21.0.0 resolution: "@types/yargs-parser@npm:21.0.0" @@ -2685,13 +1781,6 @@ __metadata: languageName: node linkType: hard -"@ungap/promise-all-settled@npm:1.1.2": - version: 1.1.2 - resolution: "@ungap/promise-all-settled@npm:1.1.2" - checksum: 08d37fdfa23a6fe8139f1305313562ebad973f3fac01bcce2773b2bda5bcb0146dfdcf3cb6a722cf0a5f2ca0bc56a827eac8f1e7b3beddc548f654addf1fc34c - languageName: node - linkType: hard - "@vitejs/plugin-vue@npm:^3.1.0": version: 3.1.0 resolution: "@vitejs/plugin-vue@npm:3.1.0" @@ -2814,39 +1903,20 @@ __metadata: languageName: node linkType: hard -"abab@npm:^2.0.3, abab@npm:^2.0.5, abab@npm:^2.0.6": +"abab@npm:^2.0.3, abab@npm:^2.0.5": version: 2.0.6 resolution: "abab@npm:2.0.6" checksum: 6ffc1af4ff315066c62600123990d87551ceb0aafa01e6539da77b0f5987ac7019466780bf480f1787576d4385e3690c81ccc37cfda12819bf510b8ab47e5a3e languageName: node linkType: hard -"abbrev@npm:1, abbrev@npm:^1.0.0": +"abbrev@npm:1": version: 1.1.1 resolution: "abbrev@npm:1.1.1" checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 languageName: node linkType: hard -"abort-controller@npm:3.0.0, abort-controller@npm:^3.0.0": - version: 3.0.0 - resolution: "abort-controller@npm:3.0.0" - dependencies: - event-target-shim: ^5.0.0 - checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 - languageName: node - linkType: hard - -"accepts@npm:^1.3.5": - version: 1.3.8 - resolution: "accepts@npm:1.3.8" - dependencies: - mime-types: ~2.1.34 - negotiator: 0.6.3 - checksum: 50c43d32e7b50285ebe84b613ee4a3aa426715a7d131b65b786e2ead0fd76b6b60091b9916d3478a75f11f162628a2139991b6c03ab3f1d9ab7c86075dc8eab4 - languageName: node - linkType: hard - "acorn-globals@npm:^6.0.0": version: 6.0.0 resolution: "acorn-globals@npm:6.0.0" @@ -2889,7 +1959,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.0": +"acorn@npm:^8.2.4, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.8.0": version: 8.8.0 resolution: "acorn@npm:8.8.0" bin: @@ -2928,28 +1998,7 @@ __metadata: languageName: node linkType: hard -"ajv-keywords@npm:^3.5.2": - version: 3.5.2 - resolution: "ajv-keywords@npm:3.5.2" - peerDependencies: - ajv: ^6.9.1 - checksum: 7dc5e5931677a680589050f79dcbe1fefbb8fea38a955af03724229139175b433c63c68f7ae5f86cf8f65d55eb7c25f75a046723e2e58296707617ca690feae9 - languageName: node - linkType: hard - -"ajv@npm:8.11.0": - version: 8.11.0 - resolution: "ajv@npm:8.11.0" - dependencies: - fast-deep-equal: ^3.1.1 - json-schema-traverse: ^1.0.0 - require-from-string: ^2.0.2 - uri-js: ^4.2.2 - checksum: 5e0ff226806763be73e93dd7805b634f6f5921e3e90ca04acdf8db81eed9d8d3f0d4c5f1213047f45ebbf8047ffe0c840fa1ef2ec42c3a644899f69aa72b5bef - languageName: node - linkType: hard - -"ajv@npm:^6.10.0, ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:~6.12.6": +"ajv@npm:^6.10.0, ajv@npm:^6.12.4, ajv@npm:~6.12.6": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -2968,13 +2017,6 @@ __metadata: languageName: node linkType: hard -"ansi-colors@npm:4.1.1, ansi-colors@npm:^4.1.1": - version: 4.1.1 - resolution: "ansi-colors@npm:4.1.1" - checksum: 138d04a51076cb085da0a7e2d000c5c0bb09f6e772ed5c65c53cb118d37f6c5f1637506d7155fb5f330f0abcf6f12fa2e489ac3f8cdab9da393bf1bb4f9a32b0 - languageName: node - linkType: hard - "ansi-colors@npm:^1.0.1": version: 1.1.0 resolution: "ansi-colors@npm:1.1.0" @@ -2984,6 +2026,13 @@ __metadata: languageName: node linkType: hard +"ansi-colors@npm:^4.1.1": + version: 4.1.1 + resolution: "ansi-colors@npm:4.1.1" + checksum: 138d04a51076cb085da0a7e2d000c5c0bb09f6e772ed5c65c53cb118d37f6c5f1637506d7155fb5f330f0abcf6f12fa2e489ac3f8cdab9da393bf1bb4f9a32b0 + languageName: node + linkType: hard + "ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" @@ -3063,13 +2112,6 @@ __metadata: languageName: node linkType: hard -"any-promise@npm:^1.0.0": - version: 1.3.0 - resolution: "any-promise@npm:1.3.0" - checksum: 0ee8a9bdbe882c90464d75d1f55cf027f5458650c4bd1f0467e65aec38ccccda07ca5844969ee77ed46d04e7dded3eaceb027e8d32f385688523fe305fa7e1de - languageName: node - linkType: hard - "anymatch@npm:^2.0.0": version: 2.0.0 resolution: "anymatch@npm:2.0.0" @@ -3100,13 +2142,6 @@ __metadata: languageName: node linkType: hard -"app-root-path@npm:^3.0.0": - version: 3.1.0 - resolution: "app-root-path@npm:3.1.0" - checksum: e3db3957aee197143a0f6c75e39fe89b19e7244f28b4f2944f7276a9c526d2a7ab2d115b4b2d70a51a65a9a3ca17506690e5b36f75a068a7e5a13f8c092389ba - languageName: node - linkType: hard - "append-buffer@npm:^1.0.2": version: 1.0.2 resolution: "append-buffer@npm:1.0.2" @@ -3116,13 +2151,6 @@ __metadata: languageName: node linkType: hard -"append-field@npm:^1.0.0": - version: 1.0.0 - resolution: "append-field@npm:1.0.0" - checksum: 482ba08acc0ecef00fe7da6bf2f8e48359a9905ee1af525f3120c9260c02e91eedf0579b59d898e8d8455b6c199e340bc0a2fd4b9e02adaa29a8a86c722b37f9 - languageName: node - linkType: hard - "aproba@npm:^1.0.3 || ^2.0.0": version: 2.0.0 resolution: "aproba@npm:2.0.0" @@ -3137,39 +2165,6 @@ __metadata: languageName: node linkType: hard -"archiver-utils@npm:^2.1.0": - version: 2.1.0 - resolution: "archiver-utils@npm:2.1.0" - dependencies: - glob: ^7.1.4 - graceful-fs: ^4.2.0 - lazystream: ^1.0.0 - lodash.defaults: ^4.2.0 - lodash.difference: ^4.5.0 - lodash.flatten: ^4.4.0 - lodash.isplainobject: ^4.0.6 - lodash.union: ^4.6.0 - normalize-path: ^3.0.0 - readable-stream: ^2.0.0 - checksum: 5665f40bde87ee82cb638177bdccca8cc6e55edea1b94338f7e6b56a1d9367b0d9a39e42b47866eaf84b8c67669a7d250900a226207ecc30fa163b52aae859a5 - languageName: node - linkType: hard - -"archiver@npm:5.3.1": - version: 5.3.1 - resolution: "archiver@npm:5.3.1" - dependencies: - archiver-utils: ^2.1.0 - async: ^3.2.3 - buffer-crc32: ^0.2.1 - readable-stream: ^3.6.0 - readdir-glob: ^1.0.0 - tar-stream: ^2.2.0 - zip-stream: ^4.1.0 - checksum: 905b198ed04d26c951b80545d45c7f2e0432ef89977a93af8a762501d659886e39dda0fbffb0d517ff3fa450a3d09a29146e4273c2170624e1988f889fb5302c - languageName: node - linkType: hard - "archy@npm:^1.0.0": version: 1.0.0 resolution: "archy@npm:1.0.0" @@ -3360,25 +2355,6 @@ __metadata: languageName: node linkType: hard -"asap@npm:~2.0.3": - version: 2.0.6 - resolution: "asap@npm:2.0.6" - checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d - languageName: node - linkType: hard - -"asn1.js@npm:^5.3.0": - version: 5.4.1 - resolution: "asn1.js@npm:5.4.1" - dependencies: - bn.js: ^4.0.0 - inherits: ^2.0.1 - minimalistic-assert: ^1.0.0 - safer-buffer: ^2.1.0 - checksum: 3786a101ac6f304bd4e9a7df79549a7561950a13d4bcaec0c7790d44c80d147c1a94ba3d4e663673406064642a40b23fcd6c82a9952468e386c1a1376d747f9a - languageName: node - linkType: hard - "asn1@npm:~0.2.3": version: 0.2.4 resolution: "asn1@npm:0.2.4" @@ -3388,13 +2364,6 @@ __metadata: languageName: node linkType: hard -"assert-never@npm:^1.2.1": - version: 1.2.1 - resolution: "assert-never@npm:1.2.1" - checksum: ea4f1756d90f55254c4dc7a20d6c5d5bc169160562aefe3d8756b598c10e695daf568f21b6d6b12245d7f3782d3ff83ef6a01ab75d487adfc6909470a813bf8c - languageName: node - linkType: hard - "assert-plus@npm:1.0.0, assert-plus@npm:^1.0.0": version: 1.0.0 resolution: "assert-plus@npm:1.0.0" @@ -3437,13 +2406,6 @@ __metadata: languageName: node linkType: hard -"async@npm:>=0.2.9, async@npm:^3.2.3": - version: 3.2.4 - resolution: "async@npm:3.2.4" - checksum: 43d07459a4e1d09b84a20772414aa684ff4de085cbcaec6eea3c7a8f8150e8c62aa6cd4e699fe8ee93c3a5b324e777d34642531875a0817a35697522c1b02e89 - languageName: node - linkType: hard - "async@npm:^3.2.0": version: 3.2.3 resolution: "async@npm:3.2.3" @@ -3502,23 +2464,6 @@ __metadata: languageName: node linkType: hard -"aws-sdk@npm:2.1165.0": - version: 2.1165.0 - resolution: "aws-sdk@npm:2.1165.0" - dependencies: - buffer: 4.9.2 - events: 1.1.1 - ieee754: 1.1.13 - jmespath: 0.16.0 - querystring: 0.2.0 - sax: 1.2.1 - url: 0.10.3 - uuid: 8.0.0 - xml2js: 0.4.19 - checksum: 0c76d867b95bb4101cbcd0b0f5867a52f631cdce1961971c283d026f474cdde7d221766d50b48428733119d1009050c91cfee8725cc2fe3ba9cc52cd7380900d - languageName: node - linkType: hard - "aws-sign2@npm:~0.7.0": version: 0.7.0 resolution: "aws-sign2@npm:0.7.0" @@ -3542,15 +2487,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^0.24.0": - version: 0.24.0 - resolution: "axios@npm:0.24.0" - dependencies: - follow-redirects: ^1.14.4 - checksum: 468cf496c08a6aadfb7e699bebdac02851e3043d4e7d282350804ea8900e30d368daa6e3cd4ab83b8ddb5a3b1e17a5a21ada13fc9cebd27b74828f47a4236316 - languageName: node - linkType: hard - "babel-jest@npm:^27.5.1": version: 27.5.1 resolution: "babel-jest@npm:27.5.1" @@ -3628,15 +2564,6 @@ __metadata: languageName: node linkType: hard -"babel-walk@npm:3.0.0-canary-5": - version: 3.0.0-canary-5 - resolution: "babel-walk@npm:3.0.0-canary-5" - dependencies: - "@babel/types": ^7.9.6 - checksum: 6fe7ee3889343a6602f665c28ea135956a0767d7f7ca5fc1d72c5243e2f6e9d8a64f51254bf2fd0cce47b79fceeccf7a357f37cfa755a509dfb930a21151837c - languageName: node - linkType: hard - "bach@npm:^1.0.0": version: 1.2.0 resolution: "bach@npm:1.2.0" @@ -3654,170 +2581,6 @@ __metadata: languageName: node linkType: hard -"backend@workspace:packages/backend": - version: 0.0.0-use.local - resolution: "backend@workspace:packages/backend" - dependencies: - "@bull-board/api": ^4.3.1 - "@bull-board/koa": ^4.3.1 - "@discordapp/twemoji": 14.0.2 - "@elastic/elasticsearch": 7.11.0 - "@koa/cors": 3.1.0 - "@koa/multer": 3.0.0 - "@koa/router": 9.0.1 - "@peertube/http-signature": 1.7.0 - "@redocly/openapi-core": 1.0.0-beta.97 - "@sinonjs/fake-timers": 9.1.2 - "@syuilo/aiscript": 0.11.1 - "@types/bcryptjs": 2.4.2 - "@types/bull": 3.15.8 - "@types/cbor": 6.0.0 - "@types/color-convert": ^2.0.0 - "@types/escape-regexp": 0.0.1 - "@types/fluent-ffmpeg": 2.1.20 - "@types/is-url": 1.2.30 - "@types/js-yaml": 4.0.5 - "@types/jsdom": 16.2.14 - "@types/jsonld": 1.5.6 - "@types/jsrsasign": 10.5.1 - "@types/koa": 2.13.5 - "@types/koa-bodyparser": 4.3.7 - "@types/koa-cors": 0.0.2 - "@types/koa-favicon": 2.0.21 - "@types/koa-logger": 3.1.2 - "@types/koa-mount": 4.0.1 - "@types/koa-send": 4.1.3 - "@types/koa-views": 7.0.0 - "@types/koa__cors": 3.1.1 - "@types/koa__multer": 2.0.4 - "@types/koa__router": 8.0.11 - "@types/mocha": 9.1.1 - "@types/node": 18.7.16 - "@types/node-fetch": 3.0.3 - "@types/nodemailer": 6.4.5 - "@types/pg": ^8.6.5 - "@types/pug": 2.0.6 - "@types/punycode": 2.1.0 - "@types/qrcode": 1.5.0 - "@types/random-seed": 0.3.3 - "@types/ratelimiter": 3.4.3 - "@types/redis": 4.0.11 - "@types/rename": 1.0.4 - "@types/sanitize-html": 2.6.2 - "@types/semver": 7.3.12 - "@types/sharp": 0.30.5 - "@types/sinon": ^10.0.13 - "@types/sinonjs__fake-timers": 8.1.2 - "@types/speakeasy": 2.0.7 - "@types/syslog-pro": ^1.0.0 - "@types/tinycolor2": 1.4.3 - "@types/tmp": 0.2.3 - "@types/uuid": 8.3.4 - "@types/web-push": 3.3.2 - "@types/ws": 8.5.3 - "@typescript-eslint/eslint-plugin": ^5.46.1 - "@typescript-eslint/parser": ^5.46.1 - abort-controller: 3.0.0 - ajv: 8.11.0 - archiver: 5.3.1 - autobind-decorator: 2.4.0 - aws-sdk: 2.1165.0 - bcryptjs: 2.4.3 - blurhash: 1.1.5 - bull: 4.8.4 - cacheable-lookup: 6.0.4 - cbor: 8.1.0 - chalk: 5.0.1 - chalk-template: 0.4.0 - cli-highlight: 2.1.11 - color-convert: 2.0.1 - content-disposition: 0.5.4 - cross-env: 7.0.3 - date-fns: 2.28.0 - deep-email-validator: 0.1.21 - escape-regexp: 0.0.1 - eslint: ^8.29.0 - eslint-plugin-foundkey-custom-rules: "file:../shared/custom-rules" - eslint-plugin-import: ^2.26.0 - execa: 6.1.0 - feed: 4.2.2 - file-type: 18.0.0 - fluent-ffmpeg: 2.1.2 - form-data: ^4.0.0 - foundkey-js: "workspace:*" - got: 12.3.1 - hpagent: 0.1.2 - ioredis: 4.28.5 - ip-cidr: 3.0.10 - is-svg: 4.3.2 - js-yaml: 4.1.0 - jsdom: 20.0.0 - json5: 2.2.1 - json5-loader: 4.0.1 - jsonld: 6.0.0 - jsrsasign: 10.5.25 - koa: 2.13.4 - koa-bodyparser: 4.3.0 - koa-favicon: 2.1.0 - koa-json-body: 5.3.0 - koa-logger: 3.2.1 - koa-mount: 4.0.0 - koa-send: 5.0.1 - koa-slow: 2.1.0 - koa-views: 7.0.2 - mfm-js: 0.22.1 - mime-types: 2.1.35 - mocha: 10.0.0 - multer: 1.4.5-lts.1 - nested-property: 4.0.0 - node-fetch: 3.2.6 - nodemailer: 6.7.6 - os-utils: 0.0.14 - parse5: 7.0.0 - pg: 8.7.3 - private-ip: 2.3.3 - probe-image-size: 7.2.3 - promise-limit: 2.7.0 - pug: 3.0.2 - punycode: 2.1.1 - pureimage: 0.3.14 - qrcode: 1.5.1 - random-seed: 0.3.0 - ratelimiter: 3.4.1 - re2: 1.17.8 - redis-lock: 0.1.4 - reflect-metadata: 0.1.13 - rename: 1.0.4 - require-all: 3.0.0 - rss-parser: 3.12.0 - sanitize-html: 2.7.0 - semver: 7.3.7 - sharp: 0.31.2 - sinon: ^14.0.2 - speakeasy: 2.0.0 - strict-event-emitter-types: 2.0.0 - stringz: 2.1.0 - style-loader: 3.3.1 - summaly: 2.6.0 - syslog-pro: 1.0.0 - systeminformation: 5.11.22 - tinycolor2: 1.4.2 - tmp: 0.2.1 - ts-loader: 9.3.1 - ts-node: 10.9.1 - tsc-alias: 1.7.0 - tsconfig-paths: 4.1.0 - twemoji-parser: 14.0.0 - typeorm: 0.3.7 - typescript: ^4.9.4 - unzipper: 0.10.11 - uuid: 8.3.2 - web-push: 3.5.0 - ws: 8.8.0 - xev: 3.0.2 - languageName: unknown - linkType: soft - "balanced-match@npm:^0.4.2": version: 0.4.2 resolution: "balanced-match@npm:0.4.2" @@ -3832,14 +2595,7 @@ __metadata: languageName: node linkType: hard -"base32.js@npm:0.0.1": - version: 0.0.1 - resolution: "base32.js@npm:0.0.1" - checksum: 74e520ca832b034c43c0996f52ffa29792fa2f2bb2b75fc8415fb6cd42194a486cac8a68531d73e401ae836194093e2746f7c4325e860ff0a6de8d05a54af132 - languageName: node - linkType: hard - -"base64-js@npm:^1.0.2, base64-js@npm:^1.3.1": +"base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 @@ -3870,27 +2626,6 @@ __metadata: languageName: node linkType: hard -"bcryptjs@npm:2.4.3": - version: 2.4.3 - resolution: "bcryptjs@npm:2.4.3" - checksum: 0e80ed852a41f5dfb1853f53ee14a7390b0ef263ce05dba6e2ef3cd919dfad025a7c21ebcfe5bc7fa04b100990edf90c7a877ff7fe623d3e479753253131b629 - languageName: node - linkType: hard - -"big-integer@npm:^1.6.17": - version: 1.6.51 - resolution: "big-integer@npm:1.6.51" - checksum: 3d444173d1b2e20747e2c175568bedeebd8315b0637ea95d75fd27830d3b8e8ba36c6af40374f36bdaea7b5de376dcada1b07587cb2a79a928fccdb6e6e3c518 - languageName: node - linkType: hard - -"big.js@npm:^5.2.2": - version: 5.2.2 - resolution: "big.js@npm:5.2.2" - checksum: b89b6e8419b097a8fb4ed2399a1931a68c612bce3cfd5ca8c214b2d017531191070f990598de2fc6f3f993d91c0f08aa82697717f6b3b8732c9731866d233c9e - languageName: node - linkType: hard - "binary-extensions@npm:^2.0.0": version: 2.0.0 resolution: "binary-extensions@npm:2.0.0" @@ -3898,16 +2633,6 @@ __metadata: languageName: node linkType: hard -"binary@npm:~0.3.0": - version: 0.3.0 - resolution: "binary@npm:0.3.0" - dependencies: - buffers: ~0.1.1 - chainsaw: ~0.1.0 - checksum: b4699fda9e2c2981e74a46b0115cf0d472eda9b68c0e9d229ef494e92f29ce81acf0a834415094cffcc340dfee7c4ef8ce5d048c65c18067a7ed850323f777af - languageName: node - linkType: hard - "binaryextensions@npm:^2.2.0": version: 2.3.0 resolution: "binaryextensions@npm:2.3.0" @@ -3915,17 +2640,6 @@ __metadata: languageName: node linkType: hard -"bl@npm:^4.0.3": - version: 4.1.0 - resolution: "bl@npm:4.1.0" - dependencies: - buffer: ^5.5.0 - inherits: ^2.0.4 - readable-stream: ^3.4.0 - checksum: 9e8521fa7e83aa9427c6f8ccdcba6e8167ef30cc9a22df26effcc5ab682ef91d2cbc23a239f945d099289e4bbcfae7a192e9c28c84c6202e710a0dfec3722662 - languageName: node - linkType: hard - "blob-util@npm:^2.0.2": version: 2.0.2 resolution: "blob-util@npm:2.0.2" @@ -3940,13 +2654,6 @@ __metadata: languageName: node linkType: hard -"bluebird@npm:~3.4.1": - version: 3.4.7 - resolution: "bluebird@npm:3.4.7" - checksum: bffa9dee7d3a41ab15c4f3f24687b49959b4e64e55c058a062176feb8ccefc2163414fb4e1a0f3053bf187600936509660c3ebd168fd9f0e48c7eba23b019466 - languageName: node - linkType: hard - "blurhash@npm:1.1.5": version: 1.1.5 resolution: "blurhash@npm:1.1.5" @@ -3954,14 +2661,7 @@ __metadata: languageName: node linkType: hard -"bn.js@npm:^4.0.0": - version: 4.12.0 - resolution: "bn.js@npm:4.12.0" - checksum: 39afb4f15f4ea537b55eaf1446c896af28ac948fdcf47171961475724d1bb65118cca49fa6e3d67706e4790955ec0e74de584e45c8f1ef89f46c812bee5b5a12 - languageName: node - linkType: hard - -"boolbase@npm:^1.0.0, boolbase@npm:~1.0.0": +"boolbase@npm:^1.0.0": version: 1.0.0 resolution: "boolbase@npm:1.0.0" checksum: 3e25c80ef626c3a3487c73dbfc70ac322ec830666c9ad915d11b701142fab25ec1e63eff2c450c74347acfd2de854ccde865cd79ef4db1683f7c7b046ea43bb0 @@ -4005,7 +2705,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.1, braces@npm:^3.0.2, braces@npm:~3.0.2": +"braces@npm:^3.0.1, braces@npm:~3.0.2": version: 3.0.2 resolution: "braces@npm:3.0.2" dependencies: @@ -4043,13 +2743,6 @@ __metadata: languageName: node linkType: hard -"browser-stdout@npm:1.3.1": - version: 1.3.1 - resolution: "browser-stdout@npm:1.3.1" - checksum: b717b19b25952dd6af483e368f9bcd6b14b87740c3d226c2977a65e84666ffd67000bddea7d911f111a9b6ddc822b234de42d52ab6507bce4119a4cc003ef7b3 - languageName: node - linkType: hard - "browserslist@npm:^1.3.6, browserslist@npm:^1.5.2, browserslist@npm:^1.7.6": version: 1.7.7 resolution: "browserslist@npm:1.7.7" @@ -4094,20 +2787,13 @@ __metadata: languageName: node linkType: hard -"buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13, buffer-crc32@npm:~0.2.3": +"buffer-crc32@npm:~0.2.3": version: 0.2.13 resolution: "buffer-crc32@npm:0.2.13" checksum: 06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c languageName: node linkType: hard -"buffer-equal-constant-time@npm:1.0.1": - version: 1.0.1 - resolution: "buffer-equal-constant-time@npm:1.0.1" - checksum: 80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab - languageName: node - linkType: hard - "buffer-equal@npm:^1.0.0": version: 1.0.0 resolution: "buffer-equal@npm:1.0.0" @@ -4122,32 +2808,7 @@ __metadata: languageName: node linkType: hard -"buffer-indexof-polyfill@npm:~1.0.0": - version: 1.0.2 - resolution: "buffer-indexof-polyfill@npm:1.0.2" - checksum: fbfb2d69c6bb2df235683126f9dc140150c08ac3630da149913a9971947b667df816a913b6993bc48f4d611999cb99a1589914d34c02dccd2234afda5cb75bbc - languageName: node - linkType: hard - -"buffer-writer@npm:2.0.0": - version: 2.0.0 - resolution: "buffer-writer@npm:2.0.0" - checksum: 11736b48bb75106c52ca8ec9f025e7c1b3b25ce31875f469d7210eabd5c576c329e34f6b805d4a8d605ff3f0db1e16342328802c4c963e9c826b0e43a4e631c2 - languageName: node - linkType: hard - -"buffer@npm:4.9.2": - version: 4.9.2 - resolution: "buffer@npm:4.9.2" - dependencies: - base64-js: ^1.0.2 - ieee754: ^1.1.4 - isarray: ^1.0.0 - checksum: 8801bc1ba08539f3be70eee307a8b9db3d40f6afbfd3cf623ab7ef41dffff1d0a31de0addbe1e66e0ca5f7193eeb667bfb1ecad3647f8f1b0750de07c13295c3 - languageName: node - linkType: hard - -"buffer@npm:^5.5.0, buffer@npm:^5.6.0": +"buffer@npm:^5.6.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -4157,56 +2818,6 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^6.0.3": - version: 6.0.3 - resolution: "buffer@npm:6.0.3" - dependencies: - base64-js: ^1.3.1 - ieee754: ^1.2.1 - checksum: 5ad23293d9a731e4318e420025800b42bf0d264004c0286c8cc010af7a270c7a0f6522e84f54b9ad65cbd6db20b8badbfd8d2ebf4f80fa03dab093b89e68c3f9 - languageName: node - linkType: hard - -"buffers@npm:~0.1.1": - version: 0.1.1 - resolution: "buffers@npm:0.1.1" - checksum: ad6f8e483efab39cefd92bdc04edbff6805e4211b002f4d1cfb70c6c472a61cc89fb18c37bcdfdd4ee416ca096e9ff606286698a7d41a18b539bac12fd76d4d5 - languageName: node - linkType: hard - -"bull@npm:4.8.4": - version: 4.8.4 - resolution: "bull@npm:4.8.4" - dependencies: - cron-parser: ^4.2.1 - debuglog: ^1.0.0 - get-port: ^5.1.1 - ioredis: ^4.28.5 - lodash: ^4.17.21 - msgpackr: ^1.5.2 - p-timeout: ^3.2.0 - semver: ^7.3.2 - uuid: ^8.3.0 - checksum: d9e15a38a610b63062d2fb449a6a66fc99db7760e4047f88ad9ed0e8b8b46c4be08e31bcecb5f144f406922602b91d6473bbfa6fe5093d50bc7901feafbd455b - languageName: node - linkType: hard - -"busboy@npm:^1.0.0": - version: 1.6.0 - resolution: "busboy@npm:1.6.0" - dependencies: - streamsearch: ^1.1.0 - checksum: 32801e2c0164e12106bf236291a00795c3c4e4b709ae02132883fe8478ba2ae23743b11c5735a0aae8afe65ac4b6ca4568b91f0d9fed1fdbc32ede824a73746e - languageName: node - linkType: hard - -"bytes@npm:3.1.2, bytes@npm:^3.1.0": - version: 3.1.2 - resolution: "bytes@npm:3.1.2" - checksum: e4bcd3948d289c5127591fbedf10c0b639ccbf00243504e4e127374a15c3bc8eed0d28d4aaab08ff6f1cf2abc0cce6ba3085ed32f4f90e82a5683ce0014e1b6e - languageName: node - linkType: hard - "cacache@npm:^16.1.0": version: 16.1.3 resolution: "cacache@npm:16.1.3" @@ -4250,52 +2861,6 @@ __metadata: languageName: node linkType: hard -"cache-content-type@npm:^1.0.0": - version: 1.0.1 - resolution: "cache-content-type@npm:1.0.1" - dependencies: - mime-types: ^2.1.18 - ylru: ^1.2.0 - checksum: 18db4d59452669ccbfd7146a1510a37eb28e9eccf18ca7a4eb603dff2edc5cccdca7498fc3042a2978f76f11151fba486eb9eb69d9afa3fb124957870aef4fd3 - languageName: node - linkType: hard - -"cacheable-lookup@npm:6.0.4": - version: 6.0.4 - resolution: "cacheable-lookup@npm:6.0.4" - checksum: 7aea70f5ea081aed12bf54fc165b9f80b580b0d210c85d55cc8fed2beaa9027fd321c1939c65dad945fe9eb207cea45442e01a48b5aa57542e125b716f022b6d - languageName: node - linkType: hard - -"cacheable-lookup@npm:^5.0.3": - version: 5.0.4 - resolution: "cacheable-lookup@npm:5.0.4" - checksum: 763e02cf9196bc9afccacd8c418d942fc2677f22261969a4c2c2e760fa44a2351a81557bd908291c3921fe9beb10b976ba8fa50c5ca837c5a0dd945f16468f2d - languageName: node - linkType: hard - -"cacheable-lookup@npm:^6.0.4": - version: 6.1.0 - resolution: "cacheable-lookup@npm:6.1.0" - checksum: 4e37afe897219b1035335b0765106a2c970ffa930497b43cac5000b860f3b17f48d004187279fae97e2e4cbf6a3693709b6d64af65279c7d6c8453321d36d118 - languageName: node - linkType: hard - -"cacheable-request@npm:^7.0.1, cacheable-request@npm:^7.0.2": - version: 7.0.2 - resolution: "cacheable-request@npm:7.0.2" - dependencies: - clone-response: ^1.0.2 - get-stream: ^5.1.0 - http-cache-semantics: ^4.0.0 - keyv: ^4.0.0 - lowercase-keys: ^2.0.0 - normalize-url: ^6.0.1 - responselike: ^2.0.0 - checksum: 6152813982945a5c9989cb457a6c499f12edcc7ade323d2fbfd759abc860bdbd1306e08096916bb413c3c47e812f8e4c0a0cc1e112c8ce94381a960f115bc77f - languageName: node - linkType: hard - "cachedir@npm:^2.3.0": version: 2.3.0 resolution: "cachedir@npm:2.3.0" @@ -4338,14 +2903,14 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": +"camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b languageName: node linkType: hard -"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0": +"camelcase@npm:^6.2.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -4378,13 +2943,6 @@ __metadata: languageName: node linkType: hard -"canonicalize@npm:^1.0.1": - version: 1.0.8 - resolution: "canonicalize@npm:1.0.8" - checksum: c31ea64160171bbcd7ac0dc081058fbcff055410a1d532d7b3959e7b02a3001c5d5f4f8bad934ed5246eafc9a928d333cc0c29846c16fb6d0be97b8fb444de3c - languageName: node - linkType: hard - "caseless@npm:~0.12.0": version: 0.12.0 resolution: "caseless@npm:0.12.0" @@ -4392,33 +2950,6 @@ __metadata: languageName: node linkType: hard -"cbor@npm:*, cbor@npm:8.1.0": - version: 8.1.0 - resolution: "cbor@npm:8.1.0" - dependencies: - nofilter: ^3.1.0 - checksum: a90338435dc7b45cc01461af979e3bb6ddd4f2a08584c437586039cd5f2235014c06e49d664295debbfb3514d87b2f06728092ab6aa6175e2e85e9cd7dc0c1fd - languageName: node - linkType: hard - -"chainsaw@npm:~0.1.0": - version: 0.1.0 - resolution: "chainsaw@npm:0.1.0" - dependencies: - traverse: ">=0.3.0 <0.4" - checksum: 22a96b9fb0cd9fb20813607c0869e61817d1acc81b5d455cc6456b5e460ea1dd52630e0f76b291cf8294bfb6c1fc42e299afb52104af9096242699d6d3aa6d3e - languageName: node - linkType: hard - -"chalk-template@npm:0.4.0": - version: 0.4.0 - resolution: "chalk-template@npm:0.4.0" - dependencies: - chalk: ^4.1.2 - checksum: 6c706802a79a7963cbce18f022b046fe86e438a67843151868852f80ea7346e975a6a9749991601e7e5d3b6a6c4852a04c53dc966a9a3d04031bd0e0ed53c819 - languageName: node - linkType: hard - "chalk@npm:4.0.0": version: 4.0.0 resolution: "chalk@npm:4.0.0" @@ -4429,13 +2960,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:5.0.1": - version: 5.0.1 - resolution: "chalk@npm:5.0.1" - checksum: 7b45300372b908f0471fbf7389ce2f5de8d85bb949026fd51a1b95b10d0ed32c7ed5aab36dd5e9d2bf3191867909b4404cef75c5f4d2d1daeeacd301dd280b76 - languageName: node - linkType: hard - "chalk@npm:^1.1.3": version: 1.1.3 resolution: "chalk@npm:1.1.3" @@ -4449,7 +2973,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.0.0, chalk@npm:^2.4.2": +"chalk@npm:^2.0.0": version: 2.4.2 resolution: "chalk@npm:2.4.2" dependencies: @@ -4460,7 +2984,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.2": +"chalk@npm:^4.0.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -4487,15 +3011,6 @@ __metadata: languageName: node linkType: hard -"character-parser@npm:^2.2.0": - version: 2.2.0 - resolution: "character-parser@npm:2.2.0" - dependencies: - is-regex: ^1.0.3 - checksum: 71826fae509d4dc3ef07c2e824da9c8853f910ba0d8fe699edaab263051fd3b8db77bb96e46ed896bb36ed1d86108e6d6ceedff436bec7786ba7f0b585a0bc93 - languageName: node - linkType: hard - "chart.js@npm:3.8.0": version: 3.8.0 resolution: "chart.js@npm:3.8.0" @@ -4539,30 +3054,6 @@ __metadata: languageName: node linkType: hard -"cheerio@npm:0.22.0": - version: 0.22.0 - resolution: "cheerio@npm:0.22.0" - dependencies: - css-select: ~1.2.0 - dom-serializer: ~0.1.0 - entities: ~1.1.1 - htmlparser2: ^3.9.1 - lodash.assignin: ^4.0.9 - lodash.bind: ^4.1.4 - lodash.defaults: ^4.0.1 - lodash.filter: ^4.4.0 - lodash.flatten: ^4.2.0 - lodash.foreach: ^4.3.0 - lodash.map: ^4.4.0 - lodash.merge: ^4.4.0 - lodash.pick: ^4.2.1 - lodash.reduce: ^4.4.0 - lodash.reject: ^4.4.0 - lodash.some: ^4.4.0 - checksum: b0a6cfa61eb7ae96e4cb8cfeeb14eb45bb790fa40098509268629c4cecca5b99124aabe6daa1154c497ac8def47bc3f9706cef5f0e8a6177a0c137d4bdaaf8b7 - languageName: node - linkType: hard - "chokidar@npm:^3.3.1": version: 3.3.1 resolution: "chokidar@npm:3.3.1" @@ -4582,13 +3073,6 @@ __metadata: languageName: node linkType: hard -"chownr@npm:^1.1.1": - version: 1.1.4 - resolution: "chownr@npm:1.1.4" - checksum: 115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d - languageName: node - linkType: hard - "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -4654,22 +3138,6 @@ __metadata: languageName: node linkType: hard -"cli-highlight@npm:2.1.11, cli-highlight@npm:^2.1.11": - version: 2.1.11 - resolution: "cli-highlight@npm:2.1.11" - dependencies: - chalk: ^4.0.0 - highlight.js: ^10.7.1 - mz: ^2.4.0 - parse5: ^5.1.1 - parse5-htmlparser2-tree-adapter: ^6.0.0 - yargs: ^16.0.0 - bin: - highlight: bin/highlight - checksum: 0a60e60545e39efea78c1732a25b91692017ec40fb6e9497208dc0eeeae69991d3923a8d6e4edd0543db3c395ed14529a33dd4d0353f1679c5b6dded792a8496 - languageName: node - linkType: hard - "cli-table3@npm:~0.6.1": version: 0.6.1 resolution: "cli-table3@npm:0.6.1" @@ -4769,17 +3237,6 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^6.0.0": - version: 6.0.0 - resolution: "cliui@npm:6.0.0" - dependencies: - string-width: ^4.2.0 - strip-ansi: ^6.0.0 - wrap-ansi: ^6.2.0 - checksum: 4fcfd26d292c9f00238117f39fc797608292ae36bac2168cfee4c85923817d0607fe21b3329a8621e01aedf512c99b7eaa60e363a671ffd378df6649fb48ae42 - languageName: node - linkType: hard - "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -4798,15 +3255,6 @@ __metadata: languageName: node linkType: hard -"clone-response@npm:^1.0.2": - version: 1.0.3 - resolution: "clone-response@npm:1.0.3" - dependencies: - mimic-response: ^1.0.0 - checksum: 4e671cac39b11c60aa8ba0a450657194a5d6504df51bca3fac5b3bd0145c4f8e8464898f87c8406b83232e3bc5cca555f51c1f9c8ac023969ebfbf7f6bdabb2e - languageName: node - linkType: hard - "clone-stats@npm:^1.0.0": version: 1.0.0 resolution: "clone-stats@npm:1.0.0" @@ -4839,37 +3287,6 @@ __metadata: languageName: node linkType: hard -"cluster-key-slot@npm:1.1.0, cluster-key-slot@npm:^1.1.0": - version: 1.1.0 - resolution: "cluster-key-slot@npm:1.1.0" - checksum: fc953c75209b1ef9088081bab4e40a0b2586491c974ab93460569c014515ca5a2e31c043f185285e177007162fc353d07836d98f570c171dbe055775430e495b - languageName: node - linkType: hard - -"co-body@npm:^5.0.0": - version: 5.2.0 - resolution: "co-body@npm:5.2.0" - dependencies: - inflation: ^2.0.0 - qs: ^6.4.0 - raw-body: ^2.2.0 - type-is: ^1.6.14 - checksum: 48e1ffe00b8717b68154a939fa19f36d75aa66bba627f2977f28d11b732da56bdda445acda7053f7a85dfbac8a09a8aa257bceedaff7b6467cb25ab08ada9c8d - languageName: node - linkType: hard - -"co-body@npm:^6.0.0": - version: 6.1.0 - resolution: "co-body@npm:6.1.0" - dependencies: - inflation: ^2.0.0 - qs: ^6.5.2 - raw-body: ^2.3.3 - type-is: ^1.6.16 - checksum: d0a78831a6651f2085fce16b0ecdc49f45fb5baf4f94148c2f499e7ec89d188205362548b9c500eae15a819360cfda208079e68a72c204cf66ca3ffa2fc0f57e - languageName: node - linkType: hard - "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -4921,15 +3338,6 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:2.0.1, color-convert@npm:^2.0.1": - version: 2.0.1 - resolution: "color-convert@npm:2.0.1" - dependencies: - color-name: ~1.1.4 - checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336 - languageName: node - linkType: hard - "color-convert@npm:^1.3.0, color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -4939,6 +3347,15 @@ __metadata: languageName: node linkType: hard +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: ~1.1.4 + checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336 + languageName: node + linkType: hard + "color-name@npm:1.1.3": version: 1.1.3 resolution: "color-name@npm:1.1.3" @@ -4962,16 +3379,6 @@ __metadata: languageName: node linkType: hard -"color-string@npm:^1.9.0": - version: 1.9.1 - resolution: "color-string@npm:1.9.1" - dependencies: - color-name: ^1.0.0 - simple-swizzle: ^0.2.2 - checksum: c13fe7cff7885f603f49105827d621ce87f4571d78ba28ef4a3f1a104304748f620615e6bf065ecd2145d0d9dad83a3553f52bb25ede7239d18e9f81622f1cc5 - languageName: node - linkType: hard - "color-support@npm:^1.1.2, color-support@npm:^1.1.3": version: 1.1.3 resolution: "color-support@npm:1.1.3" @@ -4992,23 +3399,6 @@ __metadata: languageName: node linkType: hard -"color@npm:^4.2.3": - version: 4.2.3 - resolution: "color@npm:4.2.3" - dependencies: - color-convert: ^2.0.1 - color-string: ^1.9.0 - checksum: 0579629c02c631b426780038da929cca8e8d80a40158b09811a0112a107c62e10e4aad719843b791b1e658ab4e800558f2e87ca4522c8b32349d497ecb6adeb4 - languageName: node - linkType: hard - -"colorette@npm:^1.2.0": - version: 1.4.0 - resolution: "colorette@npm:1.4.0" - checksum: 01c3c16058b182a4ab4c126a65a75faa4d38a20fa7c845090b25453acec6c371bb2c5dceb0a2338511f17902b9d1a9af0cadd8509c9403894b79311032c256c3 - languageName: node - linkType: hard - "colorette@npm:^1.2.2": version: 1.2.2 resolution: "colorette@npm:1.2.2" @@ -5057,7 +3447,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^2.19.0, commander@npm:^2.20.0, commander@npm:^2.20.3": +"commander@npm:^2.20.0, commander@npm:^2.20.3": version: 2.20.3 resolution: "commander@npm:2.20.3" checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e @@ -5106,28 +3496,6 @@ __metadata: languageName: node linkType: hard -"compress-brotli@npm:^1.3.8": - version: 1.3.8 - resolution: "compress-brotli@npm:1.3.8" - dependencies: - "@types/json-buffer": ~3.0.0 - json-buffer: ~3.0.1 - checksum: de7589d692d40eb362f6c91070b5e51bc10b05a89eabb4a7c76c1aa21b625756f8c101c6999e4df0c4dc6199c5ca2e1353573bfdcca5615810f27485394162a5 - languageName: node - linkType: hard - -"compress-commons@npm:^4.1.0": - version: 4.1.1 - resolution: "compress-commons@npm:4.1.1" - dependencies: - buffer-crc32: ^0.2.13 - crc32-stream: ^4.0.2 - normalize-path: ^3.0.0 - readable-stream: ^3.6.0 - checksum: 0176483211a7304a4a8aa52dbcc149a4c9181ac8a04bfbcc3d1a379174bf5fa56c3b15cec19e5ae3d31f1b1ce35ebb275b792b867000c77bac7162ce4e0ca268 - languageName: node - linkType: hard - "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -5135,7 +3503,7 @@ __metadata: languageName: node linkType: hard -"concat-stream@npm:^1.5.2, concat-stream@npm:^1.6.0": +"concat-stream@npm:^1.6.0": version: 1.6.2 resolution: "concat-stream@npm:1.6.2" dependencies: @@ -5147,27 +3515,6 @@ __metadata: languageName: node linkType: hard -"condense-newlines@npm:^0.2.1": - version: 0.2.1 - resolution: "condense-newlines@npm:0.2.1" - dependencies: - extend-shallow: ^2.0.1 - is-whitespace: ^0.3.0 - kind-of: ^3.0.2 - checksum: 3c20ff6ee88b5d2e81c122f33b5ba5d6976cdf86d83527fadea308b3020ed70af7ed98c2e2d94d36f27fcd723a7a477941c19575e0d2c8db6afc4aac6926a54e - languageName: node - linkType: hard - -"config-chain@npm:^1.1.13": - version: 1.1.13 - resolution: "config-chain@npm:1.1.13" - dependencies: - ini: ^1.3.4 - proto-list: ~1.2.1 - checksum: 828137a28e7c2fc4b7fb229bd0cd6c1397bcf83434de54347e608154008f411749041ee392cbe42fab6307e02de4c12480260bf769b7d44b778fdea3839eafab - languageName: node - linkType: hard - "console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" @@ -5175,41 +3522,6 @@ __metadata: languageName: node linkType: hard -"consolidate@npm:^0.16.0": - version: 0.16.0 - resolution: "consolidate@npm:0.16.0" - dependencies: - bluebird: ^3.7.2 - checksum: f17164ffb2c4f79b4cbf685f1c76a49f59d329a40954b436425498861dc137b46fe821b2aadafa2dcfeb7eebd46846f35bd2c36b4a704d38521b4210a22a7515 - languageName: node - linkType: hard - -"constantinople@npm:^4.0.1": - version: 4.0.1 - resolution: "constantinople@npm:4.0.1" - dependencies: - "@babel/parser": ^7.6.0 - "@babel/types": ^7.6.1 - checksum: 8f70f16ddf97cdc263ca16b398bc52470c25e2ec5ed27bc015f251b849597223ce3a123e6924f43efddeb75422c1f55b7e56e0e2e594e4dd2964bfc9392b9b82 - languageName: node - linkType: hard - -"content-disposition@npm:0.5.4, content-disposition@npm:~0.5.2": - version: 0.5.4 - resolution: "content-disposition@npm:0.5.4" - dependencies: - safe-buffer: 5.2.1 - checksum: afb9d545e296a5171d7574fcad634b2fdf698875f4006a9dd04a3e1333880c5c0c98d47b560d01216fb6505a54a2ba6a843ee3a02ec86d7e911e8315255f56c3 - languageName: node - linkType: hard - -"content-type@npm:^1.0.4": - version: 1.0.4 - resolution: "content-type@npm:1.0.4" - checksum: 3d93585fda985d1554eca5ebd251994327608d2e200978fdbfba21c0c679914d5faf266d17027de44b34a72c7b0745b18584ecccaa7e1fdfb6a68ac7114f12e0 - languageName: node - linkType: hard - "convert-source-map@npm:^1.4.0, convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": version: 1.8.0 resolution: "convert-source-map@npm:1.8.0" @@ -5228,16 +3540,6 @@ __metadata: languageName: node linkType: hard -"cookies@npm:~0.8.0": - version: 0.8.0 - resolution: "cookies@npm:0.8.0" - dependencies: - depd: ~2.0.0 - keygrip: ~1.1.0 - checksum: 806055a44f128705265b1bc6a853058da18bf80dea3654ad99be20985b1fa1b14f86c1eef73644aab8071241f8a78acd57202b54c4c5c70769fc694fbb9c4edc - languageName: node - linkType: hard - "copy-descriptor@npm:^0.1.0": version: 0.1.1 resolution: "copy-descriptor@npm:0.1.1" @@ -5255,13 +3557,6 @@ __metadata: languageName: node linkType: hard -"copy-to@npm:^2.0.1": - version: 2.0.1 - resolution: "copy-to@npm:2.0.1" - checksum: 05ea12875bdc96ae053a3b30148e9d992026035ff2bfcc0b615e8d49d1cf8fc3d1f40843f9a4b7b1b6d9118eeebcba31e621076d7de525828aa9c07d22a81dab - languageName: node - linkType: hard - "core-util-is@npm:1.0.2, core-util-is@npm:~1.0.0": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" @@ -5269,25 +3564,6 @@ __metadata: languageName: node linkType: hard -"crc-32@npm:^1.2.0": - version: 1.2.2 - resolution: "crc-32@npm:1.2.2" - bin: - crc32: bin/crc32.njs - checksum: ad2d0ad0cbd465b75dcaeeff0600f8195b686816ab5f3ba4c6e052a07f728c3e70df2e3ca9fd3d4484dc4ba70586e161ca5a2334ec8bf5a41bf022a6103ff243 - languageName: node - linkType: hard - -"crc32-stream@npm:^4.0.2": - version: 4.0.2 - resolution: "crc32-stream@npm:4.0.2" - dependencies: - crc-32: ^1.2.0 - readable-stream: ^3.4.0 - checksum: 1099559283b86e8a55390228b57ff4d57a74cac6aa8086aa4730f84317c9f93e914aeece115352f2d706a9df7ed75327ffacd86cfe23f040aef821231b528e76 - languageName: node - linkType: hard - "create-require@npm:^1.1.0": version: 1.1.1 resolution: "create-require@npm:1.1.1" @@ -5295,15 +3571,6 @@ __metadata: languageName: node linkType: hard -"cron-parser@npm:^4.2.1": - version: 4.6.0 - resolution: "cron-parser@npm:4.6.0" - dependencies: - luxon: ^3.0.1 - checksum: cef63dee396732a8247c2c55d99512db7ad39797459f4bfd534ce5c18efdbf88b16ae8265c3b2abc40cdfadf8930bb1be6778e6ae664ae70e4ed7f206487d0cd - languageName: node - linkType: hard - "cropperjs@npm:2.0.0-beta.1": version: 2.0.0-beta.1 resolution: "cropperjs@npm:2.0.0-beta.1" @@ -5353,25 +3620,6 @@ __metadata: languageName: node linkType: hard -"css-select@npm:~1.2.0": - version: 1.2.0 - resolution: "css-select@npm:1.2.0" - dependencies: - boolbase: ~1.0.0 - css-what: 2.1 - domutils: 1.5.1 - nth-check: ~1.0.1 - checksum: 607cca60d2f5c56701fe5f800bbe668b114395c503d4e4808edbbbe70b8be3c96a6407428dc0227fcbdf335b20468e6a9e7fd689185edfb57d402e1e4837c9b7 - languageName: node - linkType: hard - -"css-what@npm:2.1": - version: 2.1.3 - resolution: "css-what@npm:2.1.3" - checksum: a52d56c591a7e1c37506d0d8c4fdef72537fb8eb4cb68711485997a88d76b5a3342b73a7c79176268f95b428596c447ad7fa3488224a6b8b532e2f1f2ee8545c - languageName: node - linkType: hard - "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -5440,13 +3688,6 @@ __metadata: languageName: node linkType: hard -"cssom@npm:^0.5.0": - version: 0.5.0 - resolution: "cssom@npm:0.5.0" - checksum: 823471aa30091c59e0a305927c30e7768939b6af70405808f8d2ce1ca778cddcb24722717392438329d1691f9a87cb0183b64b8d779b56a961546d54854fde01 - languageName: node - linkType: hard - "cssom@npm:~0.3.6": version: 0.3.8 resolution: "cssom@npm:0.3.8" @@ -5541,13 +3782,6 @@ __metadata: languageName: node linkType: hard -"data-uri-to-buffer@npm:^4.0.0": - version: 4.0.0 - resolution: "data-uri-to-buffer@npm:4.0.0" - checksum: a010653869abe8bb51259432894ac62c52bf79ad761d418d94396f48c346f2ae739c46b254e8bb5987bded8a653d467db1968db3a69bab1d33aa5567baa5cfc7 - languageName: node - linkType: hard - "data-urls@npm:^2.0.0": version: 2.0.0 resolution: "data-urls@npm:2.0.0" @@ -5559,17 +3793,6 @@ __metadata: languageName: node linkType: hard -"data-urls@npm:^3.0.2": - version: 3.0.2 - resolution: "data-urls@npm:3.0.2" - dependencies: - abab: ^2.0.6 - whatwg-mimetype: ^3.0.0 - whatwg-url: ^11.0.0 - checksum: 033fc3dd0fba6d24bc9a024ddcf9923691dd24f90a3d26f6545d6a2f71ec6956f93462f2cdf2183cc46f10dc01ed3bcb36731a8208456eb1a08147e571fe2a76 - languageName: node - linkType: hard - "date-fns@npm:2.28.0": version: 2.28.0 resolution: "date-fns@npm:2.28.0" @@ -5577,13 +3800,6 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^2.28.0": - version: 2.29.2 - resolution: "date-fns@npm:2.29.2" - checksum: 08bebcceb0a5dbadae4c55e6592b9d5c07dbd7833433c7e9a1d4a424300db32589b8b48e5979b32863c9b00a48d9bab6663e580c2a4f9f203d46cbf9113b5664 - languageName: node - linkType: hard - "dayjs@npm:^1.10.4": version: 1.10.6 resolution: "dayjs@npm:1.10.6" @@ -5591,16 +3807,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2, debug@npm:^2.2.0, debug@npm:^2.3.3, debug@npm:^2.5.2, debug@npm:^2.6.9": - version: 2.6.9 - resolution: "debug@npm:2.6.9" - dependencies: - ms: 2.0.0 - checksum: d2f51589ca66df60bf36e1fa6e4386b318c3f1e06772280eea5b1ae9fd3d05e9c2b7fd8a7d862457d00853c75b00451aa2d7459b924629ee385287a650f58fe6 - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -5624,19 +3831,16 @@ __metadata: languageName: node linkType: hard -"debug@npm:4.3.3": - version: 4.3.3 - resolution: "debug@npm:4.3.3" +"debug@npm:^2.2.0, debug@npm:^2.3.3, debug@npm:^2.6.9": + version: 2.6.9 + resolution: "debug@npm:2.6.9" dependencies: - ms: 2.1.2 - peerDependenciesMeta: - supports-color: - optional: true - checksum: 14472d56fe4a94dbcfaa6dbed2dd3849f1d72ba78104a1a328047bb564643ca49df0224c3a17fa63533fd11dd3d4c8636cd861191232a2c6735af00cc2d4de16 + ms: 2.0.0 + checksum: d2f51589ca66df60bf36e1fa6e4386b318c3f1e06772280eea5b1ae9fd3d05e9c2b7fd8a7d862457d00853c75b00451aa2d7459b924629ee385287a650f58fe6 languageName: node linkType: hard -"debug@npm:^3.1.0, debug@npm:^3.2.6, debug@npm:^3.2.7": +"debug@npm:^3.1.0, debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" dependencies: @@ -5645,13 +3849,6 @@ __metadata: languageName: node linkType: hard -"debuglog@npm:^1.0.0": - version: 1.0.1 - resolution: "debuglog@npm:1.0.1" - checksum: 970679f2eb7a73867e04d45b52583e7ec6dee1f33c058e9147702e72a665a9647f9c3d6e7c2f66f6bf18510b23eb5ded1b617e48ac1db23603809c5ddbbb9763 - languageName: node - linkType: hard - "decamelize-keys@npm:^1.1.0": version: 1.1.0 resolution: "decamelize-keys@npm:1.1.0" @@ -5669,14 +3866,7 @@ __metadata: languageName: node linkType: hard -"decamelize@npm:^4.0.0": - version: 4.0.0 - resolution: "decamelize@npm:4.0.0" - checksum: b7d09b82652c39eead4d6678bb578e3bebd848add894b76d0f6b395bc45b2d692fb88d977e7cfb93c4ed6c119b05a1347cef261174916c2e75c0a8ca57da1809 - languageName: node - linkType: hard - -"decimal.js@npm:^10.2.1, decimal.js@npm:^10.3.1": +"decimal.js@npm:^10.2.1": version: 10.4.0 resolution: "decimal.js@npm:10.4.0" checksum: 98702d9d817a9e5b3767ea6580e7f3b35544b9454e463a5dd5d3232131470f39067d02864c45cab009eb1200bc162cd26a33d34c622cd79e4657a3e25e95fb4e @@ -5690,15 +3880,6 @@ __metadata: languageName: node linkType: hard -"decompress-response@npm:^6.0.0": - version: 6.0.0 - resolution: "decompress-response@npm:6.0.0" - dependencies: - mimic-response: ^3.1.0 - checksum: d377cf47e02d805e283866c3f50d3d21578b779731e8c5072d6ce8c13cc31493db1c2f6784da9d1d5250822120cefa44f1deab112d5981015f2e17444b763812 - languageName: node - linkType: hard - "dedent@npm:^0.7.0": version: 0.7.0 resolution: "dedent@npm:0.7.0" @@ -5706,32 +3887,6 @@ __metadata: languageName: node linkType: hard -"deep-email-validator@npm:0.1.21": - version: 0.1.21 - resolution: "deep-email-validator@npm:0.1.21" - dependencies: - "@types/disposable-email-domains": ^1.0.1 - axios: ^0.24.0 - disposable-email-domains: ^1.0.59 - mailcheck: ^1.1.1 - checksum: 3ed0597eb1835e09b9d77f1ccd7f9a903b0d6d6554cb35c9415db25cbf2cd460193cb60ec988d172a78f06cb50cfb64d0b222c5496c58b8b70fdfd6b8edf45a2 - languageName: node - linkType: hard - -"deep-equal@npm:~1.0.1": - version: 1.0.1 - resolution: "deep-equal@npm:1.0.1" - checksum: 5af8cbfcebf190491878a498caccc7dc9592f8ebd1685b976eacc3825619d222b5e929923163b92c4f414494e2b884f7ebf00c022e8198e8292deb70dd9785f4 - languageName: node - linkType: hard - -"deep-extend@npm:^0.6.0": - version: 0.6.0 - resolution: "deep-extend@npm:0.6.0" - checksum: 7be7e5a8d468d6b10e6a67c3de828f55001b6eb515d014f7aeb9066ce36bd5717161eb47d6a0f7bed8a9083935b465bc163ee2581c8b128d29bf61092fdf57a7 - languageName: node - linkType: hard - "deep-is@npm:^0.1.3, deep-is@npm:~0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -5762,13 +3917,6 @@ __metadata: languageName: node linkType: hard -"defer-to-connect@npm:^2.0.0, defer-to-connect@npm:^2.0.1": - version: 2.0.1 - resolution: "defer-to-connect@npm:2.0.1" - checksum: 8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b - languageName: node - linkType: hard - "define-properties@npm:^1.1.2": version: 1.1.3 resolution: "define-properties@npm:1.1.3" @@ -5837,34 +3985,13 @@ __metadata: languageName: node linkType: hard -"denque@npm:^1.1.0": - version: 1.5.1 - resolution: "denque@npm:1.5.1" - checksum: 4375ad19d5cea99f90effa82a8cecdaa10f4eb261fbcd7e47cd753ff2737f037aac8f7f4e031cc77f3966314c491c86a0d3b20c128aeee57f791b4662c45108e - languageName: node - linkType: hard - -"depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0": - version: 2.0.0 - resolution: "depd@npm:2.0.0" - checksum: abbe19c768c97ee2eed6282d8ce3031126662252c58d711f646921c9623f9052e3e1906443066beec1095832f534e57c523b7333f8e7e0d93051ab6baef5ab3a - languageName: node - linkType: hard - -"depd@npm:^1.1.2, depd@npm:~1.1.2": +"depd@npm:^1.1.2": version: 1.1.2 resolution: "depd@npm:1.1.2" checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 languageName: node linkType: hard -"destroy@npm:^1.0.4": - version: 1.2.0 - resolution: "destroy@npm:1.2.0" - checksum: 0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 - languageName: node - linkType: hard - "detect-file@npm:^1.0.0": version: 1.0.0 resolution: "detect-file@npm:1.0.0" @@ -5872,7 +3999,7 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1": +"detect-libc@npm:^2.0.0": version: 2.0.1 resolution: "detect-libc@npm:2.0.1" checksum: ccb05fcabbb555beb544d48080179c18523a343face9ee4e1a86605a8715b4169f94d663c21a03c310ac824592f2ba9a5270218819bb411ad7be578a527593d7 @@ -5907,13 +4034,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:5.0.0": - version: 5.0.0 - resolution: "diff@npm:5.0.0" - checksum: f19fe29284b633afdb2725c2a8bb7d25761ea54d321d8e67987ac851c5294be4afeab532bd84531e02583a3fe7f4014aa314a3eda84f5590e7a9e6b371ef3b46 - languageName: node - linkType: hard - "diff@npm:^4.0.1": version: 4.0.2 resolution: "diff@npm:4.0.2" @@ -5921,20 +4041,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^5.0.0": - version: 5.1.0 - resolution: "diff@npm:5.1.0" - checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 - languageName: node - linkType: hard - -"dijkstrajs@npm:^1.0.1": - version: 1.0.2 - resolution: "dijkstrajs@npm:1.0.2" - checksum: 8cd822441a26f190da24d69bfab7b433d080b09e069e41e046ac84e152f182a1ed9478d531b34126e000adaa7b73114a0f85fcac117a7d25b3edf302d57c0d09 - languageName: node - linkType: hard - "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -5944,13 +4050,6 @@ __metadata: languageName: node linkType: hard -"disposable-email-domains@npm:^1.0.59": - version: 1.0.59 - resolution: "disposable-email-domains@npm:1.0.59" - checksum: 4f5b721040938c36233ae4407b0822ff8d88a01a3b390df2083d620de3e91aaaa02ca2204b912ca680c63b3976ecb49cdb4caf4917785c7bcd5ab5062f7d6d8c - languageName: node - linkType: hard - "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -5969,58 +4068,6 @@ __metadata: languageName: node linkType: hard -"doctypes@npm:^1.1.0": - version: 1.1.0 - resolution: "doctypes@npm:1.1.0" - checksum: 6e6c2d1a80f2072dc4831994c914c44455e341c5ab18c16797368a0afd59d7c22f3335805ba2c1dd2931e9539d1ba8b613b7650dc63f6ab56b77b8d888055de8 - languageName: node - linkType: hard - -"dom-serializer@npm:0": - version: 0.2.2 - resolution: "dom-serializer@npm:0.2.2" - dependencies: - domelementtype: ^2.0.1 - entities: ^2.0.0 - checksum: 376344893e4feccab649a14ca1a46473e9961f40fe62479ea692d4fee4d9df1c00ca8654811a79c1ca7b020096987e1ca4fb4d7f8bae32c1db800a680a0e5d5e - languageName: node - linkType: hard - -"dom-serializer@npm:^1.0.1": - version: 1.4.1 - resolution: "dom-serializer@npm:1.4.1" - dependencies: - domelementtype: ^2.0.1 - domhandler: ^4.2.0 - entities: ^2.0.0 - checksum: fbb0b01f87a8a2d18e6e5a388ad0f7ec4a5c05c06d219377da1abc7bb0f674d804f4a8a94e3f71ff15f6cb7dcfc75704a54b261db672b9b3ab03da6b758b0b22 - languageName: node - linkType: hard - -"dom-serializer@npm:~0.1.0": - version: 0.1.1 - resolution: "dom-serializer@npm:0.1.1" - dependencies: - domelementtype: ^1.3.0 - entities: ^1.1.1 - checksum: 4f6a3eff802273741931cfd3c800fab4e683236eed10628d6605f52538a6bc0ce4770f3ca2ad68a27412c103ae9b6cdaed3c0a8e20d2704192bde497bc875215 - languageName: node - linkType: hard - -"domelementtype@npm:1, domelementtype@npm:^1.3.0, domelementtype@npm:^1.3.1": - version: 1.3.1 - resolution: "domelementtype@npm:1.3.1" - checksum: 7893da40218ae2106ec6ffc146b17f203487a52f5228b032ea7aa470e41dfe03e1bd762d0ee0139e792195efda765434b04b43cddcf63207b098f6ae44b36ad6 - languageName: node - linkType: hard - -"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0": - version: 2.3.0 - resolution: "domelementtype@npm:2.3.0" - checksum: ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 - languageName: node - linkType: hard - "domexception@npm:^2.0.1": version: 2.0.1 resolution: "domexception@npm:2.0.1" @@ -6030,80 +4077,6 @@ __metadata: languageName: node linkType: hard -"domexception@npm:^4.0.0": - version: 4.0.0 - resolution: "domexception@npm:4.0.0" - dependencies: - webidl-conversions: ^7.0.0 - checksum: ddbc1268edf33a8ba02ccc596735ede80375ee0cf124b30d2f05df5b464ba78ef4f49889b6391df4a04954e63d42d5631c7fcf8b1c4f12bc531252977a5f13d5 - languageName: node - linkType: hard - -"domhandler@npm:^2.3.0": - version: 2.4.2 - resolution: "domhandler@npm:2.4.2" - dependencies: - domelementtype: 1 - checksum: 49bd70c9c784f845cd047e1dfb3611bd10891c05719acfc93f01fc726a419ed09fbe0b69f9064392d556a63fffc5a02010856cedae9368f4817146d95a97011f - languageName: node - linkType: hard - -"domhandler@npm:^4.0.0, domhandler@npm:^4.2.0": - version: 4.3.1 - resolution: "domhandler@npm:4.3.1" - dependencies: - domelementtype: ^2.2.0 - checksum: 4c665ceed016e1911bf7d1dadc09dc888090b64dee7851cccd2fcf5442747ec39c647bb1cb8c8919f8bbdd0f0c625a6bafeeed4b2d656bbecdbae893f43ffaaa - languageName: node - linkType: hard - -"domutils@npm:1.5.1": - version: 1.5.1 - resolution: "domutils@npm:1.5.1" - dependencies: - dom-serializer: 0 - domelementtype: 1 - checksum: 800d1f9d1c2e637267dae078ff6e24461e6be1baeb52fa70f2e7e7520816c032a925997cd15d822de53ef9896abb1f35e5c439d301500a9cd6b46a395f6f6ca0 - languageName: node - linkType: hard - -"domutils@npm:^1.5.1": - version: 1.7.0 - resolution: "domutils@npm:1.7.0" - dependencies: - dom-serializer: 0 - domelementtype: 1 - checksum: f60a725b1f73c1ae82f4894b691601ecc6ecb68320d87923ac3633137627c7865725af813ae5d188ad3954283853bcf46779eb50304ec5d5354044569fcefd2b - languageName: node - linkType: hard - -"domutils@npm:^2.5.2": - version: 2.8.0 - resolution: "domutils@npm:2.8.0" - dependencies: - dom-serializer: ^1.0.1 - domelementtype: ^2.2.0 - domhandler: ^4.2.0 - checksum: abf7434315283e9aadc2a24bac0e00eab07ae4313b40cc239f89d84d7315ebdfd2fb1b5bf750a96bc1b4403d7237c7b2ebf60459be394d625ead4ca89b934391 - languageName: node - linkType: hard - -"dotenv@npm:^16.0.0": - version: 16.0.1 - resolution: "dotenv@npm:16.0.1" - checksum: f459ffce07b977b7f15d8cc4ee69cdff77d4dd8c5dc8c85d2d485ee84655352c2415f9dd09d42b5b5985ced3be186130871b34e2f3e2569ebc72fbc2e8096792 - languageName: node - linkType: hard - -"duplexer2@npm:~0.1.4": - version: 0.1.4 - resolution: "duplexer2@npm:0.1.4" - dependencies: - readable-stream: ^2.0.2 - checksum: 744961f03c7f54313f90555ac20284a3fb7bf22fdff6538f041a86c22499560eb6eac9d30ab5768054137cb40e6b18b40f621094e0261d7d8c35a37b7a5ad241 - languageName: node - linkType: hard - "duplexer@npm:~0.1.1": version: 0.1.2 resolution: "duplexer@npm:0.1.2" @@ -6143,47 +4116,6 @@ __metadata: languageName: node linkType: hard -"ecdsa-sig-formatter@npm:1.0.11": - version: 1.0.11 - resolution: "ecdsa-sig-formatter@npm:1.0.11" - dependencies: - safe-buffer: ^5.0.1 - checksum: 207f9ab1c2669b8e65540bce29506134613dd5f122cccf1e6a560f4d63f2732d427d938f8481df175505aad94583bcb32c688737bb39a6df0625f903d6d93c03 - languageName: node - linkType: hard - -"editorconfig@npm:^0.15.3": - version: 0.15.3 - resolution: "editorconfig@npm:0.15.3" - dependencies: - commander: ^2.19.0 - lru-cache: ^4.1.5 - semver: ^5.6.0 - sigmund: ^1.0.1 - bin: - editorconfig: bin/editorconfig - checksum: a94afeda19f12a4bcc4a573f0858df13dd3a2d1a3268cc0f17a6326ebe7ddd6cb0c026f8e4e73c17d34f3892bf6f8b561512d9841e70063f61da71b4c57dc5f0 - languageName: node - linkType: hard - -"ee-first@npm:1.1.1": - version: 1.1.1 - resolution: "ee-first@npm:1.1.1" - checksum: 1b4cac778d64ce3b582a7e26b218afe07e207a0f9bfe13cc7395a6d307849cfe361e65033c3251e00c27dd060cab43014c2d6b2647676135e18b77d2d05b3f4f - languageName: node - linkType: hard - -"ejs@npm:^3.1.7": - version: 3.1.8 - resolution: "ejs@npm:3.1.8" - dependencies: - jake: ^10.8.5 - bin: - ejs: bin/cli.js - checksum: 1d40d198ad52e315ccf37e577bdec06e24eefdc4e3c27aafa47751a03a0c7f0ec4310254c9277a5f14763c3cd4bbacce27497332b2d87c74232b9b1defef8efc - languageName: node - linkType: hard - "electron-to-chromium@npm:^1.2.7": version: 1.3.633 resolution: "electron-to-chromium@npm:1.3.633" @@ -6212,27 +4144,6 @@ __metadata: languageName: node linkType: hard -"emojis-list@npm:^3.0.0": - version: 3.0.0 - resolution: "emojis-list@npm:3.0.0" - checksum: ddaaa02542e1e9436c03970eeed445f4ed29a5337dfba0fe0c38dfdd2af5da2429c2a0821304e8a8d1cadf27fdd5b22ff793571fa803ae16852a6975c65e8e70 - languageName: node - linkType: hard - -"encode-utf8@npm:^1.0.3": - version: 1.0.3 - resolution: "encode-utf8@npm:1.0.3" - checksum: 550224bf2a104b1d355458c8a82e9b4ea07f9fc78387bc3a49c151b940ad26473de8dc9e121eefc4e84561cb0b46de1e4cd2bc766f72ee145e9ea9541482817f - languageName: node - linkType: hard - -"encodeurl@npm:^1.0.2": - version: 1.0.2 - resolution: "encodeurl@npm:1.0.2" - checksum: e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c - languageName: node - linkType: hard - "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -6242,7 +4153,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": +"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -6251,16 +4162,6 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.0.0": - version: 5.10.0 - resolution: "enhanced-resolve@npm:5.10.0" - dependencies: - graceful-fs: ^4.2.4 - tapable: ^2.2.0 - checksum: 0bb9830704db271610f900e8d79d70a740ea16f251263362b0c91af545576d09fe50103496606c1300a05e588372d6f9780a9bc2e30ce8ef9b827ec8f44687ff - languageName: node - linkType: hard - "enquirer@npm:^2.3.6": version: 2.3.6 resolution: "enquirer@npm:2.3.6" @@ -6270,27 +4171,6 @@ __metadata: languageName: node linkType: hard -"entities@npm:^1.1.1, entities@npm:~1.1.1": - version: 1.1.2 - resolution: "entities@npm:1.1.2" - checksum: d537b02799bdd4784ffd714d000597ed168727bddf4885da887c5a491d735739029a00794f1998abbf35f3f6aeda32ef5c15010dca1817d401903a501b6d3e05 - languageName: node - linkType: hard - -"entities@npm:^2.0.0, entities@npm:^2.0.3": - version: 2.2.0 - resolution: "entities@npm:2.2.0" - checksum: 19010dacaf0912c895ea262b4f6128574f9ccf8d4b3b65c7e8334ad0079b3706376360e28d8843ff50a78aabcb8f08f0a32dbfacdc77e47ed77ca08b713669b3 - languageName: node - linkType: hard - -"entities@npm:^4.3.0": - version: 4.3.1 - resolution: "entities@npm:4.3.1" - checksum: e8f6d2bac238494b2355e90551893882d2675142be7e7bdfcb15248ed0652a630678ba0e3a8dc750693e736cb6011f504c27dabeb4cd3330560092e88b105090 - languageName: node - linkType: hard - "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -6844,27 +4724,6 @@ __metadata: languageName: node linkType: hard -"escape-html@npm:^1.0.3": - version: 1.0.3 - resolution: "escape-html@npm:1.0.3" - checksum: 6213ca9ae00d0ab8bccb6d8d4e0a98e76237b2410302cf7df70aaa6591d509a2a37ce8998008cbecae8fc8ffaadf3fb0229535e6a145f3ce0b211d060decbb24 - languageName: node - linkType: hard - -"escape-regexp@npm:0.0.1": - version: 0.0.1 - resolution: "escape-regexp@npm:0.0.1" - checksum: abb22ce7a1df640c128abf590d954e6bc4f34915dc9d48597cfc1c5047d60925a35c5eed2bb2f92c1810055c4b17075e6b85ca8b563047f1020d15813a4709dd - languageName: node - linkType: hard - -"escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 - languageName: node - linkType: hard - "escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.3, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -6879,6 +4738,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + languageName: node + linkType: hard + "escodegen@npm:^2.0.0": version: 2.0.0 resolution: "escodegen@npm:2.0.0" @@ -6936,13 +4802,6 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-foundkey-custom-rules@file:../shared/custom-rules::locator=backend%40workspace%3Apackages%2Fbackend": - version: 0.0.0 - resolution: "eslint-plugin-foundkey-custom-rules@file:../shared/custom-rules#../shared/custom-rules::hash=45c4fe&locator=backend%40workspace%3Apackages%2Fbackend" - checksum: 420d8341c1208ab068d9c8dd875241d452b199e46ebe9b75c07449051475e7814eed1c812d9552bdc0fbe2054d4dfd467577f7890ad050ab812347ccf04ae063 - languageName: node - linkType: hard - "eslint-plugin-import@npm:^2.26.0": version: 2.26.0 resolution: "eslint-plugin-import@npm:2.26.0" @@ -7194,13 +5053,6 @@ __metadata: languageName: node linkType: hard -"event-target-shim@npm:^5.0.0": - version: 5.0.1 - resolution: "event-target-shim@npm:5.0.1" - checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166 - languageName: node - linkType: hard - "eventemitter2@npm:^6.4.3": version: 6.4.4 resolution: "eventemitter2@npm:6.4.4" @@ -7215,13 +5067,6 @@ __metadata: languageName: node linkType: hard -"events@npm:1.1.1": - version: 1.1.1 - resolution: "events@npm:1.1.1" - checksum: 40431eb005cc4c57861b93d44c2981a49e7feb99df84cf551baed299ceea4444edf7744733f6a6667e942af687359b1f4a87ec1ec4f21d5127dac48a782039b9 - languageName: node - linkType: hard - "execa@npm:4.1.0": version: 4.1.0 resolution: "execa@npm:4.1.0" @@ -7256,23 +5101,6 @@ __metadata: languageName: node linkType: hard -"execa@npm:6.1.0": - version: 6.1.0 - resolution: "execa@npm:6.1.0" - dependencies: - cross-spawn: ^7.0.3 - get-stream: ^6.0.1 - human-signals: ^3.0.1 - is-stream: ^3.0.0 - merge-stream: ^2.0.0 - npm-run-path: ^5.1.0 - onetime: ^6.0.0 - signal-exit: ^3.0.7 - strip-final-newline: ^3.0.0 - checksum: 1a4af799839134f5c72eb63d525b87304c1114a63aa71676c91d57ccef2e26f2f53e14c11384ab11c4ec479be1efa83d11c8190e00040355c2c5c3364327fa8e - languageName: node - linkType: hard - "executable@npm:^4.1.1": version: 4.1.1 resolution: "executable@npm:4.1.1" @@ -7304,13 +5132,6 @@ __metadata: languageName: node linkType: hard -"expand-template@npm:^2.0.3": - version: 2.0.3 - resolution: "expand-template@npm:2.0.3" - checksum: 588c19847216421ed92befb521767b7018dc88f88b0576df98cb242f20961425e96a92cbece525ef28cc5becceae5d544ae0f5b9b5e2aa05acb13716ca5b3099 - languageName: node - linkType: hard - "expand-tilde@npm:^2.0.0, expand-tilde@npm:^2.0.2": version: 2.0.2 resolution: "expand-tilde@npm:2.0.2" @@ -7460,17 +5281,6 @@ __metadata: languageName: node linkType: hard -"fast-xml-parser@npm:^3.19.0": - version: 3.21.1 - resolution: "fast-xml-parser@npm:3.21.1" - dependencies: - strnum: ^1.0.4 - bin: - xml2js: cli.js - checksum: 73b9c907a424cc2f9b11a8a2f1b7448d936f1db6fa574b85cbe4be9739c2f77d99a827bb27d738a0db0047b20c71a5d663f64937fbdb9c38977fc6cd145221d2 - languageName: node - linkType: hard - "fastq@npm:^1.6.0": version: 1.13.0 resolution: "fastq@npm:1.13.0" @@ -7498,25 +5308,6 @@ __metadata: languageName: node linkType: hard -"feed@npm:4.2.2": - version: 4.2.2 - resolution: "feed@npm:4.2.2" - dependencies: - xml-js: ^1.6.11 - checksum: 2e6992a675a049511eef7bda8ca6c08cb9540cd10e8b275ec4c95d166228ec445a335fa8de990358759f248a92861e51decdcd32bf1c54737d5b7aed7c7ffe97 - languageName: node - linkType: hard - -"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": - version: 3.2.0 - resolution: "fetch-blob@npm:3.2.0" - dependencies: - node-domexception: ^1.0.0 - web-streams-polyfill: ^3.0.3 - checksum: f19bc28a2a0b9626e69fd7cf3a05798706db7f6c7548da657cbf5026a570945f5eeaedff52007ea35c8bcd3d237c58a20bf1543bc568ab2422411d762dd3d5bf - languageName: node - linkType: hard - "figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -7535,26 +5326,6 @@ __metadata: languageName: node linkType: hard -"file-type@npm:18.0.0": - version: 18.0.0 - resolution: "file-type@npm:18.0.0" - dependencies: - readable-web-to-node-stream: ^3.0.2 - strtok3: ^7.0.0 - token-types: ^5.0.1 - checksum: 67f5a927b8030e35a4faf9dd9dea9e17bcb042fb61b9851b7dd1b1b3bb3ecfdd9f83bc3bc72686316ea2bac70df652c61e10affa9b5957b1a3d731df4925e3cb - languageName: node - linkType: hard - -"filelist@npm:^1.0.1": - version: 1.0.4 - resolution: "filelist@npm:1.0.4" - dependencies: - minimatch: ^5.0.1 - checksum: a303573b0821e17f2d5e9783688ab6fbfce5d52aaac842790ae85e704a6f5e4e3538660a63183d6453834dedf1e0f19a9dadcebfa3e926c72397694ea11f5160 - languageName: node - linkType: hard - "fill-range@npm:^4.0.0": version: 4.0.0 resolution: "fill-range@npm:4.0.0" @@ -7576,16 +5347,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:5.0.0, find-up@npm:^5.0.0": - version: 5.0.0 - resolution: "find-up@npm:5.0.0" - dependencies: - locate-path: ^6.0.0 - path-exists: ^4.0.0 - checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 - languageName: node - linkType: hard - "find-up@npm:^1.0.0": version: 1.1.2 resolution: "find-up@npm:1.1.2" @@ -7606,6 +5367,16 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: ^6.0.0 + path-exists: ^4.0.0 + checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 + languageName: node + linkType: hard + "findup-sync@npm:^2.0.0": version: 2.0.0 resolution: "findup-sync@npm:2.0.0" @@ -7660,15 +5431,6 @@ __metadata: languageName: node linkType: hard -"flat@npm:^5.0.2": - version: 5.0.2 - resolution: "flat@npm:5.0.2" - bin: - flat: cli.js - checksum: 12a1536ac746db74881316a181499a78ef953632ddd28050b7a3a43c62ef5462e3357c8c29d76072bb635f147f7a9a1f0c02efef6b4be28f8db62ceb3d5c7f5d - languageName: node - linkType: hard - "flatted@npm:^3.1.0": version: 3.2.7 resolution: "flatted@npm:3.2.7" @@ -7683,16 +5445,6 @@ __metadata: languageName: node linkType: hard -"fluent-ffmpeg@npm:2.1.2": - version: 2.1.2 - resolution: "fluent-ffmpeg@npm:2.1.2" - dependencies: - async: ">=0.2.9" - which: ^1.1.1 - checksum: ab7ed909486298f33b991af051c38e40410ae5b03b0b6d33d0855636a0b56330ffe9efed6c7eedb00f1c6c713c795cbdb283f6f2216db925ae5e737a22c6a97f - languageName: node - linkType: hard - "flush-write-stream@npm:^1.0.2": version: 1.1.1 resolution: "flush-write-stream@npm:1.1.1" @@ -7713,16 +5465,6 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.14.4": - version: 1.15.1 - resolution: "follow-redirects@npm:1.15.1" - peerDependenciesMeta: - debug: - optional: true - checksum: 6aa4e3e3cdfa3b9314801a1cd192ba756a53479d9d8cca65bf4db3a3e8834e62139245cd2f9566147c8dfe2efff1700d3e6aefd103de4004a7b99985e71dd533 - languageName: node - linkType: hard - "for-in@npm:^1.0.1, for-in@npm:^1.0.2": version: 1.0.2 resolution: "for-in@npm:1.0.2" @@ -7746,13 +5488,6 @@ __metadata: languageName: node linkType: hard -"form-data-encoder@npm:^2.0.1": - version: 2.1.0 - resolution: "form-data-encoder@npm:2.1.0" - checksum: f8799a1bcc2230c3e4e77649d11662f301d84c944da30dc1e7225a17826ba65bd330d0ec8b8b1832ee4c9fd56b2b95d9ca9e9ca464dccef62b90f77a0e913e01 - languageName: node - linkType: hard - "form-data@npm:^3.0.0": version: 3.0.1 resolution: "form-data@npm:3.0.1" @@ -7764,17 +5499,6 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": - version: 4.0.0 - resolution: "form-data@npm:4.0.0" - dependencies: - asynckit: ^0.4.0 - combined-stream: ^1.0.8 - mime-types: ^2.1.12 - checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c - languageName: node - linkType: hard - "form-data@npm:~2.3.2": version: 2.3.3 resolution: "form-data@npm:2.3.3" @@ -7786,15 +5510,6 @@ __metadata: languageName: node linkType: hard -"formdata-polyfill@npm:^4.0.10": - version: 4.0.10 - resolution: "formdata-polyfill@npm:4.0.10" - dependencies: - fetch-blob: ^3.1.2 - checksum: 82a34df292afadd82b43d4a740ce387bc08541e0a534358425193017bf9fb3567875dc5f69564984b1da979979b70703aa73dee715a17b6c229752ae736dd9db - languageName: node - linkType: hard - "foundkey-js@workspace:*, foundkey-js@workspace:packages/foundkey-js": version: 0.0.0-use.local resolution: "foundkey-js@workspace:packages/foundkey-js" @@ -7823,6 +5538,7 @@ __metadata: version: 0.0.0-use.local resolution: "foundkey@workspace:." dependencies: + "@discordapp/twemoji": ^14.0.2 "@types/gulp": 4.0.9 "@types/gulp-rename": 2.0.1 "@typescript-eslint/parser": ^5.46.1 @@ -7850,13 +5566,6 @@ __metadata: languageName: node linkType: hard -"fresh@npm:~0.5.2": - version: 0.5.2 - resolution: "fresh@npm:0.5.2" - checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 - languageName: node - linkType: hard - "from@npm:~0": version: 0.1.7 resolution: "from@npm:0.1.7" @@ -7864,13 +5573,6 @@ __metadata: languageName: node linkType: hard -"fs-constants@npm:^1.0.0": - version: 1.0.0 - resolution: "fs-constants@npm:1.0.0" - checksum: 18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d - languageName: node - linkType: hard - "fs-extra@npm:^8.0.1": version: 8.1.0 resolution: "fs-extra@npm:8.1.0" @@ -7969,18 +5671,6 @@ __metadata: languageName: node linkType: hard -"fstream@npm:^1.0.12": - version: 1.0.12 - resolution: "fstream@npm:1.0.12" - dependencies: - graceful-fs: ^4.1.2 - inherits: ~2.0.0 - mkdirp: ">=0.5 0" - rimraf: 2 - checksum: e6998651aeb85fd0f0a8a68cec4d05a3ada685ecc4e3f56e0d063d0564a4fc39ad11a856f9020f926daf869fc67f7a90e891def5d48e4cadab875dc313094536 - languageName: node - linkType: hard - "function-bind@npm:^1.1.1": version: 1.1.1 resolution: "function-bind@npm:1.1.1" @@ -8040,13 +5730,6 @@ __metadata: languageName: node linkType: hard -"generic-pool@npm:3.8.2": - version: 3.8.2 - resolution: "generic-pool@npm:3.8.2" - checksum: f549077d90265e5e4d32a2410205b357ec61cf73d17861f1013637984390e09fe7bf537129a2c6ed30ae57662a57c8d54194f80046408d3349836330f422dbde - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -8061,7 +5744,7 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": +"get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 @@ -8086,22 +5769,6 @@ __metadata: languageName: node linkType: hard -"get-paths@npm:0.0.7": - version: 0.0.7 - resolution: "get-paths@npm:0.0.7" - dependencies: - pify: ^4.0.1 - checksum: a17edf61fb9934b8e58a7d8ce0d9702040b7020dda86e67ce088db865c21cb230f490f25f38064cebeb2c367abc2bf39a75db6acdfddf01da63a699a47f8aba4 - languageName: node - linkType: hard - -"get-port@npm:^5.1.1": - version: 5.1.1 - resolution: "get-port@npm:5.1.1" - checksum: 0162663ffe5c09e748cd79d97b74cd70e5a5c84b760a475ce5767b357fb2a57cb821cee412d646aa8a156ed39b78aab88974eddaa9e5ee926173c036c0713787 - languageName: node - linkType: hard - "get-stream@npm:^5.0.0, get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -8118,13 +5785,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^6.0.1": - version: 6.0.1 - resolution: "get-stream@npm:6.0.1" - checksum: e04ecece32c92eebf5b8c940f51468cd53554dcbb0ea725b2748be583c9523d00128137966afce410b9b051eb2ef16d657cd2b120ca8edafcf5a65e81af63cad - languageName: node - linkType: hard - "get-symbol-description@npm:^1.0.0": version: 1.0.0 resolution: "get-symbol-description@npm:1.0.0" @@ -8160,13 +5820,6 @@ __metadata: languageName: node linkType: hard -"github-from-package@npm:0.0.0": - version: 0.0.0 - resolution: "github-from-package@npm:0.0.0" - checksum: 14e448192a35c1e42efee94c9d01a10f42fe790375891a24b25261246ce9336ab9df5d274585aedd4568f7922246c2a78b8a8cd2571bfe99c693a9718e7dd0e3 - languageName: node - linkType: hard - "glob-parent@npm:^3.1.0": version: 3.1.0 resolution: "glob-parent@npm:3.1.0" @@ -8236,20 +5889,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.2.0": - version: 7.2.0 - resolution: "glob@npm:7.2.0" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.0.4 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 78a8ea942331f08ed2e055cb5b9e40fe6f46f579d7fd3d694f3412fe5db23223d29b7fee1575440202e9a7ff9a72ab106a39fee39934c7bedafe5e5f8ae20134 - languageName: node - linkType: hard - "glob@npm:^7.1.1, glob@npm:^7.1.3": version: 7.1.6 resolution: "glob@npm:7.1.6" @@ -8264,7 +5903,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.2, glob@npm:^7.1.4, glob@npm:^7.2.0": +"glob@npm:^7.1.2, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -8278,7 +5917,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.1, glob@npm:^8.0.3": +"glob@npm:^8.0.1": version: 8.0.3 resolution: "glob@npm:8.0.3" dependencies: @@ -8363,46 +6002,6 @@ __metadata: languageName: node linkType: hard -"got@npm:11.5.1": - version: 11.5.1 - resolution: "got@npm:11.5.1" - dependencies: - "@sindresorhus/is": ^3.0.0 - "@szmarczak/http-timer": ^4.0.5 - "@types/cacheable-request": ^6.0.1 - "@types/responselike": ^1.0.0 - cacheable-lookup: ^5.0.3 - cacheable-request: ^7.0.1 - decompress-response: ^6.0.0 - http2-wrapper: ^1.0.0-beta.5.0 - lowercase-keys: ^2.0.0 - p-cancelable: ^2.0.0 - responselike: ^2.0.0 - checksum: 3be52e602feb62a810de1337f92a054f49f5e74f8ee27ad4140331b63a99aeaddaeb8ec49b2dae493622f11ad55c7ee4bde5a7808147cc354f93b8c389957828 - languageName: node - linkType: hard - -"got@npm:12.3.1": - version: 12.3.1 - resolution: "got@npm:12.3.1" - dependencies: - "@sindresorhus/is": ^5.2.0 - "@szmarczak/http-timer": ^5.0.1 - "@types/cacheable-request": ^6.0.2 - "@types/responselike": ^1.0.0 - cacheable-lookup: ^6.0.4 - cacheable-request: ^7.0.2 - decompress-response: ^6.0.0 - form-data-encoder: ^2.0.1 - get-stream: ^6.0.1 - http2-wrapper: ^2.1.10 - lowercase-keys: ^3.0.0 - p-cancelable: ^3.0.0 - responselike: ^2.0.0 - checksum: 156bc4dd7300b5945a2648b18d03610a674d9d3ae54c557f85cf5026f3f6362801b561413898d5e86f554791de88841685ca48674b1fc4d462621de8d2186300 - languageName: node - linkType: hard - "graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6": version: 4.2.4 resolution: "graceful-fs@npm:4.2.4" @@ -8417,7 +6016,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.10 resolution: "graceful-fs@npm:4.2.10" checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da @@ -8665,22 +6264,6 @@ __metadata: languageName: node linkType: hard -"he@npm:1.2.0": - version: 1.2.0 - resolution: "he@npm:1.2.0" - bin: - he: bin/he - checksum: 3d4d6babccccd79c5c5a3f929a68af33360d6445587d628087f39a965079d84f18ce9c3d3f917ee1e3978916fc833bb8b29377c3b403f919426f91bc6965e7a7 - languageName: node - linkType: hard - -"highlight.js@npm:^10.7.1": - version: 10.7.3 - resolution: "highlight.js@npm:10.7.3" - checksum: defeafcd546b535d710d8efb8e650af9e3b369ef53e28c3dc7893eacfe263200bba4c5fcf43524ae66d5c0c296b1af0870523ceae3e3104d24b7abf6374a4fea - languageName: node - linkType: hard - "homedir-polyfill@npm:^1.0.1": version: 1.0.3 resolution: "homedir-polyfill@npm:1.0.3" @@ -8706,13 +6289,6 @@ __metadata: languageName: node linkType: hard -"hpagent@npm:0.1.2, hpagent@npm:^0.1.1": - version: 0.1.2 - resolution: "hpagent@npm:0.1.2" - checksum: 1918518ab937d9fa615a47b94489e23662547bc1edf27069ee9bf40bfefb94da65eb142b6f42336b4b0752fce87f66c284d92b97340fd2a90b24aa3616b5450d - languageName: node - linkType: hard - "html-comment-regex@npm:^1.1.0": version: 1.1.2 resolution: "html-comment-regex@npm:1.1.2" @@ -8729,22 +6305,6 @@ __metadata: languageName: node linkType: hard -"html-encoding-sniffer@npm:^3.0.0": - version: 3.0.0 - resolution: "html-encoding-sniffer@npm:3.0.0" - dependencies: - whatwg-encoding: ^2.0.0 - checksum: 8d806aa00487e279e5ccb573366a951a9f68f65c90298eac9c3a2b440a7ffe46615aff2995a2f61c6746c639234e6179a97e18ca5ccbbf93d3725ef2099a4502 - languageName: node - linkType: hard - -"html-entities@npm:2.3.2": - version: 2.3.2 - resolution: "html-entities@npm:2.3.2" - checksum: 522d8d202df301ff51b517a379e642023ed5c81ea9fb5674ffad88cff386165733d00b6089d5c2fcc644e44777d6072017b6216d8fa40f271d3610420d00a886 - languageName: node - linkType: hard - "html-entities@npm:^1.4.0": version: 1.4.0 resolution: "html-entities@npm:1.4.0" @@ -8759,87 +6319,13 @@ __metadata: languageName: node linkType: hard -"htmlparser2@npm:^3.9.1": - version: 3.10.1 - resolution: "htmlparser2@npm:3.10.1" - dependencies: - domelementtype: ^1.3.1 - domhandler: ^2.3.0 - domutils: ^1.5.1 - entities: ^1.1.1 - inherits: ^2.0.1 - readable-stream: ^3.1.1 - checksum: 6875f7dd875aa10be17d9b130e3738cd8ed4010b1f2edaf4442c82dfafe9d9336b155870dcc39f38843cbf7fef5e4fcfdf0c4c1fd4db3a1b91a1e0ee8f6c3475 - languageName: node - linkType: hard - -"htmlparser2@npm:^6.0.0": - version: 6.1.0 - resolution: "htmlparser2@npm:6.1.0" - dependencies: - domelementtype: ^2.0.1 - domhandler: ^4.0.0 - domutils: ^2.5.2 - entities: ^2.0.0 - checksum: 81a7b3d9c3bb9acb568a02fc9b1b81ffbfa55eae7f1c41ae0bf840006d1dbf54cb3aa245b2553e2c94db674840a9f0fdad7027c9a9d01a062065314039058c4e - languageName: node - linkType: hard - -"http-assert@npm:^1.3.0": - version: 1.5.0 - resolution: "http-assert@npm:1.5.0" - dependencies: - deep-equal: ~1.0.1 - http-errors: ~1.8.0 - checksum: 69c9b3c14cf8b2822916360a365089ce936c883c49068f91c365eccba5c141a9964d19fdda589150a480013bf503bf37d8936c732e9635819339e730ab0e7527 - languageName: node - linkType: hard - -"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0": +"http-cache-semantics@npm:^4.1.0": version: 4.1.0 resolution: "http-cache-semantics@npm:4.1.0" checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 languageName: node linkType: hard -"http-errors@npm:2.0.0": - version: 2.0.0 - resolution: "http-errors@npm:2.0.0" - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - checksum: 9b0a3782665c52ce9dc658a0d1560bcb0214ba5699e4ea15aefb2a496e2ca83db03ebc42e1cce4ac1f413e4e0d2d736a3fd755772c556a9a06853ba2a0b7d920 - languageName: node - linkType: hard - -"http-errors@npm:^1.6.3, http-errors@npm:^1.7.3, http-errors@npm:~1.8.0": - version: 1.8.1 - resolution: "http-errors@npm:1.8.1" - dependencies: - depd: ~1.1.2 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: ">= 1.5.0 < 2" - toidentifier: 1.0.1 - checksum: d3c7e7e776fd51c0a812baff570bdf06fe49a5dc448b700ab6171b1250e4cf7db8b8f4c0b133e4bfe2451022a5790c1ca6c2cae4094dedd6ac8304a1267f91d2 - languageName: node - linkType: hard - -"http-errors@npm:~1.6.2": - version: 1.6.3 - resolution: "http-errors@npm:1.6.3" - dependencies: - depd: ~1.1.2 - inherits: 2.0.3 - setprototypeof: 1.1.0 - statuses: ">= 1.4.0 < 2" - checksum: a9654ee027e3d5de305a56db1d1461f25709ac23267c6dc28cdab8323e3f96caa58a9a6a5e93ac15d7285cee0c2f019378c3ada9026e7fe19c872d695f27de7c - languageName: node - linkType: hard - "http-proxy-agent@npm:^4.0.1": version: 4.0.1 resolution: "http-proxy-agent@npm:4.0.1" @@ -8873,36 +6359,7 @@ __metadata: languageName: node linkType: hard -"http2-wrapper@npm:^1.0.0-beta.5.0": - version: 1.0.3 - resolution: "http2-wrapper@npm:1.0.3" - dependencies: - quick-lru: ^5.1.1 - resolve-alpn: ^1.0.0 - checksum: 74160b862ec699e3f859739101ff592d52ce1cb207b7950295bf7962e4aa1597ef709b4292c673bece9c9b300efad0559fc86c71b1409c7a1e02b7229456003e - languageName: node - linkType: hard - -"http2-wrapper@npm:^2.1.10": - version: 2.1.11 - resolution: "http2-wrapper@npm:2.1.11" - dependencies: - quick-lru: ^5.1.1 - resolve-alpn: ^1.2.0 - checksum: 5da05aa2c77226ac9cc82c616383f59c8f31b79897b02ecbe44b09714be1fca1f21bb184e672a669ca2830eefea4edac5f07e71c00cb5a8c5afec8e5a20cfaf7 - languageName: node - linkType: hard - -"http_ece@npm:1.1.0": - version: 1.1.0 - resolution: "http_ece@npm:1.1.0" - dependencies: - urlsafe-base64: ~1.0.0 - checksum: 220a9a1c09165b4a3194c9b68a1597d3afc66a6bf8bc3ae1ae8d80d5117d8529f1346d59da7f096df8f3adbe2f42697a6edb31a6075b946207b910b72f28adba - languageName: node - linkType: hard - -"https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1": +"https-proxy-agent@npm:^5.0.0": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" dependencies: @@ -8926,13 +6383,6 @@ __metadata: languageName: node linkType: hard -"human-signals@npm:^3.0.1": - version: 3.0.1 - resolution: "human-signals@npm:3.0.1" - checksum: f252a7769c8094a5c9dc6772816bdb417b188820b04c8b42d0fc468e03a0ba905b1dd07afabe9385cc83504af1ccc2b985cd1e4aeeeb8e0029896c5af2e6f354 - languageName: node - linkType: hard - "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1" @@ -8942,14 +6392,7 @@ __metadata: languageName: node linkType: hard -"humanize-number@npm:0.0.2": - version: 0.0.2 - resolution: "humanize-number@npm:0.0.2" - checksum: 9c98c9d06b0f3d801960be3957199232a5df52377e2502acae92e4f71de633fa62c315a83f24bf96bef76f47b2e3e0e1e4f4157c891e27074fd3272cad6724bb - languageName: node - linkType: hard - -"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.4": +"iconv-lite@npm:0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" dependencies: @@ -8958,7 +6401,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": +"iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -8976,14 +6419,7 @@ __metadata: languageName: node linkType: hard -"ieee754@npm:1.1.13": - version: 1.1.13 - resolution: "ieee754@npm:1.1.13" - checksum: 102df1ba662e316e6160f7ce29c7c7fa3e04f2014c288336c5a9ff40bbcc2a27d209fa2a81ebfb33f28b1941021343d30e9ad8ee85a2d61f79f5936c35edc33d - languageName: node - linkType: hard - -"ieee754@npm:^1.1.13, ieee754@npm:^1.1.4, ieee754@npm:^1.2.1": +"ieee754@npm:^1.1.13": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e @@ -9061,13 +6497,6 @@ __metadata: languageName: node linkType: hard -"inflation@npm:^2.0.0": - version: 2.0.0 - resolution: "inflation@npm:2.0.0" - checksum: a0494871b12275afdef9e2710ee1af1e0fc642b04613a9be69c05ef8b5e9627f3bd7d358a937fa47aa20235ee7313a4f30255048533add0ad4918beb918a586e - languageName: node - linkType: hard - "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -9078,20 +6507,13 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.0, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 languageName: node linkType: hard -"inherits@npm:2.0.3": - version: 2.0.3 - resolution: "inherits@npm:2.0.3" - checksum: 78cb8d7d850d20a5e9a7f3620db31483aa00ad5f722ce03a55b110e5a723539b3716a3b463e2b96ce3fe286f33afc7c131fa2f91407528ba80cea98a7545d4c0 - languageName: node - linkType: hard - "ini@npm:2.0.0": version: 2.0.0 resolution: "ini@npm:2.0.0" @@ -9106,13 +6528,6 @@ __metadata: languageName: node linkType: hard -"ini@npm:~1.3.0": - version: 1.3.8 - resolution: "ini@npm:1.3.8" - checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3 - languageName: node - linkType: hard - "insert-text-at-cursor@npm:0.3.0": version: 0.3.0 resolution: "insert-text-at-cursor@npm:0.3.0" @@ -9120,16 +6535,6 @@ __metadata: languageName: node linkType: hard -"install-artifact-from-github@npm:^1.3.1": - version: 1.3.1 - resolution: "install-artifact-from-github@npm:1.3.1" - bin: - install-from-cache: bin/install-from-cache.js - save-to-github-cache: bin/save-to-github-cache.js - checksum: 9d33af5d021f0ec9ee28d55a95a63bd956a75940c4bb39c3107a5c4c583d61c079751deb15e7cceebcca2f8a4e6ba2e4cdc91ea9487589a9225047e9a1524d03 - languageName: node - linkType: hard - "internal-slot@npm:^1.0.3": version: 1.0.3 resolution: "internal-slot@npm:1.0.3" @@ -9155,52 +6560,6 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:4.28.5, ioredis@npm:^4.28.5": - version: 4.28.5 - resolution: "ioredis@npm:4.28.5" - dependencies: - cluster-key-slot: ^1.1.0 - debug: ^4.3.1 - denque: ^1.1.0 - lodash.defaults: ^4.2.0 - lodash.flatten: ^4.4.0 - lodash.isarguments: ^3.1.0 - p-map: ^2.1.0 - redis-commands: 1.7.0 - redis-errors: ^1.2.0 - redis-parser: ^3.0.0 - standard-as-callback: ^2.1.0 - checksum: a8793c3324cd69fa55b4baacbda118ce6724e574260157761276b31411dd3e168c75490f7155c6ce34d79e01488efa98e0cdb162991970fd56da7cbcdafb8fb8 - languageName: node - linkType: hard - -"ip-address@npm:^7.1.0": - version: 7.1.0 - resolution: "ip-address@npm:7.1.0" - dependencies: - jsbn: 1.1.0 - sprintf-js: 1.1.2 - checksum: b514b93b76639b204e52a16b1cebdc23c69fa71464665583278cbd0adf402d0b0e15f606049010b7230d35f6e9fa4d0a4ffd61d63fbc00f71adf08f00bc7614b - languageName: node - linkType: hard - -"ip-cidr@npm:3.0.10": - version: 3.0.10 - resolution: "ip-cidr@npm:3.0.10" - dependencies: - ip-address: ^7.1.0 - jsbn: ^1.1.0 - checksum: 8444d05b0fa728fc59ee2e495f5bca0d0a3cea89fef409501ef2a409dfe80433f5fb50b4c084f6bad17a393bcad2c28a98aa6c120875b27186b835727b350db8 - languageName: node - linkType: hard - -"ip-regex@npm:^4.0.0, ip-regex@npm:^4.3.0": - version: 4.3.0 - resolution: "ip-regex@npm:4.3.0" - checksum: 7ff904b891221b1847f3fdf3dbb3e6a8660dc39bc283f79eb7ed88f5338e1a3d1104b779bc83759159be266249c59c2160e779ee39446d79d4ed0890dfd06f08 - languageName: node - linkType: hard - "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -9208,13 +6567,6 @@ __metadata: languageName: node linkType: hard -"ipaddr.js@npm:^2.0.1": - version: 2.0.1 - resolution: "ipaddr.js@npm:2.0.1" - checksum: dd194a394a843d470f88d17191b0948f383ed1c8e320813f850c336a0fcb5e9215d97ec26ca35ab4fbbd31392c8b3467f3e8344628029ed3710b2ff6b5d1034e - languageName: node - linkType: hard - "irregular-plurals@npm:^3.2.0": version: 3.3.0 resolution: "irregular-plurals@npm:3.3.0" @@ -9264,13 +6616,6 @@ __metadata: languageName: node linkType: hard -"is-arrayish@npm:^0.3.1": - version: 0.3.2 - resolution: "is-arrayish@npm:0.3.2" - checksum: 977e64f54d91c8f169b59afcd80ff19227e9f5c791fa28fa2e5bce355cbaf6c2c356711b734656e80c9dd4a854dd7efcf7894402f1031dfc5de5d620775b4d5f - languageName: node - linkType: hard - "is-bigint@npm:^1.0.1": version: 1.0.4 resolution: "is-bigint@npm:1.0.4" @@ -9391,16 +6736,6 @@ __metadata: languageName: node linkType: hard -"is-expression@npm:^4.0.0": - version: 4.0.0 - resolution: "is-expression@npm:4.0.0" - dependencies: - acorn: ^7.1.1 - object-assign: ^4.1.1 - checksum: 0f01d0ff53fbbec36abae8fbb7ef056c6d024f7128646856a3e6c500b205788d3e0f337025e72df979d7d7cf4674a00370633d7f8974c668b2d3fdb7e8a83bdb - languageName: node - linkType: hard - "is-extendable@npm:^0.1.0, is-extendable@npm:^0.1.1": version: 0.1.1 resolution: "is-extendable@npm:0.1.1" @@ -9447,15 +6782,6 @@ __metadata: languageName: node linkType: hard -"is-generator-function@npm:^1.0.7": - version: 1.0.10 - resolution: "is-generator-function@npm:1.0.10" - dependencies: - has-tostringtag: ^1.0.0 - checksum: d54644e7dbaccef15ceb1e5d91d680eb5068c9ee9f9eb0a9e04173eb5542c9b51b5ab52c5537f5703e48d5fddfd376817c1ca07a84a407b7115b769d4bdde72b - languageName: node - linkType: hard - "is-glob@npm:^3.1.0": version: 3.1.0 resolution: "is-glob@npm:3.1.0" @@ -9493,15 +6819,6 @@ __metadata: languageName: node linkType: hard -"is-ip@npm:^3.1.0": - version: 3.1.0 - resolution: "is-ip@npm:3.1.0" - dependencies: - ip-regex: ^4.0.0 - checksum: da2c2b282407194adf2320bade0bad94be9c9d0bdab85ff45b1b62d8185f31c65dff3884519d57bf270277e5ea2046c7916a6e5a6db22fe4b7ddcdd3760f23eb - languageName: node - linkType: hard - "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -9569,13 +6886,6 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^2.1.0": - version: 2.1.0 - resolution: "is-plain-obj@npm:2.1.0" - checksum: cec9100678b0a9fe0248a81743041ed990c2d4c99f893d935545cfbc42876cbe86d207f3b895700c690ad2fa520e568c44afc1605044b535a7820c1d40e38daa - languageName: node - linkType: hard - "is-plain-object@npm:^2.0.1, is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4": version: 2.0.4 resolution: "is-plain-object@npm:2.0.4" @@ -9599,14 +6909,7 @@ __metadata: languageName: node linkType: hard -"is-promise@npm:^2.0.0": - version: 2.2.2 - resolution: "is-promise@npm:2.2.2" - checksum: 18bf7d1c59953e0ad82a1ed963fb3dc0d135c8f299a14f89a17af312fc918373136e56028e8831700e1933519630cc2fd4179a777030330fde20d34e96f40c78 - languageName: node - linkType: hard - -"is-regex@npm:^1.0.3, is-regex@npm:^1.1.4": +"is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" dependencies: @@ -9641,13 +6944,6 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "is-stream@npm:3.0.0" - checksum: 172093fe99119ffd07611ab6d1bcccfe8bc4aa80d864b15f43e63e54b7abc71e779acd69afdb854c4e2a67fdc16ae710e370eda40088d1cfc956a50ed82d8f16 - languageName: node - linkType: hard - "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -9657,15 +6953,6 @@ __metadata: languageName: node linkType: hard -"is-svg@npm:4.3.2": - version: 4.3.2 - resolution: "is-svg@npm:4.3.2" - dependencies: - fast-xml-parser: ^3.19.0 - checksum: b3bce2395e25a0127e65e33fbfd8654225c303f7b45005af8eea69cc0870e252d8a01d62fd333c27247eadd38720b5459d623c292e02c14e328bf4970da891c7 - languageName: node - linkType: hard - "is-svg@npm:^2.0.0": version: 2.1.0 resolution: "is-svg@npm:2.1.0" @@ -9730,13 +7017,6 @@ __metadata: languageName: node linkType: hard -"is-whitespace@npm:^0.3.0": - version: 0.3.0 - resolution: "is-whitespace@npm:0.3.0" - checksum: dac8fc9a9b797afeef703f625269601715552883790d1385d6bb27dd04ffdafd5fddca8f2d85ee96913850211595da2ba483dac1f166829c4078fb58ce815140 - languageName: node - linkType: hard - "is-windows@npm:^1.0.1, is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" @@ -9744,14 +7024,7 @@ __metadata: languageName: node linkType: hard -"isarray@npm:0.0.1": - version: 0.0.1 - resolution: "isarray@npm:0.0.1" - checksum: 49191f1425681df4a18c2f0f93db3adb85573bcdd6a4482539d98eac9e705d8961317b01175627e860516a2fc45f8f9302db26e5a380a97a520e272e2a40a8d4 - languageName: node - linkType: hard - -"isarray@npm:1.0.0, isarray@npm:^1.0.0, isarray@npm:~1.0.0": +"isarray@npm:1.0.0, isarray@npm:~1.0.0": version: 1.0.0 resolution: "isarray@npm:1.0.0" checksum: f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab @@ -9850,20 +7123,6 @@ __metadata: languageName: node linkType: hard -"jake@npm:^10.8.5": - version: 10.8.5 - resolution: "jake@npm:10.8.5" - dependencies: - async: ^3.2.3 - chalk: ^4.0.2 - filelist: ^1.0.1 - minimatch: ^3.0.4 - bin: - jake: ./bin/cli.js - checksum: 56c913ecf5a8d74325d0af9bc17a233bad50977438d44864d925bb6c45c946e0fee8c4c1f5fe2225471ef40df5222e943047982717ebff0d624770564d3c46ba - languageName: node - linkType: hard - "jest-changed-files@npm:^27.5.1": version: 27.5.1 resolution: "jest-changed-files@npm:27.5.1" @@ -10399,13 +7658,6 @@ __metadata: languageName: node linkType: hard -"jmespath@npm:0.16.0": - version: 0.16.0 - resolution: "jmespath@npm:0.16.0" - checksum: 2d602493a1e4addfd1350ac8c9d54b1b03ed09e305fd863bab84a4ee1f52868cf939dd1a08c5cdea29ce9ba8f86875ebb458b6ed45dab3e1c3f2694503fb2fd9 - languageName: node - linkType: hard - "joi@npm:^17.4.0": version: 17.4.2 resolution: "joi@npm:17.4.2" @@ -10419,13 +7671,6 @@ __metadata: languageName: node linkType: hard -"jpeg-js@npm:^0.4.1": - version: 0.4.4 - resolution: "jpeg-js@npm:0.4.4" - checksum: bd7cb61aa8df40a9ee2c2106839c3df6054891e56cfc22c0ac581402e06c6295f962a4754b0b2ac50a401789131b1c6dc9df8d24400f1352168be1894833c590 - languageName: node - linkType: hard - "js-base64@npm:^2.1.9": version: 2.6.4 resolution: "js-base64@npm:2.6.4" @@ -10433,29 +7678,6 @@ __metadata: languageName: node linkType: hard -"js-beautify@npm:^1.6.12": - version: 1.14.6 - resolution: "js-beautify@npm:1.14.6" - dependencies: - config-chain: ^1.1.13 - editorconfig: ^0.15.3 - glob: ^8.0.3 - nopt: ^6.0.0 - bin: - css-beautify: js/bin/css-beautify.js - html-beautify: js/bin/html-beautify.js - js-beautify: js/bin/js-beautify.js - checksum: 2e61c1183c73d7464ec7d5354763ee9e17ecb4b5c7a3744e9a9dc2e9c406fc6e0c37399130d1ee48313ff502e03e8a949dce96acdaee309dc44317249565934d - languageName: node - linkType: hard - -"js-levenshtein@npm:^1.1.6": - version: 1.1.6 - resolution: "js-levenshtein@npm:1.1.6" - checksum: 409f052a7f1141be4058d97da7860e08efd97fc588b7a4c5cfa0548bc04f6d576644dae65ab630266dff685d56fb90d494e03d4d79cb484c287746b4f1bf0694 - languageName: node - linkType: hard - "js-sdsl@npm:^4.1.4": version: 4.2.0 resolution: "js-sdsl@npm:4.2.0" @@ -10463,13 +7685,6 @@ __metadata: languageName: node linkType: hard -"js-stringify@npm:^1.0.2": - version: 1.0.2 - resolution: "js-stringify@npm:1.0.2" - checksum: f9701d9e535d3ac0f62bbf2624b76c5d0af5b889187232817ae284a41ba21fd7a8b464c2dce3815d8cf52c8bea3480be6b368cfc2c67da799cad458058e8bbf5 - languageName: node - linkType: hard - "js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -10512,13 +7727,6 @@ __metadata: languageName: node linkType: hard -"jsbn@npm:1.1.0, jsbn@npm:^1.1.0": - version: 1.1.0 - resolution: "jsbn@npm:1.1.0" - checksum: 944f924f2bd67ad533b3850eee47603eed0f6ae425fd1ee8c760f477e8c34a05f144c1bd4f5a5dd1963141dc79a2c55f89ccc5ab77d039e7077f3ad196b64965 - languageName: node - linkType: hard - "jsbn@npm:~0.1.0": version: 0.1.1 resolution: "jsbn@npm:0.1.1" @@ -10526,53 +7734,6 @@ __metadata: languageName: node linkType: hard -"jschardet@npm:3.0.0": - version: 3.0.0 - resolution: "jschardet@npm:3.0.0" - checksum: f391df4512bbd7edf97f82be09ba841a80cb4408c3ab96cf7e9c307737bd08cd8b3d10986f3db844d04151530395dcd174b3288f17144206d42675ecf482c286 - languageName: node - linkType: hard - -"jsdom@npm:20.0.0": - version: 20.0.0 - resolution: "jsdom@npm:20.0.0" - dependencies: - abab: ^2.0.6 - acorn: ^8.7.1 - acorn-globals: ^6.0.0 - cssom: ^0.5.0 - cssstyle: ^2.3.0 - data-urls: ^3.0.2 - decimal.js: ^10.3.1 - domexception: ^4.0.0 - escodegen: ^2.0.0 - form-data: ^4.0.0 - html-encoding-sniffer: ^3.0.0 - http-proxy-agent: ^5.0.0 - https-proxy-agent: ^5.0.1 - is-potential-custom-element-name: ^1.0.1 - nwsapi: ^2.2.0 - parse5: ^7.0.0 - saxes: ^6.0.0 - symbol-tree: ^3.2.4 - tough-cookie: ^4.0.0 - w3c-hr-time: ^1.0.2 - w3c-xmlserializer: ^3.0.0 - webidl-conversions: ^7.0.0 - whatwg-encoding: ^2.0.0 - whatwg-mimetype: ^3.0.0 - whatwg-url: ^11.0.0 - ws: ^8.8.0 - xml-name-validator: ^4.0.0 - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - checksum: f69b40679d8cfaee2353615445aaff08b823c53dc7778ede6592d02ed12b3e9fb4e8db2b6d033551b67e08424a3adb2b79d231caa7dcda2d16019c20c705c11f - languageName: node - linkType: hard - "jsdom@npm:^16.6.0": version: 16.7.0 resolution: "jsdom@npm:16.7.0" @@ -10622,13 +7783,6 @@ __metadata: languageName: node linkType: hard -"json-buffer@npm:3.0.1, json-buffer@npm:~3.0.1": - version: 3.0.1 - resolution: "json-buffer@npm:3.0.1" - checksum: 9026b03edc2847eefa2e37646c579300a1f3a4586cfb62bf857832b60c852042d0d6ae55d1afb8926163fa54c2b01d83ae24705f34990348bdac6273a29d4581 - languageName: node - linkType: hard - "json-parse-even-better-errors@npm:^2.3.0": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -10643,13 +7797,6 @@ __metadata: languageName: node linkType: hard -"json-schema-traverse@npm:^1.0.0": - version: 1.0.0 - resolution: "json-schema-traverse@npm:1.0.0" - checksum: 02f2f466cdb0362558b2f1fd5e15cce82ef55d60cd7f8fa828cf35ba74330f8d767fcae5c5c2adb7851fa811766c694b9405810879bc4e1ddd78a7c0e03658ad - languageName: node - linkType: hard - "json-schema@npm:0.4.0": version: 0.4.0 resolution: "json-schema@npm:0.4.0" @@ -10664,27 +7811,14 @@ __metadata: languageName: node linkType: hard -"json-stringify-safe@npm:^5.0.1, json-stringify-safe@npm:~5.0.1": +"json-stringify-safe@npm:~5.0.1": version: 5.0.1 resolution: "json-stringify-safe@npm:5.0.1" checksum: 48ec0adad5280b8a96bb93f4563aa1667fd7a36334f79149abd42446d0989f2ddc58274b479f4819f1f00617957e6344c886c55d05a4e15ebb4ab931e4a6a8ee languageName: node linkType: hard -"json5-loader@npm:4.0.1": - version: 4.0.1 - resolution: "json5-loader@npm:4.0.1" - dependencies: - json5: ^2.1.3 - loader-utils: ^2.0.0 - schema-utils: ^3.0.0 - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 - checksum: 89e82429dc71d4c76b3e5d27e4f1437de38e1963bb81ba392b340e3993b6b5fb7a16de6aec233f173f5e8249b5ac2cc371bf0be6734f678acee6f3498294f0e3 - languageName: node - linkType: hard - -"json5@npm:2.2.1, json5@npm:2.x, json5@npm:^2.1.2, json5@npm:^2.1.3, json5@npm:^2.2.1": +"json5@npm:2.2.1, json5@npm:2.x, json5@npm:^2.2.1": version: 2.2.1 resolution: "json5@npm:2.2.1" bin: @@ -10742,30 +7876,6 @@ __metadata: languageName: node linkType: hard -"jsonld@npm:6.0.0": - version: 6.0.0 - resolution: "jsonld@npm:6.0.0" - dependencies: - "@digitalbazaar/http-client": ^3.2.0 - canonicalize: ^1.0.1 - lru-cache: ^6.0.0 - rdf-canonize: ^3.0.0 - checksum: b062cecc01ed1c68ada8e3179532c4f514c49427781c41bd19f34a0d52fb758c13eb3b2ada548558630e4c5ac3b697f8dbe64499a6f214912684cbf5bf68aea8 - languageName: node - linkType: hard - -"jsprim@npm:^1.2.2": - version: 1.4.2 - resolution: "jsprim@npm:1.4.2" - dependencies: - assert-plus: 1.0.0 - extsprintf: 1.3.0 - json-schema: 0.4.0 - verror: 1.10.0 - checksum: 2ad1b9fdcccae8b3d580fa6ced25de930eaa1ad154db21bbf8478a4d30bbbec7925b5f5ff29b933fba9412b16a17bd484a8da4fdb3663b5e27af95dd693bab2a - languageName: node - linkType: hard - "jsprim@npm:^2.0.2": version: 2.0.2 resolution: "jsprim@npm:2.0.2" @@ -10778,23 +7888,6 @@ __metadata: languageName: node linkType: hard -"jsrsasign@npm:10.5.25": - version: 10.5.25 - resolution: "jsrsasign@npm:10.5.25" - checksum: e789cd6d5e6e236edebe91936279cdef886aafaab8d4ebc7d40b8ba0c7b7cd0d6d3c53fba26ea96eb33cc77a05368b369ef31fc9182662bce2344143ad50ea0f - languageName: node - linkType: hard - -"jstransformer@npm:1.0.0": - version: 1.0.0 - resolution: "jstransformer@npm:1.0.0" - dependencies: - is-promise: ^2.0.0 - promise: ^7.0.1 - checksum: 1e019fde17a38766a5b96bccf0738156badc60cfa61e2ba8a8bbd3b855e7d5d7e17492b8a66e4aaabc39483e335d23217343ae32d0f7e5a81af42a95c3e075f9 - languageName: node - linkType: hard - "just-debounce@npm:^1.0.0": version: 1.0.0 resolution: "just-debounce@npm:1.0.0" @@ -10802,34 +7895,6 @@ __metadata: languageName: node linkType: hard -"just-extend@npm:^4.0.2": - version: 4.2.1 - resolution: "just-extend@npm:4.2.1" - checksum: ff9fdede240fad313efeeeb68a660b942e5586d99c0058064c78884894a2690dc09bba44c994ad4e077e45d913fef01a9240c14a72c657b53687ac58de53b39c - languageName: node - linkType: hard - -"jwa@npm:^2.0.0": - version: 2.0.0 - resolution: "jwa@npm:2.0.0" - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: ^5.0.1 - checksum: 8f00b71ad5fe94cb55006d0d19202f8f56889109caada2f7eeb63ca81755769ce87f4f48101967f398462e3b8ae4faebfbd5a0269cb755dead5d63c77ba4d2f1 - languageName: node - linkType: hard - -"jws@npm:^4.0.0": - version: 4.0.0 - resolution: "jws@npm:4.0.0" - dependencies: - jwa: ^2.0.0 - safe-buffer: ^5.0.1 - checksum: d68d07aa6d1b8cb35c363a9bd2b48f15064d342a5d9dc18a250dbbce8dc06bd7e4792516c50baa16b8d14f61167c19e851fd7f66b59ecc68b7f6a013759765f7 - languageName: node - linkType: hard - "katex@npm:0.16.0": version: 0.16.0 resolution: "katex@npm:0.16.0" @@ -10841,25 +7906,6 @@ __metadata: languageName: node linkType: hard -"keygrip@npm:~1.1.0": - version: 1.1.0 - resolution: "keygrip@npm:1.1.0" - dependencies: - tsscmp: 1.0.6 - checksum: 078cd16a463d187121f0a27c1c9c95c52ad392b620f823431689f345a0501132cee60f6e96914b07d570105af470b96960402accd6c48a0b1f3cd8fac4fa2cae - languageName: node - linkType: hard - -"keyv@npm:^4.0.0": - version: 4.4.1 - resolution: "keyv@npm:4.4.1" - dependencies: - compress-brotli: ^1.3.8 - json-buffer: 3.0.1 - checksum: efce046d161381121b727e9d753deeaad4ce06a98db6d68442cf1542a3731a46f461d0834fa1937c6ce7b27c807fe7892d4de3074440f4d3dff01ac4c7b32692 - languageName: node - linkType: hard - "kind-of@npm:^3.0.2, kind-of@npm:^3.0.3, kind-of@npm:^3.2.0": version: 3.2.2 resolution: "kind-of@npm:3.2.2" @@ -10899,211 +7945,6 @@ __metadata: languageName: node linkType: hard -"koa-bodyparser@npm:4.3.0": - version: 4.3.0 - resolution: "koa-bodyparser@npm:4.3.0" - dependencies: - co-body: ^6.0.0 - copy-to: ^2.0.1 - checksum: c227fe0fb5a55b98fc91d865e80229b60178d216d53b732b07833eb38f48a7ed6aa768a083bc06e359db33298547e9a65842fbe9d3f0fdaf5149fe0becafc88f - languageName: node - linkType: hard - -"koa-compose@npm:^4.1.0": - version: 4.1.0 - resolution: "koa-compose@npm:4.1.0" - checksum: 46cb16792d96425e977c2ae4e5cb04930280740e907242ec9c25e3fb8b4a1d7b54451d7432bc24f40ec62255edea71894d2ceeb8238501842b4e48014f2e83db - languageName: node - linkType: hard - -"koa-convert@npm:^2.0.0": - version: 2.0.0 - resolution: "koa-convert@npm:2.0.0" - dependencies: - co: ^4.6.0 - koa-compose: ^4.1.0 - checksum: 7385b3391995f59c1312142e110d5dff677f9850dbfbcf387cd36a7b0af03b5d26e82b811eb9bb008b4f3e661cdab1f8817596e46b1929da2cf6e97a2f7456ed - languageName: node - linkType: hard - -"koa-favicon@npm:2.1.0": - version: 2.1.0 - resolution: "koa-favicon@npm:2.1.0" - dependencies: - mz: ^2.7.0 - checksum: 024051a0be3560a77e65651ad87690d432a28bed3c4dd24cbcbe3249a38d6cc0c80613d37b45107c8b709cb01fecf8723e77d50cbb2ce67c8451fa29891d228f - languageName: node - linkType: hard - -"koa-json-body@npm:5.3.0": - version: 5.3.0 - resolution: "koa-json-body@npm:5.3.0" - dependencies: - co-body: ^5.0.0 - checksum: b3ae5f304cbb6f8f6a4a2ba36047d1ea6fe32005e7665d795802eb4c3dd1f341a0b8e61087fffc6bd2da6f554d71b06b047cbb3669b1eea814285f75d9bdd736 - languageName: node - linkType: hard - -"koa-logger@npm:3.2.1": - version: 3.2.1 - resolution: "koa-logger@npm:3.2.1" - dependencies: - bytes: ^3.1.0 - chalk: ^2.4.2 - humanize-number: 0.0.2 - passthrough-counter: ^1.0.0 - checksum: b29ba25eb433452bfda48e51acd5d206128411966acc09bb13ce3a0cec9192f78bb27e23efd615d0e7f46eeb2588ee8d2541d72665a4aa18d27a177e78dca909 - languageName: node - linkType: hard - -"koa-mount@npm:4.0.0, koa-mount@npm:^4.0.0": - version: 4.0.0 - resolution: "koa-mount@npm:4.0.0" - dependencies: - debug: ^4.0.1 - koa-compose: ^4.1.0 - checksum: c7e8c5cca4d2ccc4742e63c81b86b44f0290075148897b5d633acdd137e90f554c60c232fbc62e843eaedb913b67c5a49367c1142e290b8cfd9c28eb4a0480ec - languageName: node - linkType: hard - -"koa-router@npm:^10.0.0": - version: 10.1.1 - resolution: "koa-router@npm:10.1.1" - dependencies: - debug: ^4.1.1 - http-errors: ^1.7.3 - koa-compose: ^4.1.0 - methods: ^1.1.2 - path-to-regexp: ^6.1.0 - checksum: 65e6cd4a7f8a4d98c665b00ee4c2c05340cb38ca035590ce71c23a25c0a01f6d2434d9a68366d7c218af9c94e5d8e20c7fe9e7f7dfbb98d69b11b5ae3246aaf8 - languageName: node - linkType: hard - -"koa-send@npm:5.0.1, koa-send@npm:^5.0.0": - version: 5.0.1 - resolution: "koa-send@npm:5.0.1" - dependencies: - debug: ^4.1.1 - http-errors: ^1.7.3 - resolve-path: ^1.4.0 - checksum: a9fbaadbe0f50efd157a733df4a1cc2b3b79b0cdf12e67c718641e6038d1792c0bebe40913e6d4ceb707d970301155be3859b98d1ef08b0fd1766f7326b82853 - languageName: node - linkType: hard - -"koa-slow@npm:2.1.0": - version: 2.1.0 - resolution: "koa-slow@npm:2.1.0" - dependencies: - lodash.isregexp: 3.0.5 - q: 1.4.1 - checksum: 1b2fa6c709cd4016f5c5c4f45a8bd569910fdfef482c85120f2bbddd5cf274d714b0d231659ac3335d15b03f0debdb71b14f3cc54624921be7d808df7f8ac513 - languageName: node - linkType: hard - -"koa-static@npm:^5.0.0": - version: 5.0.0 - resolution: "koa-static@npm:5.0.0" - dependencies: - debug: ^3.1.0 - koa-send: ^5.0.0 - checksum: 8d9b9c4d2b3b13e8818e804245d784099c4b353b55ddd7dbeeb90f27a2e9f5b6f86bd16a4909e337cb89db4d332d9002e6c0f5056caf75749cab62f93c1f0cc5 - languageName: node - linkType: hard - -"koa-views@npm:*": - version: 8.0.0 - resolution: "koa-views@npm:8.0.0" - dependencies: - consolidate: ^0.16.0 - debug: ^4.1.0 - get-paths: 0.0.7 - koa-send: ^5.0.0 - mz: ^2.4.0 - pretty: ^2.0.0 - resolve-path: ^1.4.0 - peerDependencies: - "@types/koa": ^2.13.1 - peerDependenciesMeta: - "@types/koa": - optional: true - checksum: 6e7e4df04fcab8e5c50a82b38518b7e87491fef1a80f8105e34aaf82a93c0abf051a011d02a8bed8542255724bdb2b83cda7b63e094983c01a5c3927ea08be2c - languageName: node - linkType: hard - -"koa-views@npm:7.0.2, koa-views@npm:^7.0.1": - version: 7.0.2 - resolution: "koa-views@npm:7.0.2" - dependencies: - consolidate: ^0.16.0 - debug: ^4.1.0 - get-paths: 0.0.7 - koa-send: ^5.0.0 - mz: ^2.4.0 - pretty: ^2.0.0 - resolve-path: ^1.4.0 - peerDependencies: - "@types/koa": ^2.13.1 - peerDependenciesMeta: - "@types/koa": - optional: true - checksum: e591a131de09cf2676ae0492dabf420015404cd1198092a2aa217118c5e7df5da848f49c658f46af172028a508cecbdb0a81e45c5acf5cf40c2baf7c9d08675e - languageName: node - linkType: hard - -"koa@npm:2.13.4, koa@npm:^2.13.1": - version: 2.13.4 - resolution: "koa@npm:2.13.4" - dependencies: - accepts: ^1.3.5 - cache-content-type: ^1.0.0 - content-disposition: ~0.5.2 - content-type: ^1.0.4 - cookies: ~0.8.0 - debug: ^4.3.2 - delegates: ^1.0.0 - depd: ^2.0.0 - destroy: ^1.0.4 - encodeurl: ^1.0.2 - escape-html: ^1.0.3 - fresh: ~0.5.2 - http-assert: ^1.3.0 - http-errors: ^1.6.3 - is-generator-function: ^1.0.7 - koa-compose: ^4.1.0 - koa-convert: ^2.0.0 - on-finished: ^2.3.0 - only: ~0.0.2 - parseurl: ^1.3.2 - statuses: ^1.5.0 - type-is: ^1.6.16 - vary: ^1.1.2 - checksum: c9a6f9c803433b2d143a0788308048c1432a71c5febcfea2af7f2e8bd732b9bfd75c2c220d553752ee9ab9a3f52490f006cfd521db97cd01d8461d67cc1ccc1f - languageName: node - linkType: hard - -"ky-universal@npm:^0.10.1": - version: 0.10.1 - resolution: "ky-universal@npm:0.10.1" - dependencies: - abort-controller: ^3.0.0 - node-fetch: ^3.2.2 - peerDependencies: - ky: ">=0.26.0" - web-streams-polyfill: ">=3.0.1" - peerDependenciesMeta: - web-streams-polyfill: - optional: true - checksum: 61459a8bf62a9c3d2f3efe7893b9a621b21a68208acae6fb7f571d64a57be802d713a1cd147928e734b82539ffab23c12c8cffd6e5b2fa550c22e9d7510035b1 - languageName: node - linkType: hard - -"ky@npm:^0.30.0": - version: 0.30.0 - resolution: "ky@npm:0.30.0" - checksum: d6ec6461e18cd2e4abb809b0574ff704c2e17bd61248843471c2492dd9486dfeadd4d139a5a6ec194b0175f9bc7b8c634685880d1ac646553a9f440c0982e8d3 - languageName: node - linkType: hard - "last-run@npm:^1.1.0": version: 1.1.1 resolution: "last-run@npm:1.1.1" @@ -11198,13 +8039,6 @@ __metadata: languageName: node linkType: hard -"listenercount@npm:~1.0.1": - version: 1.0.1 - resolution: "listenercount@npm:1.0.1" - checksum: 0f1c9077cdaf2ebc16473c7d72eb7de6d983898ca42500f03da63c3914b6b312dd5f7a90d2657691ea25adf3fe0ac5a43226e8b2c673fd73415ed038041f4757 - languageName: node - linkType: hard - "listr2@npm:^3.8.3": version: 3.11.0 resolution: "listr2@npm:3.11.0" @@ -11235,17 +8069,6 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:^2.0.0": - version: 2.0.2 - resolution: "loader-utils@npm:2.0.2" - dependencies: - big.js: ^5.2.2 - emojis-list: ^3.0.0 - json5: ^2.1.2 - checksum: 9078d1ed47cadc57f4c6ddbdb2add324ee7da544cea41de3b7f1128e8108fcd41cd3443a85b7ee8d7d8ac439148aa221922774efe4cf87506d4fb054d5889303 - languageName: node - linkType: hard - "locate-path@npm:^5.0.0": version: 5.0.0 resolution: "locate-path@npm:5.0.0" @@ -11264,55 +8087,6 @@ __metadata: languageName: node linkType: hard -"lodash.assignin@npm:^4.0.9": - version: 4.2.0 - resolution: "lodash.assignin@npm:4.2.0" - checksum: 4b55bc1d65ccd7648fdba8a4316d10546929bf0beb5950830d86c559948cf170f0e65b77c95e66b45b511b85a31161714de8b2008d2537627ef3c7759afe36a6 - languageName: node - linkType: hard - -"lodash.bind@npm:^4.1.4": - version: 4.2.1 - resolution: "lodash.bind@npm:4.2.1" - checksum: cf0e41de2fca7704fc0adadc00f7fc871f8cf428990972f072136e4cd153c4d42d88c1418218121380914021c5547be05e4252e61f6280c736a2195cc8b6f4e5 - languageName: node - linkType: hard - -"lodash.defaults@npm:^4.0.1, lodash.defaults@npm:^4.2.0": - version: 4.2.0 - resolution: "lodash.defaults@npm:4.2.0" - checksum: 84923258235592c8886e29de5491946ff8c2ae5c82a7ac5cddd2e3cb697e6fbdfbbb6efcca015795c86eec2bb953a5a2ee4016e3735a3f02720428a40efbb8f1 - languageName: node - linkType: hard - -"lodash.difference@npm:^4.5.0": - version: 4.5.0 - resolution: "lodash.difference@npm:4.5.0" - checksum: ecee276aa578f300e79350805a14a51be8d1f12b3c1389a19996d8ab516f814211a5f65c68331571ecdad96522b863ccc484b55504ce8c9947212a29f8857d5a - languageName: node - linkType: hard - -"lodash.filter@npm:^4.4.0": - version: 4.6.0 - resolution: "lodash.filter@npm:4.6.0" - checksum: f21d245d24818e15b560cb6cadc8404a1bf98bd87d037e5e51858aad57ca2b9db64d87e450a23c8f72dd2c66968efd09b034055ce86d93eef4a4eb6f1bbaf100 - languageName: node - linkType: hard - -"lodash.flatten@npm:^4.2.0, lodash.flatten@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.flatten@npm:4.4.0" - checksum: 0ac34a393d4b795d4b7421153d27c13ae67e08786c9cbb60ff5b732210d46f833598eee3fb3844bb10070e8488efe390ea53bb567377e0cb47e9e630bf0811cb - languageName: node - linkType: hard - -"lodash.foreach@npm:^4.3.0": - version: 4.5.0 - resolution: "lodash.foreach@npm:4.5.0" - checksum: a940386b158ca0d62994db41fc16529eb8ae67138f29ced38e91f912cb5435d1b0ed34b18e6f7b9ddfc32ab676afc6dfec60d1e22633d8e3e4b33413402ab4ad - languageName: node - linkType: hard - "lodash.get@npm:^4.4.2": version: 4.4.2 resolution: "lodash.get@npm:4.4.2" @@ -11320,13 +8094,6 @@ __metadata: languageName: node linkType: hard -"lodash.isarguments@npm:^3.1.0": - version: 3.1.0 - resolution: "lodash.isarguments@npm:3.1.0" - checksum: ae1526f3eb5c61c77944b101b1f655f846ecbedcb9e6b073526eba6890dc0f13f09f72e11ffbf6540b602caee319af9ac363d6cdd6be41f4ee453436f04f13b5 - languageName: node - linkType: hard - "lodash.isequal@npm:^4.5.0": version: 4.5.0 resolution: "lodash.isequal@npm:4.5.0" @@ -11334,27 +8101,6 @@ __metadata: languageName: node linkType: hard -"lodash.isplainobject@npm:^4.0.6": - version: 4.0.6 - resolution: "lodash.isplainobject@npm:4.0.6" - checksum: 29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337 - languageName: node - linkType: hard - -"lodash.isregexp@npm:3.0.5": - version: 3.0.5 - resolution: "lodash.isregexp@npm:3.0.5" - checksum: 973f4887f003af746bf838267d9d1ea39d912f579cf402cca67049b1e4487daf2a25b10c70e4fc1c7ad97ee3be6d43d38c9839bc9c55c40e94b62dfc60f601c7 - languageName: node - linkType: hard - -"lodash.map@npm:^4.4.0": - version: 4.6.0 - resolution: "lodash.map@npm:4.6.0" - checksum: 7369a41d7d24d15ce3bbd02a7faa3a90f6266c38184e64932571b9b21b758bd10c04ffd117d1859be1a44156f29b94df5045eff172bf8a97fddf68bf1002d12f - languageName: node - linkType: hard - "lodash.memoize@npm:4.x, lodash.memoize@npm:^4.1.2": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2" @@ -11362,7 +8108,7 @@ __metadata: languageName: node linkType: hard -"lodash.merge@npm:^4.4.0, lodash.merge@npm:^4.6.2": +"lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005 @@ -11376,41 +8122,6 @@ __metadata: languageName: node linkType: hard -"lodash.pick@npm:^4.2.1": - version: 4.4.0 - resolution: "lodash.pick@npm:4.4.0" - checksum: 2c36cab7da6b999a20bd3373b40e31a3ef81fa264f34a6979c852c5bc8ac039379686b27380f0cb8e3781610844fafec6949c6fbbebc059c98f8fa8570e3675f - languageName: node - linkType: hard - -"lodash.reduce@npm:^4.4.0": - version: 4.6.0 - resolution: "lodash.reduce@npm:4.6.0" - checksum: 81f2a1045440554f8427f895ef479f1de5c141edd7852dde85a894879312801efae0295116e5cf830c531c1a51cdab8f3628c3ad39fa21a9874bb9158d9ea075 - languageName: node - linkType: hard - -"lodash.reject@npm:^4.4.0": - version: 4.6.0 - resolution: "lodash.reject@npm:4.6.0" - checksum: 730acc78d29ab0a60e0f3cd87bbfe9071625a835791ef66daac7a405c43ec21209fd795fdf9b7485aecead4869f645801bd65c27b9acadce80dee26393793111 - languageName: node - linkType: hard - -"lodash.some@npm:^4.4.0": - version: 4.6.0 - resolution: "lodash.some@npm:4.6.0" - checksum: 4469e76a389446d1166a29f844fb21398c36060d00258ce799710e046c55ed3c1af150c31b4856504e252bc813ba3fdcb6f255c490d9846738dd363a44665322 - languageName: node - linkType: hard - -"lodash.union@npm:^4.6.0": - version: 4.6.0 - resolution: "lodash.union@npm:4.6.0" - checksum: 1514dc6508b2614ec071a6470f36eb7a70f69bf1abb6d55bdfdc21069635a4517783654b28504c0f025059a7598d37529766888e6d5902b8ab28b712228f7b2a - languageName: node - linkType: hard - "lodash.uniq@npm:^4.5.0": version: 4.5.0 resolution: "lodash.uniq@npm:4.5.0" @@ -11425,7 +8136,7 @@ __metadata: languageName: node linkType: hard -"log-symbols@npm:4.1.0, log-symbols@npm:^4.0.0": +"log-symbols@npm:^4.0.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" dependencies: @@ -11454,30 +8165,6 @@ __metadata: languageName: node linkType: hard -"lowercase-keys@npm:^2.0.0": - version: 2.0.0 - resolution: "lowercase-keys@npm:2.0.0" - checksum: 24d7ebd56ccdf15ff529ca9e08863f3c54b0b9d1edb97a3ae1af34940ae666c01a1e6d200707bce730a8ef76cb57cc10e65f245ecaaf7e6bc8639f2fb460ac23 - languageName: node - linkType: hard - -"lowercase-keys@npm:^3.0.0": - version: 3.0.0 - resolution: "lowercase-keys@npm:3.0.0" - checksum: 67a3f81409af969bc0c4ca0e76cd7d16adb1e25aa1c197229587eaf8671275c8c067cd421795dbca4c81be0098e4c426a086a05e30de8a9c587b7a13c0c7ccc5 - languageName: node - linkType: hard - -"lru-cache@npm:^4.1.5": - version: 4.1.5 - resolution: "lru-cache@npm:4.1.5" - dependencies: - pseudomap: ^1.0.2 - yallist: ^2.1.2 - checksum: 4bb4b58a36cd7dc4dcec74cbe6a8f766a38b7426f1ff59d4cf7d82a2aa9b9565cd1cb98f6ff60ce5cd174524868d7bc9b7b1c294371851356066ca9ac4cf135a - languageName: node - linkType: hard - "lru-cache@npm:^6.0.0": version: 6.0.0 resolution: "lru-cache@npm:6.0.0" @@ -11494,13 +8181,6 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^3.0.1": - version: 3.0.1 - resolution: "luxon@npm:3.0.1" - checksum: aa966eb919bf95b1bd819cda784d1f6f66e3fb65bd9ec7bf68b6a978eeb4e3e14f7e2275021b473f93b15b6b7ba2e5a30471e53add3929a7e695fcfd6dd40ec8 - languageName: node - linkType: hard - "magic-string@npm:^0.25.7": version: 0.25.9 resolution: "magic-string@npm:0.25.9" @@ -11510,13 +8190,6 @@ __metadata: languageName: node linkType: hard -"mailcheck@npm:^1.1.1": - version: 1.1.1 - resolution: "mailcheck@npm:1.1.1" - checksum: 7c49dc01c43411186a1f038f0187a73231451c8306331ad9188ae55eabfdd903c325e601816da0cccbb6a212a7b4673639a2f93467f5d1a2aadbc5e12f9a92f4 - languageName: node - linkType: hard - "make-dir@npm:^3.0.0, make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -11638,13 +8311,6 @@ __metadata: languageName: node linkType: hard -"media-typer@npm:0.3.0": - version: 0.3.0 - resolution: "media-typer@npm:0.3.0" - checksum: af1b38516c28ec95d6b0826f6c8f276c58aec391f76be42aa07646b4e39d317723e869700933ca6995b056db4b09a78c92d5440dc23657e6764be5d28874bba1 - languageName: node - linkType: hard - "meow@npm:^9.0.0": version: 9.0.0 resolution: "meow@npm:9.0.0" @@ -11679,13 +8345,6 @@ __metadata: languageName: node linkType: hard -"methods@npm:^1.1.2": - version: 1.1.2 - resolution: "methods@npm:1.1.2" - checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a - languageName: node - linkType: hard - "mfm-js@npm:0.22.1": version: 0.22.1 resolution: "mfm-js@npm:0.22.1" @@ -11716,16 +8375,6 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.0": - version: 4.0.5 - resolution: "micromatch@npm:4.0.5" - dependencies: - braces: ^3.0.2 - picomatch: ^2.3.1 - checksum: 02a17b671c06e8fefeeb6ef996119c1e597c942e632a21ef589154f23898c9c6a9858526246abb14f8bca6e77734aa9dcf65476fca47cedfb80d9577d52843fc - languageName: node - linkType: hard - "micromatch@npm:^4.0.4": version: 4.0.4 resolution: "micromatch@npm:4.0.4" @@ -11754,22 +8403,6 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:1.52.0": - version: 1.52.0 - resolution: "mime-db@npm:1.52.0" - checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f - languageName: node - linkType: hard - -"mime-types@npm:2.1.35, mime-types@npm:^2.1.18, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": - version: 2.1.35 - resolution: "mime-types@npm:2.1.35" - dependencies: - mime-db: 1.52.0 - checksum: 89a5b7f1def9f3af5dad6496c5ed50191ae4331cc5389d7c521c8ad28d5fdad2d06fd81baf38fed813dc4e46bb55c8145bb0ff406330818c9cf712fb2e9b3836 - languageName: node - linkType: hard - "mime-types@npm:^2.1.12, mime-types@npm:~2.1.19": version: 2.1.27 resolution: "mime-types@npm:2.1.27" @@ -11786,27 +8419,6 @@ __metadata: languageName: node linkType: hard -"mimic-fn@npm:^4.0.0": - version: 4.0.0 - resolution: "mimic-fn@npm:4.0.0" - checksum: 995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56 - languageName: node - linkType: hard - -"mimic-response@npm:^1.0.0": - version: 1.0.1 - resolution: "mimic-response@npm:1.0.1" - checksum: 034c78753b0e622bc03c983663b1cdf66d03861050e0c8606563d149bc2b02d63f62ce4d32be4ab50d0553ae0ffe647fc34d1f5281184c6e1e8cf4d85e8d9823 - languageName: node - linkType: hard - -"mimic-response@npm:^3.1.0": - version: 3.1.0 - resolution: "mimic-response@npm:3.1.0" - checksum: 25739fee32c17f433626bf19f016df9036b75b3d84a3046c7d156e72ec963dd29d7fc8a302f55a3d6c5a4ff24259676b15d915aad6480815a969ff2ec0836867 - languageName: node - linkType: hard - "min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -11814,22 +8426,6 @@ __metadata: languageName: node linkType: hard -"minimalistic-assert@npm:^1.0.0": - version: 1.0.1 - resolution: "minimalistic-assert@npm:1.0.1" - checksum: cc7974a9268fbf130fb055aff76700d7e2d8be5f761fb5c60318d0ed010d839ab3661a533ad29a5d37653133385204c503bfac995aaa4236f4e847461ea32ba7 - languageName: node - linkType: hard - -"minimatch@npm:5.0.1": - version: 5.0.1 - resolution: "minimatch@npm:5.0.1" - dependencies: - brace-expansion: ^2.0.1 - checksum: b34b98463da4754bc526b244d680c69d4d6089451ebe512edaf6dd9eeed0279399cfa3edb19233513b8f830bf4bfcad911dddcdf125e75074100d52f724774f0 - languageName: node - linkType: hard - "minimatch@npm:^3.0.4": version: 3.0.4 resolution: "minimatch@npm:3.0.4" @@ -11848,7 +8444,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": +"minimatch@npm:^5.0.1": version: 5.1.0 resolution: "minimatch@npm:5.1.0" dependencies: @@ -11868,7 +8464,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6": +"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.6 resolution: "minimist@npm:1.2.6" checksum: d15428cd1e11eb14e1233bcfb88ae07ed7a147de251441d61158619dfb32c4d7e9061d09cab4825fdee18ecd6fce323228c8c47b5ba7cd20af378ca4048fb3fb @@ -11955,24 +8551,6 @@ __metadata: languageName: node linkType: hard -"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": - version: 0.5.3 - resolution: "mkdirp-classic@npm:0.5.3" - checksum: 3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac - languageName: node - linkType: hard - -"mkdirp@npm:>=0.5 0, mkdirp@npm:^0.5.4": - version: 0.5.6 - resolution: "mkdirp@npm:0.5.6" - dependencies: - minimist: ^1.2.6 - bin: - mkdirp: bin/cmd.js - checksum: 0c91b721bb12c3f9af4b77ebf73604baf350e64d80df91754dc509491ae93bf238581e59c7188360cec7cb62fc4100959245a42cfe01834efedc5e9d068376c2 - languageName: node - linkType: hard - "mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -12011,39 +8589,6 @@ __metadata: languageName: node linkType: hard -"mocha@npm:10.0.0": - version: 10.0.0 - resolution: "mocha@npm:10.0.0" - dependencies: - "@ungap/promise-all-settled": 1.1.2 - ansi-colors: 4.1.1 - browser-stdout: 1.3.1 - chokidar: 3.5.3 - debug: 4.3.4 - diff: 5.0.0 - escape-string-regexp: 4.0.0 - find-up: 5.0.0 - glob: 7.2.0 - he: 1.2.0 - js-yaml: 4.1.0 - log-symbols: 4.1.0 - minimatch: 5.0.1 - ms: 2.1.3 - nanoid: 3.3.3 - serialize-javascript: 6.0.0 - strip-json-comments: 3.1.1 - supports-color: 8.1.1 - workerpool: 6.2.1 - yargs: 16.2.0 - yargs-parser: 20.2.4 - yargs-unparser: 2.0.0 - bin: - _mocha: bin/_mocha - mocha: bin/mocha.js - checksum: ba49ddcf8015a467e744b06c396aab361b1281302e38e7c1269af25ba51ff9ab681a9c36e9046bb7491e751cd7d5ce85e276a00ce7e204f96b2c418e4595edfe - languageName: node - linkType: hard - "mock-socket@npm:^9.0.8, mock-socket@npm:^9.1.0": version: 9.1.5 resolution: "mock-socket@npm:9.1.5" @@ -12051,13 +8596,6 @@ __metadata: languageName: node linkType: hard -"moment@npm:^2.22.2": - version: 2.29.4 - resolution: "moment@npm:2.29.4" - checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e - languageName: node - linkType: hard - "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -12072,71 +8610,13 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:^2.0.0, ms@npm:^2.1.1": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d languageName: node linkType: hard -"msgpackr-extract@npm:^2.0.2": - version: 2.1.2 - resolution: "msgpackr-extract@npm:2.1.2" - dependencies: - "@msgpackr-extract/msgpackr-extract-darwin-arm64": 2.1.2 - "@msgpackr-extract/msgpackr-extract-darwin-x64": 2.1.2 - "@msgpackr-extract/msgpackr-extract-linux-arm": 2.1.2 - "@msgpackr-extract/msgpackr-extract-linux-arm64": 2.1.2 - "@msgpackr-extract/msgpackr-extract-linux-x64": 2.1.2 - "@msgpackr-extract/msgpackr-extract-win32-x64": 2.1.2 - node-gyp: latest - node-gyp-build-optional-packages: 5.0.3 - dependenciesMeta: - "@msgpackr-extract/msgpackr-extract-darwin-arm64": - optional: true - "@msgpackr-extract/msgpackr-extract-darwin-x64": - optional: true - "@msgpackr-extract/msgpackr-extract-linux-arm": - optional: true - "@msgpackr-extract/msgpackr-extract-linux-arm64": - optional: true - "@msgpackr-extract/msgpackr-extract-linux-x64": - optional: true - "@msgpackr-extract/msgpackr-extract-win32-x64": - optional: true - bin: - download-msgpackr-prebuilds: bin/download-prebuilds.js - checksum: bf068baa690d3e5c5609c10aa363901ac43d3f32b9d89f9dfb77293afa866eb1b943482338da6c38d50790a66c966fd7e0fbc9187b2a35f40f253931f649f97f - languageName: node - linkType: hard - -"msgpackr@npm:^1.5.2": - version: 1.6.2 - resolution: "msgpackr@npm:1.6.2" - dependencies: - msgpackr-extract: ^2.0.2 - dependenciesMeta: - msgpackr-extract: - optional: true - checksum: 1bb1ac0d1b5de491c835e330769f090608a19d349689f73204979258d22836419f81456a6e911adc301f68b5e06cb28ed289e135efb605e2a0f03a8784b42f62 - languageName: node - linkType: hard - -"multer@npm:1.4.5-lts.1": - version: 1.4.5-lts.1 - resolution: "multer@npm:1.4.5-lts.1" - dependencies: - append-field: ^1.0.0 - busboy: ^1.0.0 - concat-stream: ^1.5.2 - mkdirp: ^0.5.4 - object-assign: ^4.1.1 - type-is: ^1.6.4 - xtend: ^4.0.0 - checksum: d6dfa78a6ec592b74890412f8962da8a87a3dcfe20f612e039b735b8e0faa72c735516c447f7de694ee0d981eb0a1b892fb9e2402a0348dc6091d18c38d89ecc - languageName: node - linkType: hard - "mute-stdout@npm:^1.0.0": version: 1.0.1 resolution: "mute-stdout@npm:1.0.1" @@ -12151,35 +8631,6 @@ __metadata: languageName: node linkType: hard -"mz@npm:^2.4.0, mz@npm:^2.7.0": - version: 2.7.0 - resolution: "mz@npm:2.7.0" - dependencies: - any-promise: ^1.0.0 - object-assign: ^4.0.1 - thenify-all: ^1.0.0 - checksum: 8427de0ece99a07e9faed3c0c6778820d7543e3776f9a84d22cf0ec0a8eb65f6e9aee9c9d353ff9a105ff62d33a9463c6ca638974cc652ee8140cd1e35951c87 - languageName: node - linkType: hard - -"nan@npm:^2.17.0": - version: 2.17.0 - resolution: "nan@npm:2.17.0" - dependencies: - node-gyp: latest - checksum: ec609aeaf7e68b76592a3ba96b372aa7f5df5b056c1e37410b0f1deefbab5a57a922061e2c5b369bae9c7c6b5e6eecf4ad2dac8833a1a7d3a751e0a7c7f849ed - languageName: node - linkType: hard - -"nanoid@npm:3.3.3": - version: 3.3.3 - resolution: "nanoid@npm:3.3.3" - bin: - nanoid: bin/nanoid.cjs - checksum: ada019402a07464a694553c61d2dca8a4353645a7d92f2830f0d487fedff403678a0bee5323a46522752b2eab95a0bc3da98b6cccaa7c0c55cd9975130e6d6f0 - languageName: node - linkType: hard - "nanoid@npm:^3.3.4": version: 3.3.4 resolution: "nanoid@npm:3.3.4" @@ -12208,13 +8659,6 @@ __metadata: languageName: node linkType: hard -"napi-build-utils@npm:^1.0.1": - version: 1.0.2 - resolution: "napi-build-utils@npm:1.0.2" - checksum: 06c14271ee966e108d55ae109f340976a9556c8603e888037145d6522726aebe89dd0c861b4b83947feaf6d39e79e08817559e8693deedc2c94e82c5cbd090c7 - languageName: node - linkType: hard - "natural-compare-lite@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare-lite@npm:1.4.0" @@ -12229,40 +8673,13 @@ __metadata: languageName: node linkType: hard -"needle@npm:^2.5.2": - version: 2.9.1 - resolution: "needle@npm:2.9.1" - dependencies: - debug: ^3.2.6 - iconv-lite: ^0.4.4 - sax: ^1.2.4 - bin: - needle: ./bin/needle - checksum: 746ae3a3782f0a057ff304a98843cc6f2009f978a0fad0c3e641a9d46d0b5702bb3e197ba08aecd48678067874a991c4f5fc320c7e51a4c041d9dd3441146cf0 - languageName: node - linkType: hard - -"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": +"negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 languageName: node linkType: hard -"nested-property@npm:4.0.0": - version: 4.0.0 - resolution: "nested-property@npm:4.0.0" - checksum: 9c86f2c722429e167876d5becf276139a6aa4b8732b6d9e32de9aa44dfd017702b60614cc87aec961dea47ae50dae0951d5b5f66fc30288f18bf581c16e42ca2 - languageName: node - linkType: hard - -"netmask@npm:^2.0.2": - version: 2.0.2 - resolution: "netmask@npm:2.0.2" - checksum: c65cb8d3f7ea5669edddb3217e4c96910a60d0d9a4b52d9847ff6b28b2d0277cd8464eee0ef85133cdee32605c57940cacdd04a9a019079b091b6bba4cb0ec22 - languageName: node - linkType: hard - "next-tick@npm:~1.0.0": version: 1.0.0 resolution: "next-tick@npm:1.0.0" @@ -12270,28 +8687,6 @@ __metadata: languageName: node linkType: hard -"nise@npm:^5.1.2": - version: 5.1.2 - resolution: "nise@npm:5.1.2" - dependencies: - "@sinonjs/commons": ^2.0.0 - "@sinonjs/fake-timers": ^7.0.4 - "@sinonjs/text-encoding": ^0.7.1 - just-extend: ^4.0.2 - path-to-regexp: ^1.7.0 - checksum: 688c557333dcbc5b41f4f1f1b0ea32fb0f8b424541a8958140bc61074980362c954b2aeb027c282de26b9ddcb4b230656f68ac4206777499e405dd7e716ec1f8 - languageName: node - linkType: hard - -"node-abi@npm:^3.3.0": - version: 3.24.0 - resolution: "node-abi@npm:3.24.0" - dependencies: - semver: ^7.3.5 - checksum: d90ab48802497b2203800cac71018668e99c246435395ca4f67afcabf689e7e81568ed36e8036bae79a052b63ea5707375bece6ca0a1d2e2b99bfafde7a5c9b2 - languageName: node - linkType: hard - "node-addon-api@npm:^1.2.0": version: 1.7.2 resolution: "node-addon-api@npm:1.7.2" @@ -12310,25 +8705,7 @@ __metadata: languageName: node linkType: hard -"node-domexception@npm:^1.0.0": - version: 1.0.0 - resolution: "node-domexception@npm:1.0.0" - checksum: ee1d37dd2a4eb26a8a92cd6b64dfc29caec72bff5e1ed9aba80c294f57a31ba4895a60fd48347cf17dd6e766da0ae87d75657dfd1f384ebfa60462c2283f5c7f - languageName: node - linkType: hard - -"node-fetch@npm:*, node-fetch@npm:^3.2.2": - version: 3.2.10 - resolution: "node-fetch@npm:3.2.10" - dependencies: - data-uri-to-buffer: ^4.0.0 - fetch-blob: ^3.1.4 - formdata-polyfill: ^4.0.10 - checksum: e65322431f4897ded04197aa5923eaec63a8d53e00432de4e70a4f7006625c8dc32629c5c35f4fe8ee719a4825544d07bf53f6e146a7265914262f493e8deac1 - languageName: node - linkType: hard - -"node-fetch@npm:2.6.7, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": +"node-fetch@npm:2.6.7, node-fetch@npm:^2.6.7": version: 2.6.7 resolution: "node-fetch@npm:2.6.7" dependencies: @@ -12342,28 +8719,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:3.2.6": - version: 3.2.6 - resolution: "node-fetch@npm:3.2.6" - dependencies: - data-uri-to-buffer: ^4.0.0 - fetch-blob: ^3.1.4 - formdata-polyfill: ^4.0.10 - checksum: ece307ace3a3ff28638ccb0fa0545bb5c0cfd673a9d06fc314d937a73ae6a55917d5bacd7f080f9a9f1449ca20a5c01b1281d0e616acb20315e5ac315971da46 - languageName: node - linkType: hard - -"node-gyp-build-optional-packages@npm:5.0.3": - version: 5.0.3 - resolution: "node-gyp-build-optional-packages@npm:5.0.3" - bin: - node-gyp-build-optional-packages: bin.js - node-gyp-build-optional-packages-optional: optional.js - node-gyp-build-optional-packages-test: build-test.js - checksum: be3f0235925c8361e5bc1a03848f5e24815b0df8aa90bd13f1eac91cd86264bbb8b7689ca6cd083b02c8099c7b54f9fb83066c7bb77c2389dc4eceab921f084f - languageName: node - linkType: hard - "node-gyp-build@npm:^3.8.0": version: 3.9.0 resolution: "node-gyp-build@npm:3.9.0" @@ -12375,26 +8730,6 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:^9.3.0": - version: 9.3.0 - resolution: "node-gyp@npm:9.3.0" - dependencies: - env-paths: ^2.2.0 - glob: ^7.1.4 - graceful-fs: ^4.2.6 - make-fetch-happen: ^10.0.3 - nopt: ^6.0.0 - npmlog: ^6.0.0 - rimraf: ^3.0.2 - semver: ^7.3.5 - tar: ^6.1.2 - which: ^2.0.2 - bin: - node-gyp: bin/node-gyp.js - checksum: 589ddd3ed967724ef425f9624bfa47cf73022640ab3eba6d556e92cdc4ddef33b63fce3a467c93b995a3f61df92eafd3c3d1e8dbe4a2c00c383334487dea99c3 - languageName: node - linkType: hard - "node-gyp@npm:latest": version: 9.1.0 resolution: "node-gyp@npm:9.1.0" @@ -12429,20 +8764,6 @@ __metadata: languageName: node linkType: hard -"nodemailer@npm:6.7.6": - version: 6.7.6 - resolution: "nodemailer@npm:6.7.6" - checksum: 60f59e6525656b95506092c0928c40e3b2997588b220c897515f1614c23c76318a4a71c96e1a83900f43153fab8baabfe7e9558608fafd3f74c9c5c634732731 - languageName: node - linkType: hard - -"nofilter@npm:^3.1.0": - version: 3.1.0 - resolution: "nofilter@npm:3.1.0" - checksum: 58aa85a5b4b35cbb6e42de8a8591c5e338061edc9f3e7286f2c335e9e9b9b8fa7c335ae45daa8a1f3433164dc0b9a3d187fa96f9516e04a17a1f9ce722becc4f - languageName: node - linkType: hard - "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0" @@ -12454,17 +8775,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^6.0.0": - version: 6.0.0 - resolution: "nopt@npm:6.0.0" - dependencies: - abbrev: ^1.0.0 - bin: - nopt: bin/nopt.js - checksum: 82149371f8be0c4b9ec2f863cc6509a7fd0fa729929c009f3a58e4eb0c9e4cae9920e8f1f8eb46e7d032fec8fb01bede7f0f41a67eb3553b7b8e14fa53de1dac - languageName: node - linkType: hard - "normalize-package-data@npm:^2.3.2, normalize-package-data@npm:^2.5.0": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -12524,13 +8834,6 @@ __metadata: languageName: node linkType: hard -"normalize-url@npm:^6.0.1": - version: 6.1.0 - resolution: "normalize-url@npm:6.1.0" - checksum: 4a4944631173e7d521d6b80e4c85ccaeceb2870f315584fa30121f505a6dfd86439c5e3fdd8cd9e0e291290c41d0c3599f0cb12ab356722ed242584c30348e50 - languageName: node - linkType: hard - "now-and-later@npm:^2.0.0": version: 2.0.1 resolution: "now-and-later@npm:2.0.1" @@ -12549,15 +8852,6 @@ __metadata: languageName: node linkType: hard -"npm-run-path@npm:^5.1.0": - version: 5.1.0 - resolution: "npm-run-path@npm:5.1.0" - dependencies: - path-key: ^4.0.0 - checksum: dc184eb5ec239d6a2b990b43236845332ef12f4e0beaa9701de724aa797fe40b6bbd0157fb7639d24d3ab13f5d5cf22d223a19c6300846b8126f335f788bee66 - languageName: node - linkType: hard - "npmlog@npm:^5.0.1": version: 5.0.1 resolution: "npmlog@npm:5.0.1" @@ -12591,15 +8885,6 @@ __metadata: languageName: node linkType: hard -"nth-check@npm:~1.0.1": - version: 1.0.2 - resolution: "nth-check@npm:1.0.2" - dependencies: - boolbase: ~1.0.0 - checksum: 59e115fdd75b971d0030f42ada3aac23898d4c03aa13371fa8b3339d23461d1badf3fde5aad251fb956aaa75c0a3b9bfcd07c08a34a83b4f9dadfdce1d19337c - languageName: node - linkType: hard - "num2fraction@npm:^1.2.2": version: 1.2.2 resolution: "num2fraction@npm:1.2.2" @@ -12759,15 +9044,6 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:^2.3.0": - version: 2.4.1 - resolution: "on-finished@npm:2.4.1" - dependencies: - ee-first: 1.1.1 - checksum: d20929a25e7f0bb62f937a425b5edeb4e4cde0540d77ba146ec9357f00b0d497cdb3b9b05b9c8e46222407d1548d08166bff69cc56dfa55ba0e4469228920ff0 - languageName: node - linkType: hard - "once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.3.2, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -12786,31 +9062,6 @@ __metadata: languageName: node linkType: hard -"onetime@npm:^6.0.0": - version: 6.0.0 - resolution: "onetime@npm:6.0.0" - dependencies: - mimic-fn: ^4.0.0 - checksum: 0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788 - languageName: node - linkType: hard - -"only@npm:~0.0.2": - version: 0.0.2 - resolution: "only@npm:0.0.2" - checksum: d399710db867a1ef436dd3ce74499c87ece794aa81ab0370b5d153968766ee4aed2f98d3f92fc87c963e45b7a74d400d6f463ef651a5e7cfb861b15e88e9efe6 - languageName: node - linkType: hard - -"opentype.js@npm:^0.4.3": - version: 0.4.11 - resolution: "opentype.js@npm:0.4.11" - bin: - ot: ./bin/ot - checksum: bd5292c83051017b90d0669d656557623248339279f364217ad80a9498eed7b5ef0047ba7c639fa2e5d907e5170b2d8677adbf0288fde004f806416a488f7cc3 - languageName: node - linkType: hard - "optionator@npm:^0.8.1": version: 0.8.3 resolution: "optionator@npm:0.8.3" @@ -12857,13 +9108,6 @@ __metadata: languageName: node linkType: hard -"os-utils@npm:0.0.14": - version: 0.0.14 - resolution: "os-utils@npm:0.0.14" - checksum: bc0ee91ffca6f05150ab5d6a7fe5c691cca8eedff43afcc7a4e24a5d228c3b0a26fa841236d20b5fa385e3a07d8aed79a0eceebc8818a196cd18d2ca3546e098 - languageName: node - linkType: hard - "ospath@npm:^1.2.2": version: 1.2.2 resolution: "ospath@npm:1.2.2" @@ -12871,20 +9115,6 @@ __metadata: languageName: node linkType: hard -"p-cancelable@npm:^2.0.0": - version: 2.1.1 - resolution: "p-cancelable@npm:2.1.1" - checksum: 3dba12b4fb4a1e3e34524535c7858fc82381bbbd0f247cc32dedc4018592a3950ce66b106d0880b4ec4c2d8d6576f98ca885dc1d7d0f274d1370be20e9523ddf - languageName: node - linkType: hard - -"p-cancelable@npm:^3.0.0": - version: 3.0.0 - resolution: "p-cancelable@npm:3.0.0" - checksum: 2b5ae34218f9c2cf7a7c18e5d9a726ef9b165ef07e6c959f6738371509e747334b5f78f3bcdeb03d8a12dcb978faf641fd87eb21486ed7d36fb823b8ddef3219 - languageName: node - linkType: hard - "p-finally@npm:^1.0.0": version: 1.0.0 resolution: "p-finally@npm:1.0.0" @@ -12928,13 +9158,6 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^2.1.0": - version: 2.1.0 - resolution: "p-map@npm:2.1.0" - checksum: 9e3ad3c9f6d75a5b5661bcad78c91f3a63849189737cd75e4f1225bf9ac205194e5c44aac2ef6f09562b1facdb9bd1425584d7ac375bfaa17b3f1a142dab936d - languageName: node - linkType: hard - "p-map@npm:^4.0.0": version: 4.0.0 resolution: "p-map@npm:4.0.0" @@ -12970,13 +9193,6 @@ __metadata: languageName: node linkType: hard -"packet-reader@npm:1.0.0": - version: 1.0.0 - resolution: "packet-reader@npm:1.0.0" - checksum: 0b7516f0cbf3e322aad591bed29ba544220088c53943145c0d9121a6f59182ad811f7fd6785a8979a34356aca69d97653689029964c5998dc02645633d88ffd7 - languageName: node - linkType: hard - "pandemonium@npm:^2.0.0": version: 2.3.0 resolution: "pandemonium@npm:2.3.0" @@ -13041,52 +9257,13 @@ __metadata: languageName: node linkType: hard -"parse-srcset@npm:^1.0.2": - version: 1.0.2 - resolution: "parse-srcset@npm:1.0.2" - checksum: 3a0380380c6082021fcce982f0b89fb8a493ce9dfd7d308e5e6d855201e80db8b90438649b31fdd82a3d6089a8ca17dccddaa2b730a718389af4c037b8539ebf - languageName: node - linkType: hard - -"parse5-htmlparser2-tree-adapter@npm:^6.0.0": - version: 6.0.1 - resolution: "parse5-htmlparser2-tree-adapter@npm:6.0.1" - dependencies: - parse5: ^6.0.1 - checksum: 1848378b355d027915645c13f13f982e60502d201f53bc2067a508bf2dba4aac08219fc781dcd160167f5f50f0c73f58d20fa4fb3d90ee46762c20234fa90a6d - languageName: node - linkType: hard - -"parse5@npm:*, parse5@npm:7.0.0, parse5@npm:^7.0.0": - version: 7.0.0 - resolution: "parse5@npm:7.0.0" - dependencies: - entities: ^4.3.0 - checksum: 7da5d61cc18eb36ffa71fc861e65cbfd1f23d96483a6631254e627be667dbc9c93ac0b0e6cb17a13a2e4033dab19bfb2f76f38e5936cfb57240ed49036a83fcc - languageName: node - linkType: hard - -"parse5@npm:6.0.1, parse5@npm:^6.0.1": +"parse5@npm:6.0.1": version: 6.0.1 resolution: "parse5@npm:6.0.1" checksum: 7d569a176c5460897f7c8f3377eff640d54132b9be51ae8a8fa4979af940830b2b0c296ce75e5bd8f4041520aadde13170dbdec44889975f906098ea0002f4bd languageName: node linkType: hard -"parse5@npm:^5.1.1": - version: 5.1.1 - resolution: "parse5@npm:5.1.1" - checksum: 613a714af4c1101d1cb9f7cece2558e35b9ae8a0c03518223a4a1e35494624d9a9ad5fad4c13eab66a0e0adccd9aa3d522fc8f5f9cc19789e0579f3fa0bdfc65 - languageName: node - linkType: hard - -"parseurl@npm:^1.3.2": - version: 1.3.3 - resolution: "parseurl@npm:1.3.3" - checksum: 407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 - languageName: node - linkType: hard - "pascalcase@npm:^0.1.1": version: 0.1.1 resolution: "pascalcase@npm:0.1.1" @@ -13094,13 +9271,6 @@ __metadata: languageName: node linkType: hard -"passthrough-counter@npm:^1.0.0": - version: 1.0.0 - resolution: "passthrough-counter@npm:1.0.0" - checksum: 942a0addeb677e24ddb154b04cc29ce1c5720032efc268689446420f9350d47e94f2f1f76d469686bc87c1543c2f2165f2d004d265fe1b81465c76e02d272c63 - languageName: node - linkType: hard - "path-dirname@npm:^1.0.0": version: 1.0.2 resolution: "path-dirname@npm:1.0.2" @@ -13124,7 +9294,7 @@ __metadata: languageName: node linkType: hard -"path-is-absolute@npm:1.0.1, path-is-absolute@npm:^1.0.0": +"path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 @@ -13138,13 +9308,6 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^4.0.0": - version: 4.0.0 - resolution: "path-key@npm:4.0.0" - checksum: 8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 - languageName: node - linkType: hard - "path-parse@npm:^1.0.6, path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -13168,22 +9331,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^1.7.0": - version: 1.8.0 - resolution: "path-to-regexp@npm:1.8.0" - dependencies: - isarray: 0.0.1 - checksum: 709f6f083c0552514ef4780cb2e7e4cf49b0cc89a97439f2b7cc69a608982b7690fb5d1720a7473a59806508fc2dae0be751ba49f495ecf89fd8fbc62abccbcd - languageName: node - linkType: hard - -"path-to-regexp@npm:^6.1.0": - version: 6.2.1 - resolution: "path-to-regexp@npm:6.2.1" - checksum: f0227af8284ea13300f4293ba111e3635142f976d4197f14d5ad1f124aebd9118783dd2e5f1fe16f7273743cc3dbeddfb7493f237bb27c10fdae07020cc9b698 - languageName: node - linkType: hard - "path-type@npm:^1.0.0": version: 1.1.0 resolution: "path-type@npm:1.1.0" @@ -13211,13 +9358,6 @@ __metadata: languageName: node linkType: hard -"peek-readable@npm:^5.0.0": - version: 5.0.0 - resolution: "peek-readable@npm:5.0.0" - checksum: bef5ceb50586eb42e14efba274ac57ffe97f0ed272df9239ce029f688f495d9bf74b2886fa27847c706a9db33acda4b7d23bbd09a2d21eb4c2a54da915117414 - languageName: node - linkType: hard - "pend@npm:~1.2.0": version: 1.2.0 resolution: "pend@npm:1.2.0" @@ -13232,78 +9372,6 @@ __metadata: languageName: node linkType: hard -"pg-connection-string@npm:^2.5.0": - version: 2.5.0 - resolution: "pg-connection-string@npm:2.5.0" - checksum: a6f3a068f7c9416a5b33a326811caf0dfaaee045c225b7c628b4c9b4e9a2b25bdd12a21e4c48940e1000ea223a4e608ca122d2ff3dd08c8b1db0fc9f5705133a - languageName: node - linkType: hard - -"pg-int8@npm:1.0.1": - version: 1.0.1 - resolution: "pg-int8@npm:1.0.1" - checksum: a1e3a05a69005ddb73e5f324b6b4e689868a447c5fa280b44cd4d04e6916a344ac289e0b8d2695d66e8e89a7fba023affb9e0e94778770ada5df43f003d664c9 - languageName: node - linkType: hard - -"pg-pool@npm:^3.5.1": - version: 3.5.2 - resolution: "pg-pool@npm:3.5.2" - peerDependencies: - pg: ">=8.0" - checksum: a5d029200257671f0c17ca54b9ab6213e2060e64e5cc792b78edd50ab471a26cd364890d05d546d9296949e079e943821cf2ceb4d488f4e6a6789fb781a628bf - languageName: node - linkType: hard - -"pg-protocol@npm:*, pg-protocol@npm:^1.5.0": - version: 1.5.0 - resolution: "pg-protocol@npm:1.5.0" - checksum: b839d12cafe942ef9cbc5b13c174eb2356804fb4fe8ead8279f46a36be90722d19a91409955beb8a3d5301639c44854e49749de4aef02dc361fee3e2a61fb1e4 - languageName: node - linkType: hard - -"pg-types@npm:^2.1.0, pg-types@npm:^2.2.0": - version: 2.2.0 - resolution: "pg-types@npm:2.2.0" - dependencies: - pg-int8: 1.0.1 - postgres-array: ~2.0.0 - postgres-bytea: ~1.0.0 - postgres-date: ~1.0.4 - postgres-interval: ^1.1.0 - checksum: bf4ec3f594743442857fb3a8dfe5d2478a04c98f96a0a47365014557cbc0b4b0cee01462c79adca863b93befbf88f876299b75b72c665b5fb84a2c94fbd10316 - languageName: node - linkType: hard - -"pg@npm:8.7.3": - version: 8.7.3 - resolution: "pg@npm:8.7.3" - dependencies: - buffer-writer: 2.0.0 - packet-reader: 1.0.0 - pg-connection-string: ^2.5.0 - pg-pool: ^3.5.1 - pg-protocol: ^1.5.0 - pg-types: ^2.1.0 - pgpass: 1.x - peerDependencies: - pg-native: ">=2.0.0" - peerDependenciesMeta: - pg-native: - optional: true - checksum: d0e7040967779b9ccea16897f099510bcaf6bc86f77a6d8fa7e293c24d8bd2fd2ec46d99d6d1adc9be4cc8f254aa909361346b693088c1ba4501414f7afb2fe3 - languageName: node - linkType: hard - -"pgpass@npm:1.x": - version: 1.0.5 - resolution: "pgpass@npm:1.0.5" - dependencies: - split2: ^4.1.0 - checksum: 947ac096c031eebdf08d989de2e9f6f156b8133d6858c7c2c06c041e1e71dda6f5f3bad3c0ec1e96a09497bbc6ef89e762eefe703b5ef9cb2804392ec52ec400 - languageName: node - linkType: hard - "photoswipe@npm:5.2.8": version: 5.2.8 resolution: "photoswipe@npm:5.2.8" @@ -13325,7 +9393,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.2.2, picomatch@npm:^2.3.1": +"picomatch@npm:^2.2.2": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf @@ -13346,13 +9414,6 @@ __metadata: languageName: node linkType: hard -"pify@npm:^4.0.1": - version: 4.0.1 - resolution: "pify@npm:4.0.1" - checksum: 9c4e34278cb09987685fa5ef81499c82546c033713518f6441778fbec623fc708777fe8ac633097c72d88470d5963094076c7305cafc7ad340aae27cfacd856b - languageName: node - linkType: hard - "pinkie-promise@npm:^2.0.0": version: 2.0.1 resolution: "pinkie-promise@npm:2.0.1" @@ -13415,27 +9476,6 @@ __metadata: languageName: node linkType: hard -"pluralize@npm:^8.0.0": - version: 8.0.0 - resolution: "pluralize@npm:8.0.0" - checksum: 08931d4a6a4a5561a7f94f67a31c17e6632cb21e459ab3ff4f6f629d9a822984cf8afef2311d2005fbea5d7ef26016ebb090db008e2d8bce39d0a9a9d218736e - languageName: node - linkType: hard - -"pngjs@npm:^3.3.1": - version: 3.4.0 - resolution: "pngjs@npm:3.4.0" - checksum: 8bd40bd698abd16b72c97b85cb858c80894fbedc76277ce72a784aa441e14795d45d9856e97333ca469b34b67528860ffc8a7317ca6beea349b645366df00bcd - languageName: node - linkType: hard - -"pngjs@npm:^5.0.0": - version: 5.0.0 - resolution: "pngjs@npm:5.0.0" - checksum: 04e912cc45fb9601564e2284efaf0c5d20d131d9b596244f8a6789fc6cdb6b18d2975a6bbf7a001858d7e159d5c5c5dd7b11592e97629b7137f7f5cef05904c8 - languageName: node - linkType: hard - "posix-character-classes@npm:^0.1.0": version: 0.1.1 resolution: "posix-character-classes@npm:0.1.1" @@ -13750,7 +9790,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.1.10, postcss@npm:^8.3.11, postcss@npm:^8.4.16": +"postcss@npm:^8.1.10, postcss@npm:^8.4.16": version: 8.4.16 resolution: "postcss@npm:8.4.16" dependencies: @@ -13761,58 +9801,6 @@ __metadata: languageName: node linkType: hard -"postgres-array@npm:~2.0.0": - version: 2.0.0 - resolution: "postgres-array@npm:2.0.0" - checksum: 0e1e659888147c5de579d229a2d95c0d83ebdbffc2b9396d890a123557708c3b758a0a97ed305ce7f58edfa961fa9f0bbcd1ea9f08b6e5df73322e683883c464 - languageName: node - linkType: hard - -"postgres-bytea@npm:~1.0.0": - version: 1.0.0 - resolution: "postgres-bytea@npm:1.0.0" - checksum: d844ae4ca7a941b70e45cac1261a73ee8ed39d72d3d74ab1d645248185a1b7f0ac91a3c63d6159441020f4e1f7fe64689ac56536a307b31cef361e5187335090 - languageName: node - linkType: hard - -"postgres-date@npm:~1.0.4": - version: 1.0.7 - resolution: "postgres-date@npm:1.0.7" - checksum: 5745001d47e51cd767e46bcb1710649cd705d91a24d42fa661c454b6dcbb7353c066a5047983c90a626cd3bbfea9e626cc6fa84a35ec57e5bbb28b49f78e13ed - languageName: node - linkType: hard - -"postgres-interval@npm:^1.1.0": - version: 1.2.0 - resolution: "postgres-interval@npm:1.2.0" - dependencies: - xtend: ^4.0.0 - checksum: 746b71f93805ae33b03528e429dc624706d1f9b20ee81bf743263efb6a0cd79ae02a642a8a480dbc0f09547b4315ab7df6ce5ec0be77ed700bac42730f5c76b2 - languageName: node - linkType: hard - -"prebuild-install@npm:^7.1.1": - version: 7.1.1 - resolution: "prebuild-install@npm:7.1.1" - dependencies: - detect-libc: ^2.0.0 - expand-template: ^2.0.3 - github-from-package: 0.0.0 - minimist: ^1.2.3 - mkdirp-classic: ^0.5.3 - napi-build-utils: ^1.0.1 - node-abi: ^3.3.0 - pump: ^3.0.0 - rc: ^1.2.7 - simple-get: ^4.0.0 - tar-fs: ^2.0.0 - tunnel-agent: ^0.6.0 - bin: - prebuild-install: bin.js - checksum: dbf96d0146b6b5827fc8f67f72074d2e19c69628b9a7a0a17d0fad1bf37e9f06922896972e074197fc00a52eae912993e6ef5a0d471652f561df5cb516f3f467 - languageName: node - linkType: hard - "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -13871,17 +9859,6 @@ __metadata: languageName: node linkType: hard -"pretty@npm:^2.0.0": - version: 2.0.0 - resolution: "pretty@npm:2.0.0" - dependencies: - condense-newlines: ^0.2.1 - extend-shallow: ^2.0.1 - js-beautify: ^1.6.12 - checksum: 9c41ae0559195af2fb2496d84c6f442843e045d269d4008a6dd336f8372d7481395ed5ab23e5711b6172682c27cb0542e1ab3ca11b38da48f1109c0b701d0ef9 - languageName: node - linkType: hard - "prismjs@npm:1.28.0": version: 1.28.0 resolution: "prismjs@npm:1.28.0" @@ -13889,29 +9866,6 @@ __metadata: languageName: node linkType: hard -"private-ip@npm:2.3.3": - version: 2.3.3 - resolution: "private-ip@npm:2.3.3" - dependencies: - ip-regex: ^4.3.0 - ipaddr.js: ^2.0.1 - is-ip: ^3.1.0 - netmask: ^2.0.2 - checksum: c362d1b07ecbcc01127333b659e6240f80fca8df597b87011c5276412acfc5ff2ef0453b545bcea28bd45e3642e2bfeaf07de2c431352b366f48e221e6ec6a3a - languageName: node - linkType: hard - -"probe-image-size@npm:7.2.3": - version: 7.2.3 - resolution: "probe-image-size@npm:7.2.3" - dependencies: - lodash.merge: ^4.6.2 - needle: ^2.5.2 - stream-parser: ~0.3.1 - checksum: 1a5eeb8f5cb979172144a5d7a017c70fcd664ccc8af9ad3a803903ee81864abea4036adae4fc6e66e9ae21bd3ce0febefaf1f32e65a77ff226b2eb61e9e4978c - languageName: node - linkType: hard - "process-nextick-args@npm:^2.0.0, process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -13926,13 +9880,6 @@ __metadata: languageName: node linkType: hard -"promise-limit@npm:2.7.0": - version: 2.7.0 - resolution: "promise-limit@npm:2.7.0" - checksum: 3e20a46d752ab41c921feceb668b3a6d000438573e71342d42bf63373fdbafbf4b6a3f88ef5ff902a2a37be28152dc74e618e863134fd4c08c196448f4a6d28f - languageName: node - linkType: hard - "promise-polyfill@npm:^8.1.3": version: 8.2.3 resolution: "promise-polyfill@npm:8.2.3" @@ -13950,15 +9897,6 @@ __metadata: languageName: node linkType: hard -"promise@npm:^7.0.1": - version: 7.3.1 - resolution: "promise@npm:7.3.1" - dependencies: - asap: ~2.0.3 - checksum: 475bb069130179fbd27ed2ab45f26d8862376a137a57314cf53310bdd85cc986a826fd585829be97ebc0aaf10e9d8e68be1bfe5a4a0364144b1f9eedfa940cf1 - languageName: node - linkType: hard - "prompts@npm:^2.0.1": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -13969,13 +9907,6 @@ __metadata: languageName: node linkType: hard -"proto-list@npm:~1.2.1": - version: 1.2.4 - resolution: "proto-list@npm:1.2.4" - checksum: 4d4826e1713cbfa0f15124ab0ae494c91b597a3c458670c9714c36e8baddf5a6aad22842776f2f5b137f259c8533e741771445eb8df82e861eea37a6eaba03f7 - languageName: node - linkType: hard - "proxy-from-env@npm:1.0.0": version: 1.0.0 resolution: "proxy-from-env@npm:1.0.0" @@ -13994,13 +9925,6 @@ __metadata: languageName: node linkType: hard -"pseudomap@npm:^1.0.2": - version: 1.0.2 - resolution: "pseudomap@npm:1.0.2" - checksum: 856c0aae0ff2ad60881168334448e898ad7a0e45fe7386d114b150084254c01e200c957cf378378025df4e052c7890c5bd933939b0e0d2ecfcc1dc2f0b2991f5 - languageName: node - linkType: hard - "psl@npm:^1.1.28": version: 1.8.0 resolution: "psl@npm:1.8.0" @@ -14015,133 +9939,6 @@ __metadata: languageName: node linkType: hard -"pug-attrs@npm:^3.0.0": - version: 3.0.0 - resolution: "pug-attrs@npm:3.0.0" - dependencies: - constantinople: ^4.0.1 - js-stringify: ^1.0.2 - pug-runtime: ^3.0.0 - checksum: 2ca2d34de3065239f01f0fc3c0e104c17f7a7105684d088bb71df623005a45f40a2301e65f49ec4581bb31794c74e691862643d4e34062d1509e92fa56a15aa5 - languageName: node - linkType: hard - -"pug-code-gen@npm:^3.0.2": - version: 3.0.2 - resolution: "pug-code-gen@npm:3.0.2" - dependencies: - constantinople: ^4.0.1 - doctypes: ^1.1.0 - js-stringify: ^1.0.2 - pug-attrs: ^3.0.0 - pug-error: ^2.0.0 - pug-runtime: ^3.0.0 - void-elements: ^3.1.0 - with: ^7.0.0 - checksum: 1644d3a4d673392794248749eb146299704639a8197746454b7d03b240b83ee102f25b76d203381501e283be3927ab01eb3f4563ff51c45a478de1f3435a400d - languageName: node - linkType: hard - -"pug-error@npm:^2.0.0": - version: 2.0.0 - resolution: "pug-error@npm:2.0.0" - checksum: c5372d018c897c1d6a141dd803c50957feecfda1f3d84a6adc6149801315d6c7f8c28b05f3e186d98d774fc9718699d1e1caa675630dd3c4453f8c5ec4e4a986 - languageName: node - linkType: hard - -"pug-filters@npm:^4.0.0": - version: 4.0.0 - resolution: "pug-filters@npm:4.0.0" - dependencies: - constantinople: ^4.0.1 - jstransformer: 1.0.0 - pug-error: ^2.0.0 - pug-walk: ^2.0.0 - resolve: ^1.15.1 - checksum: 44eb3273195e3f42f034ad81109452236377780557eaf5a28db6e478f297675e19b8598cca9de65a0ba9c1d57e2ca2a93e332f0ab4be79dc5dd042375228cdff - languageName: node - linkType: hard - -"pug-lexer@npm:^5.0.1": - version: 5.0.1 - resolution: "pug-lexer@npm:5.0.1" - dependencies: - character-parser: ^2.2.0 - is-expression: ^4.0.0 - pug-error: ^2.0.0 - checksum: afdd2f43f2c3ba96001a7b734c0c3bc745eb5d7dd68c787c2690c606d34573ca46ba807e4b4c7e70db9b4556fb938625dbb9c25b79cdb8857868e6deb2574d3e - languageName: node - linkType: hard - -"pug-linker@npm:^4.0.0": - version: 4.0.0 - resolution: "pug-linker@npm:4.0.0" - dependencies: - pug-error: ^2.0.0 - pug-walk: ^2.0.0 - checksum: 7433aa65181cd5b7bc631ab5f14baae7496fd8da98608cbd55bbea9bc72fe69a863e72026781a9fe76ab429d7037465b942145455420ee1178e2875ec87a1e12 - languageName: node - linkType: hard - -"pug-load@npm:^3.0.0": - version: 3.0.0 - resolution: "pug-load@npm:3.0.0" - dependencies: - object-assign: ^4.1.1 - pug-walk: ^2.0.0 - checksum: 1800ec51994c92338401bcf79bbfa0d5ef9aa312bc415c2618263d6c04d1d7c5be5ac4a333c47a0eaa823f6231b4ade1a1c40f5784b99eb576d25853597bff2f - languageName: node - linkType: hard - -"pug-parser@npm:^6.0.0": - version: 6.0.0 - resolution: "pug-parser@npm:6.0.0" - dependencies: - pug-error: ^2.0.0 - token-stream: 1.0.0 - checksum: a6954d1383601233ec9d58e8fb22339f4809cf938272db16c551d8574566f388af3bf5560ec95ad5e23902bc358e6fa857409e840de4ed1ff5120a1dd6892cca - languageName: node - linkType: hard - -"pug-runtime@npm:^3.0.0, pug-runtime@npm:^3.0.1": - version: 3.0.1 - resolution: "pug-runtime@npm:3.0.1" - checksum: 48a71b587caa08a5bccf9c1164206a34067edc1d13c2164bebad2dc562b529317578f889a0c41f0e16ddab3853c599696ff29a085f2d4554b783228f0002c41b - languageName: node - linkType: hard - -"pug-strip-comments@npm:^2.0.0": - version: 2.0.0 - resolution: "pug-strip-comments@npm:2.0.0" - dependencies: - pug-error: ^2.0.0 - checksum: 2cfcbf506c14bb3e64204a1d93f12ca61658d2540475b0f0911c35531ad28421e8d1e73a646d841d58cfa2c20f8593c52e492dfe5b6bec968e20b614e4dea1e4 - languageName: node - linkType: hard - -"pug-walk@npm:^2.0.0": - version: 2.0.0 - resolution: "pug-walk@npm:2.0.0" - checksum: bee64e133b711e1ed58022c0869b59e62f9f3ebb7084293857f074120b3cb588e7b8f74c4566426bf2b26dc1ec176ca6b64a2d1e53782f3fbbe039c5d4816638 - languageName: node - linkType: hard - -"pug@npm:3.0.2": - version: 3.0.2 - resolution: "pug@npm:3.0.2" - dependencies: - pug-code-gen: ^3.0.2 - pug-filters: ^4.0.0 - pug-lexer: ^5.0.1 - pug-linker: ^4.0.0 - pug-load: ^3.0.0 - pug-parser: ^6.0.0 - pug-runtime: ^3.0.1 - pug-strip-comments: ^2.0.0 - checksum: 3e1a3d48897c0c7dedd4f959ce8afaf6417a63756b149e1b5382bef16de5792ec7c7ae6a7d41641059cb149520f20b0d1ecf57014c0661526e96f0bad88541e5 - languageName: node - linkType: hard - "pump@npm:^2.0.0": version: 2.0.1 resolution: "pump@npm:2.0.1" @@ -14173,13 +9970,6 @@ __metadata: languageName: node linkType: hard -"punycode@npm:1.3.2": - version: 1.3.2 - resolution: "punycode@npm:1.3.2" - checksum: b8807fd594b1db33335692d1f03e8beeddde6fda7fbb4a2e32925d88d20a3aa4cd8dcc0c109ccaccbd2ba761c208dfaaada83007087ea8bfb0129c9ef1b99ed6 - languageName: node - linkType: hard - "punycode@npm:2.1.1, punycode@npm:^2.1.0, punycode@npm:^2.1.1": version: 2.1.1 resolution: "punycode@npm:2.1.1" @@ -14187,24 +9977,6 @@ __metadata: languageName: node linkType: hard -"pureimage@npm:0.3.14": - version: 0.3.14 - resolution: "pureimage@npm:0.3.14" - dependencies: - jpeg-js: ^0.4.1 - opentype.js: ^0.4.3 - pngjs: ^3.3.1 - checksum: e2fe6cbe2504d0815d778a7fb536c1588546e2eeb97639b4516cb70cefe2e6ed4075bbb6bbb1f8cad74500dd02af2c3119e198dfaae9a0bd02c7138053b45fdf - languageName: node - linkType: hard - -"q@npm:1.4.1": - version: 1.4.1 - resolution: "q@npm:1.4.1" - checksum: 22c8e1f24f416d0977e6da63f24712189c5dd789489999fc040467480e4e0ef4bd0e3126cce1b8ef72c709bbe1fcce10eba0f4991a03fc64ecb5a17e05ed8d35 - languageName: node - linkType: hard - "q@npm:^1.1.2": version: 1.5.1 resolution: "q@npm:1.5.1" @@ -14212,29 +9984,6 @@ __metadata: languageName: node linkType: hard -"qrcode@npm:1.5.1": - version: 1.5.1 - resolution: "qrcode@npm:1.5.1" - dependencies: - dijkstrajs: ^1.0.1 - encode-utf8: ^1.0.3 - pngjs: ^5.0.0 - yargs: ^15.3.1 - bin: - qrcode: bin/qrcode - checksum: 842f899d95caaad2ac507408b5498be3197e1df16bc6b537b20069d2cb1330e4588b50f672ce4a9ccf01338f7c97b5732ff9b5caaa6eb2338187d3c25a973e79 - languageName: node - linkType: hard - -"qs@npm:^6.4.0, qs@npm:^6.5.2": - version: 6.11.0 - resolution: "qs@npm:6.11.0" - dependencies: - side-channel: ^1.0.4 - checksum: 6e1f29dd5385f7488ec74ac7b6c92f4d09a90408882d0c208414a34dd33badc1a621019d4c799a3df15ab9b1d0292f97c1dd71dc7c045e69f81a8064e5af7297 - languageName: node - linkType: hard - "qs@npm:~6.5.2": version: 6.5.2 resolution: "qs@npm:6.5.2" @@ -14252,13 +10001,6 @@ __metadata: languageName: node linkType: hard -"querystring@npm:0.2.0": - version: 0.2.0 - resolution: "querystring@npm:0.2.0" - checksum: 8258d6734f19be27e93f601758858c299bdebe71147909e367101ba459b95446fbe5b975bf9beb76390156a592b6f4ac3a68b6087cea165c259705b8b4e56a69 - languageName: node - linkType: hard - "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -14287,84 +10029,6 @@ __metadata: languageName: node linkType: hard -"quick-lru@npm:^5.1.1": - version: 5.1.1 - resolution: "quick-lru@npm:5.1.1" - checksum: a516faa25574be7947969883e6068dbe4aa19e8ef8e8e0fd96cddd6d36485e9106d85c0041a27153286b0770b381328f4072aa40d3b18a19f5f7d2b78b94b5ed - languageName: node - linkType: hard - -"random-seed@npm:0.3.0": - version: 0.3.0 - resolution: "random-seed@npm:0.3.0" - dependencies: - json-stringify-safe: ^5.0.1 - checksum: 0a82692fc8d830bc26259dad78a911969343c247afb4be5d578c30fd9fbe76c7ce3f6900422310fc90fe05d6baa12e84a1b9ff93b0b7fda757ecfa40645a1f72 - languageName: node - linkType: hard - -"randombytes@npm:^2.1.0": - version: 2.1.0 - resolution: "randombytes@npm:2.1.0" - dependencies: - safe-buffer: ^5.1.0 - checksum: d779499376bd4cbb435ef3ab9a957006c8682f343f14089ed5f27764e4645114196e75b7f6abf1cbd84fd247c0cb0651698444df8c9bf30e62120fbbc52269d6 - languageName: node - linkType: hard - -"ratelimiter@npm:3.4.1": - version: 3.4.1 - resolution: "ratelimiter@npm:3.4.1" - checksum: e4451423871405aafa300399063d3a0aefef3eb42704d09cf95a0d3f70bdd74865397e408cd78aeb695cbf7afc7e5a34481e412bd2a04eeabdf7c52a48bae87e - languageName: node - linkType: hard - -"raw-body@npm:^2.2.0, raw-body@npm:^2.3.3": - version: 2.5.1 - resolution: "raw-body@npm:2.5.1" - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - checksum: 5362adff1575d691bb3f75998803a0ffed8c64eabeaa06e54b4ada25a0cd1b2ae7f4f5ec46565d1bec337e08b5ac90c76eaa0758de6f72a633f025d754dec29e - languageName: node - linkType: hard - -"rc@npm:^1.2.7": - version: 1.2.8 - resolution: "rc@npm:1.2.8" - dependencies: - deep-extend: ^0.6.0 - ini: ~1.3.0 - minimist: ^1.2.0 - strip-json-comments: ~2.0.1 - bin: - rc: ./cli.js - checksum: 2e26e052f8be2abd64e6d1dabfbd7be03f80ec18ccbc49562d31f617d0015fbdbcf0f9eed30346ea6ab789e0fdfe4337f033f8016efdbee0df5354751842080e - languageName: node - linkType: hard - -"rdf-canonize@npm:^3.0.0": - version: 3.0.0 - resolution: "rdf-canonize@npm:3.0.0" - dependencies: - setimmediate: ^1.0.5 - checksum: 744ab17b8e1af717d28b61cf066081963de94ca2f4765d078fd986d837c67c92519b5d80213262de0422436afc5a16804f5df0dfea9ee194441e0407c7198de3 - languageName: node - linkType: hard - -"re2@npm:1.17.8": - version: 1.17.8 - resolution: "re2@npm:1.17.8" - dependencies: - install-artifact-from-github: ^1.3.1 - nan: ^2.17.0 - node-gyp: ^9.3.0 - checksum: 451a12888650fb1b9baaf5ec7441396f00fc5a7289fc0c0704732cb8eec668489ec6f4372aea9cd3fc5ddf6ff5dc42cd7c93c118939fea3e7f4f5c512e292fe3 - languageName: node - linkType: hard - "react-is@npm:^17.0.1": version: 17.0.2 resolution: "react-is@npm:17.0.2" @@ -14423,7 +10087,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:3, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:3, readable-stream@npm:^3.6.0": version: 3.6.0 resolution: "readable-stream@npm:3.6.0" dependencies: @@ -14449,24 +10113,6 @@ __metadata: languageName: node linkType: hard -"readable-web-to-node-stream@npm:^3.0.2": - version: 3.0.2 - resolution: "readable-web-to-node-stream@npm:3.0.2" - dependencies: - readable-stream: ^3.6.0 - checksum: 8c56cc62c68513425ddfa721954875b382768f83fa20e6b31e365ee00cbe7a3d6296f66f7f1107b16cd3416d33aa9f1680475376400d62a081a88f81f0ea7f9c - languageName: node - linkType: hard - -"readdir-glob@npm:^1.0.0": - version: 1.1.2 - resolution: "readdir-glob@npm:1.1.2" - dependencies: - minimatch: ^5.1.0 - checksum: 1e5f701d3c94af5653e1736dfef99e991869c6e1c87bf08835d8c641f767e73ae25b829d3d1f8504fab8cad49b70b718ef960d3afee5be45cd779ccaeb264ed4 - languageName: node - linkType: hard - "readdirp@npm:~3.3.0": version: 3.3.0 resolution: "readdirp@npm:3.3.0" @@ -14502,59 +10148,6 @@ __metadata: languageName: node linkType: hard -"redis-commands@npm:1.7.0": - version: 1.7.0 - resolution: "redis-commands@npm:1.7.0" - checksum: d1ff7fbcb5e54768c77f731f1d49679d2a62c3899522c28addb4e2e5813aea8bcac3f22519d71d330224c3f2937f935dfc3d8dc65e90db0f5fe22dc2c1515aa7 - languageName: node - linkType: hard - -"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": - version: 1.2.0 - resolution: "redis-errors@npm:1.2.0" - checksum: f28ac2692113f6f9c222670735aa58aeae413464fd58ccf3fce3f700cae7262606300840c802c64f2b53f19f65993da24dc918afc277e9e33ac1ff09edb394f4 - languageName: node - linkType: hard - -"redis-info@npm:^3.0.8": - version: 3.1.0 - resolution: "redis-info@npm:3.1.0" - dependencies: - lodash: ^4.17.11 - checksum: d72ff0584ebb4a2149cfcfcf9142d9a7f9d0b96ae53fbf431f2738f33f1f42add6505ff73b2d640cab345923a34b217d7c728fa706cc81ad8bd8ad4c48987445 - languageName: node - linkType: hard - -"redis-lock@npm:0.1.4": - version: 0.1.4 - resolution: "redis-lock@npm:0.1.4" - checksum: 5cffe8a67accbf0193c2dbdb96baf67b04e75f9d9995ad7c43f02ce25004cda9f2ec21ac7e2b61238426790acdca368ab5843908778f8ba6746382d8697ced1c - languageName: node - linkType: hard - -"redis-parser@npm:^3.0.0": - version: 3.0.0 - resolution: "redis-parser@npm:3.0.0" - dependencies: - redis-errors: ^1.0.0 - checksum: 89290ae530332f2ae37577647fa18208d10308a1a6ba750b9d9a093e7398f5e5253f19855b64c98757f7129cccce958e4af2573fdc33bad41405f87f1943459a - languageName: node - linkType: hard - -"redis@npm:*": - version: 4.3.0 - resolution: "redis@npm:4.3.0" - dependencies: - "@redis/bloom": 1.0.2 - "@redis/client": 1.3.0 - "@redis/graph": 1.0.1 - "@redis/json": 1.0.3 - "@redis/search": 1.1.0 - "@redis/time-series": 1.0.3 - checksum: f5da538cbd79e864e2df7197c9fda1a2d8d4df3be1bdfd3a62a80c0be19b30616f366587e92cdb94b5c027cef4640ff1fc2f1d98873792545ef5f9b1ca53513f - languageName: node - linkType: hard - "reduce-css-calc@npm:^1.2.6": version: 1.3.0 resolution: "reduce-css-calc@npm:1.3.0" @@ -14575,13 +10168,6 @@ __metadata: languageName: node linkType: hard -"reflect-metadata@npm:0.1.13, reflect-metadata@npm:^0.1.13": - version: 0.1.13 - resolution: "reflect-metadata@npm:0.1.13" - checksum: 798d379a7b6f6455501145419505c97dd11cbc23857a386add2b9ef15963ccf15a48d9d15507afe01d4cd74116df8a213247200bac00320bd7c11ddeaa5e8fb4 - languageName: node - linkType: hard - "regenerator-runtime@npm:^0.13.4": version: 0.13.9 resolution: "regenerator-runtime@npm:0.13.9" @@ -14645,15 +10231,6 @@ __metadata: languageName: node linkType: hard -"rename@npm:1.0.4": - version: 1.0.4 - resolution: "rename@npm:1.0.4" - dependencies: - debug: ^2.5.2 - checksum: 40d86b011e49069bf0f4af1dc1bfacafcab20afde55bfe6b4a1140e2598cc14c4348b8ddc4cd9072948a1c9992927232e237fd31f7fc13d9b83a671be03659bd - languageName: node - linkType: hard - "repeat-element@npm:^1.1.2": version: 1.1.3 resolution: "repeat-element@npm:1.1.3" @@ -14706,13 +10283,6 @@ __metadata: languageName: node linkType: hard -"require-all@npm:3.0.0": - version: 3.0.0 - resolution: "require-all@npm:3.0.0" - checksum: f2d652d6bca4201bda1ff2f7d4f46a3edd136cc6fd81d38caff50eaa4669ae7ae459f32a06b0892e754bf0f7aae21fbe69172409dff1abd78be5c12cab750a01 - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -14720,13 +10290,6 @@ __metadata: languageName: node linkType: hard -"require-from-string@npm:^2.0.2": - version: 2.0.2 - resolution: "require-from-string@npm:2.0.2" - checksum: a03ef6895445f33a4015300c426699bc66b2b044ba7b670aa238610381b56d3f07c686251740d575e22f4c87531ba662d06937508f0f3c0f1ddc04db3130560b - languageName: node - linkType: hard - "require-main-filename@npm:^1.0.1": version: 1.0.1 resolution: "require-main-filename@npm:1.0.1" @@ -14734,13 +10297,6 @@ __metadata: languageName: node linkType: hard -"require-main-filename@npm:^2.0.0": - version: 2.0.0 - resolution: "require-main-filename@npm:2.0.0" - checksum: e9e294695fea08b076457e9ddff854e81bffbe248ed34c1eec348b7abbd22a0d02e8d75506559e2265e96978f3c4720bd77a6dad84755de8162b357eb6c778c7 - languageName: node - linkType: hard - "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -14748,13 +10304,6 @@ __metadata: languageName: node linkType: hard -"resolve-alpn@npm:^1.0.0, resolve-alpn@npm:^1.2.0": - version: 1.2.1 - resolution: "resolve-alpn@npm:1.2.1" - checksum: f558071fcb2c60b04054c99aebd572a2af97ef64128d59bef7ab73bd50d896a222a056de40ffc545b633d99b304c259ea9d0c06830d5c867c34f0bfa60b8eae0 - languageName: node - linkType: hard - "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -14797,16 +10346,6 @@ __metadata: languageName: node linkType: hard -"resolve-path@npm:^1.4.0": - version: 1.4.0 - resolution: "resolve-path@npm:1.4.0" - dependencies: - http-errors: ~1.6.2 - path-is-absolute: 1.0.1 - checksum: 1a39f569ee54dd5f8ee8576ef8671c9724bea65d9f9982fbb5352af9fb4e500e1e459c1bfb1ae3ebfd8d43a709c3a01dfa4f46cf5b831e45e2caed4f1a208300 - languageName: node - linkType: hard - "resolve-url@npm:^0.2.1": version: 0.2.1 resolution: "resolve-url@npm:0.2.1" @@ -14831,7 +10370,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.15.1, resolve@npm:^1.20.0, resolve@npm:^1.22.0, resolve@npm:^1.22.1": +"resolve@npm:^1.20.0, resolve@npm:^1.22.0, resolve@npm:^1.22.1": version: 1.22.1 resolution: "resolve@npm:1.22.1" dependencies: @@ -14873,7 +10412,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.15.1#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin": +"resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin": version: 1.22.1 resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=c3c19d" dependencies: @@ -14905,15 +10444,6 @@ __metadata: languageName: node linkType: hard -"responselike@npm:^2.0.0": - version: 2.0.1 - resolution: "responselike@npm:2.0.1" - dependencies: - lowercase-keys: ^2.0.0 - checksum: b122535466e9c97b55e69c7f18e2be0ce3823c5d47ee8de0d9c0b114aa55741c6db8bfbfce3766a94d1272e61bfb1ebf0a15e9310ac5629fbb7446a861b4fd3a - languageName: node - linkType: hard - "restore-cursor@npm:^3.1.0": version: 3.1.0 resolution: "restore-cursor@npm:3.1.0" @@ -14945,17 +10475,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:2": - version: 2.7.1 - resolution: "rimraf@npm:2.7.1" - dependencies: - glob: ^7.1.3 - bin: - rimraf: ./bin.js - checksum: cdc7f6eacb17927f2a075117a823e1c5951792c6498ebcce81ca8203454a811d4cf8900314154d3259bb8f0b42ab17f67396a8694a54cae3283326e57ad250cd - languageName: node - linkType: hard - "rimraf@npm:3.0.2, rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -14981,16 +10500,6 @@ __metadata: languageName: node linkType: hard -"rss-parser@npm:3.12.0": - version: 3.12.0 - resolution: "rss-parser@npm:3.12.0" - dependencies: - entities: ^2.0.3 - xml2js: ^0.4.19 - checksum: aa0f0eb2e3a5c70677a1c7cb6c2e96420f12c8963a8bed922ec2ff1bb9dbbb725fc5783be31ca8140154c3d5589ccd31580ced7d32ebd0dda7572f78ce242a41 - languageName: node - linkType: hard - "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -15025,13 +10534,6 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 - languageName: node - linkType: hard - "safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.2, safe-buffer@npm:~5.2.0": version: 5.2.0 resolution: "safe-buffer@npm:5.2.0" @@ -15062,20 +10564,6 @@ __metadata: languageName: node linkType: hard -"sanitize-html@npm:2.7.0": - version: 2.7.0 - resolution: "sanitize-html@npm:2.7.0" - dependencies: - deepmerge: ^4.2.2 - escape-string-regexp: ^4.0.0 - htmlparser2: ^6.0.0 - is-plain-object: ^5.0.0 - parse-srcset: ^1.0.2 - postcss: ^8.3.11 - checksum: 73a4d66f69578bace3506519ca0734279b7117e15c33c7e4d075cdb483b1586261a18acd35934f6c6109f3b2a4a82f82c242171a94d5dc23fba5b09b01ea5b22 - languageName: node - linkType: hard - "sass@npm:1.53.0": version: 1.53.0 resolution: "sass@npm:1.53.0" @@ -15089,14 +10577,7 @@ __metadata: languageName: node linkType: hard -"sax@npm:1.2.1": - version: 1.2.1 - resolution: "sax@npm:1.2.1" - checksum: 8dca7d5e1cd7d612f98ac50bdf0b9f63fbc964b85f0c4e2eb271f8b9b47fd3bf344c4d6a592e69ecf726d1485ca62cd8a52e603bbc332d18a66af25a9a1045ad - languageName: node - linkType: hard - -"sax@npm:>=0.6.0, sax@npm:^1.2.4, sax@npm:~1.2.1": +"sax@npm:~1.2.1": version: 1.2.4 resolution: "sax@npm:1.2.4" checksum: d3df7d32b897a2c2f28e941f732c71ba90e27c24f62ee918bd4d9a8cfb3553f2f81e5493c7f0be94a11c1911b643a9108f231dd6f60df3fa9586b5d2e3e9e1fe @@ -15112,33 +10593,6 @@ __metadata: languageName: node linkType: hard -"saxes@npm:^6.0.0": - version: 6.0.0 - resolution: "saxes@npm:6.0.0" - dependencies: - xmlchars: ^2.2.0 - checksum: d3fa3e2aaf6c65ed52ee993aff1891fc47d5e47d515164b5449cbf5da2cbdc396137e55590472e64c5c436c14ae64a8a03c29b9e7389fc6f14035cf4e982ef3b - languageName: node - linkType: hard - -"schema-utils@npm:^3.0.0": - version: 3.1.1 - resolution: "schema-utils@npm:3.1.1" - dependencies: - "@types/json-schema": ^7.0.8 - ajv: ^6.12.5 - ajv-keywords: ^3.5.2 - checksum: fb73f3d759d43ba033c877628fe9751620a26879f6301d3dbeeb48cf2a65baec5cdf99da65d1bf3b4ff5444b2e59cbe4f81c2456b5e0d2ba7d7fd4aed5da29ce - languageName: node - linkType: hard - -"secure-json-parse@npm:^2.1.0": - version: 2.5.0 - resolution: "secure-json-parse@npm:2.5.0" - checksum: 84147a32615ce0d93d2fbba60cde85ae362f45cc948ea134e4d6d1e678bb4b7f3a5ce9b9692ed052baefeb2e1c8ba183b34920390e6a089925b97b0d8f7ab064 - languageName: node - linkType: hard - "seedrandom@npm:3.0.5": version: 3.0.5 resolution: "seedrandom@npm:3.0.5" @@ -15155,7 +10609,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.6.0": +"semver@npm:2 || 3 || 4 || 5": version: 5.7.1 resolution: "semver@npm:5.7.1" bin: @@ -15164,7 +10618,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.3.7, semver@npm:7.x, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.6, semver@npm:^7.3.7, semver@npm:~7.3.0": +"semver@npm:7.x, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.6, semver@npm:^7.3.7, semver@npm:~7.3.0": version: 7.3.7 resolution: "semver@npm:7.3.7" dependencies: @@ -15195,26 +10649,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.8": - version: 7.3.8 - resolution: "semver@npm:7.3.8" - dependencies: - lru-cache: ^6.0.0 - bin: - semver: bin/semver.js - checksum: ba9c7cbbf2b7884696523450a61fee1a09930d888b7a8d7579025ad93d459b2d1949ee5bbfeb188b2be5f4ac163544c5e98491ad6152df34154feebc2cc337c1 - languageName: node - linkType: hard - -"serialize-javascript@npm:6.0.0": - version: 6.0.0 - resolution: "serialize-javascript@npm:6.0.0" - dependencies: - randombytes: ^2.1.0 - checksum: 56f90b562a1bdc92e55afb3e657c6397c01a902c588c0fe3d4c490efdcc97dcd2a3074ba12df9e94630f33a5ce5b76a74784a7041294628a6f4306e0ec84bf93 - languageName: node - linkType: hard - "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -15234,56 +10668,6 @@ __metadata: languageName: node linkType: hard -"setimmediate@npm:^1.0.5, setimmediate@npm:~1.0.4": - version: 1.0.5 - resolution: "setimmediate@npm:1.0.5" - checksum: c9a6f2c5b51a2dabdc0247db9c46460152ffc62ee139f3157440bd48e7c59425093f42719ac1d7931f054f153e2d26cf37dfeb8da17a794a58198a2705e527fd - languageName: node - linkType: hard - -"setprototypeof@npm:1.1.0": - version: 1.1.0 - resolution: "setprototypeof@npm:1.1.0" - checksum: 27cb44304d6c9e1a23bc6c706af4acaae1a7aa1054d4ec13c05f01a99fd4887109a83a8042b67ad90dbfcd100d43efc171ee036eb080667172079213242ca36e - languageName: node - linkType: hard - -"setprototypeof@npm:1.2.0": - version: 1.2.0 - resolution: "setprototypeof@npm:1.2.0" - checksum: be18cbbf70e7d8097c97f713a2e76edf84e87299b40d085c6bf8b65314e994cc15e2e317727342fa6996e38e1f52c59720b53fe621e2eb593a6847bf0356db89 - languageName: node - linkType: hard - -"sha.js@npm:^2.4.11": - version: 2.4.11 - resolution: "sha.js@npm:2.4.11" - dependencies: - inherits: ^2.0.1 - safe-buffer: ^5.0.1 - bin: - sha.js: ./bin.js - checksum: ebd3f59d4b799000699097dadb831c8e3da3eb579144fd7eb7a19484cbcbb7aca3c68ba2bb362242eb09e33217de3b4ea56e4678184c334323eca24a58e3ad07 - languageName: node - linkType: hard - -"sharp@npm:0.31.2": - version: 0.31.2 - resolution: "sharp@npm:0.31.2" - dependencies: - color: ^4.2.3 - detect-libc: ^2.0.1 - node-addon-api: ^5.0.0 - node-gyp: latest - prebuild-install: ^7.1.1 - semver: ^7.3.8 - simple-get: ^4.0.1 - tar-fs: ^2.1.1 - tunnel-agent: ^0.6.0 - checksum: 076717b7a073ea47bb522ff2931b74b6608daeb6f7ae334e4848d47fdf4d23bcb18cd49044fd5fb27ef27a1a4aa87d141894d67d1c4bb15a6e2e63cf4dbe329e - languageName: node - linkType: hard - "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -15311,13 +10695,6 @@ __metadata: languageName: node linkType: hard -"sigmund@npm:^1.0.1": - version: 1.0.1 - resolution: "sigmund@npm:1.0.1" - checksum: 793f81f8083ad75ff3903ffd93cf35be8d797e872822cf880aea27ce6db522b508d93ea52ae292bccf357ce34dd5c7faa544cc51c2216e70bbf5fcf09b62707c - languageName: node - linkType: hard - "signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -15332,47 +10709,6 @@ __metadata: languageName: node linkType: hard -"simple-concat@npm:^1.0.0": - version: 1.0.1 - resolution: "simple-concat@npm:1.0.1" - checksum: 4d211042cc3d73a718c21ac6c4e7d7a0363e184be6a5ad25c8a1502e49df6d0a0253979e3d50dbdd3f60ef6c6c58d756b5d66ac1e05cda9cacd2e9fc59e3876a - languageName: node - linkType: hard - -"simple-get@npm:^4.0.0, simple-get@npm:^4.0.1": - version: 4.0.1 - resolution: "simple-get@npm:4.0.1" - dependencies: - decompress-response: ^6.0.0 - once: ^1.3.1 - simple-concat: ^1.0.0 - checksum: e4132fd27cf7af230d853fa45c1b8ce900cb430dd0a3c6d3829649fe4f2b26574c803698076c4006450efb0fad2ba8c5455fbb5755d4b0a5ec42d4f12b31d27e - languageName: node - linkType: hard - -"simple-swizzle@npm:^0.2.2": - version: 0.2.2 - resolution: "simple-swizzle@npm:0.2.2" - dependencies: - is-arrayish: ^0.3.1 - checksum: a7f3f2ab5c76c4472d5c578df892e857323e452d9f392e1b5cf74b74db66e6294a1e1b8b390b519fa1b96b5b613f2a37db6cffef52c3f1f8f3c5ea64eb2d54c0 - languageName: node - linkType: hard - -"sinon@npm:^14.0.2": - version: 14.0.2 - resolution: "sinon@npm:14.0.2" - dependencies: - "@sinonjs/commons": ^2.0.0 - "@sinonjs/fake-timers": ^9.1.2 - "@sinonjs/samsam": ^7.0.1 - diff: ^5.0.0 - nise: ^5.1.2 - supports-color: ^7.2.0 - checksum: de7730cd7785a457e42f9a93e955780c870296036a13816e3c0c5648360afae82fdc748e36c854cf26fb8abd117855a7211aee49265c334fa61439aae17a1b72 - languageName: node - linkType: hard - "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -15612,15 +10948,6 @@ __metadata: languageName: node linkType: hard -"speakeasy@npm:2.0.0": - version: 2.0.0 - resolution: "speakeasy@npm:2.0.0" - dependencies: - base32.js: 0.0.1 - checksum: 2a44aa4336cd39a0dcdc051c7adcd3fc76fdba239dc1ded50052164c9d80d773183827934a62850a5fb7a7a19f13bf5561ec88bd23777c6d228c4aaa175bea1a - languageName: node - linkType: hard - "split-string@npm:^3.0.1, split-string@npm:^3.0.2": version: 3.1.0 resolution: "split-string@npm:3.1.0" @@ -15630,13 +10957,6 @@ __metadata: languageName: node linkType: hard -"split2@npm:^4.1.0": - version: 4.1.0 - resolution: "split2@npm:4.1.0" - checksum: ec581597cb74c13cdfb5e2047543dd40cb1e8e9803c7b1e0c29ede05f2b4f049b2d6e7f2788a225d544549375719658b8f38e9366364dec35dc7a12edfda5ee5 - languageName: node - linkType: hard - "split@npm:0.3": version: 0.3.3 resolution: "split@npm:0.3.3" @@ -15646,13 +10966,6 @@ __metadata: languageName: node linkType: hard -"sprintf-js@npm:1.1.2": - version: 1.1.2 - resolution: "sprintf-js@npm:1.1.2" - checksum: d4bb46464632b335e5faed381bd331157e0af64915a98ede833452663bc672823db49d7531c32d58798e85236581fb7342fd0270531ffc8f914e186187bf1c90 - languageName: node - linkType: hard - "sprintf-js@npm:~1.0.2": version: 1.0.3 resolution: "sprintf-js@npm:1.0.3" @@ -15706,13 +11019,6 @@ __metadata: languageName: node linkType: hard -"standard-as-callback@npm:^2.1.0": - version: 2.1.0 - resolution: "standard-as-callback@npm:2.1.0" - checksum: 88bec83ee220687c72d94fd86a98d5272c91d37ec64b66d830dbc0d79b62bfa6e47f53b71646011835fc9ce7fae62739545d13124262b53be4fbb3e2ebad551c - languageName: node - linkType: hard - "start-server-and-test@npm:1.14.0": version: 1.14.0 resolution: "start-server-and-test@npm:1.14.0" @@ -15742,20 +11048,6 @@ __metadata: languageName: node linkType: hard -"statuses@npm:2.0.1": - version: 2.0.1 - resolution: "statuses@npm:2.0.1" - checksum: 18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb - languageName: node - linkType: hard - -"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0": - version: 1.5.0 - resolution: "statuses@npm:1.5.0" - checksum: c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c - languageName: node - linkType: hard - "stream-combiner@npm:~0.0.4": version: 0.0.4 resolution: "stream-combiner@npm:0.0.4" @@ -15772,15 +11064,6 @@ __metadata: languageName: node linkType: hard -"stream-parser@npm:~0.3.1": - version: 0.3.1 - resolution: "stream-parser@npm:0.3.1" - dependencies: - debug: 2 - checksum: 4d86ff8cffe7c7587dc91433fff9dce38a93ea7e9f47560055addc81eae6b6befab22b75643ce539faf325fe2b17d371778242566bed086e75f6cffb1e76c06c - languageName: node - linkType: hard - "stream-shift@npm:^1.0.0": version: 1.0.1 resolution: "stream-shift@npm:1.0.1" @@ -15788,20 +11071,6 @@ __metadata: languageName: node linkType: hard -"streamsearch@npm:^1.1.0": - version: 1.1.0 - resolution: "streamsearch@npm:1.1.0" - checksum: 1cce16cea8405d7a233d32ca5e00a00169cc0e19fbc02aa839959985f267335d435c07f96e5e0edd0eadc6d39c98d5435fb5bbbdefc62c41834eadc5622ad942 - languageName: node - linkType: hard - -"strict-event-emitter-types@npm:2.0.0": - version: 2.0.0 - resolution: "strict-event-emitter-types@npm:2.0.0" - checksum: 91ef62364cad9ece9ab9984e806b1c6d947d0617437a25605fff0cbfae59ac6a8d641257a168c1d5f2909809a467c714f027fdccb70b6155d68eac0dc1535299 - languageName: node - linkType: hard - "strict-uri-encode@npm:^1.0.0": version: 1.1.0 resolution: "strict-uri-encode@npm:1.1.0" @@ -15965,13 +11234,6 @@ __metadata: languageName: node linkType: hard -"strip-final-newline@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-final-newline@npm:3.0.0" - checksum: 23ee263adfa2070cd0f23d1ac14e2ed2f000c9b44229aec9c799f1367ec001478469560abefd00c5c99ee6f0b31c137d53ec6029c53e9f32a93804e18c201050 - languageName: node - linkType: hard - "strip-indent@npm:^3.0.0": version: 3.0.0 resolution: "strip-indent@npm:3.0.0" @@ -15981,74 +11243,13 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:3.1.1, strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1, strip-json-comments@npm:~3.1.1": +"strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1, strip-json-comments@npm:~3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 languageName: node linkType: hard -"strip-json-comments@npm:~2.0.1": - version: 2.0.1 - resolution: "strip-json-comments@npm:2.0.1" - checksum: 1074ccb63270d32ca28edfb0a281c96b94dc679077828135141f27d52a5a398ef5e78bcf22809d23cadc2b81dfbe345eb5fd8699b385c8b1128907dec4a7d1e1 - languageName: node - linkType: hard - -"strnum@npm:^1.0.4": - version: 1.0.5 - resolution: "strnum@npm:1.0.5" - checksum: 651b2031db5da1bf4a77fdd2f116a8ac8055157c5420f5569f64879133825915ad461513e7202a16d7fec63c54fd822410d0962f8ca12385c4334891b9ae6dd2 - languageName: node - linkType: hard - -"strtok3@npm:^7.0.0": - version: 7.0.0 - resolution: "strtok3@npm:7.0.0" - dependencies: - "@tokenizer/token": ^0.3.0 - peek-readable: ^5.0.0 - checksum: 2ebe7ad8f2aea611dec6742cf6a42e82764892a362907f7ce493faf334501bf981ce21c828dcc300457e6d460dc9c34d644ededb3b01dcb9e37559203cf1748c - languageName: node - linkType: hard - -"style-loader@npm:3.3.1": - version: 3.3.1 - resolution: "style-loader@npm:3.3.1" - peerDependencies: - webpack: ^5.0.0 - checksum: 470feef680f59e2fce4d6601b5c55b88c01ad8d1dd693c528ffd591ff5fd7c01a4eff3bdbe62f26f847d6bd2430c9ab594be23307cfe7a3446ab236683f0d066 - languageName: node - linkType: hard - -"summaly@npm:2.6.0": - version: 2.6.0 - resolution: "summaly@npm:2.6.0" - dependencies: - cheerio: 0.22.0 - debug: 4.3.3 - escape-regexp: 0.0.1 - got: 11.5.1 - html-entities: 2.3.2 - iconv-lite: 0.6.3 - jschardet: 3.0.0 - koa: 2.13.4 - private-ip: 2.3.3 - require-all: 3.0.0 - trace-redirect: 1.0.6 - checksum: 5eff928fc66dc9c2845b43f4242e1f5d37666ef829a6c0bd1ac6467ab91ff92e90eb53f4166c67c573c136a6f512310e34ab6a1acb866418dba8c2c6590dd139 - languageName: node - linkType: hard - -"supports-color@npm:8.1.1, supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: ^4.0.0 - checksum: c052193a7e43c6cdc741eb7f378df605636e01ad434badf7324f17fb60c69a880d8d8fcdcb562cf94c2350e57b937d7425ab5b8326c67c2adc48f7c87c1db406 - languageName: node - linkType: hard - "supports-color@npm:^2.0.0": version: 2.0.0 resolution: "supports-color@npm:2.0.0" @@ -16074,7 +11275,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -16083,6 +11284,15 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: ^4.0.0 + checksum: c052193a7e43c6cdc741eb7f378df605636e01ad434badf7324f17fb60c69a880d8d8fcdcb562cf94c2350e57b937d7425ab5b8326c67c2adc48f7c87c1db406 + languageName: node + linkType: hard + "supports-hyperlinks@npm:^2.0.0": version: 2.2.0 resolution: "supports-hyperlinks@npm:2.2.0" @@ -16148,25 +11358,6 @@ __metadata: languageName: node linkType: hard -"syslog-pro@npm:1.0.0": - version: 1.0.0 - resolution: "syslog-pro@npm:1.0.0" - dependencies: - moment: ^2.22.2 - checksum: 7d6399e4ca3a9305758f77b3e720469b39c156b5a8219ed4ce27b4ad8f960f8e395aebb0ccc84e4438b50a6b2cda2e20251e278307833ed7ac1045ae9516a33c - languageName: node - linkType: hard - -"systeminformation@npm:5.11.22": - version: 5.11.22 - resolution: "systeminformation@npm:5.11.22" - bin: - systeminformation: lib/cli.js - checksum: 82edcb6e6e4f7feec44e9bfddf7a7dff280977a504171cc0affa98541667a01baee53c87d1d587f47c75c38c7a4abc002d50b06dee63af34ecb2ffeb4365c743 - conditions: (os=darwin | os=linux | os=win32 | os=freebsd | os=openbsd | os=netbsd | os=sunos | os=android) - languageName: node - linkType: hard - "syuilo-password-strength@npm:0.0.1": version: 0.0.1 resolution: "syuilo-password-strength@npm:0.0.1" @@ -16188,38 +11379,6 @@ __metadata: languageName: node linkType: hard -"tapable@npm:^2.2.0": - version: 2.2.1 - resolution: "tapable@npm:2.2.1" - checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51 - languageName: node - linkType: hard - -"tar-fs@npm:^2.0.0, tar-fs@npm:^2.1.1": - version: 2.1.1 - resolution: "tar-fs@npm:2.1.1" - dependencies: - chownr: ^1.1.1 - mkdirp-classic: ^0.5.2 - pump: ^3.0.0 - tar-stream: ^2.1.4 - checksum: f5b9a70059f5b2969e65f037b4e4da2daf0fa762d3d232ffd96e819e3f94665dbbbe62f76f084f1acb4dbdcce16c6e4dac08d12ffc6d24b8d76720f4d9cf032d - languageName: node - linkType: hard - -"tar-stream@npm:^2.1.4, tar-stream@npm:^2.2.0": - version: 2.2.0 - resolution: "tar-stream@npm:2.2.0" - dependencies: - bl: ^4.0.3 - end-of-stream: ^1.4.1 - fs-constants: ^1.0.0 - inherits: ^2.0.3 - readable-stream: ^3.1.1 - checksum: 699831a8b97666ef50021c767f84924cfee21c142c2eb0e79c63254e140e6408d6d55a065a2992548e72b06de39237ef2b802b99e3ece93ca3904a37622a66f3 - languageName: node - linkType: hard - "tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.1.11 resolution: "tar@npm:6.1.11" @@ -16289,24 +11448,6 @@ __metadata: languageName: node linkType: hard -"thenify-all@npm:^1.0.0": - version: 1.6.0 - resolution: "thenify-all@npm:1.6.0" - dependencies: - thenify: ">= 3.1.0 < 4" - checksum: dba7cc8a23a154cdcb6acb7f51d61511c37a6b077ec5ab5da6e8b874272015937788402fd271fdfc5f187f8cb0948e38d0a42dcc89d554d731652ab458f5343e - languageName: node - linkType: hard - -"thenify@npm:>= 3.1.0 < 4": - version: 3.3.1 - resolution: "thenify@npm:3.3.1" - dependencies: - any-promise: ^1.0.0 - checksum: 84e1b804bfec49f3531215f17b4a6e50fd4397b5f7c1bccc427b9c656e1ecfb13ea79d899930184f78bc2f57285c54d9a50a590c8868f4f0cef5c1d9f898b05e - languageName: node - linkType: hard - "throat@npm:^6.0.1": version: 6.0.1 resolution: "throat@npm:6.0.1" @@ -16378,7 +11519,7 @@ __metadata: languageName: node linkType: hard -"tmp@npm:0.2.1, tmp@npm:~0.2.1": +"tmp@npm:~0.2.1": version: 0.2.1 resolution: "tmp@npm:0.2.1" dependencies: @@ -16460,30 +11601,6 @@ __metadata: languageName: node linkType: hard -"toidentifier@npm:1.0.1": - version: 1.0.1 - resolution: "toidentifier@npm:1.0.1" - checksum: 952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 - languageName: node - linkType: hard - -"token-stream@npm:1.0.0": - version: 1.0.0 - resolution: "token-stream@npm:1.0.0" - checksum: e8adb56f31b813b6157130e7fc2fe14eb60e7cbf7b746e70e8293c7e55664d8e7ad5d93d7ae3aa4cad7fcb2b0aaf59dad6f2fd4ee0269204e55af5b05bc369e2 - languageName: node - linkType: hard - -"token-types@npm:^5.0.1": - version: 5.0.1 - resolution: "token-types@npm:5.0.1" - dependencies: - "@tokenizer/token": ^0.3.0 - ieee754: ^1.2.1 - checksum: 32780123bc6ce8b6a2231d860445c994a02a720abf38df5583ea957aa6626873cd1c4dd8af62314da4cf16ede00c379a765707a3b06f04b8808c38efdae1c785 - languageName: node - linkType: hard - "tough-cookie@npm:^4.0.0": version: 4.1.2 resolution: "tough-cookie@npm:4.1.2" @@ -16515,15 +11632,6 @@ __metadata: languageName: node linkType: hard -"tr46@npm:^3.0.0": - version: 3.0.0 - resolution: "tr46@npm:3.0.0" - dependencies: - punycode: ^2.1.1 - checksum: 44c3cc6767fb800490e6e9fd64fd49041aa4e49e1f6a012b34a75de739cc9ed3a6405296072c1df8b6389ae139c5e7c6496f659cfe13a04a4bff3a1422981270 - languageName: node - linkType: hard - "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -16531,20 +11639,6 @@ __metadata: languageName: node linkType: hard -"trace-redirect@npm:1.0.6": - version: 1.0.6 - resolution: "trace-redirect@npm:1.0.6" - checksum: c378538d0b3a2f314846231bae879ebf2f62319d4a1f8449588954637f8ce6c6910b0df13fce07c2c84fefd35fb7632c7e087b511c7a6a5194f1cae7538fba23 - languageName: node - linkType: hard - -"traverse@npm:>=0.3.0 <0.4": - version: 0.3.9 - resolution: "traverse@npm:0.3.9" - checksum: 982982e4e249e9bbf063732a41fe5595939892758524bbef5d547c67cdf371b13af72b5434c6a61d88d4bb4351d6dabc6e22d832e0d16bc1bc684ef97a1cc59e - languageName: node - linkType: hard - "trim-newlines@npm:^3.0.0": version: 3.0.1 resolution: "trim-newlines@npm:3.0.1" @@ -16585,21 +11679,6 @@ __metadata: languageName: node linkType: hard -"ts-loader@npm:9.3.1": - version: 9.3.1 - resolution: "ts-loader@npm:9.3.1" - dependencies: - chalk: ^4.1.0 - enhanced-resolve: ^5.0.0 - micromatch: ^4.0.0 - semver: ^7.3.4 - peerDependencies: - typescript: "*" - webpack: ^5.0.0 - checksum: 462a8ac315017cf4961dafd2be29d5abe7c3af63c4515e325269f79b9d0212b35c59184d7fd01fc378749c88454752e1599301d2190eb6844ea5fe332de5f695 - languageName: node - linkType: hard - "ts-node@npm:10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" @@ -16707,13 +11786,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.3.1": - version: 2.4.0 - resolution: "tslib@npm:2.4.0" - checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113 - languageName: node - linkType: hard - "tslib@npm:~2.1.0": version: 2.1.0 resolution: "tslib@npm:2.1.0" @@ -16721,13 +11793,6 @@ __metadata: languageName: node linkType: hard -"tsscmp@npm:1.0.6": - version: 1.0.6 - resolution: "tsscmp@npm:1.0.6" - checksum: 1512384def36bccc9125cabbd4c3b0e68608d7ee08127ceaa0b84a71797263f1a01c7f82fa69be8a3bd3c1396e2965d2f7b52d581d3a5eeaf3967fbc52e3b3bf - languageName: node - linkType: hard - "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -16780,7 +11845,7 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:4.0.8, type-detect@npm:^4.0.8": +"type-detect@npm:4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15 @@ -16822,16 +11887,6 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^1.6.14, type-is@npm:^1.6.16, type-is@npm:^1.6.4": - version: 1.6.18 - resolution: "type-is@npm:1.6.18" - dependencies: - media-typer: 0.3.0 - mime-types: ~2.1.24 - checksum: 2c8e47675d55f8b4e404bcf529abdf5036c537a04c2b20177bcf78c9e3c1da69da3942b1346e6edb09e823228c0ee656ef0e033765ec39a70d496ef601a0c657 - languageName: node - linkType: hard - "type@npm:^1.0.1": version: 1.2.0 resolution: "type@npm:1.2.0" @@ -16862,88 +11917,6 @@ __metadata: languageName: node linkType: hard -"typeorm@npm:0.3.7": - version: 0.3.7 - resolution: "typeorm@npm:0.3.7" - dependencies: - "@sqltools/formatter": ^1.2.2 - app-root-path: ^3.0.0 - buffer: ^6.0.3 - chalk: ^4.1.0 - cli-highlight: ^2.1.11 - date-fns: ^2.28.0 - debug: ^4.3.3 - dotenv: ^16.0.0 - glob: ^7.2.0 - js-yaml: ^4.1.0 - mkdirp: ^1.0.4 - reflect-metadata: ^0.1.13 - sha.js: ^2.4.11 - tslib: ^2.3.1 - uuid: ^8.3.2 - xml2js: ^0.4.23 - yargs: ^17.3.1 - peerDependencies: - "@google-cloud/spanner": ^5.18.0 - "@sap/hana-client": ^2.12.25 - better-sqlite3: ^7.1.2 - hdb-pool: ^0.1.6 - ioredis: ^5.0.4 - mongodb: ^3.6.0 - mssql: ^7.3.0 - mysql2: ^2.2.5 - oracledb: ^5.1.0 - pg: ^8.5.1 - pg-native: ^3.0.0 - pg-query-stream: ^4.0.0 - redis: ^3.1.1 || ^4.0.0 - sql.js: ^1.4.0 - sqlite3: ^5.0.3 - ts-node: ^10.7.0 - typeorm-aurora-data-api-driver: ^2.0.0 - peerDependenciesMeta: - "@google-cloud/spanner": - optional: true - "@sap/hana-client": - optional: true - better-sqlite3: - optional: true - hdb-pool: - optional: true - ioredis: - optional: true - mongodb: - optional: true - mssql: - optional: true - mysql2: - optional: true - oracledb: - optional: true - pg: - optional: true - pg-native: - optional: true - pg-query-stream: - optional: true - redis: - optional: true - sql.js: - optional: true - sqlite3: - optional: true - ts-node: - optional: true - typeorm-aurora-data-api-driver: - optional: true - bin: - typeorm: cli.js - typeorm-ts-node-commonjs: cli-ts-node-commonjs.js - typeorm-ts-node-esm: cli-ts-node-esm.js - checksum: b9a709fa1c103c98340ff055419f5de7fde478e35f246b2f6b212ad6c8954085f347c5a97ed2d94634cf9d51577035d8d71c4c4ccfd58b5f7a9579f434e71802 - languageName: node - linkType: hard - "typescript@npm:^4.9.4": version: 4.9.4 resolution: "typescript@npm:4.9.4" @@ -17027,13 +12000,6 @@ __metadata: languageName: node linkType: hard -"undici@npm:^5.2.0": - version: 5.10.0 - resolution: "undici@npm:5.10.0" - checksum: 7ba2b71dccc74cd2bdf645b83e9aaef374ae04855943d0a2f42a3d0b9e5556f37cc9b5156fb5288277a2fa95fd46a56f3ae0d5cf73db3f008d75ec41104b136c - languageName: node - linkType: hard - "union-value@npm:^1.0.0": version: 1.0.1 resolution: "union-value@npm:1.0.1" @@ -17119,13 +12085,6 @@ __metadata: languageName: node linkType: hard -"unpipe@npm:1.0.0": - version: 1.0.0 - resolution: "unpipe@npm:1.0.0" - checksum: 4fa18d8d8d977c55cb09715385c203197105e10a6d220087ec819f50cb68870f02942244f1017565484237f1f8c5d3cd413631b1ae104d3096f24fdfde1b4aa2 - languageName: node - linkType: hard - "unset-value@npm:^1.0.0": version: 1.0.0 resolution: "unset-value@npm:1.0.0" @@ -17143,24 +12102,6 @@ __metadata: languageName: node linkType: hard -"unzipper@npm:0.10.11": - version: 0.10.11 - resolution: "unzipper@npm:0.10.11" - dependencies: - big-integer: ^1.6.17 - binary: ~0.3.0 - bluebird: ~3.4.1 - buffer-indexof-polyfill: ~1.0.0 - duplexer2: ~0.1.4 - fstream: ^1.0.12 - graceful-fs: ^4.2.2 - listenercount: ~1.0.1 - readable-stream: ~2.3.6 - setimmediate: ~1.0.4 - checksum: 006cd43ec4d6df47d86aa6b15044a606f50cdcd6a3d6f96f64f54ca0b663c09abb221f76edca0e9592511036d37ea094b1d76ce92c5bf10d7c6eb56f0be678f8 - languageName: node - linkType: hard - "update-browserslist-db@npm:^1.0.5": version: 1.0.5 resolution: "update-browserslist-db@npm:1.0.5" @@ -17201,23 +12142,6 @@ __metadata: languageName: node linkType: hard -"url@npm:0.10.3": - version: 0.10.3 - resolution: "url@npm:0.10.3" - dependencies: - punycode: 1.3.2 - querystring: 0.2.0 - checksum: 7b83ddb106c27bf9bde8629ccbe8d26e9db789c8cda5aa7db72ca2c6f9b8a88a5adf206f3e10db78e6e2d042b327c45db34c7010c1bf0d9908936a17a2b57d05 - languageName: node - linkType: hard - -"urlsafe-base64@npm:^1.0.0, urlsafe-base64@npm:~1.0.0": - version: 1.0.0 - resolution: "urlsafe-base64@npm:1.0.0" - checksum: 41d28a337044e5ad287174e928227b025d03424c5cd316956fdcbd916fccdc70981fa9a67e77325c5250c8150ba90bca0de65e783aa6235567b7f820e1146cb6 - languageName: node - linkType: hard - "use@npm:^3.1.0": version: 3.1.1 resolution: "use@npm:3.1.1" @@ -17241,16 +12165,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:8.0.0": - version: 8.0.0 - resolution: "uuid@npm:8.0.0" - bin: - uuid: dist/bin/uuid - checksum: 56d4e23aa7ac26fa2db6bd1778db34cb8c9f5a10df1770a27167874bf6705fc8f14a4ac414af58a0d96c7653b2bd4848510b29d1c2ef8c91ccb17429c1872b5e - languageName: node - linkType: hard - -"uuid@npm:8.3.2, uuid@npm:^8.3.0, uuid@npm:^8.3.2": +"uuid@npm:8.3.2, uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" bin: @@ -17310,13 +12225,6 @@ __metadata: languageName: node linkType: hard -"vary@npm:^1.1.2": - version: 1.1.2 - resolution: "vary@npm:1.1.2" - checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b - languageName: node - linkType: hard - "vendors@npm:^1.0.0": version: 1.0.4 resolution: "vendors@npm:1.0.4" @@ -17430,13 +12338,6 @@ __metadata: languageName: node linkType: hard -"void-elements@npm:^3.1.0": - version: 3.1.0 - resolution: "void-elements@npm:3.1.0" - checksum: 0390f818107fa8fce55bb0a5c3f661056001c1d5a2a48c28d582d4d847347c2ab5b7f8272314cac58acf62345126b6b09bea623a185935f6b1c3bbce0dfd7f7f - languageName: node - linkType: hard - "vue-eslint-parser@npm:^9.0.1": version: 9.0.3 resolution: "vue-eslint-parser@npm:9.0.3" @@ -17505,15 +12406,6 @@ __metadata: languageName: node linkType: hard -"w3c-xmlserializer@npm:^3.0.0": - version: 3.0.0 - resolution: "w3c-xmlserializer@npm:3.0.0" - dependencies: - xml-name-validator: ^4.0.0 - checksum: 0af8589942eeb11c9fe29eb31a1a09f3d5dd136aea53a9848dfbabff79ac0dd26fe13eb54d330d5555fe27bb50b28dca0715e09f9cc2bfa7670ccc8b7f919ca2 - languageName: node - linkType: hard - "wait-on@npm:6.0.0": version: 6.0.0 resolution: "wait-on@npm:6.0.0" @@ -17538,29 +12430,6 @@ __metadata: languageName: node linkType: hard -"web-push@npm:3.5.0": - version: 3.5.0 - resolution: "web-push@npm:3.5.0" - dependencies: - asn1.js: ^5.3.0 - http_ece: 1.1.0 - https-proxy-agent: ^5.0.0 - jws: ^4.0.0 - minimist: ^1.2.5 - urlsafe-base64: ^1.0.0 - bin: - web-push: src/cli.js - checksum: 36085323e930a48b82c29466c80785e75fe725752cf10cba23ce3daec3f505bb5481fe769788a92cb4a07a0e9c632e50238cdb223e4d738acc55b7cf00d805d7 - languageName: node - linkType: hard - -"web-streams-polyfill@npm:^3.0.3": - version: 3.2.1 - resolution: "web-streams-polyfill@npm:3.2.1" - checksum: b119c78574b6d65935e35098c2afdcd752b84268e18746606af149e3c424e15621b6f1ff0b42b2676dc012fc4f0d313f964b41a4b5031e525faa03997457da02 - languageName: node - linkType: hard - "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -17582,13 +12451,6 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^7.0.0": - version: 7.0.0 - resolution: "webidl-conversions@npm:7.0.0" - checksum: f05588567a2a76428515333eff87200fae6c83c3948a7482ebb109562971e77ef6dc49749afa58abb993391227c5697b3ecca52018793e0cb4620a48f10bd21b - languageName: node - linkType: hard - "whatwg-encoding@npm:^1.0.5": version: 1.0.5 resolution: "whatwg-encoding@npm:1.0.5" @@ -17598,15 +12460,6 @@ __metadata: languageName: node linkType: hard -"whatwg-encoding@npm:^2.0.0": - version: 2.0.0 - resolution: "whatwg-encoding@npm:2.0.0" - dependencies: - iconv-lite: 0.6.3 - checksum: 7087810c410aa9b689cbd6af8773341a53cdc1f3aae2a882c163bd5522ec8ca4cdfc269aef417a5792f411807d5d77d50df4c24e3abb00bb60192858a40cc675 - languageName: node - linkType: hard - "whatwg-mimetype@npm:^2.3.0": version: 2.3.0 resolution: "whatwg-mimetype@npm:2.3.0" @@ -17614,23 +12467,6 @@ __metadata: languageName: node linkType: hard -"whatwg-mimetype@npm:^3.0.0": - version: 3.0.0 - resolution: "whatwg-mimetype@npm:3.0.0" - checksum: ce08bbb36b6aaf64f3a84da89707e3e6a31e5ab1c1a2379fd68df79ba712a4ab090904f0b50e6693b0dafc8e6343a6157e40bf18fdffd26e513cf95ee2a59824 - languageName: node - linkType: hard - -"whatwg-url@npm:^11.0.0": - version: 11.0.0 - resolution: "whatwg-url@npm:11.0.0" - dependencies: - tr46: ^3.0.0 - webidl-conversions: ^7.0.0 - checksum: ed4826aaa57e66bb3488a4b25c9cd476c46ba96052747388b5801f137dd740b73fde91ad207d96baf9f17fbcc80fc1a477ad65181b5eb5fa718d27c69501d7af - languageName: node - linkType: hard - "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" @@ -17679,14 +12515,7 @@ __metadata: languageName: node linkType: hard -"which-module@npm:^2.0.0": - version: 2.0.0 - resolution: "which-module@npm:2.0.0" - checksum: 809f7fd3dfcb2cdbe0180b60d68100c88785084f8f9492b0998c051d7a8efe56784492609d3f09ac161635b78ea29219eb1418a98c15ce87d085bce905705c9c - languageName: node - linkType: hard - -"which@npm:^1.1.1, which@npm:^1.2.14": +"which@npm:^1.2.14": version: 1.3.1 resolution: "which@npm:1.3.1" dependencies: @@ -17717,18 +12546,6 @@ __metadata: languageName: node linkType: hard -"with@npm:^7.0.0": - version: 7.0.2 - resolution: "with@npm:7.0.2" - dependencies: - "@babel/parser": ^7.9.6 - "@babel/types": ^7.9.6 - assert-never: ^1.2.1 - babel-walk: 3.0.0-canary-5 - checksum: a00fe87b736e434bd8b9d3e62ddcd664bde7d3990a011a0f1bdeb499db0d6c28e6d2ef921dcc47650b8d436eee55459bcae8fab4ce1ed89f4926ddda407ab755 - languageName: node - linkType: hard - "word-wrap@npm:^1.2.3, word-wrap@npm:~1.2.3": version: 1.2.3 resolution: "word-wrap@npm:1.2.3" @@ -17736,13 +12553,6 @@ __metadata: languageName: node linkType: hard -"workerpool@npm:6.2.1": - version: 6.2.1 - resolution: "workerpool@npm:6.2.1" - checksum: c2c6eebbc5225f10f758d599a5c016fa04798bcc44e4c1dffb34050cd361d7be2e97891aa44419e7afe647b1f767b1dc0b85a5e046c409d890163f655028b09d - languageName: node - linkType: hard - "wrap-ansi@npm:^2.0.0": version: 2.1.0 resolution: "wrap-ansi@npm:2.1.0" @@ -17794,21 +12604,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.8.0": - version: 8.8.0 - resolution: "ws@npm:8.8.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 6ceed1ca1cb800ef60c7fc8346c7d5d73d73be754228eb958765abf5d714519338efa20ffe674167039486eb3a813aae5a497f8d319e16b4d96216a31df5bd95 - languageName: node - linkType: hard - "ws@npm:^7.4.6": version: 7.5.9 resolution: "ws@npm:7.5.9" @@ -17824,39 +12619,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.8.0": - version: 8.8.1 - resolution: "ws@npm:8.8.1" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 2152cf862cae0693f3775bc688a6afb2e989d19d626d215e70f5fcd8eb55b1c3b0d3a6a4052905ec320e2d7734e20aeedbf9744496d62f15a26ad79cf4cf7dae - languageName: node - linkType: hard - -"xev@npm:3.0.2": - version: 3.0.2 - resolution: "xev@npm:3.0.2" - checksum: 28e97d4bb258aa5fcb16f56fc1afb3ba1869f3e32f699f003ca9149757d364cb79ae4b8511adb0dba9f94dbff1c7cc312ea6386d0a92baa429d242ec21d627b1 - languageName: node - linkType: hard - -"xml-js@npm:^1.6.11": - version: 1.6.11 - resolution: "xml-js@npm:1.6.11" - dependencies: - sax: ^1.2.4 - bin: - xml-js: ./bin/cli.js - checksum: 24a55479919413687105fc2d8ab05e613ebedb1c1bc12258a108e07cff5ef793779297db854800a4edf0281303ebd1f177bc4a588442f5344e62b3dddda26c2b - languageName: node - linkType: hard - "xml-name-validator@npm:^3.0.0": version: 3.0.0 resolution: "xml-name-validator@npm:3.0.0" @@ -17871,40 +12633,6 @@ __metadata: languageName: node linkType: hard -"xml2js@npm:0.4.19": - version: 0.4.19 - resolution: "xml2js@npm:0.4.19" - dependencies: - sax: ">=0.6.0" - xmlbuilder: ~9.0.1 - checksum: ca8b2fee430d450a18947786bfd7cd1a353ee00fc6fd550acbc8a8e65f1b4df5e9786fcb2990c1a5514ecd554d445fb74e1d716b3a4fcfffc10554aeb5db482b - languageName: node - linkType: hard - -"xml2js@npm:^0.4.19, xml2js@npm:^0.4.23": - version: 0.4.23 - resolution: "xml2js@npm:0.4.23" - dependencies: - sax: ">=0.6.0" - xmlbuilder: ~11.0.0 - checksum: ca0cf2dfbf6deeaae878a891c8fbc0db6fd04398087084edf143cdc83d0509ad0fe199b890f62f39c4415cf60268a27a6aed0d343f0658f8779bd7add690fa98 - languageName: node - linkType: hard - -"xmlbuilder@npm:~11.0.0": - version: 11.0.1 - resolution: "xmlbuilder@npm:11.0.1" - checksum: 7152695e16f1a9976658215abab27e55d08b1b97bca901d58b048d2b6e106b5af31efccbdecf9b07af37c8377d8e7e821b494af10b3a68b0ff4ae60331b415b0 - languageName: node - linkType: hard - -"xmlbuilder@npm:~9.0.1": - version: 9.0.7 - resolution: "xmlbuilder@npm:9.0.7" - checksum: 8193bb323806a002764f013bea0c6e9ff2dc26fd29109408761b16b59a8ad2214c2abe8e691755fd8b525586e3a0e1efeb92335947d7b0899032b779f1705a53 - languageName: node - linkType: hard - "xmlchars@npm:^2.2.0": version: 2.2.0 resolution: "xmlchars@npm:2.2.0" @@ -17912,7 +12640,7 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.0, xtend@npm:~4.0.0, xtend@npm:~4.0.1": +"xtend@npm:~4.0.0, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a @@ -17926,13 +12654,6 @@ __metadata: languageName: node linkType: hard -"y18n@npm:^4.0.0": - version: 4.0.3 - resolution: "y18n@npm:4.0.3" - checksum: 014dfcd9b5f4105c3bb397c1c8c6429a9df004aa560964fb36732bfb999bfe83d45ae40aeda5b55d21b1ee53d8291580a32a756a443e064317953f08025b1aa4 - languageName: node - linkType: hard - "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -17940,34 +12661,13 @@ __metadata: languageName: node linkType: hard -"yallist@npm:4.0.0, yallist@npm:^4.0.0": +"yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 languageName: node linkType: hard -"yallist@npm:^2.1.2": - version: 2.1.2 - resolution: "yallist@npm:2.1.2" - checksum: 9ba99409209f485b6fcb970330908a6d41fa1c933f75e08250316cce19383179a6b70a7e0721b89672ebb6199cc377bf3e432f55100da6a7d6e11902b0a642cb - languageName: node - linkType: hard - -"yaml-ast-parser@npm:0.0.43": - version: 0.0.43 - resolution: "yaml-ast-parser@npm:0.0.43" - checksum: fb5df4c067b6ccbd00953a46faf6ff27f0e290d623c712dc41f330251118f110e22cfd184bbff498bd969cbcda3cd27e0f9d0adb9e6d90eb60ccafc0d8e28077 - languageName: node - linkType: hard - -"yargs-parser@npm:20.2.4": - version: 20.2.4 - resolution: "yargs-parser@npm:20.2.4" - checksum: d251998a374b2743a20271c2fd752b9fbef24eb881d53a3b99a7caa5e8227fcafd9abf1f345ac5de46435821be25ec12189a11030c12ee6481fef6863ed8b924 - languageName: node - linkType: hard - "yargs-parser@npm:20.x, yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.3": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" @@ -17982,23 +12682,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^18.1.2": - version: 18.1.3 - resolution: "yargs-parser@npm:18.1.3" - dependencies: - camelcase: ^5.0.0 - decamelize: ^1.2.0 - checksum: 60e8c7d1b85814594d3719300ecad4e6ae3796748b0926137bfec1f3042581b8646d67e83c6fc80a692ef08b8390f21ddcacb9464476c39bbdf52e34961dd4d9 - languageName: node - linkType: hard - -"yargs-parser@npm:^21.0.0": - version: 21.1.1 - resolution: "yargs-parser@npm:21.1.1" - checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c - languageName: node - linkType: hard - "yargs-parser@npm:^5.0.0": version: 5.0.1 resolution: "yargs-parser@npm:5.0.1" @@ -18009,19 +12692,7 @@ __metadata: languageName: node linkType: hard -"yargs-unparser@npm:2.0.0": - version: 2.0.0 - resolution: "yargs-unparser@npm:2.0.0" - dependencies: - camelcase: ^6.0.0 - decamelize: ^4.0.0 - flat: ^5.0.2 - is-plain-obj: ^2.1.0 - checksum: 68f9a542c6927c3768c2f16c28f71b19008710abd6b8f8efbac6dcce26bbb68ab6503bed1d5994bdbc2df9a5c87c161110c1dfe04c6a3fe5c6ad1b0e15d9a8a3 - languageName: node - linkType: hard - -"yargs@npm:16.2.0, yargs@npm:^16.0.0, yargs@npm:^16.2.0": +"yargs@npm:^16.2.0": version: 16.2.0 resolution: "yargs@npm:16.2.0" dependencies: @@ -18036,40 +12707,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^15.3.1": - version: 15.4.1 - resolution: "yargs@npm:15.4.1" - dependencies: - cliui: ^6.0.0 - decamelize: ^1.2.0 - find-up: ^4.1.0 - get-caller-file: ^2.0.1 - require-directory: ^2.1.1 - require-main-filename: ^2.0.0 - set-blocking: ^2.0.0 - string-width: ^4.2.0 - which-module: ^2.0.0 - y18n: ^4.0.0 - yargs-parser: ^18.1.2 - checksum: 40b974f508d8aed28598087720e086ecd32a5fd3e945e95ea4457da04ee9bdb8bdd17fd91acff36dc5b7f0595a735929c514c40c402416bbb87c03f6fb782373 - languageName: node - linkType: hard - -"yargs@npm:^17.3.1": - version: 17.5.1 - resolution: "yargs@npm:17.5.1" - dependencies: - cliui: ^7.0.2 - escalade: ^3.1.1 - get-caller-file: ^2.0.5 - require-directory: ^2.1.1 - string-width: ^4.2.3 - y18n: ^5.0.5 - yargs-parser: ^21.0.0 - checksum: 00d58a2c052937fa044834313f07910fd0a115dec5ee35919e857eeee3736b21a4eafa8264535800ba8bac312991ce785ecb8a51f4d2cc8c4676d865af1cfbde - languageName: node - linkType: hard - "yargs@npm:^7.1.0": version: 7.1.0 resolution: "yargs@npm:7.1.0" @@ -18101,13 +12738,6 @@ __metadata: languageName: node linkType: hard -"ylru@npm:^1.2.0": - version: 1.3.2 - resolution: "ylru@npm:1.3.2" - checksum: b6bb3931144424114f2350c072cfeb180f205add93509c605ae025cbed8059846f8a5767655feeeab890d288b5b4c4b36f5d5d867ee4e6946c16bcc7ec3ddaee - languageName: node - linkType: hard - "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1" @@ -18138,14 +12768,3 @@ __metadata: checksum: eb6c2c3c2878c4c333fd3755703616c8f846158da0154f60f81f188c3c4ce7ad5b8ff5ce570d6372d4803fb5c8c9d97b4e54becdd5a832b1a31240d149b87191 languageName: node linkType: hard - -"zip-stream@npm:^4.1.0": - version: 4.1.0 - resolution: "zip-stream@npm:4.1.0" - dependencies: - archiver-utils: ^2.1.0 - compress-commons: ^4.1.0 - readable-stream: ^3.6.0 - checksum: 4a73da856738b0634700b52f4ab3fe0bf0a532bea6820ad962d0bda0163d2d5525df4859f89a7238e204a378384e12551985049790c1894c3ac191866e85887f - languageName: node - linkType: hard

Q*ie`|Cj&2i?YTA`Sqeun>bF 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 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 e69de29bb..000000000 diff --git a/packages/backend/test/resources/image.svg b/packages/backend/test/resources/image.svg deleted file mode 100644 index 1e2bf5b5bbaa6dc883d747046122e93222a96420..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 505 zcmcJM!EVAZ42JJ}3M-t`IL1r8i6RX|Z+nQT+W@Hpv{ns|j~zI1VV50ZD~^Brv$Ghc zEu4S)!_c_hIG$cZ$T?SeULAk8fe1e;}8(&IoWZ`#pz?*!S}xJ8#ilP z#y58G*|_&e0)m~73D}b=ie;H%>JY>W#H8_R4g##Uq?p&MMTrMOPpl7KRa&P*6^Ruz znU{4;48jnZp-ymZ=)yhHN}+B@FZAC?WK$cn6r|Gd9*I#lZF@zs3I4XEwN{Qb0jm=P X6~;O}UJ>f9t%u>1W=3Nlx_r6-BJFT- 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 90971215a64a6ad9e0e48aed2d72c98a881dcf70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmcJM!ES>v42JK03d=hOoYYI*iBPT8xa}b(N+}Y9sxau|&!Ic@vR!tFtvLSe&rY_x zeYP-F*P-dVfHon7sw?|r)71gL#*-1;<*RI%YS7GX2zVaH%S)Qh^PJ4S@_x&v(0fBq=@nD`^KE^Ygnzjx2R1{3FjBpJ2Zk>QoX{-k}P8Ew~nXE4f#0Xfg z@nj7GtaeF>Uav9gcyMGK>w{61)+tFv5(`F|k%RRN!eE(U1{+L# AmH+?% diff --git a/packages/backend/test/services/blocking.ts b/packages/backend/test/services/blocking.ts deleted file mode 100644 index 41e5ef5be..000000000 --- a/packages/backend/test/services/blocking.ts +++ /dev/null @@ -1,58 +0,0 @@ -process.env.NODE_ENV = 'test'; - -import * as assert from 'assert'; -import * as childProcess from 'child_process'; -import * as sinon from 'sinon'; -import { async, signup, startServer, shutdownServer, initTestDb } from '../utils.js'; - -describe('Creating a block activity', () => { - let p: childProcess.ChildProcess; - - // alice blocks bob - let alice: any; - let bob: any; - let carol: any; - - before(async () => { - await initTestDb(); - p = await startServer(); - alice = await signup({ username: 'alice' }); - bob = await signup({ username: 'bob' }); - carol = await signup({ username: 'carol' }); - bob.host = 'http://remote'; - carol.host = 'http://remote'; - }); - - beforeEach(() => { - sinon.restore(); - }); - - after(async () => { - await shutdownServer(p); - }); - - it('Should federate blocks normally', async(async () => { - const createBlock = (await import('../../src/services/blocking/create')).default; - const deleteBlock = (await import('../../src/services/blocking/delete')).default; - - const queues = await import('../../src/queue/index'); - const spy = sinon.spy(queues, 'deliver'); - await createBlock(alice, bob); - assert(spy.calledOnce); - await deleteBlock(alice, bob); - assert(spy.calledTwice); - })); - - it('Should not federate blocks if federateBlocks is false', async () => { - const createBlock = (await import('../../src/services/blocking/create')).default; - const deleteBlock = (await import('../../src/services/blocking/delete')).default; - - alice.federateBlocks = true; - - const queues = await import('../../src/queue/index'); - const spy = sinon.spy(queues, 'deliver'); - await createBlock(alice, carol); - await deleteBlock(alice, carol); - assert(spy.notCalled); - }); -}); diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.ts deleted file mode 100644 index 621d07f9c..000000000 --- a/packages/backend/test/streaming.ts +++ /dev/null @@ -1,545 +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('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 cd3e51939..000000000 --- a/packages/backend/test/thread-mute.ts +++ /dev/null @@ -1,103 +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 3f9020d46..000000000 --- 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": "es2017", - "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 4447754d6..000000000 --- a/packages/backend/test/user-notes.ts +++ /dev/null @@ -1,61 +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 a366547e6..000000000 --- a/packages/backend/test/utils.ts +++ /dev/null @@ -1,324 +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 foundkey from 'foundkey-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?: foundkey.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 193d1a1c2..000000000 --- a/packages/backend/tsconfig.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "compilerOptions": { - "incremental": true, - "allowJs": true, - "noEmitOnError": false, - "noImplicitAny": true, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": false, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": false, - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "Node16", - "allowSyntheticDefaultImports": true, - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": true, - "strictPropertyInitialization": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "resolveJsonModule": true, - "isolatedModules": true, - "rootDir": "./src", - "baseUrl": "./", - "paths": { - "@/*": [ - "./src/*" - ] - }, - "outDir": "./built", - "types": [ - "node" - ], - "typeRoots": [ - "./node_modules/@types", - "./src/@types" - ], - "lib": [ - "ES2022" - ] - }, - "compileOnSave": false, - "include": [ - "./src/**/*.ts" - ], -} diff --git a/packages/backend/watch.mjs b/packages/backend/watch.mjs deleted file mode 100644 index 9c9d2dbd8..000000000 --- a/packages/backend/watch.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { execa } from 'execa'; - -(async () => { - // なぜかchokidarが動かない影響で、watchされない - /* - execa('tsc-alias', ['-w', '-p', 'tsconfig.json'], { - stdout: process.stdout, - stderr: process.stderr, - }); - */ - - setInterval(() => { - execa('tsc-alias', ['-p', 'tsconfig.json'], { - stdout: process.stdout, - stderr: process.stderr, - }); - }, 3000); - - execa('tsc', ['-w', '-p', 'tsconfig.json'], { - stdout: process.stdout, - stderr: process.stderr, - }); -})(); diff --git a/packages/client/index.html b/packages/client/index.html new file mode 100644 index 000000000..f8d84fc34 --- /dev/null +++ b/packages/client/index.html @@ -0,0 +1,12 @@ + + + + + + lost-fe + + +