diff options
| author | Julia <julia@insertdomain.name> | 2025-03-02 19:54:32 +0000 |
|---|---|---|
| committer | Julia <julia@insertdomain.name> | 2025-03-02 19:54:32 +0000 |
| commit | 9e13c375c5ef4103ad5ee87fea583b154e9e16f3 (patch) | |
| tree | fe9e7b1a474e22fb0c37bd68cfd260f7ba39be74 /packages/backend/src/server/api | |
| parent | merge: pin corepack version (!885) (diff) | |
| parent | bump version (diff) | |
| download | sharkey-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')
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 |