summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
authorJulia <julia@insertdomain.name>2025-03-02 19:54:32 +0000
committerJulia <julia@insertdomain.name>2025-03-02 19:54:32 +0000
commit9e13c375c5ef4103ad5ee87fea583b154e9e16f3 (patch)
treefe9e7b1a474e22fb0c37bd68cfd260f7ba39be74 /packages/backend/src/server/api
parentmerge: pin corepack version (!885) (diff)
parentbump version (diff)
downloadsharkey-9e13c375c5ef4103ad5ee87fea583b154e9e16f3.tar.gz
sharkey-9e13c375c5ef4103ad5ee87fea583b154e9e16f3.tar.bz2
sharkey-9e13c375c5ef4103ad5ee87fea583b154e9e16f3.zip
merge: 2025.2.2 (!927)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/927 Approved-by: Marie <github@yuugi.dev> Approved-by: Julia <julia@insertdomain.name>
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts39
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts1614
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts13
-rw-r--r--packages/backend/src/server/api/SigninWithPasskeyApiService.ts2
-rw-r--r--packages/backend/src/server/api/SignupApiService.ts3
-rw-r--r--packages/backend/src/server/api/SkRateLimiterService.ts253
-rw-r--r--packages/backend/src/server/api/StreamingApiServerService.ts18
-rw-r--r--packages/backend/src/server/api/endpoint-list.ts421
-rw-r--r--packages/backend/src/server/api/endpoints.ts818
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/create.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/captcha/current.ts77
-rw-r--r--packages/backend/src/server/api/endpoints/admin/captcha/save.ts129
-rw-r--r--packages/backend/src/server/api/endpoints/admin/cw-user.ts67
-rw-r--r--packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/clean-remote-files.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/copy.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts14
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/delete-all-files.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts33
-rw-r--r--packages/backend/src/server/api/endpoints/admin/invite/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/nsfw-user.ts23
-rw-r--r--packages/backend/src/server/api/endpoints/admin/promo/create.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/admin/reject-quotes.ts63
-rw-r--r--packages/backend/src/server/api/endpoints/admin/relays/add.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/relays/remove.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/silence-user.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts21
-rw-r--r--packages/backend/src/server/api/endpoints/admin/unsilence-user.ts22
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/ap/show.ts92
-rw-r--r--packages/backend/src/server/api/endpoints/channels/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/delete.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/update.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/drive/folders/update.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/emojis.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/federation/update-remote-user.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/flash/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/gallery/posts/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/i.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/password-less.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/unregister.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/update-key.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/apps.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/change-password.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/claim-achievement.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/delete-account.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/regenerate-token.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/mute/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/notes/edit.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/notes/favorites/delete.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/like.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/create.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/schedule/list.ts31
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/pages/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/pages/delete.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/pages/update.ts25
-rw-r--r--packages/backend/src/server/api/endpoints/server-info.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/users/relation.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/users/show.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts126
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonApiServerService.ts850
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonDataService.ts84
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonLogger.ts39
-rw-r--r--packages/backend/src/server/api/mastodon/converters.ts241
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/account.ts294
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/auth.ts76
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/filter.ts93
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/meta.ts8
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/notifications.ts85
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/search.ts138
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/status.ts528
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/timeline.ts307
-rw-r--r--packages/backend/src/server/api/mastodon/timelineArgs.ts47
-rw-r--r--packages/backend/src/server/api/openapi/schemas.ts4
-rw-r--r--packages/backend/src/server/api/stream/Connection.ts8
-rw-r--r--packages/backend/src/server/api/stream/channel.ts61
-rw-r--r--packages/backend/src/server/api/stream/channels/admin.ts3
-rw-r--r--packages/backend/src/server/api/stream/channels/antenna.ts4
-rw-r--r--packages/backend/src/server/api/stream/channels/bubble-timeline.ts20
-rw-r--r--packages/backend/src/server/api/stream/channels/channel.ts17
-rw-r--r--packages/backend/src/server/api/stream/channels/drive.ts3
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts16
-rw-r--r--packages/backend/src/server/api/stream/channels/hashtag.ts17
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts16
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts17
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts16
-rw-r--r--packages/backend/src/server/api/stream/channels/main.ts4
-rw-r--r--packages/backend/src/server/api/stream/channels/queue-stats.ts7
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi-game.ts6
-rw-r--r--packages/backend/src/server/api/stream/channels/reversi.ts6
-rw-r--r--packages/backend/src/server/api/stream/channels/role-timeline.ts11
-rw-r--r--packages/backend/src/server/api/stream/channels/server-stats.ts7
-rw-r--r--packages/backend/src/server/api/stream/channels/user-list.ts16
116 files changed, 3063 insertions, 4251 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 03f25a51fe..5ce358d68f 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -19,7 +19,7 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { Config } from '@/config.js';
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { ApiError } from './error.js';
import { ApiLoggerService } from './ApiLoggerService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
@@ -313,35 +313,30 @@ export class ApiCallService implements OnApplicationShutdown {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (endpointLimit) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
- let limitActor: string;
+ let limitActor: string | MiLocalUser;
if (user) {
- limitActor = user.id;
+ limitActor = user;
} else {
limitActor = getIpHash(request.ip);
}
- // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
- const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
+ const limit = {
+ key: ep.name,
+ ...endpointLimit,
+ };
- if (factor > 0) {
- const limit = {
- key: ep.name,
- ...endpointLimit,
- };
+ // Rate limit
+ const info = await this.rateLimiterService.limit(limit, limitActor);
- // Rate limit
- const info = await this.rateLimiterService.limit(limit, limitActor, factor);
+ sendRateLimitHeaders(reply, info);
- sendRateLimitHeaders(reply, info);
-
- if (info.blocked) {
- throw new ApiError({
- message: 'Rate limit exceeded. Please try again later.',
- code: 'RATE_LIMIT_EXCEEDED',
- id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
- httpStatusCode: 429,
- }, info);
- }
+ if (info.blocked) {
+ throw new ApiError({
+ message: 'Rate limit exceeded. Please try again later.',
+ code: 'RATE_LIMIT_EXCEEDED',
+ id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+ httpStatusCode: 429,
+ }, info);
}
}
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index e319d6e0a4..9cfb2f0ac0 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -6,816 +6,13 @@
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
-import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
-import * as ep___admin_abuseReport_notificationRecipient_show from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
-import * as ep___admin_abuseReport_notificationRecipient_create from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
-import * as ep___admin_abuseReport_notificationRecipient_update from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
-import * as ep___admin_abuseReport_notificationRecipient_delete from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
-import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
-import * as ep___admin_meta from './endpoints/admin/meta.js';
-import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
-import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
-import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
-import * as ep___admin_ad_create from './endpoints/admin/ad/create.js';
-import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js';
-import * as ep___admin_ad_list from './endpoints/admin/ad/list.js';
-import * as ep___admin_ad_update from './endpoints/admin/ad/update.js';
-import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js';
-import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
-import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
-import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
-import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
-import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
-import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
-import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
-import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
-import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
-import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
-import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
-import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
-import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
-import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js';
-import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js';
-import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js';
-import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js';
-import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js';
-import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js';
-import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js';
-import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js';
-import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
-import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
-import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
-import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
-import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
-import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
-import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
-import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
-import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
-import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
-import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
-import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
-import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
-import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
-import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
-import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
-import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
-import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
-import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js';
-import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.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_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
-import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-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_nsfwUser from './endpoints/admin/nsfw-user.js';
-import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
-import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
-import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
-import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
-import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
-import * as ep___admin_declineUser from './endpoints/admin/decline-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_deleteAccount from './endpoints/admin/delete-account.js';
-import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
-import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
-import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
-import * as ep___admin_roles_list from './endpoints/admin/roles/list.js';
-import * as ep___admin_roles_show from './endpoints/admin/roles/show.js';
-import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
-import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
-import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
-import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
-import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
-import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
-import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
-import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
-import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
-import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
-import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
-import * as ep___announcements from './endpoints/announcements.js';
-import * as ep___announcements_show from './endpoints/announcements/show.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_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___channels_favorite from './endpoints/channels/favorite.js';
-import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
-import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
-import * as ep___channels_search from './endpoints/channels/search.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_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_pv from './endpoints/charts/user/pv.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___clips_favorite from './endpoints/clips/favorite.js';
-import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
-import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.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_update from './endpoints/following/update.js';
-import * as ep___following_update_all from './endpoints/following/update-all.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_sent from './endpoints/following/requests/sent.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___getAvatarDecorations from './endpoints/get-avatar-decorations.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_updateKey from './endpoints/i/2fa/update-key.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_claimAchievement from './endpoints/i/claim-achievement.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_exportData from './endpoints/i/export-data.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_exportClips from './endpoints/i/export-clips.js';
-import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
-import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
-import * as ep___i_exportAntennas from './endpoints/i/export-antennas.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_importBlocking from './endpoints/i/import-blocking.js';
-import * as ep___i_importFollowing from './endpoints/i/import-following.js';
-import * as ep___i_importNotes from './endpoints/i/import-notes.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_importAntennas from './endpoints/i/import-antennas.js';
-import * as ep___i_notifications from './endpoints/i/notifications.js';
-import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.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_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_getUnsecure from './endpoints/i/registry/get-unsecure.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_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.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_move from './endpoints/i/move.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___i_webhooks_test from './endpoints/i/webhooks/test.js';
-import * as ep___invite_create from './endpoints/invite/create.js';
-import * as ep___invite_delete from './endpoints/invite/delete.js';
-import * as ep___invite_list from './endpoints/invite/list.js';
-import * as ep___invite_limit from './endpoints/invite/limit.js';
-import * as ep___meta from './endpoints/meta.js';
-import * as ep___emojis from './endpoints/emojis.js';
-import * as ep___emoji from './endpoints/emoji.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___renoteMute_create from './endpoints/renote-mute/create.js';
-import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
-import * as ep___renoteMute_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_following from './endpoints/notes/following.js';
-import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
-import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-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_polls_refresh from './endpoints/notes/polls/refresh.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_like from './endpoints/notes/like.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_edit from './endpoints/notes/edit.js';
-import * as ep___notes_versions from './endpoints/notes/versions.js';
-import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js';
-import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js';
-import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.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___notifications_create from './endpoints/notifications/create.js';
-import * as ep___notifications_flush from './endpoints/notifications/flush.js';
-import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
-import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.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___flash_create from './endpoints/flash/create.js';
-import * as ep___flash_delete from './endpoints/flash/delete.js';
-import * as ep___flash_featured from './endpoints/flash/featured.js';
-import * as ep___flash_like from './endpoints/flash/like.js';
-import * as ep___flash_show from './endpoints/flash/show.js';
-import * as ep___flash_unlike from './endpoints/flash/unlike.js';
-import * as ep___flash_update from './endpoints/flash/update.js';
-import * as ep___flash_my from './endpoints/flash/my.js';
-import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
-import * as ep___ping from './endpoints/ping.js';
-import * as ep___pinnedUsers from './endpoints/pinned-users.js';
-import * as ep___promo_read from './endpoints/promo/read.js';
-import * as ep___roles_list from './endpoints/roles/list.js';
-import * as ep___roles_show from './endpoints/roles/show.js';
-import * as ep___roles_users from './endpoints/roles/users.js';
-import * as ep___roles_notes from './endpoints/roles/notes.js';
-import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
-import * as ep___resetDb from './endpoints/reset-db.js';
-import * as ep___resetPassword from './endpoints/reset-password.js';
-import * as ep___serverInfo from './endpoints/server-info.js';
-import * as ep___stats from './endpoints/stats.js';
-import * as ep___sw_show_registration from './endpoints/sw/show-registration.js';
-import * as ep___sw_update_registration from './endpoints/sw/update-registration.js';
-import * as ep___sw_register from './endpoints/sw/register.js';
-import * as ep___sw_unregister from './endpoints/sw/unregister.js';
-import * as ep___test from './endpoints/test.js';
-import * as ep___username_available from './endpoints/username/available.js';
-import * as ep___users from './endpoints/users.js';
-import * as ep___users_clips from './endpoints/users/clips.js';
-import * as ep___users_followers from './endpoints/users/followers.js';
-import * as ep___users_following from './endpoints/users/following.js';
-import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
-import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
-import * as ep___users_featuredNotes from './endpoints/users/featured-notes.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_lists_favorite from './endpoints/users/lists/favorite.js';
-import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
-import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
-import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
-import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.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_flashs from './endpoints/users/flashs.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_achievements from './endpoints/users/achievements.js';
-import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
-import * as ep___fetchRss from './endpoints/fetch-rss.js';
-import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
-import * as ep___retention from './endpoints/retention.js';
-import * as ep___sponsors from './endpoints/sponsors.js';
-import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
-import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
-import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js';
-import * as ep___reversi_games from './endpoints/reversi/games.js';
-import * as ep___reversi_match from './endpoints/reversi/match.js';
-import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
-import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
-import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
-import * as ep___reversi_verify from './endpoints/reversi/verify.js';
+import * as endpointsObject from './endpoint-list.js';
import { GetterService } from './GetterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
import type { Provider } from '@nestjs/common';
-const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default };
-const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
-const $admin_abuseReport_notificationRecipient_list: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/list', useClass: ep___admin_abuseReport_notificationRecipient_list.default };
-const $admin_abuseReport_notificationRecipient_show: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/show', useClass: ep___admin_abuseReport_notificationRecipient_show.default };
-const $admin_abuseReport_notificationRecipient_create: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/create', useClass: ep___admin_abuseReport_notificationRecipient_create.default };
-const $admin_abuseReport_notificationRecipient_update: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/update', useClass: ep___admin_abuseReport_notificationRecipient_update.default };
-const $admin_abuseReport_notificationRecipient_delete: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/delete', useClass: ep___admin_abuseReport_notificationRecipient_delete.default };
-const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
-const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default };
-const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default };
-const $admin_ad_create: Provider = { provide: 'ep:admin/ad/create', useClass: ep___admin_ad_create.default };
-const $admin_ad_delete: Provider = { provide: 'ep:admin/ad/delete', useClass: ep___admin_ad_delete.default };
-const $admin_ad_list: Provider = { provide: 'ep:admin/ad/list', useClass: ep___admin_ad_list.default };
-const $admin_ad_update: Provider = { provide: 'ep:admin/ad/update', useClass: ep___admin_ad_update.default };
-const $admin_announcements_create: Provider = { provide: 'ep:admin/announcements/create', useClass: ep___admin_announcements_create.default };
-const $admin_announcements_delete: Provider = { provide: 'ep:admin/announcements/delete', useClass: ep___admin_announcements_delete.default };
-const $admin_announcements_list: Provider = { provide: 'ep:admin/announcements/list', useClass: ep___admin_announcements_list.default };
-const $admin_announcements_update: Provider = { provide: 'ep:admin/announcements/update', useClass: ep___admin_announcements_update.default };
-const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-decorations/create', useClass: ep___admin_avatarDecorations_create.default };
-const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default };
-const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
-const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
-const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
-const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
-const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
-const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
-const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
-const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
-const $admin_drive_showFile: Provider = { provide: 'ep:admin/drive/show-file', useClass: ep___admin_drive_showFile.default };
-const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-aliases-bulk', useClass: ep___admin_emoji_addAliasesBulk.default };
-const $admin_emoji_add: Provider = { provide: 'ep:admin/emoji/add', useClass: ep___admin_emoji_add.default };
-const $admin_emoji_copy: Provider = { provide: 'ep:admin/emoji/copy', useClass: ep___admin_emoji_copy.default };
-const $admin_emoji_deleteBulk: Provider = { provide: 'ep:admin/emoji/delete-bulk', useClass: ep___admin_emoji_deleteBulk.default };
-const $admin_emoji_delete: Provider = { provide: 'ep:admin/emoji/delete', useClass: ep___admin_emoji_delete.default };
-const $admin_emoji_importZip: Provider = { provide: 'ep:admin/emoji/import-zip', useClass: ep___admin_emoji_importZip.default };
-const $admin_emoji_listRemote: Provider = { provide: 'ep:admin/emoji/list-remote', useClass: ep___admin_emoji_listRemote.default };
-const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass: ep___admin_emoji_list.default };
-const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default };
-const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default };
-const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default };
-const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default };
-const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default };
-const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default };
-const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default };
-const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default };
-const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federation/update-instance', useClass: ep___admin_federation_updateInstance.default };
-const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default };
-const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
-const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
-const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default };
-const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default };
-const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
-const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
-const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
-const $admin_queue_inboxDelayed: Provider = { provide: 'ep:admin/queue/inbox-delayed', useClass: ep___admin_queue_inboxDelayed.default };
-const $admin_queue_promote: Provider = { provide: 'ep:admin/queue/promote', useClass: ep___admin_queue_promote.default };
-const $admin_queue_stats: Provider = { provide: 'ep:admin/queue/stats', useClass: ep___admin_queue_stats.default };
-const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass: ep___admin_relays_add.default };
-const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default };
-const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
-const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
-const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
-const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
-const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
-const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
-const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
-const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
-const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default };
-const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default };
-const $admin_nsfwUser: Provider = { provide: 'ep:admin/nsfw-user', useClass: ep___admin_nsfwUser.default };
-const $admin_unnsfwUser: Provider = { provide: 'ep:admin/unnsfw-user', useClass: ep___admin_unnsfwUser.default };
-const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClass: ep___admin_silenceUser.default };
-const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default };
-const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
-const $admin_approveUser: Provider = { provide: 'ep:admin/approve-user', useClass: ep___admin_approveUser.default };
-const $admin_declineUser: Provider = { provide: 'ep:admin/decline-user', useClass: ep___admin_declineUser.default };
-const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
-const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
-const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default };
-const $admin_updateUserNote: Provider = { provide: 'ep:admin/update-user-note', useClass: ep___admin_updateUserNote.default };
-const $admin_roles_create: Provider = { provide: 'ep:admin/roles/create', useClass: ep___admin_roles_create.default };
-const $admin_roles_delete: Provider = { provide: 'ep:admin/roles/delete', useClass: ep___admin_roles_delete.default };
-const $admin_roles_list: Provider = { provide: 'ep:admin/roles/list', useClass: ep___admin_roles_list.default };
-const $admin_roles_show: Provider = { provide: 'ep:admin/roles/show', useClass: ep___admin_roles_show.default };
-const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useClass: ep___admin_roles_update.default };
-const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default };
-const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
-const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
-const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
-const $admin_systemWebhook_create: Provider = { provide: 'ep:admin/system-webhook/create', useClass: ep___admin_systemWebhook_create.default };
-const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhook/delete', useClass: ep___admin_systemWebhook_delete.default };
-const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
-const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
-const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
-const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default };
-const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
-const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
-const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
-const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
-const $antennas_list: Provider = { provide: 'ep:antennas/list', useClass: ep___antennas_list.default };
-const $antennas_notes: Provider = { provide: 'ep:antennas/notes', useClass: ep___antennas_notes.default };
-const $antennas_show: Provider = { provide: 'ep:antennas/show', useClass: ep___antennas_show.default };
-const $antennas_update: Provider = { provide: 'ep:antennas/update', useClass: ep___antennas_update.default };
-const $ap_get: Provider = { provide: 'ep:ap/get', useClass: ep___ap_get.default };
-const $ap_show: Provider = { provide: 'ep:ap/show', useClass: ep___ap_show.default };
-const $app_create: Provider = { provide: 'ep:app/create', useClass: ep___app_create.default };
-const $app_show: Provider = { provide: 'ep:app/show', useClass: ep___app_show.default };
-const $auth_accept: Provider = { provide: 'ep:auth/accept', useClass: ep___auth_accept.default };
-const $auth_session_generate: Provider = { provide: 'ep:auth/session/generate', useClass: ep___auth_session_generate.default };
-const $auth_session_show: Provider = { provide: 'ep:auth/session/show', useClass: ep___auth_session_show.default };
-const $auth_session_userkey: Provider = { provide: 'ep:auth/session/userkey', useClass: ep___auth_session_userkey.default };
-const $blocking_create: Provider = { provide: 'ep:blocking/create', useClass: ep___blocking_create.default };
-const $blocking_delete: Provider = { provide: 'ep:blocking/delete', useClass: ep___blocking_delete.default };
-const $blocking_list: Provider = { provide: 'ep:blocking/list', useClass: ep___blocking_list.default };
-const $channels_create: Provider = { provide: 'ep:channels/create', useClass: ep___channels_create.default };
-const $channels_featured: Provider = { provide: 'ep:channels/featured', useClass: ep___channels_featured.default };
-const $channels_follow: Provider = { provide: 'ep:channels/follow', useClass: ep___channels_follow.default };
-const $channels_followed: Provider = { provide: 'ep:channels/followed', useClass: ep___channels_followed.default };
-const $channels_owned: Provider = { provide: 'ep:channels/owned', useClass: ep___channels_owned.default };
-const $channels_show: Provider = { provide: 'ep:channels/show', useClass: ep___channels_show.default };
-const $channels_timeline: Provider = { provide: 'ep:channels/timeline', useClass: ep___channels_timeline.default };
-const $channels_unfollow: Provider = { provide: 'ep:channels/unfollow', useClass: ep___channels_unfollow.default };
-const $channels_update: Provider = { provide: 'ep:channels/update', useClass: ep___channels_update.default };
-const $channels_favorite: Provider = { provide: 'ep:channels/favorite', useClass: ep___channels_favorite.default };
-const $channels_unfavorite: Provider = { provide: 'ep:channels/unfavorite', useClass: ep___channels_unfavorite.default };
-const $channels_myFavorites: Provider = { provide: 'ep:channels/my-favorites', useClass: ep___channels_myFavorites.default };
-const $channels_search: Provider = { provide: 'ep:channels/search', useClass: ep___channels_search.default };
-const $charts_activeUsers: Provider = { provide: 'ep:charts/active-users', useClass: ep___charts_activeUsers.default };
-const $charts_apRequest: Provider = { provide: 'ep:charts/ap-request', useClass: ep___charts_apRequest.default };
-const $charts_drive: Provider = { provide: 'ep:charts/drive', useClass: ep___charts_drive.default };
-const $charts_federation: Provider = { provide: 'ep:charts/federation', useClass: ep___charts_federation.default };
-const $charts_instance: Provider = { provide: 'ep:charts/instance', useClass: ep___charts_instance.default };
-const $charts_notes: Provider = { provide: 'ep:charts/notes', useClass: ep___charts_notes.default };
-const $charts_user_drive: Provider = { provide: 'ep:charts/user/drive', useClass: ep___charts_user_drive.default };
-const $charts_user_following: Provider = { provide: 'ep:charts/user/following', useClass: ep___charts_user_following.default };
-const $charts_user_notes: Provider = { provide: 'ep:charts/user/notes', useClass: ep___charts_user_notes.default };
-const $charts_user_pv: Provider = { provide: 'ep:charts/user/pv', useClass: ep___charts_user_pv.default };
-const $charts_user_reactions: Provider = { provide: 'ep:charts/user/reactions', useClass: ep___charts_user_reactions.default };
-const $charts_users: Provider = { provide: 'ep:charts/users', useClass: ep___charts_users.default };
-const $clips_addNote: Provider = { provide: 'ep:clips/add-note', useClass: ep___clips_addNote.default };
-const $clips_removeNote: Provider = { provide: 'ep:clips/remove-note', useClass: ep___clips_removeNote.default };
-const $clips_create: Provider = { provide: 'ep:clips/create', useClass: ep___clips_create.default };
-const $clips_delete: Provider = { provide: 'ep:clips/delete', useClass: ep___clips_delete.default };
-const $clips_list: Provider = { provide: 'ep:clips/list', useClass: ep___clips_list.default };
-const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default };
-const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default };
-const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default };
-const $clips_favorite: Provider = { provide: 'ep:clips/favorite', useClass: ep___clips_favorite.default };
-const $clips_unfavorite: Provider = { provide: 'ep:clips/unfavorite', useClass: ep___clips_unfavorite.default };
-const $clips_myFavorites: Provider = { provide: 'ep:clips/my-favorites', useClass: ep___clips_myFavorites.default };
-const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default };
-const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default };
-const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default };
-const $drive_files_checkExistence: Provider = { provide: 'ep:drive/files/check-existence', useClass: ep___drive_files_checkExistence.default };
-const $drive_files_create: Provider = { provide: 'ep:drive/files/create', useClass: ep___drive_files_create.default };
-const $drive_files_delete: Provider = { provide: 'ep:drive/files/delete', useClass: ep___drive_files_delete.default };
-const $drive_files_findByHash: Provider = { provide: 'ep:drive/files/find-by-hash', useClass: ep___drive_files_findByHash.default };
-const $drive_files_find: Provider = { provide: 'ep:drive/files/find', useClass: ep___drive_files_find.default };
-const $drive_files_show: Provider = { provide: 'ep:drive/files/show', useClass: ep___drive_files_show.default };
-const $drive_files_update: Provider = { provide: 'ep:drive/files/update', useClass: ep___drive_files_update.default };
-const $drive_files_uploadFromUrl: Provider = { provide: 'ep:drive/files/upload-from-url', useClass: ep___drive_files_uploadFromUrl.default };
-const $drive_folders: Provider = { provide: 'ep:drive/folders', useClass: ep___drive_folders.default };
-const $drive_folders_create: Provider = { provide: 'ep:drive/folders/create', useClass: ep___drive_folders_create.default };
-const $drive_folders_delete: Provider = { provide: 'ep:drive/folders/delete', useClass: ep___drive_folders_delete.default };
-const $drive_folders_find: Provider = { provide: 'ep:drive/folders/find', useClass: ep___drive_folders_find.default };
-const $drive_folders_show: Provider = { provide: 'ep:drive/folders/show', useClass: ep___drive_folders_show.default };
-const $drive_folders_update: Provider = { provide: 'ep:drive/folders/update', useClass: ep___drive_folders_update.default };
-const $drive_stream: Provider = { provide: 'ep:drive/stream', useClass: ep___drive_stream.default };
-const $emailAddress_available: Provider = { provide: 'ep:email-address/available', useClass: ep___emailAddress_available.default };
-const $endpoint: Provider = { provide: 'ep:endpoint', useClass: ep___endpoint.default };
-const $endpoints: Provider = { provide: 'ep:endpoints', useClass: ep___endpoints.default };
-const $exportCustomEmojis: Provider = { provide: 'ep:export-custom-emojis', useClass: ep___exportCustomEmojis.default };
-const $federation_followers: Provider = { provide: 'ep:federation/followers', useClass: ep___federation_followers.default };
-const $federation_following: Provider = { provide: 'ep:federation/following', useClass: ep___federation_following.default };
-const $federation_instances: Provider = { provide: 'ep:federation/instances', useClass: ep___federation_instances.default };
-const $federation_showInstance: Provider = { provide: 'ep:federation/show-instance', useClass: ep___federation_showInstance.default };
-const $federation_updateRemoteUser: Provider = { provide: 'ep:federation/update-remote-user', useClass: ep___federation_updateRemoteUser.default };
-const $federation_users: Provider = { provide: 'ep:federation/users', useClass: ep___federation_users.default };
-const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default };
-const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
-const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
-const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default };
-const $following_update_all: Provider = { provide: 'ep:following/update-all', useClass: ep___following_update_all.default };
-const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
-const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
-const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
-const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default };
-const $following_requests_sent: Provider = { provide: 'ep:following/requests/sent', useClass: ep___following_requests_sent.default };
-const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default };
-const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default };
-const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default };
-const $gallery_posts: Provider = { provide: 'ep:gallery/posts', useClass: ep___gallery_posts.default };
-const $gallery_posts_create: Provider = { provide: 'ep:gallery/posts/create', useClass: ep___gallery_posts_create.default };
-const $gallery_posts_delete: Provider = { provide: 'ep:gallery/posts/delete', useClass: ep___gallery_posts_delete.default };
-const $gallery_posts_like: Provider = { provide: 'ep:gallery/posts/like', useClass: ep___gallery_posts_like.default };
-const $gallery_posts_show: Provider = { provide: 'ep:gallery/posts/show', useClass: ep___gallery_posts_show.default };
-const $gallery_posts_unlike: Provider = { provide: 'ep:gallery/posts/unlike', useClass: ep___gallery_posts_unlike.default };
-const $gallery_posts_update: Provider = { provide: 'ep:gallery/posts/update', useClass: ep___gallery_posts_update.default };
-const $getOnlineUsersCount: Provider = { provide: 'ep:get-online-users-count', useClass: ep___getOnlineUsersCount.default };
-const $getAvatarDecorations: Provider = { provide: 'ep:get-avatar-decorations', useClass: ep___getAvatarDecorations.default };
-const $hashtags_list: Provider = { provide: 'ep:hashtags/list', useClass: ep___hashtags_list.default };
-const $hashtags_search: Provider = { provide: 'ep:hashtags/search', useClass: ep___hashtags_search.default };
-const $hashtags_show: Provider = { provide: 'ep:hashtags/show', useClass: ep___hashtags_show.default };
-const $hashtags_trend: Provider = { provide: 'ep:hashtags/trend', useClass: ep___hashtags_trend.default };
-const $hashtags_users: Provider = { provide: 'ep:hashtags/users', useClass: ep___hashtags_users.default };
-const $i: Provider = { provide: 'ep:i', useClass: ep___i.default };
-const $i_2fa_done: Provider = { provide: 'ep:i/2fa/done', useClass: ep___i_2fa_done.default };
-const $i_2fa_keyDone: Provider = { provide: 'ep:i/2fa/key-done', useClass: ep___i_2fa_keyDone.default };
-const $i_2fa_passwordLess: Provider = { provide: 'ep:i/2fa/password-less', useClass: ep___i_2fa_passwordLess.default };
-const $i_2fa_registerKey: Provider = { provide: 'ep:i/2fa/register-key', useClass: ep___i_2fa_registerKey.default };
-const $i_2fa_register: Provider = { provide: 'ep:i/2fa/register', useClass: ep___i_2fa_register.default };
-const $i_2fa_updateKey: Provider = { provide: 'ep:i/2fa/update-key', useClass: ep___i_2fa_updateKey.default };
-const $i_2fa_removeKey: Provider = { provide: 'ep:i/2fa/remove-key', useClass: ep___i_2fa_removeKey.default };
-const $i_2fa_unregister: Provider = { provide: 'ep:i/2fa/unregister', useClass: ep___i_2fa_unregister.default };
-const $i_apps: Provider = { provide: 'ep:i/apps', useClass: ep___i_apps.default };
-const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass: ep___i_authorizedApps.default };
-const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default };
-const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default };
-const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default };
-const $i_exportData: Provider = { provide: 'ep:i/export-data', useClass: ep___i_exportData.default };
-const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default };
-const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default };
-const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default };
-const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default };
-const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default };
-const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default };
-const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default };
-const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default };
-const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default };
-const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default };
-const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default };
-const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: ep___i_importBlocking.default };
-const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default };
-const $i_importNotes: Provider = { provide: 'ep:i/import-notes', useClass: ep___i_importNotes.default };
-const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default };
-const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default };
-const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default };
-const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default };
-const $i_notificationsGrouped: Provider = { provide: 'ep:i/notifications-grouped', useClass: ep___i_notificationsGrouped.default };
-const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default };
-const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default };
-const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default };
-const $i_readAllUnreadNotes: Provider = { provide: 'ep:i/read-all-unread-notes', useClass: ep___i_readAllUnreadNotes.default };
-const $i_readAnnouncement: Provider = { provide: 'ep:i/read-announcement', useClass: ep___i_readAnnouncement.default };
-const $i_regenerateToken: Provider = { provide: 'ep:i/regenerate-token', useClass: ep___i_regenerateToken.default };
-const $i_registry_getAll: Provider = { provide: 'ep:i/registry/get-all', useClass: ep___i_registry_getAll.default };
-const $i_registry_getUnsecure: Provider = { provide: 'ep:i/registry/get-unsecure', useClass: ep___i_registry_getUnsecure.default };
-const $i_registry_getDetail: Provider = { provide: 'ep:i/registry/get-detail', useClass: ep___i_registry_getDetail.default };
-const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep___i_registry_get.default };
-const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default };
-const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default };
-const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default };
-const $i_registry_scopesWithDomain: Provider = { provide: 'ep:i/registry/scopes-with-domain', useClass: ep___i_registry_scopesWithDomain.default };
-const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default };
-const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default };
-const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: ep___i_signinHistory.default };
-const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default };
-const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default };
-const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default };
-const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default };
-const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default };
-const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default };
-const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
-const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
-const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
-const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
-const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
-const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
-const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
-const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default };
-const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
-const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
-const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default };
-const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
-const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
-const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default };
-const $mute_list: Provider = { provide: 'ep:mute/list', useClass: ep___mute_list.default };
-const $renoteMute_create: Provider = { provide: 'ep:renote-mute/create', useClass: ep___renoteMute_create.default };
-const $renoteMute_delete: Provider = { provide: 'ep:renote-mute/delete', useClass: ep___renoteMute_delete.default };
-const $renoteMute_list: Provider = { provide: 'ep:renote-mute/list', useClass: ep___renoteMute_list.default };
-const $my_apps: Provider = { provide: 'ep:my/apps', useClass: ep___my_apps.default };
-const $notes: Provider = { provide: 'ep:notes', useClass: ep___notes.default };
-const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep___notes_children.default };
-const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default };
-const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
-const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
-const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
-const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
-const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
-const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
-const $notes_following: Provider = { provide: 'ep:notes/following', useClass: ep___notes_following.default };
-const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default };
-const $notes_bubbleTimeline: Provider = { provide: 'ep:notes/bubble-timeline', useClass: ep___notes_bubbleTimeline.default };
-const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default };
-const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default };
-const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
-const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default };
-const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default };
-const $notes_polls_refresh: Provider = { provide: 'ep:notes/polls/refresh', useClass: ep___notes_polls_refresh.default };
-const $notes_reactions: Provider = { provide: 'ep:notes/reactions', useClass: ep___notes_reactions.default };
-const $notes_reactions_create: Provider = { provide: 'ep:notes/reactions/create', useClass: ep___notes_reactions_create.default };
-const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete', useClass: ep___notes_reactions_delete.default };
-const $notes_like: Provider = { provide: 'ep:notes/like', useClass: ep___notes_like.default };
-const $notes_renotes: Provider = { provide: 'ep:notes/renotes', useClass: ep___notes_renotes.default };
-const $notes_replies: Provider = { provide: 'ep:notes/replies', useClass: ep___notes_replies.default };
-const $notes_schedule_create: Provider = { provide: 'ep:notes/schedule/create', useClass: ep___notes_schedule_create.default };
-const $notes_schedule_delete: Provider = { provide: 'ep:notes/schedule/delete', useClass: ep___notes_schedule_delete.default };
-const $notes_schedule_list: Provider = { provide: 'ep:notes/schedule/list', useClass: ep___notes_schedule_list.default };
-const $notes_searchByTag: Provider = { provide: 'ep:notes/search-by-tag', useClass: ep___notes_searchByTag.default };
-const $notes_search: Provider = { provide: 'ep:notes/search', useClass: ep___notes_search.default };
-const $notes_show: Provider = { provide: 'ep:notes/show', useClass: ep___notes_show.default };
-const $notes_state: Provider = { provide: 'ep:notes/state', useClass: ep___notes_state.default };
-const $notes_threadMuting_create: Provider = { provide: 'ep:notes/thread-muting/create', useClass: ep___notes_threadMuting_create.default };
-const $notes_threadMuting_delete: Provider = { provide: 'ep:notes/thread-muting/delete', useClass: ep___notes_threadMuting_delete.default };
-const $notes_timeline: Provider = { provide: 'ep:notes/timeline', useClass: ep___notes_timeline.default };
-const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep___notes_translate.default };
-const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default };
-const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
-const $notes_edit: Provider = { provide: 'ep:notes/edit', useClass: ep___notes_edit.default };
-const $notes_versions: Provider = { provide: 'ep:notes/versions', useClass: ep___notes_versions.default };
-const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
-const $notifications_flush: Provider = { provide: 'ep:notifications/flush', useClass: ep___notifications_flush.default };
-const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
-const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default };
-const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
-const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
-const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
-const $pages_featured: Provider = { provide: 'ep:pages/featured', useClass: ep___pages_featured.default };
-const $pages_like: Provider = { provide: 'ep:pages/like', useClass: ep___pages_like.default };
-const $pages_show: Provider = { provide: 'ep:pages/show', useClass: ep___pages_show.default };
-const $pages_unlike: Provider = { provide: 'ep:pages/unlike', useClass: ep___pages_unlike.default };
-const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pages_update.default };
-const $flash_create: Provider = { provide: 'ep:flash/create', useClass: ep___flash_create.default };
-const $flash_delete: Provider = { provide: 'ep:flash/delete', useClass: ep___flash_delete.default };
-const $flash_featured: Provider = { provide: 'ep:flash/featured', useClass: ep___flash_featured.default };
-const $flash_like: Provider = { provide: 'ep:flash/like', useClass: ep___flash_like.default };
-const $flash_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default };
-const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.default };
-const $flash_update: Provider = { provide: 'ep:flash/update', useClass: ep___flash_update.default };
-const $flash_my: Provider = { provide: 'ep:flash/my', useClass: ep___flash_my.default };
-const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___flash_myLikes.default };
-const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default };
-const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default };
-const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default };
-const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default };
-const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default };
-const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default };
-const $roles_notes: Provider = { provide: 'ep:roles/notes', useClass: ep___roles_notes.default };
-const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default };
-const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default };
-const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default };
-const $serverInfo: Provider = { provide: 'ep:server-info', useClass: ep___serverInfo.default };
-const $stats: Provider = { provide: 'ep:stats', useClass: ep___stats.default };
-const $sw_show_registration: Provider = { provide: 'ep:sw/show-registration', useClass: ep___sw_show_registration.default };
-const $sw_update_registration: Provider = { provide: 'ep:sw/update-registration', useClass: ep___sw_update_registration.default };
-const $sw_register: Provider = { provide: 'ep:sw/register', useClass: ep___sw_register.default };
-const $sw_unregister: Provider = { provide: 'ep:sw/unregister', useClass: ep___sw_unregister.default };
-const $test: Provider = { provide: 'ep:test', useClass: ep___test.default };
-const $username_available: Provider = { provide: 'ep:username/available', useClass: ep___username_available.default };
-const $users: Provider = { provide: 'ep:users', useClass: ep___users.default };
-const $users_clips: Provider = { provide: 'ep:users/clips', useClass: ep___users_clips.default };
-const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default };
-const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default };
-const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default };
-const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default };
-const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default };
-const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default };
-const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default };
-const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default };
-const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass: ep___users_lists_pull.default };
-const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default };
-const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default };
-const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
-const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default };
-const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default };
-const $users_lists_createFromPublic: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_createFromPublic.default };
-const $users_lists_updateMembership: Provider = { provide: 'ep:users/lists/update-membership', useClass: ep___users_lists_updateMembership.default };
-const $users_lists_getMemberships: Provider = { provide: 'ep:users/lists/get-memberships', useClass: ep___users_lists_getMemberships.default };
-const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default };
-const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default };
-const $users_flashs: Provider = { provide: 'ep:users/flashs', useClass: ep___users_flashs.default };
-const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default };
-const $users_recommendation: Provider = { provide: 'ep:users/recommendation', useClass: ep___users_recommendation.default };
-const $users_relation: Provider = { provide: 'ep:users/relation', useClass: ep___users_relation.default };
-const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClass: ep___users_reportAbuse.default };
-const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default };
-const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
-const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
-const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
-const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
-const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
-const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
-const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
-const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.default };
-const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default };
-const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default };
-const $reversi_cancelMatch: Provider = { provide: 'ep:reversi/cancel-match', useClass: ep___reversi_cancelMatch.default };
-const $reversi_games: Provider = { provide: 'ep:reversi/games', useClass: ep___reversi_games.default };
-const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___reversi_match.default };
-const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default };
-const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default };
-const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default };
-const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default };
+const endpoints = Object.entries(endpointsObject);
+const endpointProviders = endpoints.map(([path, endpoint]): Provider => ({ provide: `ep:${path}`, useClass: endpoint.default }));
@Module({
imports: [
@@ -824,811 +21,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
providers: [
GetterService,
ApiLoggerService,
- $admin_meta,
- $admin_abuseUserReports,
- $admin_abuseReport_notificationRecipient_list,
- $admin_abuseReport_notificationRecipient_show,
- $admin_abuseReport_notificationRecipient_create,
- $admin_abuseReport_notificationRecipient_update,
- $admin_abuseReport_notificationRecipient_delete,
- $admin_accounts_create,
- $admin_accounts_delete,
- $admin_accounts_findByEmail,
- $admin_ad_create,
- $admin_ad_delete,
- $admin_ad_list,
- $admin_ad_update,
- $admin_announcements_create,
- $admin_announcements_delete,
- $admin_announcements_list,
- $admin_announcements_update,
- $admin_avatarDecorations_create,
- $admin_avatarDecorations_delete,
- $admin_avatarDecorations_list,
- $admin_avatarDecorations_update,
- $admin_deleteAllFilesOfAUser,
- $admin_unsetUserAvatar,
- $admin_unsetUserBanner,
- $admin_drive_cleanRemoteFiles,
- $admin_drive_cleanup,
- $admin_drive_files,
- $admin_drive_showFile,
- $admin_emoji_addAliasesBulk,
- $admin_emoji_add,
- $admin_emoji_copy,
- $admin_emoji_deleteBulk,
- $admin_emoji_delete,
- $admin_emoji_importZip,
- $admin_emoji_listRemote,
- $admin_emoji_list,
- $admin_emoji_removeAliasesBulk,
- $admin_emoji_setAliasesBulk,
- $admin_emoji_setCategoryBulk,
- $admin_emoji_setLicenseBulk,
- $admin_emoji_update,
- $admin_federation_deleteAllFiles,
- $admin_federation_refreshRemoteInstanceMetadata,
- $admin_federation_removeAllFollowing,
- $admin_federation_updateInstance,
- $admin_getIndexStats,
- $admin_getTableStats,
- $admin_getUserIps,
- $admin_invite_create,
- $admin_invite_list,
- $admin_promo_create,
- $admin_queue_clear,
- $admin_queue_deliverDelayed,
- $admin_queue_inboxDelayed,
- $admin_queue_promote,
- $admin_queue_stats,
- $admin_relays_add,
- $admin_relays_list,
- $admin_relays_remove,
- $admin_resetPassword,
- $admin_resolveAbuseUserReport,
- $admin_forwardAbuseUserReport,
- $admin_updateAbuseUserReport,
- $admin_sendEmail,
- $admin_serverInfo,
- $admin_showModerationLogs,
- $admin_showUser,
- $admin_showUsers,
- $admin_nsfwUser,
- $admin_unnsfwUser,
- $admin_silenceUser,
- $admin_unsilenceUser,
- $admin_suspendUser,
- $admin_approveUser,
- $admin_declineUser,
- $admin_unsuspendUser,
- $admin_updateMeta,
- $admin_deleteAccount,
- $admin_updateUserNote,
- $admin_roles_create,
- $admin_roles_delete,
- $admin_roles_list,
- $admin_roles_show,
- $admin_roles_update,
- $admin_roles_assign,
- $admin_roles_unassign,
- $admin_roles_updateDefaultPolicies,
- $admin_roles_users,
- $admin_systemWebhook_create,
- $admin_systemWebhook_delete,
- $admin_systemWebhook_list,
- $admin_systemWebhook_show,
- $admin_systemWebhook_update,
- $admin_systemWebhook_test,
- $announcements,
- $announcements_show,
- $antennas_create,
- $antennas_delete,
- $antennas_list,
- $antennas_notes,
- $antennas_show,
- $antennas_update,
- $ap_get,
- $ap_show,
- $app_create,
- $app_show,
- $auth_accept,
- $auth_session_generate,
- $auth_session_show,
- $auth_session_userkey,
- $blocking_create,
- $blocking_delete,
- $blocking_list,
- $channels_create,
- $channels_featured,
- $channels_follow,
- $channels_followed,
- $channels_owned,
- $channels_show,
- $channels_timeline,
- $channels_unfollow,
- $channels_update,
- $channels_favorite,
- $channels_unfavorite,
- $channels_myFavorites,
- $channels_search,
- $charts_activeUsers,
- $charts_apRequest,
- $charts_drive,
- $charts_federation,
- $charts_instance,
- $charts_notes,
- $charts_user_drive,
- $charts_user_following,
- $charts_user_notes,
- $charts_user_pv,
- $charts_user_reactions,
- $charts_users,
- $clips_addNote,
- $clips_removeNote,
- $clips_create,
- $clips_delete,
- $clips_list,
- $clips_notes,
- $clips_show,
- $clips_update,
- $clips_favorite,
- $clips_unfavorite,
- $clips_myFavorites,
- $drive,
- $drive_files,
- $drive_files_attachedNotes,
- $drive_files_checkExistence,
- $drive_files_create,
- $drive_files_delete,
- $drive_files_findByHash,
- $drive_files_find,
- $drive_files_show,
- $drive_files_update,
- $drive_files_uploadFromUrl,
- $drive_folders,
- $drive_folders_create,
- $drive_folders_delete,
- $drive_folders_find,
- $drive_folders_show,
- $drive_folders_update,
- $drive_stream,
- $emailAddress_available,
- $endpoint,
- $endpoints,
- $exportCustomEmojis,
- $federation_followers,
- $federation_following,
- $federation_instances,
- $federation_showInstance,
- $federation_updateRemoteUser,
- $federation_users,
- $federation_stats,
- $following_create,
- $following_delete,
- $following_update,
- $following_update_all,
- $following_invalidate,
- $following_requests_accept,
- $following_requests_cancel,
- $following_requests_list,
- $following_requests_sent,
- $following_requests_reject,
- $gallery_featured,
- $gallery_popular,
- $gallery_posts,
- $gallery_posts_create,
- $gallery_posts_delete,
- $gallery_posts_like,
- $gallery_posts_show,
- $gallery_posts_unlike,
- $gallery_posts_update,
- $getOnlineUsersCount,
- $getAvatarDecorations,
- $hashtags_list,
- $hashtags_search,
- $hashtags_show,
- $hashtags_trend,
- $hashtags_users,
- $i,
- $i_2fa_done,
- $i_2fa_keyDone,
- $i_2fa_passwordLess,
- $i_2fa_registerKey,
- $i_2fa_register,
- $i_2fa_updateKey,
- $i_2fa_removeKey,
- $i_2fa_unregister,
- $i_apps,
- $i_authorizedApps,
- $i_claimAchievement,
- $i_changePassword,
- $i_deleteAccount,
- $i_exportData,
- $i_exportBlocking,
- $i_exportFollowing,
- $i_exportMute,
- $i_exportNotes,
- $i_exportClips,
- $i_exportFavorites,
- $i_exportUserLists,
- $i_exportAntennas,
- $i_favorites,
- $i_gallery_likes,
- $i_gallery_posts,
- $i_importBlocking,
- $i_importFollowing,
- $i_importNotes,
- $i_importMuting,
- $i_importUserLists,
- $i_importAntennas,
- $i_notifications,
- $i_notificationsGrouped,
- $i_pageLikes,
- $i_pages,
- $i_pin,
- $i_readAllUnreadNotes,
- $i_readAnnouncement,
- $i_regenerateToken,
- $i_registry_getAll,
- $i_registry_getUnsecure,
- $i_registry_getDetail,
- $i_registry_get,
- $i_registry_keysWithType,
- $i_registry_keys,
- $i_registry_remove,
- $i_registry_scopesWithDomain,
- $i_registry_set,
- $i_revokeToken,
- $i_signinHistory,
- $i_unpin,
- $i_updateEmail,
- $i_update,
- $i_move,
- $i_webhooks_create,
- $i_webhooks_list,
- $i_webhooks_show,
- $i_webhooks_update,
- $i_webhooks_delete,
- $i_webhooks_test,
- $invite_create,
- $invite_delete,
- $invite_list,
- $invite_limit,
- $meta,
- $emojis,
- $emoji,
- $miauth_genToken,
- $mute_create,
- $mute_delete,
- $mute_list,
- $renoteMute_create,
- $renoteMute_delete,
- $renoteMute_list,
- $my_apps,
- $notes,
- $notes_children,
- $notes_clips,
- $notes_conversation,
- $notes_create,
- $notes_delete,
- $notes_favorites_create,
- $notes_favorites_delete,
- $notes_featured,
- $notes_following,
- $notes_globalTimeline,
- $notes_bubbleTimeline,
- $notes_hybridTimeline,
- $notes_localTimeline,
- $notes_mentions,
- $notes_polls_recommendation,
- $notes_polls_vote,
- $notes_polls_refresh,
- $notes_reactions,
- $notes_reactions_create,
- $notes_reactions_delete,
- $notes_like,
- $notes_renotes,
- $notes_replies,
- $notes_schedule_create,
- $notes_schedule_delete,
- $notes_schedule_list,
- $notes_searchByTag,
- $notes_search,
- $notes_show,
- $notes_state,
- $notes_threadMuting_create,
- $notes_threadMuting_delete,
- $notes_timeline,
- $notes_translate,
- $notes_unrenote,
- $notes_userListTimeline,
- $notes_edit,
- $notes_versions,
- $notifications_create,
- $notifications_flush,
- $notifications_markAllAsRead,
- $notifications_testNotification,
- $pagePush,
- $pages_create,
- $pages_delete,
- $pages_featured,
- $pages_like,
- $pages_show,
- $pages_unlike,
- $pages_update,
- $flash_create,
- $flash_delete,
- $flash_featured,
- $flash_like,
- $flash_show,
- $flash_unlike,
- $flash_update,
- $flash_my,
- $flash_myLikes,
- $ping,
- $pinnedUsers,
- $promo_read,
- $roles_list,
- $roles_show,
- $roles_users,
- $roles_notes,
- $requestResetPassword,
- $resetDb,
- $resetPassword,
- $serverInfo,
- $stats,
- $sw_show_registration,
- $sw_update_registration,
- $sw_register,
- $sw_unregister,
- $test,
- $username_available,
- $users,
- $users_clips,
- $users_followers,
- $users_following,
- $users_gallery_posts,
- $users_getFrequentlyRepliedUsers,
- $users_featuredNotes,
- $users_lists_create,
- $users_lists_delete,
- $users_lists_list,
- $users_lists_pull,
- $users_lists_push,
- $users_lists_show,
- $users_lists_update,
- $users_lists_favorite,
- $users_lists_unfavorite,
- $users_lists_createFromPublic,
- $users_lists_updateMembership,
- $users_lists_getMemberships,
- $users_notes,
- $users_pages,
- $users_flashs,
- $users_reactions,
- $users_recommendation,
- $users_relation,
- $users_reportAbuse,
- $users_searchByUsernameAndHost,
- $users_search,
- $users_show,
- $users_achievements,
- $users_updateMemo,
- $fetchRss,
- $fetchExternalResources,
- $retention,
- $sponsors,
- $bubbleGame_register,
- $bubbleGame_ranking,
- $reversi_cancelMatch,
- $reversi_games,
- $reversi_match,
- $reversi_invitations,
- $reversi_showGame,
- $reversi_surrender,
- $reversi_verify,
+ ...endpointProviders,
],
exports: [
- $admin_meta,
- $admin_abuseUserReports,
- $admin_abuseReport_notificationRecipient_list,
- $admin_abuseReport_notificationRecipient_show,
- $admin_abuseReport_notificationRecipient_create,
- $admin_abuseReport_notificationRecipient_update,
- $admin_abuseReport_notificationRecipient_delete,
- $admin_accounts_create,
- $admin_accounts_delete,
- $admin_accounts_findByEmail,
- $admin_ad_create,
- $admin_ad_delete,
- $admin_ad_list,
- $admin_ad_update,
- $admin_announcements_create,
- $admin_announcements_delete,
- $admin_announcements_list,
- $admin_announcements_update,
- $admin_avatarDecorations_create,
- $admin_avatarDecorations_delete,
- $admin_avatarDecorations_list,
- $admin_avatarDecorations_update,
- $admin_deleteAllFilesOfAUser,
- $admin_unsetUserAvatar,
- $admin_unsetUserBanner,
- $admin_drive_cleanRemoteFiles,
- $admin_drive_cleanup,
- $admin_drive_files,
- $admin_drive_showFile,
- $admin_emoji_addAliasesBulk,
- $admin_emoji_add,
- $admin_emoji_copy,
- $admin_emoji_deleteBulk,
- $admin_emoji_delete,
- $admin_emoji_importZip,
- $admin_emoji_listRemote,
- $admin_emoji_list,
- $admin_emoji_removeAliasesBulk,
- $admin_emoji_setAliasesBulk,
- $admin_emoji_setCategoryBulk,
- $admin_emoji_setLicenseBulk,
- $admin_emoji_update,
- $admin_federation_deleteAllFiles,
- $admin_federation_refreshRemoteInstanceMetadata,
- $admin_federation_removeAllFollowing,
- $admin_federation_updateInstance,
- $admin_getIndexStats,
- $admin_getTableStats,
- $admin_getUserIps,
- $admin_invite_create,
- $admin_invite_list,
- $admin_promo_create,
- $admin_queue_clear,
- $admin_queue_deliverDelayed,
- $admin_queue_inboxDelayed,
- $admin_queue_promote,
- $admin_queue_stats,
- $admin_relays_add,
- $admin_relays_list,
- $admin_relays_remove,
- $admin_resetPassword,
- $admin_resolveAbuseUserReport,
- $admin_forwardAbuseUserReport,
- $admin_updateAbuseUserReport,
- $admin_sendEmail,
- $admin_serverInfo,
- $admin_showModerationLogs,
- $admin_showUser,
- $admin_showUsers,
- $admin_nsfwUser,
- $admin_unnsfwUser,
- $admin_silenceUser,
- $admin_unsilenceUser,
- $admin_suspendUser,
- $admin_approveUser,
- $admin_declineUser,
- $admin_unsuspendUser,
- $admin_updateMeta,
- $admin_deleteAccount,
- $admin_updateUserNote,
- $admin_roles_create,
- $admin_roles_delete,
- $admin_roles_list,
- $admin_roles_show,
- $admin_roles_update,
- $admin_roles_assign,
- $admin_roles_unassign,
- $admin_roles_updateDefaultPolicies,
- $admin_roles_users,
- $admin_systemWebhook_create,
- $admin_systemWebhook_delete,
- $admin_systemWebhook_list,
- $admin_systemWebhook_show,
- $admin_systemWebhook_update,
- $admin_systemWebhook_test,
- $announcements,
- $announcements_show,
- $antennas_create,
- $antennas_delete,
- $antennas_list,
- $antennas_notes,
- $antennas_show,
- $antennas_update,
- $ap_get,
- $ap_show,
- $app_create,
- $app_show,
- $auth_accept,
- $auth_session_generate,
- $auth_session_show,
- $auth_session_userkey,
- $blocking_create,
- $blocking_delete,
- $blocking_list,
- $channels_create,
- $channels_featured,
- $channels_follow,
- $channels_followed,
- $channels_owned,
- $channels_show,
- $channels_timeline,
- $channels_unfollow,
- $channels_update,
- $channels_favorite,
- $channels_unfavorite,
- $channels_myFavorites,
- $channels_search,
- $charts_activeUsers,
- $charts_apRequest,
- $charts_drive,
- $charts_federation,
- $charts_instance,
- $charts_notes,
- $charts_user_drive,
- $charts_user_following,
- $charts_user_notes,
- $charts_user_pv,
- $charts_user_reactions,
- $charts_users,
- $clips_addNote,
- $clips_removeNote,
- $clips_create,
- $clips_delete,
- $clips_list,
- $clips_notes,
- $clips_show,
- $clips_update,
- $clips_favorite,
- $clips_unfavorite,
- $clips_myFavorites,
- $drive,
- $drive_files,
- $drive_files_attachedNotes,
- $drive_files_checkExistence,
- $drive_files_create,
- $drive_files_delete,
- $drive_files_findByHash,
- $drive_files_find,
- $drive_files_show,
- $drive_files_update,
- $drive_files_uploadFromUrl,
- $drive_folders,
- $drive_folders_create,
- $drive_folders_delete,
- $drive_folders_find,
- $drive_folders_show,
- $drive_folders_update,
- $drive_stream,
- $emailAddress_available,
- $endpoint,
- $endpoints,
- $exportCustomEmojis,
- $federation_followers,
- $federation_following,
- $federation_instances,
- $federation_showInstance,
- $federation_updateRemoteUser,
- $federation_users,
- $federation_stats,
- $following_create,
- $following_delete,
- $following_update,
- $following_update_all,
- $following_invalidate,
- $following_requests_accept,
- $following_requests_cancel,
- $following_requests_list,
- $following_requests_reject,
- $gallery_featured,
- $gallery_popular,
- $gallery_posts,
- $gallery_posts_create,
- $gallery_posts_delete,
- $gallery_posts_like,
- $gallery_posts_show,
- $gallery_posts_unlike,
- $gallery_posts_update,
- $getOnlineUsersCount,
- $getAvatarDecorations,
- $hashtags_list,
- $hashtags_search,
- $hashtags_show,
- $hashtags_trend,
- $hashtags_users,
- $i,
- $i_2fa_done,
- $i_2fa_keyDone,
- $i_2fa_passwordLess,
- $i_2fa_registerKey,
- $i_2fa_register,
- $i_2fa_updateKey,
- $i_2fa_removeKey,
- $i_2fa_unregister,
- $i_apps,
- $i_authorizedApps,
- $i_claimAchievement,
- $i_changePassword,
- $i_deleteAccount,
- $i_exportData,
- $i_exportBlocking,
- $i_exportFollowing,
- $i_exportMute,
- $i_exportNotes,
- $i_exportClips,
- $i_exportFavorites,
- $i_exportUserLists,
- $i_exportAntennas,
- $i_favorites,
- $i_gallery_likes,
- $i_gallery_posts,
- $i_importBlocking,
- $i_importFollowing,
- $i_importNotes,
- $i_importMuting,
- $i_importUserLists,
- $i_importAntennas,
- $i_notifications,
- $i_notificationsGrouped,
- $i_pageLikes,
- $i_pages,
- $i_pin,
- $i_readAllUnreadNotes,
- $i_readAnnouncement,
- $i_regenerateToken,
- $i_registry_getAll,
- $i_registry_getUnsecure,
- $i_registry_getDetail,
- $i_registry_get,
- $i_registry_keysWithType,
- $i_registry_keys,
- $i_registry_remove,
- $i_registry_scopesWithDomain,
- $i_registry_set,
- $i_revokeToken,
- $i_signinHistory,
- $i_unpin,
- $i_updateEmail,
- $i_update,
- $i_move,
- $i_webhooks_create,
- $i_webhooks_list,
- $i_webhooks_show,
- $i_webhooks_update,
- $i_webhooks_delete,
- $i_webhooks_test,
- $invite_create,
- $invite_delete,
- $invite_list,
- $invite_limit,
- $meta,
- $emojis,
- $emoji,
- $miauth_genToken,
- $mute_create,
- $mute_delete,
- $mute_list,
- $renoteMute_create,
- $renoteMute_delete,
- $renoteMute_list,
- $my_apps,
- $notes,
- $notes_children,
- $notes_clips,
- $notes_conversation,
- $notes_create,
- $notes_delete,
- $notes_favorites_create,
- $notes_favorites_delete,
- $notes_featured,
- $notes_following,
- $notes_globalTimeline,
- $notes_bubbleTimeline,
- $notes_hybridTimeline,
- $notes_localTimeline,
- $notes_mentions,
- $notes_polls_recommendation,
- $notes_polls_vote,
- $notes_polls_refresh,
- $notes_reactions,
- $notes_reactions_create,
- $notes_reactions_delete,
- $notes_like,
- $notes_renotes,
- $notes_replies,
- $notes_schedule_create,
- $notes_schedule_delete,
- $notes_schedule_list,
- $notes_searchByTag,
- $notes_search,
- $notes_show,
- $notes_state,
- $notes_threadMuting_create,
- $notes_threadMuting_delete,
- $notes_timeline,
- $notes_translate,
- $notes_unrenote,
- $notes_userListTimeline,
- $notes_edit,
- $notes_versions,
- $notifications_create,
- $notifications_flush,
- $notifications_markAllAsRead,
- $notifications_testNotification,
- $pagePush,
- $pages_create,
- $pages_delete,
- $pages_featured,
- $pages_like,
- $pages_show,
- $pages_unlike,
- $pages_update,
- $flash_create,
- $flash_delete,
- $flash_featured,
- $flash_like,
- $flash_show,
- $flash_unlike,
- $flash_update,
- $flash_my,
- $flash_myLikes,
- $ping,
- $pinnedUsers,
- $promo_read,
- $roles_list,
- $roles_show,
- $roles_users,
- $roles_notes,
- $requestResetPassword,
- $resetDb,
- $resetPassword,
- $serverInfo,
- $stats,
- $sw_register,
- $sw_unregister,
- $test,
- $username_available,
- $users,
- $users_clips,
- $users_followers,
- $users_following,
- $users_gallery_posts,
- $users_getFrequentlyRepliedUsers,
- $users_featuredNotes,
- $users_lists_create,
- $users_lists_delete,
- $users_lists_list,
- $users_lists_pull,
- $users_lists_push,
- $users_lists_show,
- $users_lists_update,
- $users_lists_favorite,
- $users_lists_unfavorite,
- $users_lists_createFromPublic,
- $users_lists_updateMembership,
- $users_lists_getMemberships,
- $users_notes,
- $users_pages,
- $users_flashs,
- $users_reactions,
- $users_recommendation,
- $users_relation,
- $users_reportAbuse,
- $users_searchByUsernameAndHost,
- $users_search,
- $users_show,
- $users_achievements,
- $users_updateMemo,
- $fetchRss,
- $fetchExternalResources,
- $retention,
- $sponsors,
- $bubbleGame_register,
- $bubbleGame_ranking,
- $reversi_cancelMatch,
- $reversi_games,
- $reversi_match,
- $reversi_invitations,
- $reversi_showGame,
- $reversi_surrender,
- $reversi_verify,
+ ...endpointProviders,
],
})
export class EndpointsModule {}
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index fa9155d82d..72712bce60 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -26,12 +26,19 @@ import { UserAuthService } from '@/core/UserAuthService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
-import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
+import { Keyed, RateLimit, sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
+// Up to 10 attempts, then 1 per minute
+const signinRateLimit: Keyed<RateLimit> = {
+ key: 'signin',
+ max: 10,
+ dripRate: 1000 * 60,
+};
+
@Injectable()
export class SigninApiService {
constructor(
@@ -94,7 +101,7 @@ export class SigninApiService {
}
// not more than 1 attempt per second and not more than 10 attempts per hour
- const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
+ const rateLimit = await this.rateLimiterService.limit(signinRateLimit, getIpHash(request.ip));
sendRateLimitHeaders(reply, rateLimit);
diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
index e94d2b6b68..f84f50523b 100644
--- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -21,7 +21,7 @@ import { WebAuthnService } from '@/core/WebAuthnService.js';
import Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IdentifiableError } from '@/misc/identifiable-error.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index 7aea6a0e56..42137d3298 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -19,10 +19,9 @@ import { MiLocalUser } from '@/models/User.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
+import { RoleService } from '@/core/RoleService.js';
import { SigninService } from './SigninService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
-import instance from './endpoints/charts/instance.js';
-import { RoleService } from '@/core/RoleService.js';
@Injectable()
export class SignupApiService {
diff --git a/packages/backend/src/server/api/SkRateLimiterService.ts b/packages/backend/src/server/api/SkRateLimiterService.ts
deleted file mode 100644
index 38c97b63df..0000000000
--- a/packages/backend/src/server/api/SkRateLimiterService.ts
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { Inject, Injectable } from '@nestjs/common';
-import Redis from 'ioredis';
-import { TimeService } from '@/core/TimeService.js';
-import { EnvService } from '@/core/EnvService.js';
-import { BucketRateLimit, LegacyRateLimit, LimitInfo, RateLimit, hasMinLimit, isLegacyRateLimit, Keyed, hasMaxLimit, disabledLimitInfo, MaxLegacyLimit, MinLegacyLimit } from '@/misc/rate-limit-utils.js';
-import { DI } from '@/di-symbols.js';
-
-@Injectable()
-export class SkRateLimiterService {
- private readonly disabled: boolean;
-
- constructor(
- @Inject(TimeService)
- private readonly timeService: TimeService,
-
- @Inject(DI.redis)
- private readonly redisClient: Redis.Redis,
-
- @Inject(EnvService)
- envService: EnvService,
- ) {
- this.disabled = envService.env.NODE_ENV === 'test';
- }
-
- /**
- * Check & increment a rate limit
- * @param limit The limit definition
- * @param actor Client who is calling this limit
- * @param factor Scaling factor - smaller = larger limit (less restrictive)
- */
- public async limit(limit: Keyed<RateLimit>, actor: string, factor = 1): Promise<LimitInfo> {
- if (this.disabled || factor === 0) {
- return disabledLimitInfo;
- }
-
- if (factor < 0) {
- throw new Error(`Rate limit factor is zero or negative: ${factor}`);
- }
-
- return await this.tryLimit(limit, actor, factor);
- }
-
- private async tryLimit(limit: Keyed<RateLimit>, actor: string, factor: number): Promise<LimitInfo> {
- if (isLegacyRateLimit(limit)) {
- return await this.limitLegacy(limit, actor, factor);
- } else {
- return await this.limitBucket(limit, actor, factor);
- }
- }
-
- private async limitLegacy(limit: Keyed<LegacyRateLimit>, actor: string, factor: number): Promise<LimitInfo> {
- if (hasMaxLimit(limit)) {
- return await this.limitLegacyMinMax(limit, actor, factor);
- } else if (hasMinLimit(limit)) {
- return await this.limitLegacyMinOnly(limit, actor, factor);
- } else {
- return disabledLimitInfo;
- }
- }
-
- private async limitLegacyMinMax(limit: Keyed<MaxLegacyLimit>, actor: string, factor: number): Promise<LimitInfo> {
- if (limit.duration === 0) return disabledLimitInfo;
- if (limit.duration < 0) throw new Error(`Invalid rate limit ${limit.key}: duration is negative (${limit.duration})`);
- if (limit.max < 1) throw new Error(`Invalid rate limit ${limit.key}: max is less than 1 (${limit.max})`);
-
- // Derive initial dripRate from minInterval OR duration/max.
- const initialDripRate = Math.max(limit.minInterval ?? Math.round(limit.duration / limit.max), 1);
-
- // Calculate dripSize to reach max at exactly duration
- const dripSize = Math.max(Math.round(limit.max / (limit.duration / initialDripRate)), 1);
-
- // Calculate final dripRate from dripSize and duration/max
- const dripRate = Math.max(Math.round(limit.duration / (limit.max / dripSize)), 1);
-
- const bucketLimit: Keyed<BucketRateLimit> = {
- type: 'bucket',
- key: limit.key,
- size: limit.max,
- dripRate,
- dripSize,
- };
- return await this.limitBucket(bucketLimit, actor, factor);
- }
-
- private async limitLegacyMinOnly(limit: Keyed<MinLegacyLimit>, actor: string, factor: number): Promise<LimitInfo> {
- if (limit.minInterval === 0) return disabledLimitInfo;
- if (limit.minInterval < 0) throw new Error(`Invalid rate limit ${limit.key}: minInterval is negative (${limit.minInterval})`);
-
- const dripRate = Math.max(Math.round(limit.minInterval), 1);
- const bucketLimit: Keyed<BucketRateLimit> = {
- type: 'bucket',
- key: limit.key,
- size: 1,
- dripRate,
- dripSize: 1,
- };
- return await this.limitBucket(bucketLimit, actor, factor);
- }
-
- /**
- * Implementation of Leaky Bucket rate limiting - see SkRateLimiterService.md for details.
- */
- private async limitBucket(limit: Keyed<BucketRateLimit>, actor: string, factor: number): Promise<LimitInfo> {
- if (limit.size < 1) throw new Error(`Invalid rate limit ${limit.key}: size is less than 1 (${limit.size})`);
- if (limit.dripRate != null && limit.dripRate < 1) throw new Error(`Invalid rate limit ${limit.key}: dripRate is less than 1 (${limit.dripRate})`);
- if (limit.dripSize != null && limit.dripSize < 1) throw new Error(`Invalid rate limit ${limit.key}: dripSize is less than 1 (${limit.dripSize})`);
-
- // 0 - Calculate
- const now = this.timeService.now;
- const bucketSize = Math.max(Math.ceil(limit.size / factor), 1);
- const dripRate = Math.ceil(limit.dripRate ?? 1000);
- const dripSize = Math.ceil(limit.dripSize ?? 1);
- const expirationSec = Math.max(Math.ceil((dripRate * Math.ceil(bucketSize / dripSize)) / 1000), 1);
-
- // 1 - Read
- const counterKey = createLimitKey(limit, actor, 'c');
- const timestampKey = createLimitKey(limit, actor, 't');
- const counter = await this.getLimitCounter(counterKey, timestampKey);
-
- // 2 - Drip
- const dripsSinceLastTick = Math.floor((now - counter.timestamp) / dripRate) * dripSize;
- const deltaCounter = Math.min(dripsSinceLastTick, counter.counter);
- const deltaTimestamp = dripsSinceLastTick * dripRate;
- if (deltaCounter > 0) {
- // Execute the next drip(s)
- const results = await this.executeRedisMulti(
- ['get', timestampKey],
- ['incrby', timestampKey, deltaTimestamp],
- ['expire', timestampKey, expirationSec],
- ['get', timestampKey],
- ['decrby', counterKey, deltaCounter],
- ['expire', counterKey, expirationSec],
- ['get', counterKey],
- );
- const expectedTimestamp = counter.timestamp;
- const canaryTimestamp = results[0] ? parseInt(results[0]) : 0;
- counter.timestamp = results[3] ? parseInt(results[3]) : 0;
- counter.counter = results[6] ? parseInt(results[6]) : 0;
-
- // Check for a data collision and rollback
- if (canaryTimestamp !== expectedTimestamp) {
- const rollbackResults = await this.executeRedisMulti(
- ['decrby', timestampKey, deltaTimestamp],
- ['get', timestampKey],
- ['incrby', counterKey, deltaCounter],
- ['get', counterKey],
- );
- counter.timestamp = rollbackResults[1] ? parseInt(rollbackResults[1]) : 0;
- counter.counter = rollbackResults[3] ? parseInt(rollbackResults[3]) : 0;
- }
- }
-
- // 3 - Check
- const blocked = counter.counter >= bucketSize;
- if (!blocked) {
- if (counter.timestamp === 0) {
- const results = await this.executeRedisMulti(
- ['set', timestampKey, now],
- ['expire', timestampKey, expirationSec],
- ['incr', counterKey],
- ['expire', counterKey, expirationSec],
- ['get', counterKey],
- );
- counter.timestamp = now;
- counter.counter = results[4] ? parseInt(results[4]) : 0;
- } else {
- const results = await this.executeRedisMulti(
- ['incr', counterKey],
- ['expire', counterKey, expirationSec],
- ['get', counterKey],
- );
- counter.counter = results[2] ? parseInt(results[2]) : 0;
- }
- }
-
- // Calculate how much time is needed to free up a bucket slot
- const overflow = Math.max((counter.counter + 1) - bucketSize, 0);
- const dripsNeeded = Math.ceil(overflow / dripSize);
- const timeNeeded = Math.max((dripRate * dripsNeeded) - (this.timeService.now - counter.timestamp), 0);
-
- // Calculate limit status
- const remaining = Math.max(bucketSize - counter.counter, 0);
- const resetMs = timeNeeded;
- const resetSec = Math.ceil(resetMs / 1000);
- const fullResetMs = Math.ceil(counter.counter / dripSize) * dripRate;
- const fullResetSec = Math.ceil(fullResetMs / 1000);
- return { blocked, remaining, resetSec, resetMs, fullResetSec, fullResetMs };
- }
-
- private async getLimitCounter(counterKey: string, timestampKey: string): Promise<LimitCounter> {
- const [counter, timestamp] = await this.executeRedisMulti(
- ['get', counterKey],
- ['get', timestampKey],
- );
-
- return {
- counter: counter ? parseInt(counter) : 0,
- timestamp: timestamp ? parseInt(timestamp) : 0,
- };
- }
-
- private async executeRedisMulti(...batch: RedisCommand[]): Promise<RedisResult[]> {
- const results = await this.redisClient.multi(batch).exec();
-
- // Transaction conflict (retryable)
- if (!results) {
- throw new ConflictError('Redis error: transaction conflict');
- }
-
- // Transaction failed (fatal)
- if (results.length !== batch.length) {
- throw new Error('Redis error: failed to execute batch');
- }
-
- // Map responses
- const errors: Error[] = [];
- const responses: RedisResult[] = [];
- for (const [error, response] of results) {
- if (error) errors.push(error);
- responses.push(response as RedisResult);
- }
-
- // Command failed (fatal)
- if (errors.length > 0) {
- const errorMessages = errors
- .map((e, i) => `Error in command ${i}: ${e}`)
- .join('\', \'');
- throw new AggregateError(errors, `Redis error: failed to execute command(s): '${errorMessages}'`);
- }
-
- return responses;
- }
-}
-
-// Not correct, but good enough for the basic commands we use.
-type RedisResult = string | null;
-type RedisCommand = [command: string, ...args: unknown[]];
-
-function createLimitKey(limit: Keyed<RateLimit>, actor: string, value: string): string {
- return `rl_${actor}_${limit.key}_${value}`;
-}
-
-class ConflictError extends Error {}
-
-interface LimitCounter {
- timestamp: number;
- counter: number;
-}
diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts
index e3fd1312ae..6e7abcfae6 100644
--- a/packages/backend/src/server/api/StreamingApiServerService.ts
+++ b/packages/backend/src/server/api/StreamingApiServerService.ts
@@ -18,10 +18,9 @@ import { CacheService } from '@/core/CacheService.js';
import { MiLocalUser } from '@/models/User.js';
import { UserService } from '@/core/UserService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
-import { RoleService } from '@/core/RoleService.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import { LoggerService } from '@/core/LoggerService.js';
-import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
+import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/Connection.js';
import { ChannelsService } from './stream/ChannelsService.js';
@@ -49,7 +48,6 @@ export class StreamingApiServerService {
private usersService: UserService,
private channelFollowingService: ChannelFollowingService,
private rateLimiterService: SkRateLimiterService,
- private roleService: RoleService,
private loggerService: LoggerService,
) {
}
@@ -57,22 +55,18 @@ export class StreamingApiServerService {
@bindThis
private async rateLimitThis(
user: MiLocalUser | null | undefined,
- requestIp: string | undefined,
+ requestIp: string,
limit: IEndpointMeta['limit'] & { key: NonNullable<string> },
) : Promise<boolean> {
- let limitActor: string;
+ let limitActor: string | MiLocalUser;
if (user) {
- limitActor = user.id;
+ limitActor = user;
} else {
- limitActor = getIpHash(requestIp || 'wtf');
+ limitActor = getIpHash(requestIp);
}
- const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
-
- if (factor <= 0) return false;
-
// Rate limit
- const rateLimit = await this.rateLimiterService.limit(limit, limitActor, factor);
+ const rateLimit = await this.rateLimiterService.limit(limit, limitActor);
return rateLimit.blocked;
}
diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts
new file mode 100644
index 0000000000..a641a14448
--- /dev/null
+++ b/packages/backend/src/server/api/endpoint-list.ts
@@ -0,0 +1,421 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+/*
+ * This file contains list of all endpoints exported as pathname of API endpoint
+ *
+ * When you add new endpoint, you should add it to this file.
+ * This file is used to generate API documentation and EndpointsModule.
+ */
+
+export * as 'admin/abuse-report/notification-recipient/create' from './endpoints/admin/abuse-report/notification-recipient/create.js';
+export * as 'admin/abuse-report/notification-recipient/delete' from './endpoints/admin/abuse-report/notification-recipient/delete.js';
+export * as 'admin/abuse-report/notification-recipient/list' from './endpoints/admin/abuse-report/notification-recipient/list.js';
+export * as 'admin/abuse-report/notification-recipient/show' from './endpoints/admin/abuse-report/notification-recipient/show.js';
+export * as 'admin/abuse-report/notification-recipient/update' from './endpoints/admin/abuse-report/notification-recipient/update.js';
+export * as 'admin/abuse-user-reports' from './endpoints/admin/abuse-user-reports.js';
+export * as 'admin/accounts/create' from './endpoints/admin/accounts/create.js';
+export * as 'admin/accounts/delete' from './endpoints/admin/accounts/delete.js';
+export * as 'admin/accounts/find-by-email' from './endpoints/admin/accounts/find-by-email.js';
+export * as 'admin/ad/create' from './endpoints/admin/ad/create.js';
+export * as 'admin/ad/delete' from './endpoints/admin/ad/delete.js';
+export * as 'admin/ad/list' from './endpoints/admin/ad/list.js';
+export * as 'admin/ad/update' from './endpoints/admin/ad/update.js';
+export * as 'admin/announcements/create' from './endpoints/admin/announcements/create.js';
+export * as 'admin/announcements/delete' from './endpoints/admin/announcements/delete.js';
+export * as 'admin/announcements/list' from './endpoints/admin/announcements/list.js';
+export * as 'admin/announcements/update' from './endpoints/admin/announcements/update.js';
+export * as 'admin/approve-user' from './endpoints/admin/approve-user.js';
+export * as 'admin/avatar-decorations/create' from './endpoints/admin/avatar-decorations/create.js';
+export * as 'admin/avatar-decorations/delete' from './endpoints/admin/avatar-decorations/delete.js';
+export * as 'admin/avatar-decorations/list' from './endpoints/admin/avatar-decorations/list.js';
+export * as 'admin/avatar-decorations/update' from './endpoints/admin/avatar-decorations/update.js';
+export * as 'admin/captcha/current' from './endpoints/admin/captcha/current.js';
+export * as 'admin/captcha/save' from './endpoints/admin/captcha/save.js';
+export * as 'admin/cw-user' from './endpoints/admin/cw-user.js';
+export * as 'admin/decline-user' from './endpoints/admin/decline-user.js';
+export * as 'admin/delete-account' from './endpoints/admin/delete-account.js';
+export * as 'admin/delete-all-files-of-a-user' from './endpoints/admin/delete-all-files-of-a-user.js';
+export * as 'admin/drive/clean-remote-files' from './endpoints/admin/drive/clean-remote-files.js';
+export * as 'admin/drive/cleanup' from './endpoints/admin/drive/cleanup.js';
+export * as 'admin/drive/files' from './endpoints/admin/drive/files.js';
+export * as 'admin/drive/show-file' from './endpoints/admin/drive/show-file.js';
+export * as 'admin/emoji/add' from './endpoints/admin/emoji/add.js';
+export * as 'admin/emoji/add-aliases-bulk' from './endpoints/admin/emoji/add-aliases-bulk.js';
+export * as 'admin/emoji/copy' from './endpoints/admin/emoji/copy.js';
+export * as 'admin/emoji/delete' from './endpoints/admin/emoji/delete.js';
+export * as 'admin/emoji/delete-bulk' from './endpoints/admin/emoji/delete-bulk.js';
+export * as 'admin/emoji/import-zip' from './endpoints/admin/emoji/import-zip.js';
+export * as 'admin/emoji/list' from './endpoints/admin/emoji/list.js';
+export * as 'admin/emoji/list-remote' from './endpoints/admin/emoji/list-remote.js';
+export * as 'admin/emoji/remove-aliases-bulk' from './endpoints/admin/emoji/remove-aliases-bulk.js';
+export * as 'admin/emoji/set-aliases-bulk' from './endpoints/admin/emoji/set-aliases-bulk.js';
+export * as 'admin/emoji/set-category-bulk' from './endpoints/admin/emoji/set-category-bulk.js';
+export * as 'admin/emoji/set-license-bulk' from './endpoints/admin/emoji/set-license-bulk.js';
+export * as 'admin/emoji/update' from './endpoints/admin/emoji/update.js';
+export * as 'admin/federation/delete-all-files' from './endpoints/admin/federation/delete-all-files.js';
+export * as 'admin/federation/refresh-remote-instance-metadata' from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
+export * as 'admin/federation/remove-all-following' from './endpoints/admin/federation/remove-all-following.js';
+export * as 'admin/federation/update-instance' from './endpoints/admin/federation/update-instance.js';
+export * as 'admin/forward-abuse-user-report' from './endpoints/admin/forward-abuse-user-report.js';
+export * as 'admin/gen-vapid-keys' from './endpoints/admin/gen-vapid-keys.js';
+export * as 'admin/get-index-stats' from './endpoints/admin/get-index-stats.js';
+export * as 'admin/get-table-stats' from './endpoints/admin/get-table-stats.js';
+export * as 'admin/get-user-ips' from './endpoints/admin/get-user-ips.js';
+export * as 'admin/invite/create' from './endpoints/admin/invite/create.js';
+export * as 'admin/invite/list' from './endpoints/admin/invite/list.js';
+export * as 'admin/meta' from './endpoints/admin/meta.js';
+export * as 'admin/nsfw-user' from './endpoints/admin/nsfw-user.js';
+export * as 'admin/promo/create' from './endpoints/admin/promo/create.js';
+export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js';
+export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js';
+export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js';
+export * as 'admin/queue/promote' from './endpoints/admin/queue/promote.js';
+export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
+export * as 'admin/reject-quotes' from './endpoints/admin/reject-quotes.js';
+export * as 'admin/relays/add' from './endpoints/admin/relays/add.js';
+export * as 'admin/relays/list' from './endpoints/admin/relays/list.js';
+export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js';
+export * as 'admin/reset-password' from './endpoints/admin/reset-password.js';
+export * as 'admin/resolve-abuse-user-report' from './endpoints/admin/resolve-abuse-user-report.js';
+export * as 'admin/roles/assign' from './endpoints/admin/roles/assign.js';
+export * as 'admin/roles/create' from './endpoints/admin/roles/create.js';
+export * as 'admin/roles/delete' from './endpoints/admin/roles/delete.js';
+export * as 'admin/roles/list' from './endpoints/admin/roles/list.js';
+export * as 'admin/roles/show' from './endpoints/admin/roles/show.js';
+export * as 'admin/roles/unassign' from './endpoints/admin/roles/unassign.js';
+export * as 'admin/roles/update' from './endpoints/admin/roles/update.js';
+export * as 'admin/roles/update-default-policies' from './endpoints/admin/roles/update-default-policies.js';
+export * as 'admin/roles/users' from './endpoints/admin/roles/users.js';
+export * as 'admin/send-email' from './endpoints/admin/send-email.js';
+export * as 'admin/server-info' from './endpoints/admin/server-info.js';
+export * as 'admin/show-moderation-logs' from './endpoints/admin/show-moderation-logs.js';
+export * as 'admin/show-user' from './endpoints/admin/show-user.js';
+export * as 'admin/show-users' from './endpoints/admin/show-users.js';
+export * as 'admin/silence-user' from './endpoints/admin/silence-user.js';
+export * as 'admin/suspend-user' from './endpoints/admin/suspend-user.js';
+export * as 'admin/system-webhook/create' from './endpoints/admin/system-webhook/create.js';
+export * as 'admin/system-webhook/delete' from './endpoints/admin/system-webhook/delete.js';
+export * as 'admin/system-webhook/list' from './endpoints/admin/system-webhook/list.js';
+export * as 'admin/system-webhook/show' from './endpoints/admin/system-webhook/show.js';
+export * as 'admin/system-webhook/test' from './endpoints/admin/system-webhook/test.js';
+export * as 'admin/system-webhook/update' from './endpoints/admin/system-webhook/update.js';
+export * as 'admin/unnsfw-user' from './endpoints/admin/unnsfw-user.js';
+export * as 'admin/unset-user-avatar' from './endpoints/admin/unset-user-avatar.js';
+export * as 'admin/unset-user-banner' from './endpoints/admin/unset-user-banner.js';
+export * as 'admin/unsilence-user' from './endpoints/admin/unsilence-user.js';
+export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js';
+export * as 'admin/update-abuse-user-report' from './endpoints/admin/update-abuse-user-report.js';
+export * as 'admin/update-meta' from './endpoints/admin/update-meta.js';
+export * as 'admin/update-user-note' from './endpoints/admin/update-user-note.js';
+export * as 'announcements' from './endpoints/announcements.js';
+export * as 'announcements/show' from './endpoints/announcements/show.js';
+export * as 'antennas/create' from './endpoints/antennas/create.js';
+export * as 'antennas/delete' from './endpoints/antennas/delete.js';
+export * as 'antennas/list' from './endpoints/antennas/list.js';
+export * as 'antennas/notes' from './endpoints/antennas/notes.js';
+export * as 'antennas/show' from './endpoints/antennas/show.js';
+export * as 'antennas/update' from './endpoints/antennas/update.js';
+export * as 'ap/get' from './endpoints/ap/get.js';
+export * as 'ap/show' from './endpoints/ap/show.js';
+export * as 'app/create' from './endpoints/app/create.js';
+export * as 'app/show' from './endpoints/app/show.js';
+export * as 'auth/accept' from './endpoints/auth/accept.js';
+export * as 'auth/session/generate' from './endpoints/auth/session/generate.js';
+export * as 'auth/session/show' from './endpoints/auth/session/show.js';
+export * as 'auth/session/userkey' from './endpoints/auth/session/userkey.js';
+export * as 'blocking/create' from './endpoints/blocking/create.js';
+export * as 'blocking/delete' from './endpoints/blocking/delete.js';
+export * as 'blocking/list' from './endpoints/blocking/list.js';
+export * as 'bubble-game/ranking' from './endpoints/bubble-game/ranking.js';
+export * as 'bubble-game/register' from './endpoints/bubble-game/register.js';
+export * as 'channels/create' from './endpoints/channels/create.js';
+export * as 'channels/favorite' from './endpoints/channels/favorite.js';
+export * as 'channels/featured' from './endpoints/channels/featured.js';
+export * as 'channels/follow' from './endpoints/channels/follow.js';
+export * as 'channels/followed' from './endpoints/channels/followed.js';
+export * as 'channels/my-favorites' from './endpoints/channels/my-favorites.js';
+export * as 'channels/owned' from './endpoints/channels/owned.js';
+export * as 'channels/search' from './endpoints/channels/search.js';
+export * as 'channels/show' from './endpoints/channels/show.js';
+export * as 'channels/timeline' from './endpoints/channels/timeline.js';
+export * as 'channels/unfavorite' from './endpoints/channels/unfavorite.js';
+export * as 'channels/unfollow' from './endpoints/channels/unfollow.js';
+export * as 'channels/update' from './endpoints/channels/update.js';
+export * as 'charts/active-users' from './endpoints/charts/active-users.js';
+export * as 'charts/ap-request' from './endpoints/charts/ap-request.js';
+export * as 'charts/drive' from './endpoints/charts/drive.js';
+export * as 'charts/federation' from './endpoints/charts/federation.js';
+export * as 'charts/instance' from './endpoints/charts/instance.js';
+export * as 'charts/notes' from './endpoints/charts/notes.js';
+export * as 'charts/user/drive' from './endpoints/charts/user/drive.js';
+export * as 'charts/user/following' from './endpoints/charts/user/following.js';
+export * as 'charts/user/notes' from './endpoints/charts/user/notes.js';
+export * as 'charts/user/pv' from './endpoints/charts/user/pv.js';
+export * as 'charts/user/reactions' from './endpoints/charts/user/reactions.js';
+export * as 'charts/users' from './endpoints/charts/users.js';
+export * as 'clips/add-note' from './endpoints/clips/add-note.js';
+export * as 'clips/create' from './endpoints/clips/create.js';
+export * as 'clips/delete' from './endpoints/clips/delete.js';
+export * as 'clips/favorite' from './endpoints/clips/favorite.js';
+export * as 'clips/list' from './endpoints/clips/list.js';
+export * as 'clips/my-favorites' from './endpoints/clips/my-favorites.js';
+export * as 'clips/notes' from './endpoints/clips/notes.js';
+export * as 'clips/remove-note' from './endpoints/clips/remove-note.js';
+export * as 'clips/show' from './endpoints/clips/show.js';
+export * as 'clips/unfavorite' from './endpoints/clips/unfavorite.js';
+export * as 'clips/update' from './endpoints/clips/update.js';
+export * as 'drive' from './endpoints/drive.js';
+export * as 'drive/files' from './endpoints/drive/files.js';
+export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js';
+export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js';
+export * as 'drive/files/create' from './endpoints/drive/files/create.js';
+export * as 'drive/files/delete' from './endpoints/drive/files/delete.js';
+export * as 'drive/files/find' from './endpoints/drive/files/find.js';
+export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js';
+export * as 'drive/files/show' from './endpoints/drive/files/show.js';
+export * as 'drive/files/update' from './endpoints/drive/files/update.js';
+export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js';
+export * as 'drive/folders' from './endpoints/drive/folders.js';
+export * as 'drive/folders/create' from './endpoints/drive/folders/create.js';
+export * as 'drive/folders/delete' from './endpoints/drive/folders/delete.js';
+export * as 'drive/folders/find' from './endpoints/drive/folders/find.js';
+export * as 'drive/folders/show' from './endpoints/drive/folders/show.js';
+export * as 'drive/folders/update' from './endpoints/drive/folders/update.js';
+export * as 'drive/stream' from './endpoints/drive/stream.js';
+export * as 'email-address/available' from './endpoints/email-address/available.js';
+export * as 'emoji' from './endpoints/emoji.js';
+export * as 'emojis' from './endpoints/emojis.js';
+export * as 'endpoint' from './endpoints/endpoint.js';
+export * as 'endpoints' from './endpoints/endpoints.js';
+export * as 'export-custom-emojis' from './endpoints/export-custom-emojis.js';
+export * as 'federation/followers' from './endpoints/federation/followers.js';
+export * as 'federation/following' from './endpoints/federation/following.js';
+export * as 'federation/instances' from './endpoints/federation/instances.js';
+export * as 'federation/show-instance' from './endpoints/federation/show-instance.js';
+export * as 'federation/stats' from './endpoints/federation/stats.js';
+export * as 'federation/update-remote-user' from './endpoints/federation/update-remote-user.js';
+export * as 'federation/users' from './endpoints/federation/users.js';
+export * as 'fetch-external-resources' from './endpoints/fetch-external-resources.js';
+export * as 'fetch-rss' from './endpoints/fetch-rss.js';
+export * as 'flash/create' from './endpoints/flash/create.js';
+export * as 'flash/delete' from './endpoints/flash/delete.js';
+export * as 'flash/featured' from './endpoints/flash/featured.js';
+export * as 'flash/like' from './endpoints/flash/like.js';
+export * as 'flash/my' from './endpoints/flash/my.js';
+export * as 'flash/my-likes' from './endpoints/flash/my-likes.js';
+export * as 'flash/show' from './endpoints/flash/show.js';
+export * as 'flash/unlike' from './endpoints/flash/unlike.js';
+export * as 'flash/update' from './endpoints/flash/update.js';
+export * as 'following/create' from './endpoints/following/create.js';
+export * as 'following/delete' from './endpoints/following/delete.js';
+export * as 'following/invalidate' from './endpoints/following/invalidate.js';
+export * as 'following/requests/accept' from './endpoints/following/requests/accept.js';
+export * as 'following/requests/cancel' from './endpoints/following/requests/cancel.js';
+export * as 'following/requests/list' from './endpoints/following/requests/list.js';
+export * as 'following/requests/reject' from './endpoints/following/requests/reject.js';
+export * as 'following/requests/sent' from './endpoints/following/requests/sent.js';
+export * as 'following/update' from './endpoints/following/update.js';
+export * as 'following/update-all' from './endpoints/following/update-all.js';
+export * as 'gallery/featured' from './endpoints/gallery/featured.js';
+export * as 'gallery/popular' from './endpoints/gallery/popular.js';
+export * as 'gallery/posts' from './endpoints/gallery/posts.js';
+export * as 'gallery/posts/create' from './endpoints/gallery/posts/create.js';
+export * as 'gallery/posts/delete' from './endpoints/gallery/posts/delete.js';
+export * as 'gallery/posts/like' from './endpoints/gallery/posts/like.js';
+export * as 'gallery/posts/show' from './endpoints/gallery/posts/show.js';
+export * as 'gallery/posts/unlike' from './endpoints/gallery/posts/unlike.js';
+export * as 'gallery/posts/update' from './endpoints/gallery/posts/update.js';
+export * as 'get-avatar-decorations' from './endpoints/get-avatar-decorations.js';
+export * as 'get-online-users-count' from './endpoints/get-online-users-count.js';
+export * as 'hashtags/list' from './endpoints/hashtags/list.js';
+export * as 'hashtags/search' from './endpoints/hashtags/search.js';
+export * as 'hashtags/show' from './endpoints/hashtags/show.js';
+export * as 'hashtags/trend' from './endpoints/hashtags/trend.js';
+export * as 'hashtags/users' from './endpoints/hashtags/users.js';
+export * as 'i' from './endpoints/i.js';
+export * as 'i/2fa/done' from './endpoints/i/2fa/done.js';
+export * as 'i/2fa/key-done' from './endpoints/i/2fa/key-done.js';
+export * as 'i/2fa/password-less' from './endpoints/i/2fa/password-less.js';
+export * as 'i/2fa/register' from './endpoints/i/2fa/register.js';
+export * as 'i/2fa/register-key' from './endpoints/i/2fa/register-key.js';
+export * as 'i/2fa/remove-key' from './endpoints/i/2fa/remove-key.js';
+export * as 'i/2fa/unregister' from './endpoints/i/2fa/unregister.js';
+export * as 'i/2fa/update-key' from './endpoints/i/2fa/update-key.js';
+export * as 'i/apps' from './endpoints/i/apps.js';
+export * as 'i/authorized-apps' from './endpoints/i/authorized-apps.js';
+export * as 'i/change-password' from './endpoints/i/change-password.js';
+export * as 'i/claim-achievement' from './endpoints/i/claim-achievement.js';
+export * as 'i/delete-account' from './endpoints/i/delete-account.js';
+export * as 'i/export-antennas' from './endpoints/i/export-antennas.js';
+export * as 'i/export-blocking' from './endpoints/i/export-blocking.js';
+export * as 'i/export-clips' from './endpoints/i/export-clips.js';
+export * as 'i/export-data' from './endpoints/i/export-data.js';
+export * as 'i/export-favorites' from './endpoints/i/export-favorites.js';
+export * as 'i/export-following' from './endpoints/i/export-following.js';
+export * as 'i/export-mute' from './endpoints/i/export-mute.js';
+export * as 'i/export-notes' from './endpoints/i/export-notes.js';
+export * as 'i/export-user-lists' from './endpoints/i/export-user-lists.js';
+export * as 'i/favorites' from './endpoints/i/favorites.js';
+export * as 'i/gallery/likes' from './endpoints/i/gallery/likes.js';
+export * as 'i/gallery/posts' from './endpoints/i/gallery/posts.js';
+export * as 'i/import-antennas' from './endpoints/i/import-antennas.js';
+export * as 'i/import-blocking' from './endpoints/i/import-blocking.js';
+export * as 'i/import-following' from './endpoints/i/import-following.js';
+export * as 'i/import-muting' from './endpoints/i/import-muting.js';
+export * as 'i/import-notes' from './endpoints/i/import-notes.js';
+export * as 'i/import-user-lists' from './endpoints/i/import-user-lists.js';
+export * as 'i/move' from './endpoints/i/move.js';
+export * as 'i/notifications' from './endpoints/i/notifications.js';
+export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped.js';
+export * as 'i/page-likes' from './endpoints/i/page-likes.js';
+export * as 'i/pages' from './endpoints/i/pages.js';
+export * as 'i/pin' from './endpoints/i/pin.js';
+export * as 'i/read-all-unread-notes' from './endpoints/i/read-all-unread-notes.js';
+export * as 'i/read-announcement' from './endpoints/i/read-announcement.js';
+export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js';
+export * as 'i/registry/get' from './endpoints/i/registry/get.js';
+export * as 'i/registry/get-all' from './endpoints/i/registry/get-all.js';
+export * as 'i/registry/get-detail' from './endpoints/i/registry/get-detail.js';
+export * as 'i/registry/get-unsecure' from './endpoints/i/registry/get-unsecure.js';
+export * as 'i/registry/keys' from './endpoints/i/registry/keys.js';
+export * as 'i/registry/keys-with-type' from './endpoints/i/registry/keys-with-type.js';
+export * as 'i/registry/remove' from './endpoints/i/registry/remove.js';
+export * as 'i/registry/scopes-with-domain' from './endpoints/i/registry/scopes-with-domain.js';
+export * as 'i/registry/set' from './endpoints/i/registry/set.js';
+export * as 'i/revoke-token' from './endpoints/i/revoke-token.js';
+export * as 'i/signin-history' from './endpoints/i/signin-history.js';
+export * as 'i/unpin' from './endpoints/i/unpin.js';
+export * as 'i/update' from './endpoints/i/update.js';
+export * as 'i/update-email' from './endpoints/i/update-email.js';
+export * as 'i/webhooks/create' from './endpoints/i/webhooks/create.js';
+export * as 'i/webhooks/delete' from './endpoints/i/webhooks/delete.js';
+export * as 'i/webhooks/list' from './endpoints/i/webhooks/list.js';
+export * as 'i/webhooks/show' from './endpoints/i/webhooks/show.js';
+export * as 'i/webhooks/test' from './endpoints/i/webhooks/test.js';
+export * as 'i/webhooks/update' from './endpoints/i/webhooks/update.js';
+export * as 'invite/create' from './endpoints/invite/create.js';
+export * as 'invite/delete' from './endpoints/invite/delete.js';
+export * as 'invite/limit' from './endpoints/invite/limit.js';
+export * as 'invite/list' from './endpoints/invite/list.js';
+export * as 'meta' from './endpoints/meta.js';
+export * as 'miauth/gen-token' from './endpoints/miauth/gen-token.js';
+export * as 'mute/create' from './endpoints/mute/create.js';
+export * as 'mute/delete' from './endpoints/mute/delete.js';
+export * as 'mute/list' from './endpoints/mute/list.js';
+export * as 'my/apps' from './endpoints/my/apps.js';
+export * as 'notes' from './endpoints/notes.js';
+export * as 'notes/bubble-timeline' from './endpoints/notes/bubble-timeline.js';
+export * as 'notes/children' from './endpoints/notes/children.js';
+export * as 'notes/clips' from './endpoints/notes/clips.js';
+export * as 'notes/conversation' from './endpoints/notes/conversation.js';
+export * as 'notes/create' from './endpoints/notes/create.js';
+export * as 'notes/delete' from './endpoints/notes/delete.js';
+export * as 'notes/edit' from './endpoints/notes/edit.js';
+export * as 'notes/favorites/create' from './endpoints/notes/favorites/create.js';
+export * as 'notes/favorites/delete' from './endpoints/notes/favorites/delete.js';
+export * as 'notes/featured' from './endpoints/notes/featured.js';
+export * as 'notes/following' from './endpoints/notes/following.js';
+export * as 'notes/global-timeline' from './endpoints/notes/global-timeline.js';
+export * as 'notes/hybrid-timeline' from './endpoints/notes/hybrid-timeline.js';
+export * as 'notes/like' from './endpoints/notes/like.js';
+export * as 'notes/local-timeline' from './endpoints/notes/local-timeline.js';
+export * as 'notes/mentions' from './endpoints/notes/mentions.js';
+export * as 'notes/polls/recommendation' from './endpoints/notes/polls/recommendation.js';
+export * as 'notes/polls/refresh' from './endpoints/notes/polls/refresh.js';
+export * as 'notes/polls/vote' from './endpoints/notes/polls/vote.js';
+export * as 'notes/reactions' from './endpoints/notes/reactions.js';
+export * as 'notes/reactions/create' from './endpoints/notes/reactions/create.js';
+export * as 'notes/reactions/delete' from './endpoints/notes/reactions/delete.js';
+export * as 'notes/renotes' from './endpoints/notes/renotes.js';
+export * as 'notes/replies' from './endpoints/notes/replies.js';
+export * as 'notes/search' from './endpoints/notes/search.js';
+export * as 'notes/schedule/create' from './endpoints/notes/schedule/create.js';
+export * as 'notes/schedule/delete' from './endpoints/notes/schedule/delete.js';
+export * as 'notes/schedule/list' from './endpoints/notes/schedule/list.js';
+export * as 'notes/search-by-tag' from './endpoints/notes/search-by-tag.js';
+export * as 'notes/show' from './endpoints/notes/show.js';
+export * as 'notes/state' from './endpoints/notes/state.js';
+export * as 'notes/thread-muting/create' from './endpoints/notes/thread-muting/create.js';
+export * as 'notes/thread-muting/delete' from './endpoints/notes/thread-muting/delete.js';
+export * as 'notes/timeline' from './endpoints/notes/timeline.js';
+export * as 'notes/translate' from './endpoints/notes/translate.js';
+export * as 'notes/unrenote' from './endpoints/notes/unrenote.js';
+export * as 'notes/user-list-timeline' from './endpoints/notes/user-list-timeline.js';
+export * as 'notes/versions' from './endpoints/notes/versions.js';
+export * as 'notifications/create' from './endpoints/notifications/create.js';
+export * as 'notifications/flush' from './endpoints/notifications/flush.js';
+export * as 'notifications/mark-all-as-read' from './endpoints/notifications/mark-all-as-read.js';
+export * as 'notifications/test-notification' from './endpoints/notifications/test-notification.js';
+export * as 'page-push' from './endpoints/page-push.js';
+export * as 'pages/create' from './endpoints/pages/create.js';
+export * as 'pages/delete' from './endpoints/pages/delete.js';
+export * as 'pages/featured' from './endpoints/pages/featured.js';
+export * as 'pages/like' from './endpoints/pages/like.js';
+export * as 'pages/show' from './endpoints/pages/show.js';
+export * as 'pages/unlike' from './endpoints/pages/unlike.js';
+export * as 'pages/update' from './endpoints/pages/update.js';
+export * as 'ping' from './endpoints/ping.js';
+export * as 'pinned-users' from './endpoints/pinned-users.js';
+export * as 'promo/read' from './endpoints/promo/read.js';
+export * as 'renote-mute/create' from './endpoints/renote-mute/create.js';
+export * as 'renote-mute/delete' from './endpoints/renote-mute/delete.js';
+export * as 'renote-mute/list' from './endpoints/renote-mute/list.js';
+export * as 'request-reset-password' from './endpoints/request-reset-password.js';
+export * as 'reset-db' from './endpoints/reset-db.js';
+export * as 'reset-password' from './endpoints/reset-password.js';
+export * as 'retention' from './endpoints/retention.js';
+export * as 'reversi/cancel-match' from './endpoints/reversi/cancel-match.js';
+export * as 'reversi/games' from './endpoints/reversi/games.js';
+export * as 'reversi/invitations' from './endpoints/reversi/invitations.js';
+export * as 'reversi/match' from './endpoints/reversi/match.js';
+export * as 'reversi/show-game' from './endpoints/reversi/show-game.js';
+export * as 'reversi/surrender' from './endpoints/reversi/surrender.js';
+export * as 'reversi/verify' from './endpoints/reversi/verify.js';
+export * as 'roles/list' from './endpoints/roles/list.js';
+export * as 'roles/notes' from './endpoints/roles/notes.js';
+export * as 'roles/show' from './endpoints/roles/show.js';
+export * as 'roles/users' from './endpoints/roles/users.js';
+export * as 'server-info' from './endpoints/server-info.js';
+export * as 'sponsors' from './endpoints/sponsors.js';
+export * as 'stats' from './endpoints/stats.js';
+export * as 'sw/register' from './endpoints/sw/register.js';
+export * as 'sw/show-registration' from './endpoints/sw/show-registration.js';
+export * as 'sw/unregister' from './endpoints/sw/unregister.js';
+export * as 'sw/update-registration' from './endpoints/sw/update-registration.js';
+export * as 'test' from './endpoints/test.js';
+export * as 'username/available' from './endpoints/username/available.js';
+export * as 'users' from './endpoints/users.js';
+export * as 'users/achievements' from './endpoints/users/achievements.js';
+export * as 'users/clips' from './endpoints/users/clips.js';
+export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
+export * as 'users/flashs' from './endpoints/users/flashs.js';
+export * as 'users/followers' from './endpoints/users/followers.js';
+export * as 'users/following' from './endpoints/users/following.js';
+export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
+export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';
+export * as 'users/lists/create' from './endpoints/users/lists/create.js';
+export * as 'users/lists/create-from-public' from './endpoints/users/lists/create-from-public.js';
+export * as 'users/lists/delete' from './endpoints/users/lists/delete.js';
+export * as 'users/lists/favorite' from './endpoints/users/lists/favorite.js';
+export * as 'users/lists/get-memberships' from './endpoints/users/lists/get-memberships.js';
+export * as 'users/lists/list' from './endpoints/users/lists/list.js';
+export * as 'users/lists/pull' from './endpoints/users/lists/pull.js';
+export * as 'users/lists/push' from './endpoints/users/lists/push.js';
+export * as 'users/lists/show' from './endpoints/users/lists/show.js';
+export * as 'users/lists/unfavorite' from './endpoints/users/lists/unfavorite.js';
+export * as 'users/lists/update' from './endpoints/users/lists/update.js';
+export * as 'users/lists/update-membership' from './endpoints/users/lists/update-membership.js';
+export * as 'users/notes' from './endpoints/users/notes.js';
+export * as 'users/pages' from './endpoints/users/pages.js';
+export * as 'users/reactions' from './endpoints/users/reactions.js';
+export * as 'users/recommendation' from './endpoints/users/recommendation.js';
+export * as 'users/relation' from './endpoints/users/relation.js';
+export * as 'users/report-abuse' from './endpoints/users/report-abuse.js';
+export * as 'users/search' from './endpoints/users/search.js';
+export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js';
+export * as 'users/show' from './endpoints/users/show.js';
+export * as 'users/update-memo' from './endpoints/users/update-memo.js';
+export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index b4f36234f0..fd6b9bb14b 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -7,821 +7,7 @@ import { permissions } from 'misskey-js';
import type { KeyOf, Schema } from '@/misc/json-schema.js';
import type { RateLimit } from '@/misc/rate-limit-utils.js';
-import * as ep___admin_abuseReport_notificationRecipient_list
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
-import * as ep___admin_abuseReport_notificationRecipient_show
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
-import * as ep___admin_abuseReport_notificationRecipient_create
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
-import * as ep___admin_abuseReport_notificationRecipient_update
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
-import * as ep___admin_abuseReport_notificationRecipient_delete
- from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
-import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
-import * as ep___admin_meta from './endpoints/admin/meta.js';
-import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
-import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
-import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
-import * as ep___admin_ad_create from './endpoints/admin/ad/create.js';
-import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js';
-import * as ep___admin_ad_list from './endpoints/admin/ad/list.js';
-import * as ep___admin_ad_update from './endpoints/admin/ad/update.js';
-import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js';
-import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
-import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
-import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
-import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
-import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
-import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
-import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
-import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
-import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
-import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
-import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
-import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
-import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
-import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js';
-import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js';
-import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js';
-import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js';
-import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js';
-import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js';
-import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js';
-import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js';
-import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
-import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
-import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
-import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
-import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
-import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
-import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
-import * as ep___admin_federation_refreshRemoteInstanceMetadata
- from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
-import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
-import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
-import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
-import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
-import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
-import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
-import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
-import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
-import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
-import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
-import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js';
-import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.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_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
-import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-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_nsfwUser from './endpoints/admin/nsfw-user.js';
-import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
-import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
-import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
-import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
-import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
-import * as ep___admin_declineUser from './endpoints/admin/decline-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_deleteAccount from './endpoints/admin/delete-account.js';
-import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
-import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
-import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
-import * as ep___admin_roles_list from './endpoints/admin/roles/list.js';
-import * as ep___admin_roles_show from './endpoints/admin/roles/show.js';
-import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
-import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
-import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
-import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
-import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
-import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
-import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
-import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
-import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
-import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
-import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
-import * as ep___announcements from './endpoints/announcements.js';
-import * as ep___announcements_show from './endpoints/announcements/show.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_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___channels_favorite from './endpoints/channels/favorite.js';
-import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
-import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
-import * as ep___channels_search from './endpoints/channels/search.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_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_pv from './endpoints/charts/user/pv.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___clips_favorite from './endpoints/clips/favorite.js';
-import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
-import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.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_update from './endpoints/following/update.js';
-import * as ep___following_update_all from './endpoints/following/update-all.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_sent from './endpoints/following/requests/sent.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___getAvatarDecorations from './endpoints/get-avatar-decorations.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_updateKey from './endpoints/i/2fa/update-key.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_claimAchievement from './endpoints/i/claim-achievement.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_exportData from './endpoints/i/export-data.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_exportClips from './endpoints/i/export-clips.js';
-import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
-import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
-import * as ep___i_exportAntennas from './endpoints/i/export-antennas.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_importBlocking from './endpoints/i/import-blocking.js';
-import * as ep___i_importFollowing from './endpoints/i/import-following.js';
-import * as ep___i_importNotes from './endpoints/i/import-notes.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_importAntennas from './endpoints/i/import-antennas.js';
-import * as ep___i_notifications from './endpoints/i/notifications.js';
-import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.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_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_getUnsecure from './endpoints/i/registry/get-unsecure.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_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.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_move from './endpoints/i/move.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___i_webhooks_test from './endpoints/i/webhooks/test.js';
-import * as ep___invite_create from './endpoints/invite/create.js';
-import * as ep___invite_delete from './endpoints/invite/delete.js';
-import * as ep___invite_list from './endpoints/invite/list.js';
-import * as ep___invite_limit from './endpoints/invite/limit.js';
-import * as ep___meta from './endpoints/meta.js';
-import * as ep___emojis from './endpoints/emojis.js';
-import * as ep___emoji from './endpoints/emoji.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___renoteMute_create from './endpoints/renote-mute/create.js';
-import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
-import * as ep___renoteMute_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_following from './endpoints/notes/following.js';
-import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
-import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-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_polls_refresh from './endpoints/notes/polls/refresh.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_like from './endpoints/notes/like.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_schedule_create from './endpoints/notes/schedule/create.js';
-import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js';
-import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.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_edit from './endpoints/notes/edit.js';
-import * as ep___notes_versions from './endpoints/notes/versions.js';
-import * as ep___notifications_create from './endpoints/notifications/create.js';
-import * as ep___notifications_flush from './endpoints/notifications/flush.js';
-import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
-import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.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___flash_create from './endpoints/flash/create.js';
-import * as ep___flash_delete from './endpoints/flash/delete.js';
-import * as ep___flash_featured from './endpoints/flash/featured.js';
-import * as ep___flash_like from './endpoints/flash/like.js';
-import * as ep___flash_show from './endpoints/flash/show.js';
-import * as ep___flash_unlike from './endpoints/flash/unlike.js';
-import * as ep___flash_update from './endpoints/flash/update.js';
-import * as ep___flash_my from './endpoints/flash/my.js';
-import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
-import * as ep___ping from './endpoints/ping.js';
-import * as ep___pinnedUsers from './endpoints/pinned-users.js';
-import * as ep___promo_read from './endpoints/promo/read.js';
-import * as ep___roles_list from './endpoints/roles/list.js';
-import * as ep___roles_show from './endpoints/roles/show.js';
-import * as ep___roles_users from './endpoints/roles/users.js';
-import * as ep___roles_notes from './endpoints/roles/notes.js';
-import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
-import * as ep___resetDb from './endpoints/reset-db.js';
-import * as ep___resetPassword from './endpoints/reset-password.js';
-import * as ep___serverInfo from './endpoints/server-info.js';
-import * as ep___stats from './endpoints/stats.js';
-import * as ep___sw_show_registration from './endpoints/sw/show-registration.js';
-import * as ep___sw_update_registration from './endpoints/sw/update-registration.js';
-import * as ep___sw_register from './endpoints/sw/register.js';
-import * as ep___sw_unregister from './endpoints/sw/unregister.js';
-import * as ep___test from './endpoints/test.js';
-import * as ep___username_available from './endpoints/username/available.js';
-import * as ep___users from './endpoints/users.js';
-import * as ep___users_clips from './endpoints/users/clips.js';
-import * as ep___users_followers from './endpoints/users/followers.js';
-import * as ep___users_following from './endpoints/users/following.js';
-import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
-import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
-import * as ep___users_featuredNotes from './endpoints/users/featured-notes.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_favorite from './endpoints/users/lists/favorite.js';
-import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
-import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
-import * as ep___users_lists_update from './endpoints/users/lists/update.js';
-import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
-import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.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_flashs from './endpoints/users/flashs.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_achievements from './endpoints/users/achievements.js';
-import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
-import * as ep___fetchRss from './endpoints/fetch-rss.js';
-import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
-import * as ep___retention from './endpoints/retention.js';
-import * as ep___sponsors from './endpoints/sponsors.js';
-import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
-import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
-import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js';
-import * as ep___reversi_games from './endpoints/reversi/games.js';
-import * as ep___reversi_match from './endpoints/reversi/match.js';
-import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
-import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
-import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
-import * as ep___reversi_verify from './endpoints/reversi/verify.js';
-
-const eps = [
- ['admin/meta', ep___admin_meta],
- ['admin/abuse-user-reports', ep___admin_abuseUserReports],
- ['admin/abuse-report/notification-recipient/list', ep___admin_abuseReport_notificationRecipient_list],
- ['admin/abuse-report/notification-recipient/show', ep___admin_abuseReport_notificationRecipient_show],
- ['admin/abuse-report/notification-recipient/create', ep___admin_abuseReport_notificationRecipient_create],
- ['admin/abuse-report/notification-recipient/update', ep___admin_abuseReport_notificationRecipient_update],
- ['admin/abuse-report/notification-recipient/delete', ep___admin_abuseReport_notificationRecipient_delete],
- ['admin/accounts/create', ep___admin_accounts_create],
- ['admin/accounts/delete', ep___admin_accounts_delete],
- ['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
- ['admin/ad/create', ep___admin_ad_create],
- ['admin/ad/delete', ep___admin_ad_delete],
- ['admin/ad/list', ep___admin_ad_list],
- ['admin/ad/update', ep___admin_ad_update],
- ['admin/announcements/create', ep___admin_announcements_create],
- ['admin/announcements/delete', ep___admin_announcements_delete],
- ['admin/announcements/list', ep___admin_announcements_list],
- ['admin/announcements/update', ep___admin_announcements_update],
- ['admin/avatar-decorations/create', ep___admin_avatarDecorations_create],
- ['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete],
- ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
- ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
- ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
- ['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
- ['admin/unset-user-banner', ep___admin_unsetUserBanner],
- ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
- ['admin/drive/cleanup', ep___admin_drive_cleanup],
- ['admin/drive/files', ep___admin_drive_files],
- ['admin/drive/show-file', ep___admin_drive_showFile],
- ['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk],
- ['admin/emoji/add', ep___admin_emoji_add],
- ['admin/emoji/copy', ep___admin_emoji_copy],
- ['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk],
- ['admin/emoji/delete', ep___admin_emoji_delete],
- ['admin/emoji/import-zip', ep___admin_emoji_importZip],
- ['admin/emoji/list-remote', ep___admin_emoji_listRemote],
- ['admin/emoji/list', ep___admin_emoji_list],
- ['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk],
- ['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk],
- ['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk],
- ['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
- ['admin/emoji/update', ep___admin_emoji_update],
- ['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
- ['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
- ['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],
- ['admin/federation/update-instance', ep___admin_federation_updateInstance],
- ['admin/get-index-stats', ep___admin_getIndexStats],
- ['admin/get-table-stats', ep___admin_getTableStats],
- ['admin/get-user-ips', ep___admin_getUserIps],
- ['admin/invite/create', ep___admin_invite_create],
- ['admin/invite/list', ep___admin_invite_list],
- ['admin/promo/create', ep___admin_promo_create],
- ['admin/queue/clear', ep___admin_queue_clear],
- ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
- ['admin/queue/inbox-delayed', ep___admin_queue_inboxDelayed],
- ['admin/queue/promote', ep___admin_queue_promote],
- ['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/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
- ['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
- ['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/nsfw-user', ep___admin_nsfwUser],
- ['admin/unnsfw-user', ep___admin_unnsfwUser],
- ['admin/silence-user', ep___admin_silenceUser],
- ['admin/unsilence-user', ep___admin_unsilenceUser],
- ['admin/suspend-user', ep___admin_suspendUser],
- ['admin/approve-user', ep___admin_approveUser],
- ['admin/decline-user', ep___admin_declineUser],
- ['admin/unsuspend-user', ep___admin_unsuspendUser],
- ['admin/update-meta', ep___admin_updateMeta],
- ['admin/delete-account', ep___admin_deleteAccount],
- ['admin/update-user-note', ep___admin_updateUserNote],
- ['admin/roles/create', ep___admin_roles_create],
- ['admin/roles/delete', ep___admin_roles_delete],
- ['admin/roles/list', ep___admin_roles_list],
- ['admin/roles/show', ep___admin_roles_show],
- ['admin/roles/update', ep___admin_roles_update],
- ['admin/roles/assign', ep___admin_roles_assign],
- ['admin/roles/unassign', ep___admin_roles_unassign],
- ['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
- ['admin/roles/users', ep___admin_roles_users],
- ['admin/system-webhook/create', ep___admin_systemWebhook_create],
- ['admin/system-webhook/delete', ep___admin_systemWebhook_delete],
- ['admin/system-webhook/list', ep___admin_systemWebhook_list],
- ['admin/system-webhook/show', ep___admin_systemWebhook_show],
- ['admin/system-webhook/update', ep___admin_systemWebhook_update],
- ['admin/system-webhook/test', ep___admin_systemWebhook_test],
- ['announcements', ep___announcements],
- ['announcements/show', ep___announcements_show],
- ['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/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],
- ['channels/favorite', ep___channels_favorite],
- ['channels/unfavorite', ep___channels_unfavorite],
- ['channels/my-favorites', ep___channels_myFavorites],
- ['channels/search', ep___channels_search],
- ['charts/active-users', ep___charts_activeUsers],
- ['charts/ap-request', ep___charts_apRequest],
- ['charts/drive', ep___charts_drive],
- ['charts/federation', ep___charts_federation],
- ['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/pv', ep___charts_user_pv],
- ['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],
- ['clips/favorite', ep___clips_favorite],
- ['clips/unfavorite', ep___clips_unfavorite],
- ['clips/my-favorites', ep___clips_myFavorites],
- ['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/update', ep___following_update],
- ['following/update-all', ep___following_update_all],
- ['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/sent', ep___following_requests_sent],
- ['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],
- ['get-avatar-decorations', ep___getAvatarDecorations],
- ['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/update-key', ep___i_2fa_updateKey],
- ['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/claim-achievement', ep___i_claimAchievement],
- ['i/change-password', ep___i_changePassword],
- ['i/delete-account', ep___i_deleteAccount],
- ['i/export-data', ep___i_exportData],
- ['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-clips', ep___i_exportClips],
- ['i/export-favorites', ep___i_exportFavorites],
- ['i/export-user-lists', ep___i_exportUserLists],
- ['i/export-antennas', ep___i_exportAntennas],
- ['i/favorites', ep___i_favorites],
- ['i/gallery/likes', ep___i_gallery_likes],
- ['i/gallery/posts', ep___i_gallery_posts],
- ['i/import-blocking', ep___i_importBlocking],
- ['i/import-following', ep___i_importFollowing],
- ['i/import-notes', ep___i_importNotes],
- ['i/import-muting', ep___i_importMuting],
- ['i/import-user-lists', ep___i_importUserLists],
- ['i/import-antennas', ep___i_importAntennas],
- ['i/notifications', ep___i_notifications],
- ['i/notifications-grouped', ep___i_notificationsGrouped],
- ['i/page-likes', ep___i_pageLikes],
- ['i/pages', ep___i_pages],
- ['i/pin', ep___i_pin],
- ['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-unsecure', ep___i_registry_getUnsecure],
- ['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-with-domain', ep___i_registry_scopesWithDomain],
- ['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/move', ep___i_move],
- ['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],
- ['i/webhooks/test', ep___i_webhooks_test],
- ['invite/create', ep___invite_create],
- ['invite/delete', ep___invite_delete],
- ['invite/list', ep___invite_list],
- ['invite/limit', ep___invite_limit],
- ['meta', ep___meta],
- ['emojis', ep___emojis],
- ['emoji', ep___emoji],
- ['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___renoteMute_create],
- ['renote-mute/delete', ep___renoteMute_delete],
- ['renote-mute/list', ep___renoteMute_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/following', ep___notes_following],
- ['notes/global-timeline', ep___notes_globalTimeline],
- ['notes/bubble-timeline', ep___notes_bubbleTimeline],
- ['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/polls/refresh', ep___notes_polls_refresh],
- ['notes/reactions', ep___notes_reactions],
- ['notes/reactions/create', ep___notes_reactions_create],
- ['notes/reactions/delete', ep___notes_reactions_delete],
- ['notes/like', ep___notes_like],
- ['notes/renotes', ep___notes_renotes],
- ['notes/replies', ep___notes_replies],
- ['notes/schedule/create', ep___notes_schedule_create],
- ['notes/schedule/delete', ep___notes_schedule_delete],
- ['notes/schedule/list', ep___notes_schedule_list],
- ['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/edit', ep___notes_edit],
- ['notes/versions', ep___notes_versions],
- ['notifications/create', ep___notifications_create],
- ['notifications/flush', ep___notifications_flush],
- ['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
- ['notifications/test-notification', ep___notifications_testNotification],
- ['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],
- ['flash/create', ep___flash_create],
- ['flash/delete', ep___flash_delete],
- ['flash/featured', ep___flash_featured],
- ['flash/like', ep___flash_like],
- ['flash/show', ep___flash_show],
- ['flash/unlike', ep___flash_unlike],
- ['flash/update', ep___flash_update],
- ['flash/my', ep___flash_my],
- ['flash/my-likes', ep___flash_myLikes],
- ['ping', ep___ping],
- ['pinned-users', ep___pinnedUsers],
- ['promo/read', ep___promo_read],
- ['roles/list', ep___roles_list],
- ['roles/show', ep___roles_show],
- ['roles/users', ep___roles_users],
- ['roles/notes', ep___roles_notes],
- ['request-reset-password', ep___requestResetPassword],
- ['reset-db', ep___resetDb],
- ['reset-password', ep___resetPassword],
- ['server-info', ep___serverInfo],
- ['stats', ep___stats],
- ['sw/show-registration', ep___sw_show_registration],
- ['sw/update-registration', ep___sw_update_registration],
- ['sw/register', ep___sw_register],
- ['sw/unregister', ep___sw_unregister],
- ['test', ep___test],
- ['username/available', ep___username_available],
- ['users', ep___users],
- ['users/clips', ep___users_clips],
- ['users/followers', ep___users_followers],
- ['users/following', ep___users_following],
- ['users/gallery/posts', ep___users_gallery_posts],
- ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
- ['users/featured-notes', ep___users_featuredNotes],
- ['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/favorite', ep___users_lists_favorite],
- ['users/lists/unfavorite', ep___users_lists_unfavorite],
- ['users/lists/update', ep___users_lists_update],
- ['users/lists/create-from-public', ep___users_lists_createFromPublic],
- ['users/lists/update-membership', ep___users_lists_updateMembership],
- ['users/lists/get-memberships', ep___users_lists_getMemberships],
- ['users/notes', ep___users_notes],
- ['users/pages', ep___users_pages],
- ['users/flashs', ep___users_flashs],
- ['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/achievements', ep___users_achievements],
- ['users/update-memo', ep___users_updateMemo],
- ['fetch-rss', ep___fetchRss],
- ['fetch-external-resources', ep___fetchExternalResources],
- ['retention', ep___retention],
- ['sponsors', ep___sponsors],
- ['bubble-game/register', ep___bubbleGame_register],
- ['bubble-game/ranking', ep___bubbleGame_ranking],
- ['reversi/cancel-match', ep___reversi_cancelMatch],
- ['reversi/games', ep___reversi_games],
- ['reversi/match', ep___reversi_match],
- ['reversi/invitations', ep___reversi_invitations],
- ['reversi/show-game', ep___reversi_showGame],
- ['reversi/surrender', ep___reversi_surrender],
- ['reversi/verify', ep___reversi_verify],
-];
+import * as endpointsObject from './endpoint-list.js';
interface IEndpointMetaBase {
readonly stability?: 'deprecated' | 'experimental' | 'stable';
@@ -922,7 +108,7 @@ export interface IEndpoint {
params: Schema;
}
-const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
+const endpoints: IEndpoint[] = Object.entries(endpointsObject).map(([name, ep]) => {
return {
name: name,
get meta() {
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index 53b1c4c4ec..1a47f56bc6 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
-import { MiAccessToken, MiUser } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
@@ -16,6 +15,7 @@ import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
import { Packed } from '@/misc/json-schema.js';
import { RoleService } from '@/core/RoleService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -97,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
@@ -138,6 +139,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
approved: true,
});
+ if (me) {
+ await this.moderationLogService.log(me, 'createAccount', {
+ userId: account.id,
+ userUsername: account.username,
+ });
+ }
+
const res = await this.userEntityService.pack(account, account, {
schema: 'MeDetailed',
includeSecrets: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/current.ts b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts
new file mode 100644
index 0000000000..41192c1926
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
+
+export const meta = {
+ tags: ['admin', 'captcha'],
+
+ requireCredential: true,
+ requireAdmin: true,
+
+ // 実態はmetaの取得であるため
+ kind: 'read:admin:meta',
+
+ res: {
+ type: 'object',
+ properties: {
+ provider: {
+ type: 'string',
+ enum: supportedCaptchaProviders,
+ },
+ hcaptcha: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ mcaptcha: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ instanceUrl: { type: 'string', nullable: true },
+ },
+ },
+ recaptcha: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ turnstile: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ fc: {
+ type: 'object',
+ properties: {
+ siteKey: { type: 'string', nullable: true },
+ secretKey: { type: 'string', nullable: true },
+ },
+ },
+ },
+ },
+} as const;
+
+export const paramDef = {} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private captchaService: CaptchaService,
+ ) {
+ super(meta, paramDef, async () => {
+ return this.captchaService.get();
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/save.ts b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts
new file mode 100644
index 0000000000..98ec278ebe
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts
@@ -0,0 +1,129 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { captchaErrorCodes, CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['admin', 'captcha'],
+
+ requireCredential: true,
+ requireAdmin: true,
+
+ // 実態はmetaの更新であるため
+ kind: 'write:admin:meta',
+
+ errors: {
+ invalidProvider: {
+ message: 'Invalid provider.',
+ code: 'INVALID_PROVIDER',
+ id: '14bf7ae1-80cc-4363-acb2-4fd61d086af0',
+ httpStatusCode: 400,
+ },
+ invalidParameters: {
+ message: 'Invalid parameters.',
+ code: 'INVALID_PARAMETERS',
+ id: '26654194-410e-44e2-b42e-460ff6f92476',
+ httpStatusCode: 400,
+ },
+ noResponseProvided: {
+ message: 'No response provided.',
+ code: 'NO_RESPONSE_PROVIDED',
+ id: '40acbba8-0937-41fb-bb3f-474514d40afe',
+ httpStatusCode: 400,
+ },
+ requestFailed: {
+ message: 'Request failed.',
+ code: 'REQUEST_FAILED',
+ id: '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd',
+ httpStatusCode: 500,
+ },
+ verificationFailed: {
+ message: 'Verification failed.',
+ code: 'VERIFICATION_FAILED',
+ id: 'c41c067f-24f3-4150-84b2-b5a3ae8c2214',
+ httpStatusCode: 400,
+ },
+ unknown: {
+ message: 'unknown',
+ code: 'UNKNOWN',
+ id: 'f868d509-e257-42a9-99c1-42614b031a97',
+ httpStatusCode: 500,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ provider: {
+ type: 'string',
+ enum: supportedCaptchaProviders,
+ },
+ captchaResult: {
+ type: 'string', nullable: true,
+ },
+ sitekey: {
+ type: 'string', nullable: true,
+ },
+ secret: {
+ type: 'string', nullable: true,
+ },
+ instanceUrl: {
+ type: 'string', nullable: true,
+ },
+ },
+ required: ['provider'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private captchaService: CaptchaService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const result = await this.captchaService.save(ps.provider, {
+ sitekey: ps.sitekey,
+ secret: ps.secret,
+ instanceUrl: ps.instanceUrl,
+ captchaResult: ps.captchaResult,
+ });
+
+ if (!result.success) {
+ switch (result.error.code) {
+ case captchaErrorCodes.invalidProvider:
+ throw new ApiError({
+ ...meta.errors.invalidProvider,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.invalidParameters:
+ throw new ApiError({
+ ...meta.errors.invalidParameters,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.noResponseProvided:
+ throw new ApiError({
+ ...meta.errors.noResponseProvided,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.requestFailed:
+ throw new ApiError({
+ ...meta.errors.requestFailed,
+ message: result.error.message,
+ });
+ case captchaErrorCodes.verificationFailed:
+ throw new ApiError({
+ ...meta.errors.verificationFailed,
+ message: result.error.message,
+ });
+ default:
+ throw new ApiError(meta.errors.unknown);
+ }
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/cw-user.ts b/packages/backend/src/server/api/endpoints/admin/cw-user.ts
new file mode 100644
index 0000000000..bdcfa6a0d9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/cw-user.ts
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:cw-user',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ cw: { type: 'string', nullable: true },
+ },
+ required: ['userId', 'cw'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.usersRepository)
+ private readonly usersRepository: UsersRepository,
+
+ private readonly globalEventService: GlobalEventService,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const user = await this.cacheService.findUserById(ps.userId);
+
+ // Skip if there's nothing to do
+ if (user.mandatoryCW === ps.cw) return;
+
+ // Log event first.
+ // This ensures that we don't "lose" the log if an error occurs
+ await this.moderationLogService.log(me, 'setMandatoryCW', {
+ newCW: ps.cw,
+ oldCW: user.mandatoryCW,
+ userId: user.id,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+
+ await this.usersRepository.update(ps.userId, {
+ // Collapse empty strings to null
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ mandatoryCW: ps.cw || null,
+ });
+
+ // Synchronize caches and other processes
+ this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
+ });
+ }
+}
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
index 747c9f48d0..8b4a450ccb 100644
--- 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
@@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { CacheService } from '@/core/CacheService.js';
export const meta = {
tags: ['admin'],
@@ -30,14 +32,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
+ const user = await this.cacheService.findUserById(ps.userId);
const files = await this.driveFilesRepository.findBy({
userId: ps.userId,
});
+ await this.moderationLogService.log(me, 'clearUserFiles', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ count: files.length,
+ });
+
for (const file of files) {
this.driveService.deleteFile(file, false, me);
}
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
index d420a929bd..9a7b3d5d62 100644
--- 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
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -25,9 +26,11 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
- this.queueService.createCleanRemoteFilesJob();
+ await this.moderationLogService.log(me, 'clearRemoteFiles', {});
+ await this.queueService.createCleanRemoteFilesJob();
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
index d612572e2e..f5d20366cf 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/cleanup.ts
@@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -29,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
+ private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -37,6 +38,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: IsNull(),
});
+ await this.moderationLogService.log(me, 'clearOwnerlessFiles', {
+ count: files.length,
+ });
+
for (const file of files) {
this.driveService.deleteFile(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
index f4fc79bdb3..795b579041 100644
--- 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
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ addAliases: ps.aliases,
+ });
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index b45a3c7156..1c5316a002 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -9,6 +9,7 @@ import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { FILE_TYPE_IMAGE } from '@/const.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -24,6 +25,11 @@ export const meta = {
code: 'NO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
},
+ unsupportedFileType: {
+ message: 'Unsupported file type.',
+ code: 'UNSUPPORTED_FILE_TYPE',
+ id: 'f7599d96-8750-af68-1633-9575d625c1a7',
+ },
duplicateName: {
message: 'Duplicate name.',
code: 'DUPLICATE_NAME',
@@ -47,15 +53,21 @@ export const paramDef = {
nullable: true,
description: 'Use `null` to reset the category.',
},
- aliases: { type: 'array', items: {
- type: 'string',
- } },
+ aliases: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
license: { type: 'string', nullable: true },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
- roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
- type: 'string',
- } },
+ roleIdsThatCanBeUsedThisEmojiAsReaction: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
},
required: ['name', 'fileId'],
} as const;
@@ -67,9 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
private customEmojiService: CustomEmojiService,
-
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -78,11 +88,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
+ if (!FILE_TYPE_IMAGE.includes(driveFile.type)) throw new ApiError(meta.errors.unsupportedFileType);
if (driveFile.user !== null) await this.driveFilesRepository.update(driveFile.id, { user: null });
const emoji = await this.customEmojiService.add({
- driveFile,
+ originalUrl: driveFile.url,
+ publicUrl: driveFile.webpublicUrl ?? driveFile.url,
+ fileType: driveFile.webpublicType ?? driveFile.type,
name: nameNfc,
category: ps.category?.normalize('NFC') ?? null,
aliases: ps.aliases?.map(a => a.normalize('NFC')) ?? [],
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index acd2494131..07ffa0b1c7 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -4,7 +4,6 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import { IsNull } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
@@ -88,10 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
const addedEmoji = await this.customEmojiService.add({
- driveFile,
+ originalUrl: driveFile.url,
+ publicUrl: driveFile.webpublicUrl ?? driveFile.url,
+ fileType: driveFile.webpublicType ?? driveFile.type,
name: nameNfc,
category: emoji.category?.normalize('NFC') ?? null,
- aliases: emoji.aliases?.map(a => a.normalize('NFC')),
+ aliases: emoji.aliases.map(a => a.normalize('NFC')),
host: null,
license: emoji.license,
isSensitive: emoji.isSensitive,
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
index 8e5f69c894..ee7706f31a 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts
@@ -3,9 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import type { DriveFilesRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
secure: true,
@@ -25,9 +28,16 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
+ private readonly moderationLogService: ModerationLogService,
+ @Inject(DI.driveFilesRepository)
+ private readonly driveFilesRepository: DriveFilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
- this.queueService.createImportCustomEmojisJob(me, ps.fileId);
+ const file = await driveFilesRepository.findOneByOrFail({ id: ps.fileId });
+ await this.moderationLogService.log(me, 'importCustomEmojis', {
+ fileName: file.name,
+ });
+ await this.queueService.createImportCustomEmojisJob(me, ps.fileId);
});
}
}
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
index e78620eac1..066eb1c7d9 100644
--- 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
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ delAliases: ps.aliases,
+ });
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}
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
index 69fc8e0cb5..8980ef0c86 100644
--- 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
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ setAliases: ps.aliases,
+ });
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}
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
index 679a9f9c42..2510349210 100644
--- 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
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ category: ps.category,
+ });
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category?.normalize('NFC') ?? null);
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
index 4ba99faab7..a0205ae24a 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
+ await this.moderationLogService.log(me, 'updateCustomEmojis', {
+ ids: ps.ids,
+ license: ps.license,
+ });
await this.customEmojiService.setLicenseBulk(ps.ids, ps.license ?? null);
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 071ddbef18..fd6db9c4ab 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -86,7 +86,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const error = await this.customEmojiService.update({
...required,
- driveFile,
+ originalUrl: driveFile != null ? driveFile.url : undefined,
+ publicUrl: driveFile != null ? (driveFile.webpublicUrl ?? driveFile.url) : undefined,
+ fileType: driveFile != null ? (driveFile.webpublicType ?? driveFile.type) : undefined,
category: ps.category?.normalize('NFC'),
aliases: ps.aliases?.map(a => a.normalize('NFC')),
license: ps.license,
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
index 4a54c26009..89fd4be99c 100644
--- 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
@@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -30,7 +31,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
-
+ private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -38,6 +39,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userHost: ps.host,
});
+ await this.moderationLogService.log(me, 'clearInstanceFiles', {
+ host: ps.host,
+ count: files.length,
+ });
+
for (const file of files) {
this.driveService.deleteFile(file);
}
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
index 601c898f52..e5d85e1d57 100644
--- 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
@@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -35,6 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const followings = await this.followingsRepository.findBy([
@@ -51,6 +53,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
+ await this.moderationLogService.log(me, 'severFollowRelations', {
+ host: ps.host,
+ });
+
this.queueService.createUnfollowJob(pairs.map(p => ({ from: p[0], to: p[1], silent: true })));
});
}
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
index daf19c4435..24d0b8527c 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -27,6 +27,7 @@ export const paramDef = {
isNSFW: { type: 'boolean' },
rejectReports: { type: 'boolean' },
moderationNote: { type: 'string' },
+ rejectQuotes: { type: 'boolean' },
},
required: ['host'],
} as const;
@@ -59,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
suspensionState,
isNSFW: ps.isNSFW,
rejectReports: ps.rejectReports,
+ rejectQuotes: ps.rejectQuotes,
moderationNote: ps.moderationNote,
});
@@ -92,6 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
}
+ if (ps.rejectQuotes != null && instance.rejectQuotes !== ps.rejectQuotes) {
+ const message = ps.rejectReports ? 'rejectQuotesInstance' : 'acceptQuotesInstance';
+ this.moderationLogService.log(me, message, {
+ id: instance.id,
+ host: instance.host,
+ });
+ }
+
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
id: instance.id,
diff --git a/packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts b/packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts
new file mode 100644
index 0000000000..5695866265
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/gen-vapid-keys.ts
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: marie and sharkey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import webpush from 'web-push';
+const { generateVAPIDKeys } = webpush;
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:meta',
+} as const;
+
+export const paramDef = {} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private moderationLogService: ModerationLogService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const keys = await generateVAPIDKeys();
+
+ return { public: keys.publicKey, private: keys.privateKey };
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
index 5ecae3161a..e52b177e2b 100644
--- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts
@@ -68,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
+ createdBy: me,
+ createdById: me.id,
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}));
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 6495e3b7da..436dcf27cb 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -391,6 +391,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ robotsTxt: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
enableIdenticonGeneration: {
type: 'boolean',
optional: false, nullable: false,
@@ -708,6 +712,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,
enableAchievements: instance.enableAchievements,
+ robotsTxt: instance.robotsTxt,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
bannedEmailDomains: instance.bannedEmailDomains,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
diff --git a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
index d3fa4251dd..194e793eda 100644
--- a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
@@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
+import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -27,22 +29,25 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
+ private readonly moderationLogService: ModerationLogService,
+ private readonly cacheService: CacheService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ const user = await this.cacheService.findUserById(ps.userId);
- if (user == null) {
- throw new Error('user not found');
- }
+ await this.moderationLogService.log(me, 'nsfwUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: true,
});
+
+ await this.cacheService.userProfileCache.refresh(ps.userId);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
index 1d32c6cc00..63fe2988ee 100644
--- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
@@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { PromoNotesRepository } from '@/models/_.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { CacheService } from '@/core/CacheService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -46,7 +48,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.promoNotesRepository)
private promoNotesRepository: PromoNotesRepository,
-
+ private readonly moderationLogService: ModerationLogService,
+ private readonly cacheService: CacheService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -61,6 +64,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.alreadyPromoted);
}
+ const user = await this.cacheService.findUserById(note.userId);
+ await this.moderationLogService.log(me, 'createPromo', {
+ noteId: note.id,
+ noteUserId: user.id,
+ noteUserUsername: user.username,
+ noteUserHost: user.host,
+ });
+
await this.promoNotesRepository.insert({
noteId: note.id,
expiresAt: new Date(ps.expiresAt),
diff --git a/packages/backend/src/server/api/endpoints/admin/reject-quotes.ts b/packages/backend/src/server/api/endpoints/admin/reject-quotes.ts
new file mode 100644
index 0000000000..78f94ceeff
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/reject-quotes.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:reject-quotes',
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ userId: { type: 'string', format: 'misskey:id' },
+ rejectQuotes: { type: 'boolean', nullable: false },
+ },
+ required: ['userId', 'rejectQuotes'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.usersRepository)
+ private readonly usersRepository: UsersRepository,
+
+ private readonly globalEventService: GlobalEventService,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const user = await this.cacheService.findUserById(ps.userId);
+
+ // Skip if there's nothing to do
+ if (user.rejectQuotes === ps.rejectQuotes) return;
+
+ // Log event first.
+ // This ensures that we don't "lose" the log if an error occurs
+ await this.moderationLogService.log(me, ps.rejectQuotes ? 'rejectQuotesUser' : 'acceptQuotesUser', {
+ userId: user.id,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+
+ await this.usersRepository.update(ps.userId, {
+ rejectQuotes: ps.rejectQuotes,
+ });
+
+ // Synchronize caches and other processes
+ this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
index 3d7bc4567e..129f69aca9 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts
@@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js';
import { ApiError } from '../../../error.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -64,6 +65,7 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private relayService: RelayService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
try {
@@ -72,6 +74,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.invalidUrl);
}
+ await this.moderationLogService.log(me, 'addRelay', {
+ inbox: ps.inbox,
+ });
+
return await this.relayService.addRelay(ps.inbox);
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
index 1f6e773cd4..292f21fde9 100644
--- a/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
+++ b/packages/backend/src/server/api/endpoints/admin/relays/remove.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -27,9 +28,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private relayService: RelayService,
+ private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
- return await this.relayService.removeRelay(ps.inbox);
+ await this.moderationLogService.log(me, 'removeRelay', {
+ inbox: ps.inbox,
+ });
+ await this.relayService.removeRelay(ps.inbox);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
index 7e6045049a..eed21c6576 100644
--- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
@@ -8,6 +8,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@@ -29,24 +32,32 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- private roleService: RoleService,
+ private readonly usersRepository: UsersRepository,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ private readonly roleService: RoleService,
+ private readonly globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
-
- if (user == null) {
- throw new Error('user not found');
- }
+ const user = await this.cacheService.findUserById(ps.userId);
if (await this.roleService.isModerator(user)) {
throw new Error('cannot silence moderator account');
}
+ await this.moderationLogService.log(me, 'silenceUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
+
await this.usersRepository.update(user.id, {
isSilenced: true,
});
+
+ this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
+ id: user.id,
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
index 26588365e1..52a0c076be 100644
--- a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
@@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
+import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@@ -27,18 +29,19 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ const user = await this.cacheService.findUserById(ps.userId);
- if (user == null) {
- throw new Error('user not found');
- }
+ await this.moderationLogService.log(me, 'unNsfwUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: false,
diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
index f92be0d8e0..9318943785 100644
--- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
@@ -7,6 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@@ -28,18 +31,27 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
+ private readonly usersRepository: UsersRepository,
+ private readonly cacheService: CacheService,
+ private readonly moderationLogService: ModerationLogService,
+ private readonly globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ const user = await this.cacheService.findUserById(ps.userId);
- if (user == null) {
- throw new Error('user not found');
- }
+ await this.moderationLogService.log(me, 'unSilenceUser', {
+ userId: ps.userId,
+ userUsername: user.username,
+ userHost: user.host,
+ });
await this.usersRepository.update(user.id, {
isSilenced: false,
});
+
+ this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
+ id: user.id,
+ });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 72f428d85f..b3733d3d39 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -149,6 +149,7 @@ export const paramDef = {
enableStatsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' },
enableAchievements: { type: 'boolean' },
+ robotsTxt: { type: 'string', nullable: true },
enableIdenticonGeneration: { type: 'boolean' },
serverRules: { type: 'array', items: { type: 'string' } },
bannedEmailDomains: { type: 'array', items: { type: 'string' } },
@@ -636,6 +637,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableAchievements = ps.enableAchievements;
}
+ if (ps.robotsTxt !== undefined) {
+ set.robotsTxt = ps.robotsTxt;
+ }
+
if (ps.enableIdenticonGeneration !== undefined) {
set.enableIdenticonGeneration = ps.enableIdenticonGeneration;
}
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index 616a77e337..22bec8ef95 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -4,11 +4,10 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiNote } from '@/models/Note.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
-import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
+import { isActor, isPost, getApId, getNullableApId } from '@/core/activitypub/type.js';
import type { SchemaType } from '@/misc/json-schema.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
@@ -18,7 +17,10 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
+import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
+import { InstanceActorService } from '@/core/InstanceActorService.js';
import { ApiError } from '../../error.js';
+import { IdentifiableError } from '@/misc/identifiable-error.js';
export const meta = {
tags: ['federation'],
@@ -26,12 +28,38 @@ export const meta = {
requireCredential: true,
kind: 'read:account',
+ // Up to 30 calls, then 1 per 1/2 second
limit: {
- duration: ms('1minute'),
max: 30,
+ dripRate: 500,
},
errors: {
+ federationNotAllowed: {
+ message: 'Federation for this host is not allowed.',
+ code: 'FEDERATION_NOT_ALLOWED',
+ id: '974b799e-1a29-4889-b706-18d4dd93e266',
+ },
+ uriInvalid: {
+ message: 'URI is invalid.',
+ code: 'URI_INVALID',
+ id: '1a5eab56-e47b-48c2-8d5e-217b897d70db',
+ },
+ requestFailed: {
+ message: 'Request failed.',
+ code: 'REQUEST_FAILED',
+ id: '81b539cf-4f57-4b29-bc98-032c33c0792e',
+ },
+ responseInvalid: {
+ message: 'Response from remote server is invalid.',
+ code: 'RESPONSE_INVALID',
+ id: '70193c39-54f3-4813-82f0-70a680f7495b',
+ },
+ responseInvalidIdHostNotMatch: {
+ message: 'Requested URI and response URI host does not match.',
+ code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH',
+ id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a',
+ },
noSuchObject: {
message: 'No such object.',
code: 'NO_SUCH_OBJECT',
@@ -94,6 +122,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService,
private apNoteService: ApNoteService,
+ private readonly apRequestService: ApRequestService,
+ private readonly instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, me) => {
const object = await this.fetchAny(ps.uri, me);
@@ -110,7 +140,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
*/
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
- if (!this.utilityService.isFederationAllowedUri(uri)) return null;
+ if (!this.utilityService.isFederationAllowedUri(uri)) {
+ throw new ApiError(meta.errors.federationNotAllowed);
+ }
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
@@ -118,6 +150,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]));
if (local != null) return local;
+ // No local object found with that uri.
+ // Before we fetch, resolve the URI in case it has a cross-origin redirect or anything like that.
+ // Resolver.resolve() uses strict verification, which is overly paranoid for a user-provided lookup.
+ uri = await this.resolveCanonicalUri(uri); // eslint-disable-line no-param-reassign
+ if (!this.utilityService.isFederationAllowedUri(uri)) {
+ throw new ApiError(meta.errors.federationNotAllowed);
+ }
+
const host = this.utilityService.extractDbHost(uri);
// local object, not found in db? fail
@@ -125,7 +165,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver();
- const object = await resolver.resolve(uri) as any;
+ const object = await resolver.resolve(uri).catch((err) => {
+ if (err instanceof IdentifiableError) {
+ switch (err.id) {
+ // resolve
+ case 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2':
+ throw new ApiError(meta.errors.uriInvalid);
+ case '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5':
+ case 'd592da9f-822f-4d91-83d7-4ceefabcf3d2':
+ throw new ApiError(meta.errors.requestFailed);
+ case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
+ throw new ApiError(meta.errors.federationNotAllowed);
+ case '72180409-793c-4973-868e-5a118eb5519b':
+ case 'ad2dc287-75c1-44c4-839d-3d2e64576675':
+ throw new ApiError(meta.errors.responseInvalid);
+ case 'fd93c2fa-69a8-440f-880b-bf178e0ec877':
+ throw new ApiError(meta.errors.responseInvalidIdHostNotMatch);
+
+ // resolveLocal
+ case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':
+ throw new ApiError(meta.errors.uriInvalid);
+ case 'a9d946e5-d276-47f8-95fb-f04230289bb0':
+ case '06ae3170-1796-4d93-a697-2611ea6d83b6':
+ throw new ApiError(meta.errors.noSuchObject);
+ case '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0':
+ throw new ApiError(meta.errors.responseInvalid);
+ }
+ }
+
+ throw new ApiError(meta.errors.requestFailed);
+ });
+
+ if (object.id == null) {
+ throw new ApiError(meta.errors.responseInvalid);
+ }
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
// これはDBに存在する可能性があるため再度DB検索
@@ -167,4 +240,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return null;
}
+
+ /**
+ * Resolves an arbitrary URI to its canonical, post-redirect form.
+ */
+ private async resolveCanonicalUri(uri: string): Promise<string> {
+ const user = await this.instanceActorService.getInstanceActor();
+ const res = await this.apRequestService.signedGet(uri, user, true);
+ return getNullableApId(res) ?? uri;
+ }
}
diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts
index d2a75225ed..7cca688fda 100644
--- a/packages/backend/src/server/api/endpoints/channels/update.ts
+++ b/packages/backend/src/server/api/endpoints/channels/update.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository, ChannelsRepository } from '@/models/_.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['channels'],
diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts
index 8e821da0da..5df212415d 100644
--- a/packages/backend/src/server/api/endpoints/drive/files.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
-import { Brackets } from 'typeorm';
export const meta = {
tags: ['drive'],
@@ -45,7 +45,7 @@ export const paramDef = {
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] },
- searchQuery: { type: 'string', default: '' }
+ searchQuery: { type: 'string', default: '' },
},
required: [],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
index 7a009b12a1..3065bb6711 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
@@ -11,7 +11,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['drive'],
diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts
index 94a0e673a3..306a646785 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts
@@ -11,7 +11,6 @@ import { RoleService } from '@/core/RoleService.js';
import { DriveService } from '@/core/DriveService.js';
import type { Config } from '@/config.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['drive'],
diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts
index 1245706b0d..525cb8c5d6 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders.ts
@@ -42,7 +42,7 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
- searchQuery: { type: 'string', default: '' }
+ searchQuery: { type: 'string', default: '' },
},
required: [],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
index cd47c0fc68..8d51d09ea6 100644
--- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts
@@ -10,7 +10,6 @@ import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityServi
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['drive'],
diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts
index 3cc7f89ab9..4909c948e3 100644
--- a/packages/backend/src/server/api/endpoints/emojis.ts
+++ b/packages/backend/src/server/api/endpoints/emojis.ts
@@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { EmojisRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -59,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const emojis = await this.emojisRepository.createQueryBuilder()
.where('host IS NULL')
.orderBy('LOWER(category)', 'ASC')
- .orderBy('LOWER(name)', 'ASC')
+ .addOrderBy('LOWER(name)', 'ASC')
.getMany();
return {
emojis: await this.emojiEntityService.packSimpleMany(emojis),
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
index 3ec9522c44..5217f79065 100644
--- a/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
+++ b/packages/backend/src/server/api/endpoints/federation/update-remote-user.ts
@@ -13,10 +13,11 @@ export const meta = {
requireCredential: false,
- // 2 calls per second
+ // Up to 10 calls, then 4 / second.
+ // This allows for reliable automation.
limit: {
- duration: 1000,
- max: 2,
+ max: 10,
+ dripRate: 250,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts
index 1010567113..271a44f8d5 100644
--- a/packages/backend/src/server/api/endpoints/flash/delete.ts
+++ b/packages/backend/src/server/api/endpoints/flash/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { FlashsRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['flashs'],
@@ -78,7 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
flashId: flash.id,
flashUserId: flash.userId,
flashUserUsername: user.username,
- flash,
});
}
});
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
index 68478ba55c..9854358e3e 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['gallery'],
@@ -78,7 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
postId: post.id,
postUserId: post.userId,
postUserUsername: user.username,
- post,
});
}
});
diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts
index 9347c9ca27..48a2e3b40a 100644
--- a/packages/backend/src/server/api/endpoints/i.ts
+++ b/packages/backend/src/server/api/endpoints/i.ts
@@ -31,10 +31,12 @@ export const meta = {
},
},
- // 3 calls per second
+ // up to 20 calls, then 1 per second.
+ // This handles bursty traffic when all tabs reload as a group
limit: {
- duration: 1000,
- max: 3,
+ max: 20,
+ dripSize: 1,
+ dripRate: 1000,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
index 34b6907338..12d5ef443d 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
@@ -5,12 +5,12 @@
import * as OTPAuth from 'otpauth';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
-import ms from 'ms';
export const meta = {
requireCredential: 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
index 084d4af658..370d9915a3 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -14,7 +15,6 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
@@ -63,8 +63,8 @@ export const paramDef = {
required: ['password', 'name', 'credential'],
} as const;
-// eslint-disable-next-line import/no-default-export
@Injectable()
+// eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
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
index a1c965f603..cd520cff0f 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
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
index 6ab50a57c9..893ea30391 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
@@ -6,13 +6,13 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index 888d0fc6ef..d27c14c69b 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -8,13 +8,13 @@ import * as argon2 from 'argon2';
import * as OTPAuth from 'otpauth';
import * as QRCode from 'qrcode';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
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
index 614fd0c498..b01e452056 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -13,7 +14,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index 2773825373..2fe4fdc4c0 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/_.js';
@@ -13,7 +14,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
index eb8a63b3dc..4a41c7b984 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
@@ -5,13 +5,13 @@
//import bcrypt from 'bcryptjs';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserSecurityKeysRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts
index 661fa257a6..f290ff6844 100644
--- a/packages/backend/src/server/api/endpoints/i/apps.ts
+++ b/packages/backend/src/server/api/endpoints/i/apps.ts
@@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
name: token.name ?? token.app?.name,
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
- permission: token.permission,
+ permission: token.app ? token.app.permission : token.permission,
})));
});
}
diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts
index f131c7e9d1..4069683740 100644
--- a/packages/backend/src/server/api/endpoints/i/change-password.ts
+++ b/packages/backend/src/server/api/endpoints/i/change-password.ts
@@ -6,11 +6,11 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
index ebb25ebf1c..52f96e5bbd 100644
--- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
+++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts
@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { DI } from '@/di-symbols.js';
import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
import type { MiMeta } from '@/models/_.js';
diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts
index 565eaaafc0..10fb923d4f 100644
--- a/packages/backend/src/server/api/endpoints/i/delete-account.ts
+++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts
@@ -6,12 +6,12 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
index 814ffb5488..38328bb7d4 100644
--- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts
@@ -6,12 +6,12 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
-import ms from 'ms';
export const meta = {
requireCredential: true,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 09c06a108d..f74452e2af 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -133,6 +133,12 @@ export const meta = {
id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
httpStatusCode: 422,
},
+
+ maxCwLength: {
+ message: 'You tried setting a default content warning which is too long.',
+ code: 'MAX_CW_LENGTH',
+ id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
+ },
},
res: {
@@ -243,6 +249,12 @@ export const paramDef = {
uniqueItems: true,
items: { type: 'string' },
},
+ defaultCW: { type: 'string', nullable: true },
+ defaultCWPriority: {
+ type: 'string',
+ enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
+ nullable: false,
+ },
},
} as const;
@@ -494,6 +506,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null;
}
+ let defaultCW = ps.defaultCW;
+ if (defaultCW !== undefined) {
+ if (defaultCW === '') defaultCW = null;
+ if (defaultCW && defaultCW.length > this.config.maxCwLength) {
+ throw new ApiError(meta.errors.maxCwLength);
+ }
+
+ profileUpdates.defaultCW = defaultCW;
+ }
+ if (ps.defaultCWPriority !== undefined) {
+ profileUpdates.defaultCWPriority = ps.defaultCWPriority;
+ }
+
//#region emojis/tags
let emojis = [] as string[];
@@ -592,7 +617,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html);
- const doc = window.document;
+ const doc: Document = window.document;
const myLink = `${this.config.url}/@${user.username}`;
diff --git a/packages/backend/src/server/api/endpoints/mute/delete.ts b/packages/backend/src/server/api/endpoints/mute/delete.ts
index 1e14bafc87..ccc98c4b10 100644
--- a/packages/backend/src/server/api/endpoints/mute/delete.ts
+++ b/packages/backend/src/server/api/endpoints/mute/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MutingsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['account'],
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index d1cf0123dc..b0f32bfda8 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -143,6 +143,12 @@ export const meta = {
code: 'CONTAINS_TOO_MANY_MENTIONS',
id: '4de0363a-3046-481b-9b0f-feff3e211025',
},
+
+ quoteDisabledForUser: {
+ message: 'You do not have permission to create quote posts.',
+ code: 'QUOTE_DISABLED_FOR_USER',
+ id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
+ },
},
} as const;
@@ -415,6 +421,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
+ } else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
+ throw new ApiError(meta.errors.quoteDisabledForUser);
}
}
throw e;
diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts
index dc94c78e75..cc2293c5d6 100644
--- a/packages/backend/src/server/api/endpoints/notes/edit.ts
+++ b/packages/backend/src/server/api/endpoints/notes/edit.ts
@@ -176,6 +176,12 @@ export const meta = {
id: '33510210-8452-094c-6227-4a6c05d99f02',
},
+ quoteDisabledForUser: {
+ message: 'You do not have permission to create quote posts.',
+ code: 'QUOTE_DISABLED_FOR_USER',
+ id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
+ },
+
containsProhibitedWords: {
message: 'Cannot post because it contains prohibited words.',
code: 'CONTAINS_PROHIBITED_WORDS',
@@ -469,6 +475,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
+ } else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
+ throw new ApiError(meta.errors.quoteDisabledForUser);
}
}
throw e;
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
index 19a6a5af54..742c872d45 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts
@@ -4,12 +4,12 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import type { NoteFavoritesRepository } from '@/models/_.js';
import { ApiError } from '../../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['notes', 'favorites'],
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index c45fcd7c5c..0f2592bd78 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -12,8 +12,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
-import { ApiError } from '../../error.js';
import { CacheService } from '@/core/CacheService.js';
+import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
diff --git a/packages/backend/src/server/api/endpoints/notes/like.ts b/packages/backend/src/server/api/endpoints/notes/like.ts
index 9068de2865..d6511faf95 100644
--- a/packages/backend/src/server/api/endpoints/notes/like.ts
+++ b/packages/backend/src/server/api/endpoints/notes/like.ts
@@ -1,5 +1,5 @@
-import { DI } from '@/di-symbols.js';
import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ReactionService } from '@/core/ReactionService.js';
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
index c6032fbdae..0c2e00ee8d 100644
--- a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts
@@ -14,12 +14,10 @@ import type {
NotesRepository,
BlockingsRepository,
DriveFilesRepository,
- ChannelsRepository,
NoteScheduleRepository,
} from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
-import type { MiChannel } from '@/models/Channel.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
@@ -210,9 +208,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
- @Inject(DI.channelsRepository)
- private channelsRepository: ChannelsRepository,
-
private queueService: QueueService,
private roleService: RoleService,
private idService: IdService,
diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
index 4895733d4e..4dd3d7a81a 100644
--- a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
+++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts
@@ -7,12 +7,14 @@ import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
-import type { MiNote, MiNoteSchedule, NoteScheduleRepository } from '@/models/_.js';
+import type { MiNote, MiUser, MiNoteSchedule, NoteScheduleRepository, NotesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { QueryService } from '@/core/QueryService.js';
import { Packed } from '@/misc/json-schema.js';
import { noteVisibilities } from '@/types.js';
+import { bindThis } from '@/decorators.js';
export const meta = {
tags: ['notes'],
@@ -81,7 +83,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.noteScheduleRepository)
private noteScheduleRepository: NoteScheduleRepository,
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
private userEntityService: UserEntityService,
+ private noteEntityService: NoteEntityService,
private driveFileEntityService: DriveFileEntityService,
private queryService: QueryService,
) {
@@ -106,6 +112,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: string;
scheduledAt: string;
}[] = await Promise.all(scheduleNotes.map(async (item: MiNoteSchedule) => {
+ const renote = await this.fetchNote(item.note.renote, me);
+ const reply = await this.fetchNote(item.note.reply, me);
+
return {
...item,
scheduledAt: item.scheduledAt.toISOString(),
@@ -115,12 +124,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
user: user,
visibility: item.note.visibility ?? 'public',
reactionAcceptance: item.note.reactionAcceptance ?? null,
- visibleUsers: item.note.visibleUsers ? await userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [],
+ visibleUsers: item.note.visibleUsers ? await this.userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [],
fileIds: item.note.files ? item.note.files : [],
files: await this.driveFileEntityService.packManyByIds(item.note.files),
createdAt: item.scheduledAt.toISOString(),
isSchedule: true,
id: item.id,
+ renote, reply,
+ renoteId: item.note.renote,
+ replyId: item.note.reply,
},
};
}));
@@ -128,4 +140,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return scheduleNotesPack;
});
}
+
+ @bindThis
+ private async fetchNote(
+ id: MiNote['id'] | null | undefined,
+ me: MiUser,
+ ): Promise<Packed<'Note'> | null> {
+ if (id) {
+ const note = await this.notesRepository.findOneBy({ id });
+ if (note) {
+ note.reactionAndUserPairCache ??= [];
+ return this.noteEntityService.pack(note, me);
+ }
+ }
+ return null;
+ }
}
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
index 227ac0ebbf..6bba7bf37e 100644
--- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
@@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .andWhere('note.visibility = \'public\'')
+ .andWhere("note.visibility IN ('public', 'home')") // keep in sync with NoteCreateService call to `hashtagService.updateHashtags()`
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts
index eca55cd085..f46f4d2adb 100644
--- a/packages/backend/src/server/api/endpoints/notes/search.ts
+++ b/packages/backend/src/server/api/endpoints/notes/search.ts
@@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { SearchService } from '@/core/SearchService.js';
+import { fileTypeCategories, SearchService } from '@/core/SearchService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
@@ -52,7 +52,11 @@ export const paramDef = {
type: 'string',
description: 'The local host is represented with `.`.',
},
- filetype: { type: 'string', nullable: true },
+ filetype: {
+ type: 'string',
+ nullable: true,
+ enum: fileTypeCategories,
+ },
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
order: { type: 'string' },
diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts
index fa03b0b457..6de5fe3d44 100644
--- a/packages/backend/src/server/api/endpoints/pages/create.ts
+++ b/packages/backend/src/server/api/endpoints/pages/create.ts
@@ -7,7 +7,7 @@ import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import type { DriveFilesRepository, PagesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
-import { MiPage } from '@/models/Page.js';
+import { MiPage, pageNameSchema } from '@/models/Page.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -51,7 +51,7 @@ export const paramDef = {
type: 'object',
properties: {
title: { type: 'string' },
- name: { type: 'string', minLength: 1 },
+ name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true },
content: { type: 'array', items: {
type: 'object', additionalProperties: true,
diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts
index c2c3215f49..c95f8ecf6b 100644
--- a/packages/backend/src/server/api/endpoints/pages/delete.ts
+++ b/packages/backend/src/server/api/endpoints/pages/delete.ts
@@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
import type { PagesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
-import ms from 'ms';
export const meta = {
tags: ['pages'],
@@ -79,7 +79,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
pageId: page.id,
pageUserId: page.userId,
pageUserUsername: user.username,
- page,
});
}
});
diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts
index f11bbbcb1a..a6aeb6002e 100644
--- a/packages/backend/src/server/api/endpoints/pages/update.ts
+++ b/packages/backend/src/server/api/endpoints/pages/update.ts
@@ -10,6 +10,7 @@ import type { PagesRepository, DriveFilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
+import { pageNameSchema } from '@/models/Page.js';
export const meta = {
tags: ['pages'],
@@ -31,13 +32,11 @@ export const meta = {
code: 'NO_SUCH_PAGE',
id: '21149b9e-3616-4778-9592-c4ce89f5a864',
},
-
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '3c15cd52-3b4b-4274-967d-6456fc4f792b',
},
-
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
@@ -56,7 +55,7 @@ export const paramDef = {
properties: {
pageId: { type: 'string', format: 'misskey:id' },
title: { type: 'string' },
- name: { type: 'string', minLength: 1 },
+ name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true },
content: { type: 'array', items: {
type: 'object', additionalProperties: true,
@@ -102,15 +101,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
- await this.pagesRepository.findBy({
- id: Not(ps.pageId),
- userId: me.id,
- name: ps.name,
- }).then(result => {
- if (result.length > 0) {
- throw new ApiError(meta.errors.nameAlreadyExists);
- }
- });
+ if (ps.name != null) {
+ await this.pagesRepository.findBy({
+ id: Not(ps.pageId),
+ userId: me.id,
+ name: ps.name,
+ }).then(result => {
+ if (result.length > 0) {
+ throw new ApiError(meta.errors.nameAlreadyExists);
+ }
+ });
+ }
await this.pagesRepository.update(page.id, {
updatedAt: new Date(),
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index e765163f8e..528de76707 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -64,10 +64,11 @@ export const meta = {
},
},
- // 2 calls per second
+ // 24 calls, then 7 per second-ish (1 for each type of server info graph)
limit: {
- duration: 1000,
- max: 2,
+ max: 24,
+ dripSize: 7,
+ dripRate: 900,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index e659c46713..c7016d8d32 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -58,6 +58,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ isInstanceMuted: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ memo: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
},
},
{
@@ -103,6 +111,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ isInstanceMuted: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ memo: {
+ type: 'string',
+ optional: true, nullable: true,
+ },
},
},
},
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index 7ebca78a7d..118362149d 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -57,10 +57,10 @@ export const meta = {
},
},
- // 5 calls per 2 seconds
+ // up to 50 calls @ 4 per second
limit: {
- duration: 1000 * 2,
- max: 5,
+ max: 50,
+ dripRate: 250,
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts
new file mode 100644
index 0000000000..9426318e34
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts
@@ -0,0 +1,126 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { CustomEmojiService, fetchEmojisHostTypes, fetchEmojisSortKeys } from '@/core/CustomEmojiService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireRolePolicy: 'canManageCustomEmojis',
+ kind: 'read:admin:emoji',
+
+ res: {
+ type: 'object',
+ properties: {
+ emojis: {
+ type: 'array',
+ items: {
+ type: 'object',
+ ref: 'EmojiDetailedAdmin',
+ },
+ },
+ count: { type: 'integer' },
+ allCount: { type: 'integer' },
+ allPages: { type: 'integer' },
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ query: {
+ type: 'object',
+ nullable: true,
+ properties: {
+ updatedAtFrom: { type: 'string' },
+ updatedAtTo: { type: 'string' },
+ name: { type: 'string' },
+ host: { type: 'string' },
+ uri: { type: 'string' },
+ publicUrl: { type: 'string' },
+ originalUrl: { type: 'string' },
+ type: { type: 'string' },
+ aliases: { type: 'string' },
+ category: { type: 'string' },
+ license: { type: 'string' },
+ isSensitive: { type: 'boolean' },
+ localOnly: { type: 'boolean' },
+ hostType: {
+ type: 'string',
+ enum: fetchEmojisHostTypes,
+ default: 'all',
+ },
+ roleIds: {
+ type: 'array',
+ items: { 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 },
+ page: { type: 'integer' },
+ sortKeys: {
+ type: 'array',
+ default: ['-id'],
+ items: {
+ type: 'string',
+ enum: fetchEmojisSortKeys,
+ },
+ },
+ },
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private customEmojiService: CustomEmojiService,
+ private emojiEntityService: EmojiEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const q = ps.query;
+ const result = await this.customEmojiService.fetchEmojis(
+ {
+ query: {
+ updatedAtFrom: q?.updatedAtFrom,
+ updatedAtTo: q?.updatedAtTo,
+ name: q?.name,
+ host: q?.host,
+ uri: q?.uri,
+ publicUrl: q?.publicUrl,
+ type: q?.type,
+ aliases: q?.aliases,
+ category: q?.category,
+ license: q?.license,
+ isSensitive: q?.isSensitive,
+ localOnly: q?.localOnly,
+ hostType: q?.hostType,
+ roleIds: q?.roleIds,
+ },
+ sinceId: ps.sinceId,
+ untilId: ps.untilId,
+ },
+ {
+ limit: ps.limit,
+ page: ps.page,
+ sortKeys: ps.sortKeys,
+ },
+ );
+
+ return {
+ emojis: await this.emojiEntityService.packDetailedAdminMany(result.emojis),
+ count: result.count,
+ allCount: result.allCount,
+ allPages: result.allPages,
+ };
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
index b40e4cdaa4..69799bdade 100644
--- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
+++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
@@ -3,53 +3,78 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
-import megalodon, { Entity, MegalodonInterface } from 'megalodon';
import querystring from 'querystring';
+import { megalodon, Entity, MegalodonInterface } from 'megalodon';
import { IsNull } from 'typeorm';
import multer from 'fastify-multer';
-import type { AccessTokensRepository, NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository, MiMeta } from '@/models/_.js';
+import { Inject, Injectable } from '@nestjs/common';
+import type { AccessTokensRepository, UserProfilesRepository, UsersRepository, MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Config } from '@/config.js';
+import { DriveService } from '@/core/DriveService.js';
+import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
+import { ApiAccountMastodonRoute } from '@/server/api/mastodon/endpoints/account.js';
+import { ApiSearchMastodonRoute } from '@/server/api/mastodon/endpoints/search.js';
+import { ApiFilterMastodonRoute } from '@/server/api/mastodon/endpoints/filter.js';
+import { ApiNotifyMastodonRoute } from '@/server/api/mastodon/endpoints/notifications.js';
+import { AuthenticateService } from '@/server/api/AuthenticateService.js';
+import { MiLocalUser } from '@/models/User.js';
+import { AuthMastodonRoute } from './endpoints/auth.js';
+import { toBoolean } from './timelineArgs.js';
import { convertAnnouncement, convertFilter, convertAttachment, convertFeaturedTag, convertList, MastoConverters } from './converters.js';
import { getInstance } from './endpoints/meta.js';
import { ApiAuthMastodon, ApiAccountMastodon, ApiFilterMastodon, ApiNotifyMastodon, ApiSearchMastodon, ApiTimelineMastodon, ApiStatusMastodon } from './endpoints.js';
-import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { DriveService } from '@/core/DriveService.js';
+import type { FastifyInstance, FastifyPluginOptions, FastifyRequest } from 'fastify';
-export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
+export function getAccessToken(authorization: string | undefined): string | null {
const accessTokenArr = authorization?.split(' ') ?? [null];
- const accessToken = accessTokenArr[accessTokenArr.length - 1];
- const generator = (megalodon as any).default;
- const client = generator('misskey', BASE_URL, accessToken) as MegalodonInterface;
- return client;
+ return accessTokenArr[accessTokenArr.length - 1];
+}
+
+export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
+ const accessToken = getAccessToken(authorization);
+ return megalodon('misskey', BASE_URL, accessToken);
}
@Injectable()
export class MastodonApiServerService {
constructor(
@Inject(DI.meta)
- private serverSettings: MiMeta,
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
- @Inject(DI.notesRepository)
- private notesRepository: NotesRepository,
+ private readonly serverSettings: MiMeta,
+ @Inject(DI.usersRepository)
+ private readonly usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
- @Inject(DI.noteEditRepository)
- private noteEditRepository: NoteEditRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
@Inject(DI.accessTokensRepository)
- private accessTokensRepository: AccessTokensRepository,
- @Inject(DI.config)
- private config: Config,
- private userEntityService: UserEntityService,
- private driveService: DriveService,
- private mastoConverter: MastoConverters,
+ private readonly accessTokensRepository: AccessTokensRepository,
+ @Inject(DI.config)
+ private readonly config: Config,
+ private readonly driveService: DriveService,
+ private readonly mastoConverters: MastoConverters,
+ private readonly logger: MastodonLogger,
+ private readonly authenticateService: AuthenticateService,
) { }
@bindThis
+ public async getAuthClient(request: FastifyRequest): Promise<{ client: MegalodonInterface, me: MiLocalUser | null }> {
+ const accessToken = getAccessToken(request.headers.authorization);
+ const [me] = await this.authenticateService.authenticate(accessToken);
+
+ const baseUrl = `${request.protocol}://${request.host}`;
+ const client = megalodon('misskey', baseUrl, accessToken);
+
+ return { client, me };
+ }
+
+ @bindThis
+ public async getAuthOnly(request: FastifyRequest): Promise<MiLocalUser | null> {
+ const accessToken = getAccessToken(request.headers.authorization);
+ const [me] = await this.authenticateService.authenticate(accessToken);
+ return me;
+ }
+
+ @bindThis
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
const upload = multer({
storage: multer.diskStorage({}),
@@ -59,12 +84,12 @@ export class MastodonApiServerService {
},
});
- fastify.addHook('onRequest', (request, reply, done) => {
+ fastify.addHook('onRequest', (_, reply, done) => {
reply.header('Access-Control-Allow-Origin', '*');
done();
});
- fastify.addContentTypeParser('application/x-www-form-urlencoded', (request, payload, done) => {
+ fastify.addContentTypeParser('application/x-www-form-urlencoded', (_, payload, done) => {
let body = '';
payload.on('data', (data) => {
body += data;
@@ -73,8 +98,8 @@ export class MastodonApiServerService {
try {
const parsed = querystring.parse(body);
done(null, parsed);
- } catch (e: any) {
- done(e);
+ } catch (e) {
+ done(e as Error);
}
});
payload.on('error', done);
@@ -83,20 +108,21 @@ export class MastodonApiServerService {
fastify.register(multer.contentParser);
fastify.get('/v1/custom_emojis', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getInstanceCustomEmojis();
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/custom_emojis', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/instance', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
@@ -111,118 +137,124 @@ export class MastodonApiServerService {
},
order: { id: 'ASC' },
});
- const contact = admin == null ? null : await this.mastoConverter.convertAccount((await client.getAccount(admin.id)).data);
+ const contact = admin == null ? null : await this.mastoConverters.convertAccount((await client.getAccount(admin.id)).data);
reply.send(await getInstance(data.data, contact as Entity.Account, this.config, this.serverSettings));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/instance', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/announcements', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getInstanceAnnouncements();
reply.send(data.data.map((announcement) => convertAnnouncement(announcement)));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/announcements', data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Body: { id: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<{ Body: { id?: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const data = await client.dismissInstanceAnnouncement(
- _request.body['id'],
- );
+ if (!_request.body.id) return reply.code(400).send({ error: 'Missing required payload "id"' });
+ const data = await client.dismissInstanceAnnouncement(_request.body['id']);
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/announcements/${_request.body.id}/dismiss`, data);
+ reply.code(401).send(data);
}
- },
- );
+ });
fastify.post('/v1/media', { preHandler: upload.single('file') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const multipartData = await _request.file;
+ const multipartData = await _request.file();
if (!multipartData) {
reply.code(401).send({ error: 'No image' });
return;
}
const data = await client.uploadMedia(multipartData);
reply.send(convertAttachment(data.data as Entity.Attachment));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/media', data);
+ reply.code(401).send(data);
}
});
- fastify.post('/v2/media', { preHandler: upload.single('file') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<{ Body: { description?: string; focus?: string }}>('/v2/media', { preHandler: upload.single('file') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const multipartData = await _request.file;
+ const multipartData = await _request.file();
if (!multipartData) {
reply.code(401).send({ error: 'No image' });
return;
}
- const data = await client.uploadMedia(multipartData, _request.body!);
+ const data = await client.uploadMedia(multipartData, _request.body);
reply.send(convertAttachment(data.data as Entity.Attachment));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v2/media', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/filters', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getFilters();
reply.send(data.data.map((filter) => convertFilter(filter)));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/filters', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/trends', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getInstanceTrends();
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/trends', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/trends/tags', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getInstanceTrends();
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/trends/tags', data);
+ reply.code(401).send(data);
}
});
@@ -231,50 +263,69 @@ export class MastodonApiServerService {
reply.send([]);
});
- fastify.post('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<AuthMastodonRoute>('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const client = getClient(BASE_URL, ''); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await ApiAuthMastodon(_request, client);
reply.send(data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/apps', data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/preferences', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getPreferences();
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/preferences', data);
+ reply.code(401).send(data);
}
});
//#region Accounts
- fastify.get('/v1/accounts/verify_credentials', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
- // displayed without being logged in
+ fastify.get<ApiAccountMastodonRoute>('/v1/accounts/verify_credentials', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.verifyCredentials());
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/accounts/verify_credentials', data);
+ reply.code(401).send(data);
}
});
- fastify.patch('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.patch<{
+ Body: {
+ discoverable?: string,
+ bot?: string,
+ display_name?: string,
+ note?: string,
+ avatar?: string,
+ header?: string,
+ locked?: string,
+ source?: {
+ privacy?: string,
+ sensitive?: string,
+ language?: string,
+ },
+ fields_attributes?: {
+ name: string,
+ value: string,
+ }[],
+ },
+ }>('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
@@ -332,512 +383,495 @@ export class MastodonApiServerService {
(_request.body as any).fields_attributes = fields.filter((field: any) => field.name.trim().length > 0 && field.value.length > 0);
}
- const data = await client.updateCredentials(_request.body!);
- reply.send(await this.mastoConverter.convertAccount(data.data));
- } catch (e: any) {
- //console.error(e);
- reply.code(401).send(e.response.data);
+ const options = {
+ ..._request.body,
+ discoverable: toBoolean(_request.body.discoverable),
+ bot: toBoolean(_request.body.bot),
+ locked: toBoolean(_request.body.locked),
+ source: _request.body.source ? {
+ ..._request.body.source,
+ sensitive: toBoolean(_request.body.source.sensitive),
+ } : undefined,
+ };
+ const data = await client.updateCredentials(options);
+ reply.send(await this.mastoConverters.convertAccount(data.data));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('PATCH /v1/accounts/update_credentials', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/accounts/lookup', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Querystring: { acct?: string }}>('/v1/accounts/lookup', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
- // displayed without being logged in
+ const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isn't displayed without being logged in
try {
- const data = await client.search((_request.query as any).acct, { type: 'accounts' });
+ if (!_request.query.acct) return reply.code(400).send({ error: 'Missing required property "acct"' });
+ const data = await client.search(_request.query.acct, { type: 'accounts' });
const profile = await this.userProfilesRepository.findOneBy({ userId: data.data.accounts[0].id });
- data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) || [];
- reply.send(await this.mastoConverter.convertAccount(data.data.accounts[0]));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) ?? [];
+ reply.send(await this.mastoConverters.convertAccount(data.data.accounts[0]));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/accounts/lookup', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/accounts/relationships', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
- // displayed without being logged in
- let users;
+ fastify.get<ApiAccountMastodonRoute & { Querystring: { id?: string | string[], 'id[]'?: string | string[] }}>('/v1/accounts/relationships', async (_request, reply) => {
try {
- let ids = _request.query ? (_request.query as any)['id[]'] ?? (_request.query as any)['id'] : null;
+ const { client, me } = await this.getAuthClient(_request);
+ let ids = _request.query['id[]'] ?? _request.query['id'] ?? [];
if (typeof ids === 'string') {
ids = [ids];
}
- users = ids;
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
- reply.send(await account.getRelationships(users));
- } catch (e: any) {
- /* console.error(e); */
- const data = e.response.data;
- data.users = users;
- console.error(data);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
+ reply.send(await account.getRelationships(ids));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/accounts/relationships', data);
reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const sharkId = _request.params.id;
- const data = await client.getAccount(sharkId);
- const account = await this.mastoConverter.convertAccount(data.data);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const data = await client.getAccount(_request.params.id);
+ const account = await this.mastoConverters.convertAccount(data.data);
reply.send(account);
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/statuses', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/statuses', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getStatuses());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/statuses`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getFeaturedTags();
reply.send(data.data.map((tag) => convertFeaturedTag(tag)));
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/featured_tags`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/followers', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/followers', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getFollowers());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/followers`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/following', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/following', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getFollowing());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/following`, data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getAccountLists(_request.params.id);
reply.send(data.data.map((list) => convertList(list)));
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/accounts/${_request.params.id}/lists`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.addFollow());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/follow`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.rmFollow());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/unfollow`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.addBlock());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/block`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.rmBlock());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/unblock`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.addMute());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/mute`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.rmMute());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/accounts/${_request.params.id}/unmute`, data);
+ reply.code(401).send(data);
}
});
fastify.get('/v1/followed_tags', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getFollowedTags();
reply.send(data.data);
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/followed_tags', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/bookmarks', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute>('/v1/bookmarks', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getBookmarks());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/bookmarks', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/favourites', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute>('/v1/favourites', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getFavourites());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/favourites', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/mutes', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute>('/v1/mutes', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getMutes());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/mutes', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/blocks', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiAccountMastodonRoute>('/v1/blocks', async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.getBlocks());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/blocks', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/follow_requests', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<{ Querystring: { limit?: string }}>('/v1/follow_requests', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const data = await client.getFollowRequests( ((_request.query as any) || { limit: 20 }).limit );
- reply.send(await Promise.all(data.data.map(async (account) => await this.mastoConverter.convertAccount(account as Entity.Account))));
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ const limit = _request.query.limit ? parseInt(_request.query.limit) : 20;
+ const data = await client.getFollowRequests(limit);
+ reply.send(await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account as Entity.Account))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/follow_requests', data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/follow_requests/:id/authorize', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/follow_requests/:id/authorize', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.acceptFollow());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/follow_requests/${_request.params.id}/authorize`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/follow_requests/:id/reject', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/follow_requests/:id/reject', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const account = new ApiAccountMastodon(_request, client, BASE_URL, this.mastoConverter);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
reply.send(await account.rejectFollow());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/follow_requests/${_request.params.id}/reject`, data);
+ reply.code(401).send(data);
}
});
//#endregion
//#region Search
- fastify.get('/v1/search', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiSearchMastodonRoute>('/v1/search', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
try {
- const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
reply.send(await search.SearchV1());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/search', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v2/search', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiSearchMastodonRoute>('/v2/search', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
try {
- const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
reply.send(await search.SearchV2());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v2/search', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v1/trends/statuses', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiSearchMastodonRoute>('/v1/trends/statuses', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
try {
- const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
reply.send(await search.getStatusTrends());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/trends/statuses', data);
+ reply.code(401).send(data);
}
});
- fastify.get('/v2/suggestions', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiSearchMastodonRoute>('/v2/suggestions', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
try {
- const search = new ApiSearchMastodon(_request, client, BASE_URL, this.mastoConverter);
+ const { client, me } = await this.getAuthClient(_request);
+ const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
reply.send(await search.getSuggestions());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v2/suggestions', data);
+ reply.code(401).send(data);
}
});
//#endregion
//#region Notifications
- fastify.get('/v1/notifications', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiNotifyMastodonRoute>('/v1/notifications', async (_request, reply) => {
try {
- const notify = new ApiNotifyMastodon(_request, client);
+ const { client, me } = await this.getAuthClient(_request);
+ const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
reply.send(await notify.getNotifications());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/notifications', data);
+ reply.code(401).send(data);
}
});
- fastify.get<{ Params: { id: string } }>('/v1/notification/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.get<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id', async (_request, reply) => {
try {
- const notify = new ApiNotifyMastodon(_request, client);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
reply.send(await notify.getNotification());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/notification/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const notify = new ApiNotifyMastodon(_request, client);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.getAuthClient(_request);
+ const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
reply.send(await notify.rmNotification());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/notification/${_request.params.id}/dismiss`, data);
+ reply.code(401).send(data);
}
});
- fastify.post('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ fastify.post<ApiNotifyMastodonRoute>('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => {
try {
- const notify = new ApiNotifyMastodon(_request, client);
+ const { client, me } = await this.getAuthClient(_request);
+ const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
reply.send(await notify.rmNotifications());
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/notifications/clear', data);
+ reply.code(401).send(data);
}
});
//#endregion
//#region Filters
- fastify.get<{ Params: { id: string } }>('/v1/filters/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.get<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const filter = new ApiFilterMastodon(_request, client);
- !_request.params.id ? reply.send(await filter.getFilters()) : reply.send(await filter.getFilter());
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ _request.params.id
+ ? reply.send(await filter.getFilter())
+ : reply.send(await filter.getFilters());
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/filters/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
- fastify.post('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<ApiFilterMastodonRoute>('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const filter = new ApiFilterMastodon(_request, client);
reply.send(await filter.createFilter());
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/filters', data);
+ reply.code(401).send(data);
}
});
- fastify.post<{ Params: { id: string } }>('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.post<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const filter = new ApiFilterMastodon(_request, client);
reply.send(await filter.updateFilter());
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/filters/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
- fastify.delete<{ Params: { id: string } }>('/v1/filters/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.delete<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const filter = new ApiFilterMastodon(_request, client);
reply.send(await filter.rmFilter());
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`DELETE /v1/filters/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
//#endregion
//#region Timelines
- const TLEndpoint = new ApiTimelineMastodon(fastify, this.config, this.mastoConverter);
+ const TLEndpoint = new ApiTimelineMastodon(fastify, this.mastoConverters, this.logger, this);
// GET Endpoints
TLEndpoint.getTL();
@@ -862,7 +896,7 @@ export class MastodonApiServerService {
//#endregion
//#region Status
- const NoteEndpoint = new ApiStatusMastodon(fastify, this.mastoConverter);
+ const NoteEndpoint = new ApiStatusMastodon(fastify, this.mastoConverters, this.logger, this.authenticateService, this);
// GET Endpoints
NoteEndpoint.getStatus();
@@ -889,16 +923,32 @@ export class MastodonApiServerService {
NoteEndpoint.votePoll();
// PUT Endpoint
- fastify.put<{ Params: { id: string } }>('/v1/media/:id', { preHandler: upload.none() }, async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ fastify.put<{
+ Params: {
+ id?: string,
+ },
+ Body: {
+ file?: unknown,
+ description?: string,
+ focus?: string,
+ is_sensitive?: string,
+ },
+ }>('/v1/media/:id', { preHandler: upload.none() }, async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
- const data = await client.updateMedia(_request.params.id, _request.body!);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const options = {
+ ..._request.body,
+ is_sensitive: toBoolean(_request.body.is_sensitive),
+ };
+ const data = await client.updateMedia(_request.params.id, options);
reply.send(convertAttachment(data.data));
- } catch (e: any) {
- /* console.error(e); */
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`PUT /v1/media/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
NoteEndpoint.updateStatus();
diff --git a/packages/backend/src/server/api/mastodon/MastodonDataService.ts b/packages/backend/src/server/api/mastodon/MastodonDataService.ts
new file mode 100644
index 0000000000..671ecdcbed
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/MastodonDataService.ts
@@ -0,0 +1,84 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { IsNull } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import { QueryService } from '@/core/QueryService.js';
+import type { MiNote, NotesRepository } from '@/models/_.js';
+import type { MiLocalUser } from '@/models/User.js';
+import { ApiError } from '../error.js';
+
+/**
+ * Utility service for accessing data with Mastodon semantics
+ */
+@Injectable()
+export class MastodonDataService {
+ constructor(
+ @Inject(DI.notesRepository)
+ private readonly notesRepository: NotesRepository,
+
+ @Inject(QueryService)
+ private readonly queryService: QueryService,
+ ) {}
+
+ /**
+ * Fetches a note in the context of the current user, and throws an exception if not found.
+ */
+ public async requireNote(noteId: string, me?: MiLocalUser | null): Promise<MiNote> {
+ const note = await this.getNote(noteId, me);
+
+ if (!note) {
+ throw new ApiError({
+ message: 'No such note.',
+ code: 'NO_SUCH_NOTE',
+ id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
+ kind: 'client',
+ httpStatusCode: 404,
+ });
+ }
+
+ return note;
+ }
+
+ /**
+ * Fetches a note in the context of the current user.
+ */
+ public async getNote(noteId: string, me?: MiLocalUser | null): Promise<MiNote | null> {
+ // Root query: note + required dependencies
+ const query = this.notesRepository
+ .createQueryBuilder('note')
+ .where('note.id = :noteId', { noteId })
+ .innerJoinAndSelect('note.user', 'user');
+
+ // Restrict visibility
+ this.queryService.generateVisibilityQuery(query, me);
+ if (me) {
+ this.queryService.generateBlockedUserQuery(query, me);
+ }
+
+ return await query.getOne();
+ }
+
+ /**
+ * Checks where the current user has made a reblog / boost / pure renote of a given target note.
+ */
+ public async hasReblog(noteId: string, me: MiLocalUser | null | undefined): Promise<boolean> {
+ if (!me) return false;
+
+ return await this.notesRepository.existsBy({
+ // Reblog of the target note by me
+ userId: me.id,
+ renoteId: noteId,
+
+ // That is pure (not a quote)
+ text: IsNull(),
+ cw: IsNull(),
+ replyId: IsNull(),
+ hasPoll: false,
+ fileIds: '{}',
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/mastodon/MastodonLogger.ts b/packages/backend/src/server/api/mastodon/MastodonLogger.ts
new file mode 100644
index 0000000000..bb844773c4
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/MastodonLogger.ts
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import Logger, { Data } from '@/logger.js';
+import { LoggerService } from '@/core/LoggerService.js';
+
+@Injectable()
+export class MastodonLogger {
+ public readonly logger: Logger;
+
+ constructor(loggerService: LoggerService) {
+ this.logger = loggerService.getLogger('masto-api');
+ }
+
+ public error(endpoint: string, error: Data): void {
+ this.logger.error(`Error in mastodon API endpoint ${endpoint}:`, error);
+ }
+}
+
+export function getErrorData(error: unknown): Data {
+ if (error == null) return {};
+ if (typeof(error) === 'string') return error;
+ if (typeof(error) === 'object') {
+ if ('response' in error) {
+ if (typeof(error.response) === 'object' && error.response) {
+ if ('data' in error.response) {
+ if (typeof(error.response.data) === 'object' && error.response.data) {
+ return error.response.data as Record<string, unknown>;
+ }
+ }
+ }
+ }
+ return error as Record<string, unknown>;
+ }
+ return { error };
+}
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
index 405701e826..b6ff5bc59a 100644
--- a/packages/backend/src/server/api/mastodon/converters.ts
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -9,18 +9,32 @@ import mfm from '@transfem-org/sfm-js';
import { DI } from '@/di-symbols.js';
import { MfmService } from '@/core/MfmService.js';
import type { Config } from '@/config.js';
-import type { IMentionedRemoteUsers } from '@/models/Note.js';
-import type { MiUser } from '@/models/User.js';
-import type { NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IMentionedRemoteUsers, MiNote } from '@/models/Note.js';
+import type { MiLocalUser, MiUser } from '@/models/User.js';
+import type { NoteEditRepository, UserProfilesRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { IdService } from '@/core/IdService.js';
-import { GetterService } from '../GetterService.js';
+import type { Packed } from '@/misc/json-schema.js';
+import { MastodonDataService } from '@/server/api/mastodon/MastodonDataService.js';
+import { GetterService } from '@/server/api/GetterService.js';
-export enum IdConvertType {
- MastodonId,
- SharkeyId,
+// Missing from Megalodon apparently
+// https://docs.joinmastodon.org/entities/StatusEdit/
+export interface StatusEdit {
+ content: string;
+ spoiler_text: string;
+ sensitive: boolean;
+ created_at: string;
+ account: MastodonEntity.Account;
+ poll?: {
+ options: {
+ title: string;
+ }[]
+ },
+ media_attachments: MastodonEntity.Attachment[],
+ emojis: MastodonEntity.Emoji[],
}
export const escapeMFM = (text: string): string => text
@@ -36,27 +50,25 @@ export const escapeMFM = (text: string): string => text
export class MastoConverters {
constructor(
@Inject(DI.config)
- private config: Config,
-
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
+ private readonly config: Config,
@Inject(DI.userProfilesRepository)
- private userProfilesRepository: UserProfilesRepository,
+ private readonly userProfilesRepository: UserProfilesRepository,
@Inject(DI.noteEditRepository)
- private noteEditRepository: NoteEditRepository,
+ private readonly noteEditRepository: NoteEditRepository,
- private mfmService: MfmService,
- private getterService: GetterService,
- private customEmojiService: CustomEmojiService,
- private idService: IdService,
- private driveFileEntityService: DriveFileEntityService,
- ) {
- }
+ private readonly mfmService: MfmService,
+ private readonly getterService: GetterService,
+ private readonly customEmojiService: CustomEmojiService,
+ private readonly idService: IdService,
+ private readonly driveFileEntityService: DriveFileEntityService,
+ private readonly mastodonDataService: MastodonDataService,
+ ) {}
- private encode(u: MiUser, m: IMentionedRemoteUsers): Entity.Mention {
+ private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention {
let acct = u.username;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let acctUrl = `https://${u.host || this.config.host}/@${u.username}`;
let url: string | null = null;
if (u.host) {
@@ -89,7 +101,11 @@ export class MastoConverters {
return 'unknown';
}
- public encodeFile(f: any): Entity.Attachment {
+ public encodeFile(f: Packed<'DriveFile'>): MastodonEntity.Attachment {
+ const { width, height } = f.properties;
+ const size = (width && height) ? `${width}x${height}` : undefined;
+ const aspect = (width && height) ? (width / height) : undefined;
+
return {
id: f.id,
type: this.fileType(f.type),
@@ -98,11 +114,19 @@ export class MastoConverters {
preview_url: f.thumbnailUrl,
text_url: f.url,
meta: {
- width: f.properties.width,
- height: f.properties.height,
+ original: {
+ width,
+ height,
+ size,
+ aspect,
+ },
+ width,
+ height,
+ size,
+ aspect,
},
- description: f.comment ? f.comment : null,
- blurhash: f.blurhash ? f.blurhash : null,
+ description: f.comment ?? null,
+ blurhash: f.blurhash ?? null,
};
}
@@ -112,7 +136,7 @@ export class MastoConverters {
});
}
- private async encodeField(f: Entity.Field): Promise<Entity.Field> {
+ private async encodeField(f: Entity.Field): Promise<MastodonEntity.Field> {
return {
name: f.name,
value: await this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
@@ -120,7 +144,7 @@ export class MastoConverters {
};
}
- public async convertAccount(account: Entity.Account | MiUser) {
+ public async convertAccount(account: Entity.Account | MiUser): Promise<MastodonEntity.Account> {
const user = await this.getUser(account.id);
const profile = await this.userProfilesRepository.findOneBy({ userId: user.id });
const emojis = await this.customEmojiService.populateEmojis(user.emojis, user.host ? user.host : this.config.host);
@@ -137,6 +161,7 @@ export class MastoConverters {
});
const fqn = `${user.username}@${user.host ?? this.config.hostname}`;
let acct = user.username;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let acctUrl = `https://${user.host || this.config.host}/@${user.username}`;
const acctUri = `https://${this.config.host}/users/${user.id}`;
if (user.host) {
@@ -166,19 +191,23 @@ export class MastoConverters {
fields: Promise.all(profile?.fields.map(async p => this.encodeField(p)) ?? []),
bot: user.isBot,
discoverable: user.isExplorable,
+ noindex: user.noindex,
+ group: null,
+ suspended: user.isSuspended,
+ limited: user.isSilenced,
});
}
- public async getEdits(id: string) {
- const note = await this.getterService.getNote(id);
+ public async getEdits(id: string, me?: MiLocalUser | null) {
+ const note = await this.mastodonDataService.getNote(id, me);
if (!note) {
- return {};
+ return [];
}
-
const noteUser = await this.getUser(note.userId).then(async (p) => await this.convertAccount(p));
const edits = await this.noteEditRepository.find({ where: { noteId: note.id }, order: { id: 'ASC' } });
- const history: Promise<any>[] = [];
+ const history: Promise<StatusEdit>[] = [];
+ // TODO this looks wrong, according to mastodon docs
let lastDate = this.idService.parse(note.id).date;
for (const edit of edits) {
const files = this.driveFileEntityService.packManyByIds(edit.fileIds);
@@ -187,9 +216,8 @@ export class MastoConverters {
content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
created_at: lastDate.toISOString(),
emojis: [],
- sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
+ sensitive: edit.cw != null && edit.cw.length > 0,
spoiler_text: edit.cw ?? '',
- poll: null,
media_attachments: files.then(files => files.length > 0 ? files.map((f) => this.encodeFile(f)) : []),
};
lastDate = edit.updatedAt;
@@ -199,15 +227,16 @@ export class MastoConverters {
return await Promise.all(history);
}
- private async convertReblog(status: Entity.Status | null): Promise<any> {
+ private async convertReblog(status: Entity.Status | null, me?: MiLocalUser | null): Promise<MastodonEntity.Status | null> {
if (!status) return null;
- return await this.convertStatus(status);
+ return await this.convertStatus(status, me);
}
- public async convertStatus(status: Entity.Status) {
+ public async convertStatus(status: Entity.Status, me?: MiLocalUser | null): Promise<MastodonEntity.Status> {
const convertedAccount = this.convertAccount(status.account);
- const note = await this.getterService.getNote(status.id);
+ const note = await this.mastodonDataService.requireNote(status.id, me);
const noteUser = await this.getUser(status.account.id);
+ const mentionedRemoteUsers = JSON.parse(note.mentionedRemoteUsers);
const emojis = await this.customEmojiService.populateEmojis(note.emojis, noteUser.host ? noteUser.host : this.config.host);
const emoji: Entity.Emoji[] = [];
@@ -224,7 +253,7 @@ export class MastoConverters {
const mentions = Promise.all(note.mentions.map(p =>
this.getUser(p)
- .then(u => this.encode(u, JSON.parse(note.mentionedRemoteUsers)))
+ .then(u => this.encode(u, mentionedRemoteUsers))
.catch(() => null)))
.then(p => p.filter(m => m)) as Promise<Entity.Mention[]>;
@@ -235,20 +264,26 @@ export class MastoConverters {
} as Entity.Tag;
});
- const isQuote = note.renoteId && note.text ? true : false;
+ // This must mirror the usual isQuote / isPureRenote logic used elsewhere.
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const isQuote = note.renoteId && (note.text || note.cw || note.fileIds.length > 0 || note.hasPoll || note.replyId);
- const renote = note.renoteId ? this.getterService.getNote(note.renoteId) : null;
+ const renote: Promise<MiNote> | null = note.renoteId ? this.mastodonDataService.requireNote(note.renoteId, me) : null;
const quoteUri = Promise.resolve(renote).then(renote => {
if (!renote || !isQuote) return null;
return renote.url ?? renote.uri ?? `${this.config.url}/notes/${renote.id}`;
});
- const content = note.text !== null
- ? quoteUri.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
- .then(p => p ?? escapeMFM(note.text!))
+ const text = note.text;
+ const content = text !== null
+ ? quoteUri
+ .then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(text), mentionedRemoteUsers, false, quoteUri))
+ .then(p => p ?? escapeMFM(text))
: '';
+ const reblogged = await this.mastodonDataService.hasReblog(note.id, me);
+
// noinspection ES6MissingAwait
return await awaitAll({
id: note.id,
@@ -257,7 +292,7 @@ export class MastoConverters {
account: convertedAccount,
in_reply_to_id: note.replyId,
in_reply_to_account_id: note.replyUserId,
- reblog: !isQuote ? await this.convertReblog(status.reblog) : null,
+ reblog: !isQuote ? await this.convertReblog(status.reblog, me) : null,
content: content,
content_type: 'text/x.misskeymarkdown',
text: note.text,
@@ -266,34 +301,51 @@ export class MastoConverters {
replies_count: note.repliesCount,
reblogs_count: note.renoteCount,
favourites_count: status.favourites_count,
- reblogged: false,
+ reblogged,
favourited: status.favourited,
muted: status.muted,
sensitive: status.sensitive,
- spoiler_text: note.cw ? note.cw : '',
+ spoiler_text: note.cw ?? '',
visibility: status.visibility,
- media_attachments: status.media_attachments,
+ media_attachments: status.media_attachments.map(a => convertAttachment(a)),
mentions: mentions,
tags: tags,
card: null, //FIXME
poll: status.poll ?? null,
application: null, //FIXME
language: null, //FIXME
- pinned: false,
+ pinned: false, //FIXME
reactions: status.emoji_reactions,
emoji_reactions: status.emoji_reactions,
- bookmarked: false,
- quote: isQuote ? await this.convertReblog(status.reblog) : null,
- // optional chaining cannot be used, as it evaluates to undefined, not null
- edited_at: note.updatedAt ? note.updatedAt.toISOString() : null,
+ bookmarked: false, //FIXME
+ quote: isQuote ? await this.convertReblog(status.reblog, me) : null,
+ edited_at: note.updatedAt?.toISOString() ?? null,
});
}
+
+ public async convertConversation(conversation: Entity.Conversation, me?: MiLocalUser | null): Promise<MastodonEntity.Conversation> {
+ return {
+ id: conversation.id,
+ accounts: await Promise.all(conversation.accounts.map(a => this.convertAccount(a))),
+ last_status: conversation.last_status ? await this.convertStatus(conversation.last_status, me) : null,
+ unread: conversation.unread,
+ };
+ }
+
+ public async convertNotification(notification: Entity.Notification, me?: MiLocalUser | null): Promise<MastodonEntity.Notification> {
+ return {
+ account: await this.convertAccount(notification.account),
+ created_at: notification.created_at,
+ id: notification.id,
+ status: notification.status ? await this.convertStatus(notification.status, me) : undefined,
+ type: notification.type,
+ };
+ }
}
-function simpleConvert(data: any) {
+function simpleConvert<T>(data: T): T {
// copy the object to bypass weird pass by reference bugs
- const result = Object.assign({}, data);
- return result;
+ return Object.assign({}, data);
}
export function convertAccount(account: Entity.Account) {
@@ -302,8 +354,30 @@ export function convertAccount(account: Entity.Account) {
export function convertAnnouncement(announcement: Entity.Announcement) {
return simpleConvert(announcement);
}
-export function convertAttachment(attachment: Entity.Attachment) {
- return simpleConvert(attachment);
+export function convertAttachment(attachment: Entity.Attachment): MastodonEntity.Attachment {
+ const { width, height } = attachment.meta?.original ?? attachment.meta ?? {};
+ const size = (width && height) ? `${width}x${height}` : undefined;
+ const aspect = (width && height) ? (width / height) : undefined;
+ return {
+ ...attachment,
+ meta: attachment.meta ? {
+ ...attachment.meta,
+ original: {
+ ...attachment.meta.original,
+ width,
+ height,
+ size,
+ aspect,
+ frame_rate: String(attachment.meta.fps),
+ duration: attachment.meta.duration,
+ bitrate: attachment.meta.audio_bitrate ? parseInt(attachment.meta.audio_bitrate) : undefined,
+ },
+ width,
+ height,
+ size,
+ aspect,
+ } : null,
+ };
}
export function convertFilter(filter: Entity.Filter) {
return simpleConvert(filter);
@@ -315,45 +389,40 @@ export function convertFeaturedTag(tag: Entity.FeaturedTag) {
return simpleConvert(tag);
}
-export function convertNotification(notification: Entity.Notification) {
- notification.account = convertAccount(notification.account);
- if (notification.status) notification.status = convertStatus(notification.status);
- return notification;
-}
-
export function convertPoll(poll: Entity.Poll) {
return simpleConvert(poll);
}
+
+// noinspection JSUnusedGlobalSymbols
export function convertReaction(reaction: Entity.Reaction) {
if (reaction.accounts) {
reaction.accounts = reaction.accounts.map(convertAccount);
}
return reaction;
}
-export function convertRelationship(relationship: Entity.Relationship) {
- return simpleConvert(relationship);
-}
-export function convertStatus(status: Entity.Status) {
- status.account = convertAccount(status.account);
- status.media_attachments = status.media_attachments.map((attachment) =>
- convertAttachment(attachment),
- );
- if (status.poll) status.poll = convertPoll(status.poll);
- if (status.reblog) status.reblog = convertStatus(status.reblog);
-
- return status;
+// Megalodon sometimes returns broken / stubbed relationship data
+export function convertRelationship(relationship: Partial<Entity.Relationship> & { id: string }): MastodonEntity.Relationship {
+ return {
+ id: relationship.id,
+ following: relationship.following ?? false,
+ showing_reblogs: relationship.showing_reblogs ?? true,
+ notifying: relationship.notifying ?? true,
+ languages: [],
+ followed_by: relationship.followed_by ?? false,
+ blocking: relationship.blocking ?? false,
+ blocked_by: relationship.blocked_by ?? false,
+ muting: relationship.muting ?? false,
+ muting_notifications: relationship.muting_notifications ?? false,
+ requested: relationship.requested ?? false,
+ requested_by: relationship.requested_by ?? false,
+ domain_blocking: relationship.domain_blocking ?? false,
+ endorsed: relationship.endorsed ?? false,
+ note: relationship.note ?? '',
+ };
}
+// noinspection JSUnusedGlobalSymbols
export function convertStatusSource(status: Entity.StatusSource) {
return simpleConvert(status);
}
-
-export function convertConversation(conversation: Entity.Conversation) {
- conversation.accounts = conversation.accounts.map(convertAccount);
- if (conversation.last_status) {
- conversation.last_status = convertStatus(conversation.last_status);
- }
-
- return conversation;
-}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 6fcfb0019c..79cdddcb9e 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -3,273 +3,149 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { Injectable } from '@nestjs/common';
+import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
+import { MiLocalUser } from '@/models/User.js';
import { MastoConverters, convertRelationship } from '../converters.js';
-import { argsToBools, limitToInt } from './timeline.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
-import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import type { Config } from '@/config.js';
-import { Injectable } from '@nestjs/common';
-const relationshipModel = {
- id: '',
- following: false,
- followed_by: false,
- delivery_following: false,
- blocking: false,
- blocked_by: false,
- muting: false,
- muting_notifications: false,
- requested: false,
- domain_blocking: false,
- showing_reblogs: false,
- endorsed: false,
- notifying: false,
- note: '',
-};
+export interface ApiAccountMastodonRoute {
+ Params: { id?: string },
+ Querystring: TimelineArgs & { acct?: string },
+ Body: { notifications?: boolean }
+}
@Injectable()
export class ApiAccountMastodon {
- private request: FastifyRequest;
- private client: MegalodonInterface;
- private BASE_URL: string;
-
- constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoconverter: MastoConverters) {
- this.request = request;
- this.client = client;
- this.BASE_URL = BASE_URL;
- }
+ constructor(
+ private readonly request: FastifyRequest<ApiAccountMastodonRoute>,
+ private readonly client: MegalodonInterface,
+ private readonly me: MiLocalUser | null,
+ private readonly mastoConverters: MastoConverters,
+ ) {}
public async verifyCredentials() {
- try {
- const data = await this.client.verifyAccountCredentials();
- const acct = await this.mastoconverter.convertAccount(data.data);
- const newAcct = Object.assign({}, acct, {
- source: {
- note: acct.note,
- fields: acct.fields,
- privacy: '',
- sensitive: false,
- language: '',
- },
- });
- return newAcct;
- } catch (e: any) {
- /* console.error(e);
- console.error(e.response.data); */
- return e.response;
- }
+ const data = await this.client.verifyAccountCredentials();
+ const acct = await this.mastoConverters.convertAccount(data.data);
+ return Object.assign({}, acct, {
+ source: {
+ note: acct.note,
+ fields: acct.fields,
+ privacy: '',
+ sensitive: false,
+ language: '',
+ },
+ });
}
public async lookup() {
- try {
- const data = await this.client.search((this.request.query as any).acct, { type: 'accounts' });
- return this.mastoconverter.convertAccount(data.data.accounts[0]);
- } catch (e: any) {
- /* console.error(e)
- console.error(e.response.data); */
- return e.response;
- }
+ if (!this.request.query.acct) throw new Error('Missing required property "acct"');
+ const data = await this.client.search(this.request.query.acct, { type: 'accounts' });
+ return this.mastoConverters.convertAccount(data.data.accounts[0]);
}
- public async getRelationships(users: [string]) {
- try {
- relationshipModel.id = users.toString() || '1';
-
- if (!(users.length > 0)) {
- return [relationshipModel];
- }
-
- const reqIds = [];
- for (let i = 0; i < users.length; i++) {
- reqIds.push(users[i]);
- }
-
- const data = await this.client.getRelationships(reqIds);
- return data.data.map((relationship) => convertRelationship(relationship));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ public async getRelationships(reqIds: string[]) {
+ const data = await this.client.getRelationships(reqIds);
+ return data.data.map(relationship => convertRelationship(relationship));
}
public async getStatuses() {
- try {
- const data = await this.client.getAccountStatuses((this.request.params as any).id, argsToBools(limitToInt(this.request.query as any)));
- return await Promise.all(data.data.map(async (status) => await this.mastoconverter.convertStatus(status)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getAccountStatuses(this.request.params.id, parseTimelineArgs(this.request.query));
+ return await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, this.me)));
}
public async getFollowers() {
- try {
- const data = await this.client.getAccountFollowers(
- (this.request.params as any).id,
- limitToInt(this.request.query as any),
- );
- return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getAccountFollowers(
+ this.request.params.id,
+ parseTimelineArgs(this.request.query),
+ );
+ return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
}
public async getFollowing() {
- try {
- const data = await this.client.getAccountFollowing(
- (this.request.params as any).id,
- limitToInt(this.request.query as any),
- );
- return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getAccountFollowing(
+ this.request.params.id,
+ parseTimelineArgs(this.request.query),
+ );
+ return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
}
public async addFollow() {
- try {
- const data = await this.client.followAccount( (this.request.params as any).id );
- const acct = convertRelationship(data.data);
- acct.following = true;
- return acct;
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.followAccount(this.request.params.id);
+ const acct = convertRelationship(data.data);
+ acct.following = true;
+ return acct;
}
public async rmFollow() {
- try {
- const data = await this.client.unfollowAccount( (this.request.params as any).id );
- const acct = convertRelationship(data.data);
- acct.following = false;
- return acct;
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.unfollowAccount(this.request.params.id);
+ const acct = convertRelationship(data.data);
+ acct.following = false;
+ return acct;
}
public async addBlock() {
- try {
- const data = await this.client.blockAccount( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.blockAccount(this.request.params.id);
+ return convertRelationship(data.data);
}
public async rmBlock() {
- try {
- const data = await this.client.unblockAccount( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.unblockAccount(this.request.params.id);
+ return convertRelationship(data.data);
}
public async addMute() {
- try {
- const data = await this.client.muteAccount(
- (this.request.params as any).id,
- this.request.body as any,
- );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.muteAccount(
+ this.request.params.id,
+ this.request.body.notifications ?? true,
+ );
+ return convertRelationship(data.data);
}
public async rmMute() {
- try {
- const data = await this.client.unmuteAccount( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.unmuteAccount(this.request.params.id);
+ return convertRelationship(data.data);
}
public async getBookmarks() {
- try {
- const data = await this.client.getBookmarks( limitToInt(this.request.query as any) );
- return data.data.map((status) => this.mastoconverter.convertStatus(status));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ const data = await this.client.getBookmarks(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
}
public async getFavourites() {
- try {
- const data = await this.client.getFavourites( limitToInt(this.request.query as any) );
- return data.data.map((status) => this.mastoconverter.convertStatus(status));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ const data = await this.client.getFavourites(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
}
public async getMutes() {
- try {
- const data = await this.client.getMutes( limitToInt(this.request.query as any) );
- return data.data.map((account) => this.mastoconverter.convertAccount(account));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ const data = await this.client.getMutes(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
}
public async getBlocks() {
- try {
- const data = await this.client.getBlocks( limitToInt(this.request.query as any) );
- return data.data.map((account) => this.mastoconverter.convertAccount(account));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ const data = await this.client.getBlocks(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
}
public async acceptFollow() {
- try {
- const data = await this.client.acceptFollowRequest( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.acceptFollowRequest(this.request.params.id);
+ return convertRelationship(data.data);
}
public async rejectFollow() {
- try {
- const data = await this.client.rejectFollowRequest( (this.request.params as any).id );
- return convertRelationship(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.rejectFollowRequest(this.request.params.id);
+ return convertRelationship(data.data);
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/auth.ts b/packages/backend/src/server/api/mastodon/endpoints/auth.ts
index a447bdb1b7..b58cc902da 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/auth.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/auth.ts
@@ -44,36 +44,54 @@ const writeScope = [
'write:gallery-likes',
];
-export async function ApiAuthMastodon(request: FastifyRequest, client: MegalodonInterface) {
- const body: any = request.body || request.query;
- try {
- let scope = body.scopes;
- if (typeof scope === 'string') scope = scope.split(' ') || scope.split('+');
- const pushScope = new Set<string>();
- for (const s of scope) {
- if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
- if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
- }
- const scopeArr = Array.from(pushScope);
+export interface AuthPayload {
+ scopes?: string | string[],
+ redirect_uris?: string,
+ client_name?: string,
+ website?: string,
+}
+
+// Not entirely right, but it gets TypeScript to work so *shrug*
+export type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload };
- const red = body.redirect_uris;
- const appData = await client.registerApp(body.client_name, {
- scopes: scopeArr,
- redirect_uris: red,
- website: body.website,
- });
- const returns = {
- id: Math.floor(Math.random() * 100).toString(),
- name: appData.name,
- website: body.website,
- redirect_uri: red,
- client_id: Buffer.from(appData.url || '').toString('base64'),
- client_secret: appData.clientSecret,
- };
+export async function ApiAuthMastodon(request: FastifyRequest<AuthMastodonRoute>, client: MegalodonInterface) {
+ const body = request.body ?? request.query;
+ if (!body.scopes) throw new Error('Missing required payload "scopes"');
+ if (!body.redirect_uris) throw new Error('Missing required payload "redirect_uris"');
+ if (!body.client_name) throw new Error('Missing required payload "client_name"');
- return returns;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
+ let scope = body.scopes;
+ if (typeof scope === 'string') {
+ scope = scope.split(/[ +]/g);
}
+
+ const pushScope = new Set<string>();
+ for (const s of scope) {
+ if (s.match(/^read/)) {
+ for (const r of readScope) {
+ pushScope.add(r);
+ }
+ }
+ if (s.match(/^write/)) {
+ for (const r of writeScope) {
+ pushScope.add(r);
+ }
+ }
+ }
+
+ const red = body.redirect_uris;
+ const appData = await client.registerApp(body.client_name, {
+ scopes: Array.from(pushScope),
+ redirect_uris: red,
+ website: body.website,
+ });
+
+ return {
+ id: Math.floor(Math.random() * 100).toString(),
+ name: appData.name,
+ website: body.website,
+ redirect_uri: red,
+ client_id: Buffer.from(appData.url || '').toString('base64'), // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
+ client_secret: appData.clientSecret,
+ };
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
index ce6809d230..382f0a8f1f 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
@@ -3,68 +3,73 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { toBoolean } from '@/server/api/mastodon/timelineArgs.js';
import { convertFilter } from '../converters.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
-export class ApiFilterMastodon {
- private request: FastifyRequest;
- private client: MegalodonInterface;
-
- constructor(request: FastifyRequest, client: MegalodonInterface) {
- this.request = request;
- this.client = client;
+export interface ApiFilterMastodonRoute {
+ Params: {
+ id?: string,
+ },
+ Body: {
+ phrase?: string,
+ context?: string[],
+ irreversible?: string,
+ whole_word?: string,
+ expires_in?: string,
}
+}
+
+export class ApiFilterMastodon {
+ constructor(
+ private readonly request: FastifyRequest<ApiFilterMastodonRoute>,
+ private readonly client: MegalodonInterface,
+ ) {}
public async getFilters() {
- try {
- const data = await this.client.getFilters();
- return data.data.map((filter) => convertFilter(filter));
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ const data = await this.client.getFilters();
+ return data.data.map((filter) => convertFilter(filter));
}
public async getFilter() {
- try {
- const data = await this.client.getFilter( (this.request.params as any).id );
- return convertFilter(data.data);
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getFilter(this.request.params.id);
+ return convertFilter(data.data);
}
public async createFilter() {
- try {
- const body: any = this.request.body;
- const data = await this.client.createFilter(body.pharse, body.context, body);
- return convertFilter(data.data);
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
+ if (!this.request.body.context) throw new Error('Missing required payload "context"');
+ const options = {
+ phrase: this.request.body.phrase,
+ context: this.request.body.context,
+ irreversible: toBoolean(this.request.body.irreversible),
+ whole_word: toBoolean(this.request.body.whole_word),
+ expires_in: this.request.body.expires_in,
+ };
+ const data = await this.client.createFilter(this.request.body.phrase, this.request.body.context, options);
+ return convertFilter(data.data);
}
public async updateFilter() {
- try {
- const body: any = this.request.body;
- const data = await this.client.updateFilter((this.request.params as any).id, body.pharse, body.context);
- return convertFilter(data.data);
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
+ if (!this.request.body.context) throw new Error('Missing required payload "context"');
+ const options = {
+ phrase: this.request.body.phrase,
+ context: this.request.body.context,
+ irreversible: toBoolean(this.request.body.irreversible),
+ whole_word: toBoolean(this.request.body.whole_word),
+ expires_in: this.request.body.expires_in,
+ };
+ const data = await this.client.updateFilter(this.request.params.id, this.request.body.phrase, this.request.body.context, options);
+ return convertFilter(data.data);
}
public async rmFilter() {
- try {
- const data = await this.client.deleteFilter( (this.request.params as any).id );
- return data.data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.deleteFilter(this.request.params.id);
+ return data.data;
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
index c9833b85d7..48a56138cf 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
@@ -8,6 +8,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import type { Config } from '@/config.js';
import type { MiMeta } from '@/models/Meta.js';
+/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
export async function getInstance(
response: Entity.Instance,
contact: Entity.Account,
@@ -17,11 +18,8 @@ export async function getInstance(
return {
uri: config.url,
title: meta.name || 'Sharkey',
- short_description:
- meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
- description:
- meta.description ||
- 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
+ short_description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
+ description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
email: response.email || '',
version: `3.0.0 (compatible; Sharkey ${config.version})`,
urls: response.urls,
diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
index 0eefb5894c..14eee8565a 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
@@ -3,73 +3,56 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { convertNotification } from '../converters.js';
-import type { MegalodonInterface, Entity } from 'megalodon';
+import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
+import { MiLocalUser } from '@/models/User.js';
+import { MastoConverters } from '@/server/api/mastodon/converters.js';
+import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
-function toLimitToInt(q: any) {
- if (q.limit) if (typeof q.limit === 'string') q.limit = parseInt(q.limit, 10);
- return q;
+export interface ApiNotifyMastodonRoute {
+ Params: {
+ id?: string,
+ },
+ Querystring: TimelineArgs,
}
export class ApiNotifyMastodon {
- private request: FastifyRequest;
- private client: MegalodonInterface;
-
- constructor(request: FastifyRequest, client: MegalodonInterface) {
- this.request = request;
- this.client = client;
- }
+ constructor(
+ private readonly request: FastifyRequest<ApiNotifyMastodonRoute>,
+ private readonly client: MegalodonInterface,
+ private readonly me: MiLocalUser | null,
+ private readonly mastoConverters: MastoConverters,
+ ) {}
public async getNotifications() {
- try {
- const data = await this.client.getNotifications( toLimitToInt(this.request.query) );
- const notifs = data.data;
- const processed = notifs.map((n: Entity.Notification) => {
- const convertedn = convertNotification(n);
- if (convertedn.type !== 'follow' && convertedn.type !== 'follow_request') {
- if (convertedn.type === 'reaction') convertedn.type = 'favourite';
- return convertedn;
- } else {
- return convertedn;
- }
- });
- return processed;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ const data = await this.client.getNotifications(parseTimelineArgs(this.request.query));
+ return Promise.all(data.data.map(async n => {
+ const converted = await this.mastoConverters.convertNotification(n, this.me);
+ if (converted.type === 'reaction') {
+ converted.type = 'favourite';
+ }
+ return converted;
+ }));
}
public async getNotification() {
- try {
- const data = await this.client.getNotification( (this.request.params as any).id );
- const notif = convertNotification(data.data);
- if (notif.type !== 'follow' && notif.type !== 'follow_request' && notif.type === 'reaction') notif.type = 'favourite';
- return notif;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.getNotification(this.request.params.id);
+ const converted = await this.mastoConverters.convertNotification(data.data, this.me);
+ if (converted.type === 'reaction') {
+ converted.type = 'favourite';
}
+ return converted;
}
public async rmNotification() {
- try {
- const data = await this.client.dismissNotification( (this.request.params as any).id );
- return data.data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.params.id) throw new Error('Missing required parameter "id"');
+ const data = await this.client.dismissNotification(this.request.params.id);
+ return data.data;
}
public async rmNotifications() {
- try {
- const data = await this.client.dismissNotifications();
- return data.data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ const data = await this.client.dismissNotifications();
+ return data.data;
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts
index 946e796e2a..4850b4652f 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/search.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts
@@ -3,88 +3,92 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { MiLocalUser } from '@/models/User.js';
import { MastoConverters } from '../converters.js';
-import { limitToInt } from './timeline.js';
+import { parseTimelineArgs, TimelineArgs } from '../timelineArgs.js';
+import Account = Entity.Account;
+import Status = Entity.Status;
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
-export class ApiSearchMastodon {
- private request: FastifyRequest;
- private client: MegalodonInterface;
- private BASE_URL: string;
-
- constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoConverter: MastoConverters) {
- this.request = request;
- this.client = client;
- this.BASE_URL = BASE_URL;
+export interface ApiSearchMastodonRoute {
+ Querystring: TimelineArgs & {
+ type?: 'accounts' | 'hashtags' | 'statuses';
+ q?: string;
}
+}
+
+export class ApiSearchMastodon {
+ constructor(
+ private readonly request: FastifyRequest<ApiSearchMastodonRoute>,
+ private readonly client: MegalodonInterface,
+ private readonly me: MiLocalUser | null,
+ private readonly BASE_URL: string,
+ private readonly mastoConverters: MastoConverters,
+ ) {}
public async SearchV1() {
- try {
- const query: any = limitToInt(this.request.query as any);
- const type = query.type || '';
- const data = await this.client.search(query.q, { type: type, ...query });
- return data.data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.query.q) throw new Error('Missing required property "q"');
+ const query = parseTimelineArgs(this.request.query);
+ const data = await this.client.search(this.request.query.q, { type: this.request.query.type, ...query });
+ return data.data;
}
public async SearchV2() {
- try {
- const query: any = limitToInt(this.request.query as any);
- const type = query.type;
- const acct = !type || type === 'accounts' ? await this.client.search(query.q, { type: 'accounts', ...query }) : null;
- const stat = !type || type === 'statuses' ? await this.client.search(query.q, { type: 'statuses', ...query }) : null;
- const tags = !type || type === 'hashtags' ? await this.client.search(query.q, { type: 'hashtags', ...query }) : null;
- const data = {
- accounts: await Promise.all(acct?.data.accounts.map(async (account: any) => await this.mastoConverter.convertAccount(account)) ?? []),
- statuses: await Promise.all(stat?.data.statuses.map(async (status: any) => await this.mastoConverter.convertStatus(status)) ?? []),
- hashtags: tags?.data.hashtags ?? [],
- };
- return data;
- } catch (e: any) {
- console.error(e);
- return e.response.data;
- }
+ if (!this.request.query.q) throw new Error('Missing required property "q"');
+ const query = parseTimelineArgs(this.request.query);
+ const type = this.request.query.type;
+ const acct = !type || type === 'accounts' ? await this.client.search(this.request.query.q, { type: 'accounts', ...query }) : null;
+ const stat = !type || type === 'statuses' ? await this.client.search(this.request.query.q, { type: 'statuses', ...query }) : null;
+ const tags = !type || type === 'hashtags' ? await this.client.search(this.request.query.q, { type: 'hashtags', ...query }) : null;
+ return {
+ accounts: await Promise.all(acct?.data.accounts.map(async (account: Account) => await this.mastoConverters.convertAccount(account)) ?? []),
+ statuses: await Promise.all(stat?.data.statuses.map(async (status: Status) => await this.mastoConverters.convertStatus(status, this.me)) ?? []),
+ hashtags: tags?.data.hashtags ?? [],
+ };
}
public async getStatusTrends() {
- try {
- const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
- {
- method: 'POST',
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({}),
- })
- .then(res => res.json())
- .then(data => data.map((status: any) => this.mastoConverter.convertStatus(status)));
- return data;
- } catch (e: any) {
- console.error(e);
- return [];
- }
+ const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
+ {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ i: this.request.headers.authorization?.replace('Bearer ', ''),
+ }),
+ })
+ .then(res => res.json() as Promise<Status[]>)
+ .then(data => data.map(status => this.mastoConverters.convertStatus(status, this.me)));
+ return Promise.all(data);
}
public async getSuggestions() {
- try {
- const data = await fetch(`${this.BASE_URL}/api/users`,
- {
- method: 'POST',
- headers: {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ i: this.request.headers.authorization?.replace('Bearer ', ''), limit: parseInt((this.request.query as any).limit) || 20, origin: 'local', sort: '+follower', state: 'alive' }),
- }).then((res) => res.json()).then(data => data.map(((entry: any) => { return { source: 'global', account: entry }; })));
- return Promise.all(data.map(async (suggestion: any) => { suggestion.account = await this.mastoConverter.convertAccount(suggestion.account); return suggestion; }));
- } catch (e: any) {
- console.error(e);
- return [];
- }
+ const data = await fetch(`${this.BASE_URL}/api/users`,
+ {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ i: this.request.headers.authorization?.replace('Bearer ', ''),
+ limit: parseTimelineArgs(this.request.query).limit ?? 20,
+ origin: 'local',
+ sort: '+follower',
+ state: 'alive',
+ }),
+ })
+ .then(res => res.json() as Promise<Account[]>)
+ .then(data => data.map((entry => ({
+ source: 'global',
+ account: entry,
+ }))));
+ return Promise.all(data.map(async suggestion => {
+ suggestion.account = await this.mastoConverters.convertAccount(suggestion.account);
+ return suggestion;
+ }));
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index ddc99639fa..4c49a6a293 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -3,181 +3,212 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import querystring from 'querystring';
+import querystring, { ParsedUrlQueryInput } from 'querystring';
import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
-import { convertAttachment, convertPoll, convertStatusSource, MastoConverters } from '../converters.js';
-import { getClient } from '../MastodonApiServerService.js';
-import { limitToInt } from './timeline.js';
+import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
+import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '@/server/api/mastodon/timelineArgs.js';
+import { AuthenticateService } from '@/server/api/AuthenticateService.js';
+import { convertAttachment, convertPoll, MastoConverters } from '../converters.js';
+import { getAccessToken, getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
import type { Entity } from 'megalodon';
import type { FastifyInstance } from 'fastify';
-import type { Config } from '@/config.js';
-import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-function normalizeQuery(data: any) {
- const str = querystring.stringify(data);
+function normalizeQuery(data: Record<string, unknown>) {
+ const str = querystring.stringify(data as ParsedUrlQueryInput);
return querystring.parse(str);
}
export class ApiStatusMastodon {
- private fastify: FastifyInstance;
- private mastoconverter: MastoConverters;
+ constructor(
+ private readonly fastify: FastifyInstance,
+ private readonly mastoConverters: MastoConverters,
+ private readonly logger: MastodonLogger,
+ private readonly authenticateService: AuthenticateService,
+ private readonly mastodon: MastodonApiServerService,
+ ) {}
- constructor(fastify: FastifyInstance, mastoconverter: MastoConverters) {
- this.fastify = fastify;
- this.mastoconverter = mastoconverter;
- }
-
- public async getStatus() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getStatus() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
try {
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(_request.is404 ? 404 : 401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}`, data);
+ reply.code(_request.is404 ? 404 : 401).send(data);
}
});
}
- public async getStatusSource() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getStatusSource() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatusSource(_request.params.id);
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(_request.is404 ? 404 : 401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/source`, data);
+ reply.code(_request.is404 ? 404 : 401).send(data);
}
});
}
- public async getContext() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/context', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- const query: any = _request.query;
+ public getContext() {
+ this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/statuses/:id/context', async (_request, reply) => {
try {
- const data = await client.getStatusContext(_request.params.id, limitToInt(query));
- data.data.ancestors = await Promise.all(data.data.ancestors.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
- data.data.descendants = await Promise.all(data.data.descendants.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
- reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(_request.is404 ? 404 : 401).send(e.response.data);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const { data } = await client.getStatusContext(_request.params.id, parseTimelineArgs(_request.query));
+ const ancestors = await Promise.all(data.ancestors.map(async status => await this.mastoConverters.convertStatus(status, me)));
+ const descendants = await Promise.all(data.descendants.map(async status => await this.mastoConverters.convertStatus(status, me)));
+ reply.send({ ancestors, descendants });
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/context`, data);
+ reply.code(_request.is404 ? 404 : 401).send(data);
}
});
}
- public async getHistory() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
+ public getHistory() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
try {
- const edits = await this.mastoconverter.getEdits(_request.params.id);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const [user] = await this.authenticateService.authenticate(getAccessToken(_request.headers.authorization));
+ const edits = await this.mastoConverters.getEdits(_request.params.id, user);
reply.send(edits);
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/history`, data);
+ reply.code(401).send(data);
}
});
}
- public async getReblogged() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getReblogged() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatusRebloggedBy(_request.params.id);
- reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/reblogged_by`, data);
+ reply.code(401).send(data);
}
});
}
- public async getFavourites() {
- this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getFavourites() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatusFavouritedBy(_request.params.id);
- reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/favourited_by`, data);
+ reply.code(401).send(data);
}
});
}
- public async getMedia() {
- this.fastify.get<{ Params: { id: string } }>('/v1/media/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getMedia() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getMedia(_request.params.id);
reply.send(convertAttachment(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/media/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async getPoll() {
- this.fastify.get<{ Params: { id: string } }>('/v1/polls/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public getPoll() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getPoll(_request.params.id);
reply.send(convertPoll(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/polls/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async votePoll() {
- this.fastify.post<{ Params: { id: string } }>('/v1/polls/:id/votes', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public votePoll() {
+ this.fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const body: any = _request.body;
try {
- const data = await client.votePoll(_request.params.id, body.choices);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.body.choices) return reply.code(400).send({ error: 'Missing required payload "choices"' });
+ const data = await client.votePoll(_request.params.id, _request.body.choices);
reply.send(convertPoll(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/polls/${_request.params.id}/votes`, data);
+ reply.code(401).send(data);
}
});
}
- public async postStatus() {
- this.fastify.post('/v1/statuses', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- let body: any = _request.body;
+ public postStatus() {
+ this.fastify.post<{
+ Body: {
+ media_ids?: string[],
+ poll?: {
+ options?: string[],
+ expires_in?: string,
+ multiple?: string,
+ hide_totals?: string,
+ },
+ in_reply_to_id?: string,
+ sensitive?: string,
+ spoiler_text?: string,
+ visibility?: 'public' | 'unlisted' | 'private' | 'direct',
+ scheduled_at?: string,
+ language?: string,
+ quote_id?: string,
+ status?: string,
+
+ // Broken clients
+ 'poll[options][]'?: string[],
+ 'media_ids[]'?: string[],
+ }
+ }>('/v1/statuses', async (_request, reply) => {
+ let body = _request.body;
try {
- if (
- (!body.poll && body['poll[options][]']) ||
- (!body.media_ids && body['media_ids[]'])
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]'])
) {
body = normalizeQuery(body);
}
- const text = body.status ? body.status : ' ';
+ const text = body.status ??= ' ';
const removed = text.replace(/@\S+/g, '').replace(/\s|/g, '');
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
@@ -189,226 +220,253 @@ export class ApiStatusMastodon {
reply.send(a.data);
}
if (body.in_reply_to_id && removed === '/unreact') {
- try {
- const id = body.in_reply_to_id;
- const post = await client.getStatus(id);
- const react = post.data.emoji_reactions.filter((e: any) => e.me)[0].name;
- const data = await client.deleteEmojiReaction(id, react);
- reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
- }
+ const id = body.in_reply_to_id;
+ const post = await client.getStatus(id);
+ const react = post.data.emoji_reactions.filter(e => e.me)[0].name;
+ const data = await client.deleteEmojiReaction(id, react);
+ reply.send(data.data);
}
if (!body.media_ids) body.media_ids = undefined;
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
- const { sensitive } = body;
- body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive;
-
- if (body.poll) {
- if (
- body.poll.expires_in != null &&
- typeof body.poll.expires_in === 'string'
- ) body.poll.expires_in = parseInt(body.poll.expires_in);
- if (
- body.poll.multiple != null &&
- typeof body.poll.multiple === 'string'
- ) body.poll.multiple = body.poll.multiple === 'true';
- if (
- body.poll.hide_totals != null &&
- typeof body.poll.hide_totals === 'string'
- ) body.poll.hide_totals = body.poll.hide_totals === 'true';
+ if (body.poll && !body.poll.options) {
+ return reply.code(400).send({ error: 'Missing required payload "poll.options"' });
+ }
+ if (body.poll && !body.poll.expires_in) {
+ return reply.code(400).send({ error: 'Missing required payload "poll.expires_in"' });
}
- const data = await client.postStatus(text, body);
- reply.send(await this.mastoconverter.convertStatus(data.data as Entity.Status));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ const options = {
+ ...body,
+ sensitive: toBoolean(body.sensitive),
+ poll: body.poll ? {
+ options: body.poll.options!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
+ expires_in: toInt(body.poll.expires_in)!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
+ multiple: toBoolean(body.poll.multiple),
+ hide_totals: toBoolean(body.poll.hide_totals),
+ } : undefined,
+ };
+
+ const data = await client.postStatus(text, options);
+ reply.send(await this.mastoConverters.convertStatus(data.data as Entity.Status, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/statuses', data);
+ reply.code(401).send(data);
}
});
}
- public async updateStatus() {
- this.fastify.put<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
- const body: any = _request.body;
+ public updateStatus() {
+ this.fastify.put<{
+ Params: { id: string },
+ Body: {
+ status?: string,
+ spoiler_text?: string,
+ sensitive?: string,
+ media_ids?: string[],
+ poll?: {
+ options?: string[],
+ expires_in?: string,
+ multiple?: string,
+ hide_totals?: string,
+ },
+ }
+ }>('/v1/statuses/:id', async (_request, reply) => {
try {
- if (!body.media_ids) body.media_ids = undefined;
- if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
- const data = await client.editStatus(_request.params.id, body);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(_request.is404 ? 404 : 401).send(e.response.data);
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const body = _request.body;
+
+ if (!body.media_ids || !body.media_ids.length) {
+ body.media_ids = undefined;
+ }
+
+ const options = {
+ ...body,
+ sensitive: toBoolean(body.sensitive),
+ poll: body.poll ? {
+ options: body.poll.options,
+ expires_in: toInt(body.poll.expires_in),
+ multiple: toBoolean(body.poll.multiple),
+ hide_totals: toBoolean(body.poll.hide_totals),
+ } : undefined,
+ };
+
+ const data = await client.editStatus(_request.params.id, options);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async addFavourite() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public addFavourite() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
try {
- const data = (await client.createEmojiReaction(_request.params.id, '❤')) as any;
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.createEmojiReaction(_request.params.id, '❤');
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/favorite`, data);
+ reply.code(401).send(data);
}
});
}
- public async rmFavourite() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public rmFavourite() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
try {
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.deleteEmojiReaction(_request.params.id, '❤');
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/statuses/${_request.params.id}/unfavorite`, data);
+ reply.code(401).send(data);
}
});
}
- public async reblogStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public reblogStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.reblogStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/reblog`, data);
+ reply.code(401).send(data);
}
});
}
- public async unreblogStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public unreblogStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.unreblogStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/unreblog`, data);
+ reply.code(401).send(data);
}
});
}
- public async bookmarkStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public bookmarkStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.bookmarkStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/bookmark`, data);
+ reply.code(401).send(data);
}
});
}
- public async unbookmarkStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public unbookmarkStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.unbookmarkStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/unbookmark`, data);
+ reply.code(401).send(data);
}
});
}
- public async pinStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public pinStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.pinStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/pin`, data);
+ reply.code(401).send(data);
}
});
}
- public async unpinStatus() {
- this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public unpinStatus() {
+ this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.unpinStatus(_request.params.id);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/unpin`, data);
+ reply.code(401).send(data);
}
});
}
- public async reactStatus() {
- this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public reactStatus() {
+ this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/react/${_request.params.name}`, data);
+ reply.code(401).send(data);
}
});
}
- public async unreactStatus() {
- this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public unreactStatus() {
+ this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
- reply.send(await this.mastoconverter.convertStatus(data.data));
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ reply.send(await this.mastoConverters.convertStatus(data.data, me));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/statuses/${_request.params.id}/unreact/${_request.params.name}`, data);
+ reply.code(401).send(data);
}
});
}
- public async deleteStatus() {
- this.fastify.delete<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ public deleteStatus() {
+ this.fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.deleteStatus(_request.params.id);
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`DELETE /v1/statuses/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
index 3eb4898713..1a732d62de 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
@@ -3,270 +3,231 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { ParsedUrlQuery } from 'querystring';
-import { convertConversation, convertList, MastoConverters } from '../converters.js';
-import { getClient } from '../MastodonApiServerService.js';
+import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
+import { convertList, MastoConverters } from '../converters.js';
+import { getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
+import { parseTimelineArgs, TimelineArgs, toBoolean } from '../timelineArgs.js';
import type { Entity } from 'megalodon';
import type { FastifyInstance } from 'fastify';
-import type { Config } from '@/config.js';
-import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-
-export function limitToInt(q: ParsedUrlQuery) {
- const object: any = q;
- if (q.limit) if (typeof q.limit === 'string') object.limit = parseInt(q.limit, 10);
- if (q.offset) if (typeof q.offset === 'string') object.offset = parseInt(q.offset, 10);
- return object;
-}
-
-export function argsToBools(q: ParsedUrlQuery) {
- // Values taken from https://docs.joinmastodon.org/client/intro/#boolean
- const toBoolean = (value: string) =>
- !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
-
- // Keys taken from:
- // - https://docs.joinmastodon.org/methods/accounts/#statuses
- // - https://docs.joinmastodon.org/methods/timelines/#public
- // - https://docs.joinmastodon.org/methods/timelines/#tag
- const object: any = q;
- if (q.only_media) if (typeof q.only_media === 'string') object.only_media = toBoolean(q.only_media);
- if (q.exclude_replies) if (typeof q.exclude_replies === 'string') object.exclude_replies = toBoolean(q.exclude_replies);
- if (q.exclude_reblogs) if (typeof q.exclude_reblogs === 'string') object.exclude_reblogs = toBoolean(q.exclude_reblogs);
- if (q.pinned) if (typeof q.pinned === 'string') object.pinned = toBoolean(q.pinned);
- if (q.local) if (typeof q.local === 'string') object.local = toBoolean(q.local);
- return q;
-}
export class ApiTimelineMastodon {
- private fastify: FastifyInstance;
-
- constructor(fastify: FastifyInstance, config: Config, private mastoconverter: MastoConverters) {
- this.fastify = fastify;
- }
+ constructor(
+ private readonly fastify: FastifyInstance,
+ private readonly mastoConverters: MastoConverters,
+ private readonly logger: MastodonLogger,
+ private readonly mastodon: MastodonApiServerService,
+ ) {}
- public async getTL() {
- this.fastify.get('/v1/timelines/public', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getTL() {
+ this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/public', async (_request, reply) => {
try {
- const query: any = _request.query;
- const data = query.local === 'true'
- ? await client.getLocalTimeline(argsToBools(limitToInt(query)))
- : await client.getPublicTimeline(argsToBools(limitToInt(query)));
- reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = toBoolean(_request.query.local)
+ ? await client.getLocalTimeline(parseTimelineArgs(_request.query))
+ : await client.getPublicTimeline(parseTimelineArgs(_request.query));
+ reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/timelines/public', data);
+ reply.code(401).send(data);
}
});
}
- public async getHomeTl() {
- this.fastify.get('/v1/timelines/home', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getHomeTl() {
+ this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (_request, reply) => {
try {
- const query: any = _request.query;
- const data = await client.getHomeTimeline(limitToInt(query));
- reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.getHomeTimeline(parseTimelineArgs(_request.query));
+ reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/timelines/home', data);
+ reply.code(401).send(data);
}
});
}
- public async getTagTl() {
- this.fastify.get<{ Params: { hashtag: string } }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getTagTl() {
+ this.fastify.get<{ Params: { hashtag?: string }, Querystring: TimelineArgs }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
try {
- const query: any = _request.query;
- const params: any = _request.params;
- const data = await client.getTagTimeline(params.hashtag, limitToInt(query));
- reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ if (!_request.params.hashtag) return reply.code(400).send({ error: 'Missing required parameter "hashtag"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.getTagTimeline(_request.params.hashtag, parseTimelineArgs(_request.query));
+ reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/timelines/tag/${_request.params.hashtag}`, data);
+ reply.code(401).send(data);
}
});
}
- public async getListTL() {
- this.fastify.get<{ Params: { id: string } }>('/v1/timelines/list/:id', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getListTL() {
+ this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/timelines/list/:id', async (_request, reply) => {
try {
- const query: any = _request.query;
- const params: any = _request.params;
- const data = await client.getListTimeline(params.id, limitToInt(query));
- reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.getListTimeline(_request.params.id, parseTimelineArgs(_request.query));
+ reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/timelines/list/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async getConversations() {
- this.fastify.get('/v1/conversations', async (_request, reply) => {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
- const accessTokens = _request.headers.authorization;
- const client = getClient(BASE_URL, accessTokens);
+ public getConversations() {
+ this.fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (_request, reply) => {
try {
- const query: any = _request.query;
- const data = await client.getConversationTimeline(limitToInt(query));
- reply.send(data.data.map((conversation: Entity.Conversation) => convertConversation(conversation)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ const { client, me } = await this.mastodon.getAuthClient(_request);
+ const data = await client.getConversationTimeline(parseTimelineArgs(_request.query));
+ const conversations = await Promise.all(data.data.map(async (conversation: Entity.Conversation) => await this.mastoConverters.convertConversation(conversation, me)));
+ reply.send(conversations);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/conversations', data);
+ reply.code(401).send(data);
}
});
}
- public async getList() {
- this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
+ public getList() {
+ this.fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const data = await client.getList(params.id);
+ const data = await client.getList(_request.params.id);
reply.send(convertList(data.data));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/lists/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async getLists() {
+ public getLists() {
this.fastify.get('/v1/lists', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const data = await client.getLists();
reply.send(data.data.map((list: Entity.List) => convertList(list)));
- } catch (e: any) {
- console.error(e);
- return e.response.data;
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('GET /v1/lists', data);
+ reply.code(401).send(data);
}
});
}
- public async getListAccounts() {
- this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
+ public getListAccounts() {
+ this.fastify.get<{ Params: { id?: string }, Querystring: { limit?: number, max_id?: string, since_id?: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const query: any = _request.query;
- const data = await client.getAccountsInList(params.id, query);
- reply.send(data.data.map((account: Entity.Account) => this.mastoconverter.convertAccount(account)));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ const data = await client.getAccountsInList(_request.params.id, _request.query);
+ const accounts = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
+ reply.send(accounts);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`GET /v1/lists/${_request.params.id}/accounts`, data);
+ reply.code(401).send(data);
}
});
}
- public async addListAccount() {
- this.fastify.post<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
+ public addListAccount() {
+ this.fastify.post<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const query: any = _request.query;
- const data = await client.addAccountsToList(params.id, query.accounts_id);
+ const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id);
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`POST /v1/lists/${_request.params.id}/accounts`, data);
+ reply.code(401).send(data);
}
});
}
- public async rmListAccount() {
- this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
+ public rmListAccount() {
+ this.fastify.delete<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const query: any = _request.query;
- const data = await client.deleteAccountsFromList(params.id, query.accounts_id);
+ const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id);
reply.send(data.data);
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`DELETE /v1/lists/${_request.params.id}/accounts`, data);
+ reply.code(401).send(data);
}
});
}
- public async createList() {
- this.fastify.post('/v1/lists', async (_request, reply) => {
+ public createList() {
+ this.fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const body: any = _request.body;
- const data = await client.createList(body.title);
+ const data = await client.createList(_request.body.title);
reply.send(convertList(data.data));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error('POST /v1/lists', data);
+ reply.code(401).send(data);
}
});
}
- public async updateList() {
- this.fastify.put<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
+ public updateList() {
+ this.fastify.put<{ Params: { id?: string }, Body: { title?: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const body: any = _request.body;
- const params: any = _request.params;
- const data = await client.updateList(params.id, body.title);
+ const data = await client.updateList(_request.params.id, _request.body.title);
reply.send(convertList(data.data));
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`PUT /v1/lists/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
- public async deleteList() {
- this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
+ public deleteList() {
+ this.fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
- const BASE_URL = `${_request.protocol}://${_request.hostname}`;
+ if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
+ const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
- const params: any = _request.params;
- const data = await client.deleteList(params.id);
+ await client.deleteList(_request.params.id);
reply.send({});
- } catch (e: any) {
- console.error(e);
- console.error(e.response.data);
- reply.code(401).send(e.response.data);
+ } catch (e) {
+ const data = getErrorData(e);
+ this.logger.error(`DELETE /v1/lists/${_request.params.id}`, data);
+ reply.code(401).send(data);
}
});
}
diff --git a/packages/backend/src/server/api/mastodon/timelineArgs.ts b/packages/backend/src/server/api/mastodon/timelineArgs.ts
new file mode 100644
index 0000000000..3fba8ec57a
--- /dev/null
+++ b/packages/backend/src/server/api/mastodon/timelineArgs.ts
@@ -0,0 +1,47 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+// Keys taken from:
+// - https://docs.joinmastodon.org/methods/accounts/#statuses
+// - https://docs.joinmastodon.org/methods/timelines/#public
+// - https://docs.joinmastodon.org/methods/timelines/#tag
+export interface TimelineArgs {
+ max_id?: string;
+ min_id?: string;
+ since_id?: string;
+ limit?: string;
+ offset?: string;
+ local?: string;
+ pinned?: string;
+ exclude_reblogs?: string;
+ exclude_replies?: string;
+ only_media?: string;
+}
+
+// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
+export function toBoolean(value: string | undefined): boolean | undefined {
+ if (value === undefined) return undefined;
+ return !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
+}
+
+export function toInt(value: string | undefined): number | undefined {
+ if (value === undefined) return undefined;
+ return parseInt(value);
+}
+
+export function parseTimelineArgs(q: TimelineArgs) {
+ return {
+ max_id: q.max_id,
+ min_id: q.min_id,
+ since_id: q.since_id,
+ limit: typeof(q.limit) === 'string' ? parseInt(q.limit, 10) : undefined,
+ offset: typeof(q.offset) === 'string' ? parseInt(q.offset, 10) : undefined,
+ local: typeof(q.local) === 'string' ? toBoolean(q.local) : undefined,
+ pinned: typeof(q.pinned) === 'string' ? toBoolean(q.pinned) : undefined,
+ exclude_reblogs: typeof(q.exclude_reblogs) === 'string' ? toBoolean(q.exclude_reblogs) : undefined,
+ exclude_replies: typeof(q.exclude_replies) === 'string' ? toBoolean(q.exclude_replies) : undefined,
+ only_media: typeof(q.only_media) === 'string' ? toBoolean(q.only_media) : undefined,
+ };
+}
diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts
index eb854a7141..c80dda8d96 100644
--- a/packages/backend/src/server/api/openapi/schemas.ts
+++ b/packages/backend/src/server/api/openapi/schemas.ts
@@ -3,13 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { deepClone } from '@/misc/clone.js';
import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js';
export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res', includeSelfRef: boolean): any {
// optional, nullable, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { optional, nullable, ref, selfRef, ...res }: any = schema;
+ const { optional, nullable, ref, selfRef, ..._res }: any = schema;
+ const res = deepClone(_res);
if (schema.type === 'object' && schema.properties) {
if (type === 'res') {
diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts
index f102cb42e1..e98e2a2f3f 100644
--- a/packages/backend/src/server/api/stream/Connection.ts
+++ b/packages/backend/src/server/api/stream/Connection.ts
@@ -16,11 +16,11 @@ import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import type Logger from '@/logger.js';
import type { ChannelsService } from './ChannelsService.js';
import type { EventEmitter } from 'events';
import type Channel from './channel.js';
-import { LoggerService } from '@/core/LoggerService.js';
-import type Logger from '@/logger.js';
const MAX_CHANNELS_PER_CONNECTION = 32;
@@ -45,8 +45,8 @@ export default class Connection {
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
public userMutedInstances: Set<string> = new Set();
private fetchIntervalId: NodeJS.Timeout | null = null;
- private activeRateLimitRequests: number = 0;
- private closingConnection: boolean = false;
+ private activeRateLimitRequests = 0;
+ private closingConnection = false;
private logger: Logger;
constructor(
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index ae9c7e3e99..7a6193ccfc 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -6,9 +6,10 @@
import { bindThis } from '@/decorators.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
-import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import { isRenotePacked, isQuotePacked, isPackedPureRenote } from '@/misc/is-renote.js';
import type { Packed } from '@/misc/json-schema.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import type Connection from './Connection.js';
/**
@@ -16,6 +17,7 @@ import type Connection from './Connection.js';
*/
// eslint-disable-next-line import/no-default-export
export default abstract class Channel {
+ protected readonly noteEntityService: NoteEntityService;
protected connection: Connection;
public id: string;
public abstract readonly chName: string;
@@ -74,12 +76,29 @@ export default abstract class Channel {
// 流れてきたNoteがリノートをミュートしてるユーザが行ったもの
if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true;
+ // If it's a boost (pure renote) then we need to check the target as well
+ if (isPackedPureRenote(note) && note.renote && this.isNoteMutedOrBlocked(note.renote)) return true;
+
return false;
}
- constructor(id: string, connection: Connection) {
+ protected async hideNote(note: Packed<'Note'>): Promise<void> {
+ if (note.renote) {
+ await this.hideNote(note.renote);
+ }
+
+ if (note.reply) {
+ await this.hideNote(note.reply);
+ }
+
+ const meId = this.user?.id ?? null;
+ await this.noteEntityService.hideNote(note, meId);
+ }
+
+ constructor(id: string, connection: Connection, noteEntityService: NoteEntityService) {
this.id = id;
this.connection = connection;
+ this.noteEntityService = noteEntityService;
}
public send(payload: { type: string, body: JsonValue }): void
@@ -101,6 +120,44 @@ export default abstract class Channel {
public dispose?(): void;
public onMessage?(type: string, body: JsonValue): void;
+
+ public async assignMyReaction(note: Packed<'Note'>): Promise<Packed<'Note'>> {
+ let changed = false;
+ // StreamingApiServerService creates a single EventEmitter per server process,
+ // so a new note arriving from redis gets de-serialised once per server process,
+ // and then that single object is passed to all active channels on each connection.
+ // If we didn't clone the notes here, different connections would asynchronously write
+ // different values to the same object, resulting in a random value being sent to each frontend. -- Dakkar
+ const clonedNote = { ...note };
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
+ if (note.renote && Object.keys(note.renote.reactions).length > 0) {
+ const myReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
+ if (myReaction) {
+ changed = true;
+ clonedNote.renote = { ...note.renote };
+ clonedNote.renote.myReaction = myReaction;
+ }
+ }
+ if (note.renote?.reply && Object.keys(note.renote.reply.reactions).length > 0) {
+ const myReaction = await this.noteEntityService.populateMyReaction(note.renote.reply, this.user.id);
+ if (myReaction) {
+ changed = true;
+ clonedNote.renote = { ...note.renote };
+ clonedNote.renote.reply = { ...note.renote.reply };
+ clonedNote.renote.reply.myReaction = myReaction;
+ }
+ }
+ }
+ if (this.user && note.reply && Object.keys(note.reply.reactions).length > 0) {
+ const myReaction = await this.noteEntityService.populateMyReaction(note.reply, this.user.id);
+ if (myReaction) {
+ changed = true;
+ clonedNote.reply = { ...note.reply };
+ clonedNote.reply.myReaction = myReaction;
+ }
+ }
+ return changed ? clonedNote : note;
+ }
}
export type MiChannelService<T extends boolean> = {
diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts
index 355d5dba21..a0140d395d 100644
--- a/packages/backend/src/server/api/stream/channels/admin.ts
+++ b/packages/backend/src/server/api/stream/channels/admin.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
class AdminChannel extends Channel {
@@ -30,6 +31,7 @@ export class AdminChannelService implements MiChannelService<true> {
public readonly kind = AdminChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -38,6 +40,7 @@ export class AdminChannelService implements MiChannelService<true> {
return new AdminChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 53dc7f18b6..a73d158b7f 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -18,12 +18,12 @@ class AntennaChannel extends Channel {
private antennaId: string;
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onEvent = this.onEvent.bind(this);
}
diff --git a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
index 8693f0c6ac..5ebbdcbb86 100644
--- a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
@@ -3,8 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
-import { DI } from '@/di-symbols.js';
+import { Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -27,13 +26,12 @@ class BubbleTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
private roleService: RoleService,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
- //this.onNote = this.onNote.bind(this);
+ super(id, connection, noteEntityService);
}
@bindThis
@@ -65,16 +63,12 @@ class BubbleTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 9939aa49ee..ec0bc7e13a 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -7,7 +7,6 @@ import { Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
-import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@@ -20,12 +19,12 @@ class ChannelChannel extends Channel {
private withRenotes: boolean;
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -50,16 +49,12 @@ class ChannelChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts
index 03768f3d23..fa097fdc35 100644
--- a/packages/backend/src/server/api/stream/channels/drive.ts
+++ b/packages/backend/src/server/api/stream/channels/drive.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
class DriveChannel extends Channel {
@@ -30,6 +31,7 @@ export class DriveChannelService implements MiChannelService<true> {
public readonly kind = DriveChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -38,6 +40,7 @@ export class DriveChannelService implements MiChannelService<true> {
return new DriveChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 6fe76747ee..72a8a8b156 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -24,12 +24,12 @@ class GlobalTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
private roleService: RoleService,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -60,16 +60,12 @@ class GlobalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
index 8105f15cb1..7c8df87721 100644
--- a/packages/backend/src/server/api/stream/channels/hashtag.ts
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -8,7 +8,6 @@ import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
-import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.js';
import Channel, { type MiChannelService } from '../channel.js';
@@ -19,12 +18,12 @@ class HashtagChannel extends Channel {
private q: string[][];
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -46,16 +45,12 @@ class HashtagChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index 359ab3e223..c87a21be82 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -20,12 +20,12 @@ class HomeTimelineChannel extends Channel {
private withFiles: boolean;
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -81,16 +81,12 @@ class HomeTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 01645fe657..95b762e2b7 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -26,12 +26,12 @@ class HybridTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
private roleService: RoleService,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -98,17 +98,12 @@ class HybridTimelineChannel extends Channel {
}
}
- if (this.user && note.renoteId && !note.text) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- console.log(note.renote.reactionAndUserPairCache);
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 1f9d25b44d..b9e0a4c234 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -25,12 +25,12 @@ class LocalTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
private roleService: RoleService,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -70,16 +70,12 @@ class LocalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts
index 863d7f4c4e..6b144e43e4 100644
--- a/packages/backend/src/server/api/stream/channels/main.ts
+++ b/packages/backend/src/server/api/stream/channels/main.ts
@@ -17,12 +17,12 @@ class MainChannel extends Channel {
public static kind = 'read:account';
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
}
@bindThis
diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts
index 91b62255b4..a4006ab752 100644
--- a/packages/backend/src/server/api/stream/channels/queue-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts
@@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev();
@@ -17,8 +18,8 @@ class QueueStatsChannel extends Channel {
public static shouldShare = true;
public static requireCredential = false as const;
- constructor(id: string, connection: Channel['connection']) {
- super(id, connection);
+ constructor(id: string, connection: Channel['connection'], noteEntityService: NoteEntityService) {
+ super(id, connection, noteEntityService);
//this.onStats = this.onStats.bind(this);
//this.onMessage = this.onMessage.bind(this);
}
@@ -64,6 +65,7 @@ export class QueueStatsChannelService implements MiChannelService<false> {
public readonly kind = QueueStatsChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -72,6 +74,7 @@ export class QueueStatsChannelService implements MiChannelService<false> {
return new QueueStatsChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts
index 7597a1cfa3..b7fffbe844 100644
--- a/packages/backend/src/server/api/stream/channels/reversi-game.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts
@@ -11,6 +11,7 @@ import { ReversiService } from '@/core/ReversiService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
import { reversiUpdateKeys } from 'misskey-js';
@@ -23,11 +24,12 @@ class ReversiGameChannel extends Channel {
constructor(
private reversiService: ReversiService,
private reversiGameEntityService: ReversiGameEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
}
@bindThis
@@ -116,6 +118,7 @@ export class ReversiGameChannelService implements MiChannelService<false> {
constructor(
private reversiService: ReversiService,
private reversiGameEntityService: ReversiGameEntityService,
+ private noteEntityService: NoteEntityService,
) {
}
@@ -124,6 +127,7 @@ export class ReversiGameChannelService implements MiChannelService<false> {
return new ReversiGameChannel(
this.reversiService,
this.reversiGameEntityService,
+ this.noteEntityService,
id,
connection,
);
diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts
index 6e88939724..dc73d3a3d8 100644
--- a/packages/backend/src/server/api/stream/channels/reversi.ts
+++ b/packages/backend/src/server/api/stream/channels/reversi.ts
@@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import type { JsonObject } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
class ReversiChannel extends Channel {
@@ -17,8 +18,9 @@ class ReversiChannel extends Channel {
constructor(
id: string,
connection: Channel['connection'],
+ noteEntityService: NoteEntityService,
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
}
@bindThis
@@ -40,6 +42,7 @@ export class ReversiChannelService implements MiChannelService<true> {
public readonly kind = ReversiChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -48,6 +51,7 @@ export class ReversiChannelService implements MiChannelService<true> {
return new ReversiChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index fcfa26c38b..14c4d96479 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -18,13 +18,13 @@ class RoleTimelineChannel extends Channel {
private roleId: string;
constructor(
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
private roleservice: RoleService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.onNote = this.onNote.bind(this);
}
@@ -48,7 +48,12 @@ class RoleTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- this.send('note', note);
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
+
+ this.connection.cacheNote(clonedNote);
+
+ this.send('note', clonedNote);
} else {
this.send(data.type, data.body);
}
diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts
index ec5352d12d..43cbf65110 100644
--- a/packages/backend/src/server/api/stream/channels/server-stats.ts
+++ b/packages/backend/src/server/api/stream/channels/server-stats.ts
@@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { isJsonObject } from '@/misc/json-value.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import Channel, { type MiChannelService } from '../channel.js';
const ev = new Xev();
@@ -17,8 +18,8 @@ class ServerStatsChannel extends Channel {
public static shouldShare = true;
public static requireCredential = false as const;
- constructor(id: string, connection: Channel['connection']) {
- super(id, connection);
+ constructor(id: string, connection: Channel['connection'], noteEntityService: NoteEntityService) {
+ super(id, connection, noteEntityService);
//this.onStats = this.onStats.bind(this);
//this.onMessage = this.onMessage.bind(this);
}
@@ -62,6 +63,7 @@ export class ServerStatsChannelService implements MiChannelService<false> {
public readonly kind = ServerStatsChannel.kind;
constructor(
+ private readonly noteEntityService: NoteEntityService,
) {
}
@@ -70,6 +72,7 @@ export class ServerStatsChannelService implements MiChannelService<false> {
return new ServerStatsChannel(
id,
connection,
+ this.noteEntityService,
);
}
}
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index 4f38351e94..d09a9b8d9f 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -26,12 +26,12 @@ class UserListChannel extends Channel {
constructor(
private userListsRepository: UserListsRepository,
private userListMembershipsRepository: UserListMembershipsRepository,
- private noteEntityService: NoteEntityService,
+ noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
- super(id, connection);
+ super(id, connection, noteEntityService);
//this.updateListUsers = this.updateListUsers.bind(this);
//this.onNote = this.onNote.bind(this);
}
@@ -111,16 +111,12 @@ class UserListChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
- if (note.renote && Object.keys(note.renote.reactions).length > 0) {
- const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
- note.renote.myReaction = myRenoteReaction;
- }
- }
+ const clonedNote = await this.assignMyReaction(note);
+ await this.hideNote(clonedNote);
- this.connection.cacheNote(note);
+ this.connection.cacheNote(clonedNote);
- this.send('note', note);
+ this.send('note', clonedNote);
}
@bindThis