From b8f8d8d85975a6130e05cbcea72a8f1f8c3529ef Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 31 Aug 2026 16:34:22 +0200 Subject: [PATCH] feat(expressions): remove the unfinished packs feature (#2250) --- fluxer_admin/openapi-admin.json | 5 - fluxer_api/src/api/RateLimitConfig.ts | 2 - fluxer_api/src/api/Tables.ts | 22 - fluxer_api/src/api/app/ControllerRegistry.ts | 2 - .../api/channel/services/ChannelService.ts | 3 - .../services/message/MessageContentService.ts | 7 - .../message/MessagePersistenceService.ts | 16 +- .../services/message/MessageStickerService.ts | 14 - .../src/api/database/types/UserTypes.ts | 28 - fluxer_api/src/api/invite/InviteController.ts | 70 +- fluxer_api/src/api/invite/InviteModel.ts | 69 - .../src/api/invite/InviteRequestService.ts | 51 - fluxer_api/src/api/invite/InviteService.ts | 101 +- .../middleware/GuildStackServiceFactory.ts | 23 - .../src/api/middleware/ServiceMiddleware.ts | 13 - .../src/api/middleware/ServiceSingletons.ts | 4 - .../tests/LazyServiceProvider.test.ts | 2 - fluxer_api/src/api/models/ExpressionPack.ts | 41 - fluxer_api/src/api/openapi/openapi.json | 1708 +---------------- .../api/pack/PackExpressionAccessResolver.ts | 15 - fluxer_api/src/api/pack/PackModel.ts | 20 - fluxer_api/src/api/pack/PackRepository.ts | 116 -- fluxer_api/src/api/pack/PackService.ts | 642 ------- .../api/pack/controllers/PackController.ts | 161 -- .../pack/controllers/PackEmojiController.ts | 148 -- .../pack/controllers/PackStickerController.ts | 152 -- fluxer_api/src/api/pack/controllers/index.ts | 12 - .../src/api/pack/tests/PackInviteFlow.test.ts | 199 -- .../tests/PackPremiumRequirements.test.ts | 113 -- .../src/api/pack/tests/PackTestUtils.ts | 206 -- .../rate_limit_configs/PackRateLimitConfig.ts | 75 - fluxer_api/src/api/types/HonoEnv.ts | 4 - .../UserCustomStatusEmojiPremium.test.ts | 2 +- fluxer_api/src/api/utils/EmojiUtils.ts | 33 +- .../src/api/worker/WorkerDependencies.ts | 5 - .../components/AllSettingsRenderer.tsx | 2 - .../components/FeatureComparisonTable.tsx | 5 - .../src/features/app/constants/Endpoints.ts | 11 - .../auth/flow/AuthPageStyles.module.css | 27 - .../src/features/auth/flow/InviteHeader.tsx | 94 +- .../channel/components/InviteEmbed.module.css | 42 - .../channel/components/InviteEmbed.tsx | 103 +- .../developer_tools/ResetOptions.ts | 1 - .../DeveloperOptionsToggleGroups.ts | 14 - .../devtools/state/DeveloperOptions.ts | 3 - .../expressions/commands/PackCommands.ts | 89 - .../modals/CreatePackModal.module.css | 16 - .../components/modals/CreatePackModal.tsx | 145 -- .../components/modals/EditPackModal.tsx | 132 -- .../modals/PackInviteModal.module.css | 25 - .../components/modals/PackInviteModal.tsx | 218 --- .../expressions/state/ExpressionsPacks.ts | 76 - .../locales/auto-i18n-reviewed-unchanged.json | 3 - .../invite/commands/InviteCommands.tsx | 264 +-- .../invite/commands/PackInviteCommands.ts | 50 - .../modals/InviteAcceptModal.module.css | 25 - .../components/modals/InviteAcceptModal.tsx | 86 +- .../src/features/invite/state/Invites.ts | 15 +- .../src/features/invite/types/InviteTypes.ts | 5 +- .../state/QuickSwitcherCandidateBuilder.ts | 6 - .../ui/action_menu/SettingsContextMenu.tsx | 7 +- .../components/modals/UserSettingsModal.tsx | 9 +- .../modals/tabs/ExpressionPacksTab.module.css | 105 - .../modals/tabs/ExpressionPacksTab.tsx | 366 ---- .../AdvancedSettingsCategories.ts | 1 - .../settings_utils/DesktopSettingsTabs.ts | 2 - .../settings_utils/SettingsConstants.tsx | 11 - .../settings_utils/SettingsSearchIndex.ts | 2 - .../settings_utils/UserSettingsDeepLinks.ts | 1 - .../search_index/ExpressionPacksIndex.ts | 70 - .../GeneralSettingsSections.ts | 10 - .../section_registry/SectionRegistryTypes.ts | 1 - packages/constants/src/ApiErrorCodes.ts | 5 - packages/constants/src/ChannelConstants.ts | 2 - packages/constants/src/LimitConfigMetadata.ts | 33 - packages/constants/src/LimitConstants.ts | 5 - packages/constants/src/LimitTierPerks.ts | 8 - .../src/domains/core/FeatureAccessError.ts | 10 - .../src/domains/pack/InvalidPackTypeError.ts | 13 - .../domains/pack/MaxPackExpressionsError.ts | 16 - .../src/domains/pack/MaxPackLimitError.ts | 18 - .../src/domains/pack/PackAccessDeniedError.ts | 10 - .../src/domains/pack/UnknownPackError.ts | 10 - packages/errors/src/i18n/ErrorCodeMappings.ts | 5 - packages/errors/src/i18n/ErrorI18nMessages.ts | 6 - packages/errors/src/i18n/locales/ar.ts | 5 - packages/errors/src/i18n/locales/bg.ts | 5 - packages/errors/src/i18n/locales/cs.ts | 5 - packages/errors/src/i18n/locales/da.ts | 5 - packages/errors/src/i18n/locales/de.ts | 5 - packages/errors/src/i18n/locales/el.ts | 5 - packages/errors/src/i18n/locales/en-GB.ts | 5 - packages/errors/src/i18n/locales/es-419.ts | 5 - packages/errors/src/i18n/locales/es-ES.ts | 5 - packages/errors/src/i18n/locales/fi.ts | 5 - packages/errors/src/i18n/locales/fr.ts | 5 - packages/errors/src/i18n/locales/he.ts | 5 - packages/errors/src/i18n/locales/hi.ts | 5 - packages/errors/src/i18n/locales/hr.ts | 5 - packages/errors/src/i18n/locales/hu.ts | 5 - packages/errors/src/i18n/locales/id.ts | 5 - packages/errors/src/i18n/locales/it.ts | 5 - packages/errors/src/i18n/locales/ja.ts | 5 - packages/errors/src/i18n/locales/ko.ts | 5 - packages/errors/src/i18n/locales/lt.ts | 5 - packages/errors/src/i18n/locales/nl.ts | 5 - packages/errors/src/i18n/locales/no.ts | 5 - packages/errors/src/i18n/locales/pl.ts | 5 - packages/errors/src/i18n/locales/pt-BR.ts | 5 - packages/errors/src/i18n/locales/ro.ts | 5 - packages/errors/src/i18n/locales/ru.ts | 5 - packages/errors/src/i18n/locales/sv-SE.ts | 5 - packages/errors/src/i18n/locales/th.ts | 5 - packages/errors/src/i18n/locales/tr.ts | 5 - packages/errors/src/i18n/locales/uk.ts | 5 - packages/errors/src/i18n/locales/vi.ts | 5 - packages/errors/src/i18n/locales/zh-CN.ts | 5 - packages/errors/src/i18n/locales/zh-TW.ts | 5 - .../errors/src/i18n/weblate/locales/ar.json | 5 - .../errors/src/i18n/weblate/locales/bg.json | 5 - .../errors/src/i18n/weblate/locales/cs.json | 5 - .../errors/src/i18n/weblate/locales/da.json | 5 - .../errors/src/i18n/weblate/locales/de.json | 5 - .../errors/src/i18n/weblate/locales/el.json | 5 - .../src/i18n/weblate/locales/en-GB.json | 5 - .../src/i18n/weblate/locales/es-419.json | 5 - .../src/i18n/weblate/locales/es-ES.json | 5 - .../errors/src/i18n/weblate/locales/fi.json | 5 - .../errors/src/i18n/weblate/locales/fr.json | 5 - .../errors/src/i18n/weblate/locales/he.json | 5 - .../errors/src/i18n/weblate/locales/hi.json | 5 - .../errors/src/i18n/weblate/locales/hr.json | 5 - .../errors/src/i18n/weblate/locales/hu.json | 5 - .../errors/src/i18n/weblate/locales/id.json | 5 - .../errors/src/i18n/weblate/locales/it.json | 5 - .../errors/src/i18n/weblate/locales/ja.json | 5 - .../errors/src/i18n/weblate/locales/ko.json | 5 - .../errors/src/i18n/weblate/locales/lt.json | 5 - .../errors/src/i18n/weblate/locales/nl.json | 5 - .../errors/src/i18n/weblate/locales/no.json | 5 - .../errors/src/i18n/weblate/locales/pl.json | 5 - .../src/i18n/weblate/locales/pt-BR.json | 5 - .../errors/src/i18n/weblate/locales/ro.json | 5 - .../errors/src/i18n/weblate/locales/ru.json | 5 - .../src/i18n/weblate/locales/sv-SE.json | 5 - .../errors/src/i18n/weblate/locales/th.json | 5 - .../errors/src/i18n/weblate/locales/tr.json | 5 - .../errors/src/i18n/weblate/locales/uk.json | 5 - .../errors/src/i18n/weblate/locales/vi.json | 5 - .../src/i18n/weblate/locales/zh-CN.json | 5 - .../src/i18n/weblate/locales/zh-TW.json | 5 - .../errors/src/i18n/weblate/messages.json | 5 - packages/limits/src/LimitDefaults.ts | 10 - .../src/generator/OpenAPIGeneratorCatalog.ts | 2 - .../openapi/src/registry/ParameterRegistry.ts | 7 - .../src/domains/common/CommonParamSchemas.ts | 20 - .../src/domains/invite/InviteSchemas.ts | 93 +- .../schema/src/domains/pack/PackSchemas.ts | 70 - tools/dev/cassandra_target_schema.json | 101 - 159 files changed, 42 insertions(+), 6863 deletions(-) delete mode 100644 fluxer_api/src/api/models/ExpressionPack.ts delete mode 100644 fluxer_api/src/api/pack/PackExpressionAccessResolver.ts delete mode 100644 fluxer_api/src/api/pack/PackModel.ts delete mode 100644 fluxer_api/src/api/pack/PackRepository.ts delete mode 100644 fluxer_api/src/api/pack/PackService.ts delete mode 100644 fluxer_api/src/api/pack/controllers/PackController.ts delete mode 100644 fluxer_api/src/api/pack/controllers/PackEmojiController.ts delete mode 100644 fluxer_api/src/api/pack/controllers/PackStickerController.ts delete mode 100644 fluxer_api/src/api/pack/controllers/index.ts delete mode 100644 fluxer_api/src/api/pack/tests/PackInviteFlow.test.ts delete mode 100644 fluxer_api/src/api/pack/tests/PackPremiumRequirements.test.ts delete mode 100644 fluxer_api/src/api/pack/tests/PackTestUtils.ts delete mode 100644 fluxer_api/src/api/rate_limit_configs/PackRateLimitConfig.ts delete mode 100644 fluxer_app/src/features/expressions/commands/PackCommands.ts delete mode 100644 fluxer_app/src/features/expressions/components/modals/CreatePackModal.module.css delete mode 100644 fluxer_app/src/features/expressions/components/modals/CreatePackModal.tsx delete mode 100644 fluxer_app/src/features/expressions/components/modals/EditPackModal.tsx delete mode 100644 fluxer_app/src/features/expressions/components/modals/PackInviteModal.module.css delete mode 100644 fluxer_app/src/features/expressions/components/modals/PackInviteModal.tsx delete mode 100644 fluxer_app/src/features/expressions/state/ExpressionsPacks.ts delete mode 100644 fluxer_app/src/features/invite/commands/PackInviteCommands.ts delete mode 100644 fluxer_app/src/features/user/components/modals/tabs/ExpressionPacksTab.module.css delete mode 100644 fluxer_app/src/features/user/components/modals/tabs/ExpressionPacksTab.tsx delete mode 100644 fluxer_app/src/features/user/components/settings_utils/search_index/ExpressionPacksIndex.ts delete mode 100644 packages/errors/src/domains/core/FeatureAccessError.ts delete mode 100644 packages/errors/src/domains/pack/InvalidPackTypeError.ts delete mode 100644 packages/errors/src/domains/pack/MaxPackExpressionsError.ts delete mode 100644 packages/errors/src/domains/pack/MaxPackLimitError.ts delete mode 100644 packages/errors/src/domains/pack/PackAccessDeniedError.ts delete mode 100644 packages/errors/src/domains/pack/UnknownPackError.ts delete mode 100644 packages/schema/src/domains/pack/PackSchemas.ts diff --git a/fluxer_admin/openapi-admin.json b/fluxer_admin/openapi-admin.json index 6daeda401..5d160a993 100644 --- a/fluxer_admin/openapi-admin.json +++ b/fluxer_admin/openapi-admin.json @@ -8959,7 +8959,6 @@ "INVALID_FORM_BODY", "INVALID_GRANT", "INVALID_HANDOFF_CODE", - "INVALID_PACK_TYPE", "INVALID_PERMISSIONS_INTEGER", "INVALID_PERMISSIONS_NEGATIVE", "INVALID_PHONE_NUMBER", @@ -9002,8 +9001,6 @@ "MAX_GUILDS", "NEW_ACCOUNT_GUILD_JOIN_RATE_LIMITED", "MAX_INVITES", - "MAX_PACK_EXPRESSIONS", - "MAX_PACKS", "MAX_PINS_PER_CHANNEL", "MESSAGE_TOTAL_ATTACHMENT_SIZE_TOO_LARGE", "MAX_REACTIONS", @@ -9038,7 +9035,6 @@ "NOT_OWNER_OF_ADMIN_API_KEY", "NSFW_CONTENT_AGE_RESTRICTED", "NSFW_EMOJI_STICKER_BLOCKED", - "PACK_ACCESS_DENIED", "PASSKEY_AUTHENTICATION_FAILED", "PASSKEYS_DISABLED", "PHONE_ADD_NOT_ELIGIBLE", @@ -9117,7 +9113,6 @@ "UNKNOWN_INVITE", "UNKNOWN_MEMBER", "UNKNOWN_MESSAGE", - "UNKNOWN_PACK", "UNKNOWN_REPORT", "UNKNOWN_ROLE", "UNKNOWN_STICKER", diff --git a/fluxer_api/src/api/RateLimitConfig.ts b/fluxer_api/src/api/RateLimitConfig.ts index b32bb64eb..30825b936 100644 --- a/fluxer_api/src/api/RateLimitConfig.ts +++ b/fluxer_api/src/api/RateLimitConfig.ts @@ -10,7 +10,6 @@ import {IntegrationRateLimitConfigs} from './rate_limit_configs/IntegrationRateL import {InviteRateLimitConfigs} from './rate_limit_configs/InviteRateLimitConfig'; import {MiscRateLimitConfigs} from './rate_limit_configs/MiscRateLimitConfig'; import {OAuthRateLimitConfigs} from './rate_limit_configs/OAuthRateLimitConfig'; -import {PackRateLimitConfigs} from './rate_limit_configs/PackRateLimitConfig'; import type {RateLimitSection} from './rate_limit_configs/RateLimitHelpers'; import {mergeRateLimitSections} from './rate_limit_configs/RateLimitHelpers'; import {UserRateLimitConfigs} from './rate_limit_configs/UserRateLimitConfig'; @@ -29,6 +28,5 @@ const rateLimitSections = [ IntegrationRateLimitConfigs, AdminRateLimitConfigs, MiscRateLimitConfigs, - PackRateLimitConfigs, ] satisfies ReadonlyArray; export const RateLimitConfigs = mergeRateLimitSections(...rateLimitSections); diff --git a/fluxer_api/src/api/Tables.ts b/fluxer_api/src/api/Tables.ts index df4182cc8..0a99d6e8e 100644 --- a/fluxer_api/src/api/Tables.ts +++ b/fluxer_api/src/api/Tables.ts @@ -290,13 +290,10 @@ import { type SuspiciousIpRow, } from './database/types/RiskTypes'; import { - EXPRESSION_PACK_COLUMNS, - type ExpressionPackRow, FAVORITE_MEME_COLUMNS, type FavoriteMemeRow, NOTE_COLUMNS, type NoteRow, - type PackInstallationRow, PUSH_SUBSCRIPTION_COLUMNS, type PushSubscriptionRow, RECENT_MENTION_COLUMNS, @@ -1004,25 +1001,6 @@ export const FavoriteMemesByMemeId = defineTable({ - name: 'expression_packs', - columns: EXPRESSION_PACK_COLUMNS, - primaryKey: ['pack_id'], -}); -export const ExpressionPacksByCreator = defineTable({ - name: 'expression_packs_by_creator', - columns: EXPRESSION_PACK_COLUMNS, - primaryKey: ['creator_id', 'pack_id'], - partitionKey: ['creator_id'], -}); -const PACK_INSTALLATION_COLUMNS = ['user_id', 'pack_id', 'pack_type', 'installed_at'] as const satisfies ReadonlyArray< - keyof PackInstallationRow ->; -export const PackInstallations = defineTable({ - name: 'pack_installations', - columns: PACK_INSTALLATION_COLUMNS, - primaryKey: ['user_id', 'pack_id'], -}); interface InvitesByChannelRow { channel_id: ChannelID; diff --git a/fluxer_api/src/api/app/ControllerRegistry.ts b/fluxer_api/src/api/app/ControllerRegistry.ts index 28c851d9b..c593d9e17 100644 --- a/fluxer_api/src/api/app/ControllerRegistry.ts +++ b/fluxer_api/src/api/app/ControllerRegistry.ts @@ -24,7 +24,6 @@ import {getCacheService} from '../middleware/ServiceSingletons'; import {OAuth2ApplicationsController} from '../oauth/OAuth2ApplicationsController'; import {OAuth2Controller} from '../oauth/OAuth2Controller'; import {OpenAPIController} from '../openapi/OpenAPIController'; -import {registerPackControllers} from '../pack/controllers/index'; import {PremiumController} from '../premium/PremiumController'; import {ReadStateController} from '../read_state/ReadStateController'; import {ReportController} from '../report/ReportController'; @@ -54,7 +53,6 @@ export function registerControllers(routes: HonoApp, config: APIConfig): void { FavoriteGifController(routes); FavoriteMemeController(routes); InviteController(routes); - registerPackControllers(routes); ReadStateController(routes); ReportController(routes); GuildController(routes); diff --git a/fluxer_api/src/api/channel/services/ChannelService.ts b/fluxer_api/src/api/channel/services/ChannelService.ts index a16f469e6..a9bf95744 100644 --- a/fluxer_api/src/api/channel/services/ChannelService.ts +++ b/fluxer_api/src/api/channel/services/ChannelService.ts @@ -18,7 +18,6 @@ import type {UserCacheService} from '../../infrastructure/UserCacheService'; import type {IInviteRepository} from '../../invite/IInviteRepository'; import type {LimitConfigService} from '../../limits/LimitConfigService'; import type {User} from '../../models/User'; -import type {PackService} from '../../pack/PackService'; import type {ReadStateService} from '../../read_state/ReadStateService'; import type {IUserRepository} from '../../user/IUserRepository'; import {createDirectMessageSpamMitigationService} from '../../user/services/DirectMessageSpamMitigationService'; @@ -57,7 +56,6 @@ export class ChannelService { channelRepository: IChannelRepository, userRepository: IUserRepository, guildRepository: IGuildRepositoryAggregate, - packService: PackService, userCacheService: UserCacheService, embedService: EmbedService, readStateService: ReadStateService, @@ -94,7 +92,6 @@ export class ChannelService { channelRepository, userRepository, guildRepository, - packService, embedService, storageService, attachmentUploadTraceRepository, diff --git a/fluxer_api/src/api/channel/services/message/MessageContentService.ts b/fluxer_api/src/api/channel/services/message/MessageContentService.ts index 3ff96141f..d4c31ddb4 100644 --- a/fluxer_api/src/api/channel/services/message/MessageContentService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageContentService.ts @@ -9,7 +9,6 @@ import type {GuildID, UserID, WebhookID} from '../../../BrandedTypes'; import type {IGuildRepositoryAggregate} from '../../../guild/repositories/IGuildRepositoryAggregate'; import type {LimitConfigService} from '../../../limits/LimitConfigService'; import type {Channel} from '../../../models/Channel'; -import type {PackService} from '../../../pack/PackService'; import type {IUserRepository} from '../../../user/IUserRepository'; import * as EmojiUtils from '../../../utils/EmojiUtils'; @@ -22,7 +21,6 @@ export class MessageContentService { constructor( private userRepository: IUserRepository, private guildRepository: IGuildRepositoryAggregate, - private packService: PackService, private limitConfigService: LimitConfigService, ) {} @@ -33,15 +31,10 @@ export class MessageContentService { guildId: GuildID | null; hasPermission?: (permission: bigint) => Promise; }): Promise { - const packResolver = await this.packService.createPackExpressionAccessResolver({ - userId: params.userId, - type: 'emoji', - }); return await EmojiUtils.sanitizeCustomEmojis({ ...params, userRepository: this.userRepository, guildRepository: this.guildRepository, - packResolver, limitConfigService: this.limitConfigService, }); } diff --git a/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts b/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts index a30c80a55..c9adc5b22 100644 --- a/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts +++ b/fluxer_api/src/api/channel/services/message/MessagePersistenceService.ts @@ -33,7 +33,6 @@ import type {Channel} from '../../../models/Channel'; import type {Message} from '../../../models/Message'; import type {MessageSnapshot} from '../../../models/MessageSnapshot'; import type {User} from '../../../models/User'; -import type {PackService} from '../../../pack/PackService'; import type {ReadStateService} from '../../../read_state/ReadStateService'; import type {IUserRepository} from '../../../user/IUserRepository'; import {hasVisibleContent} from '../../../utils/StringUtils'; @@ -119,7 +118,6 @@ export class MessagePersistenceService { private channelRepository: IChannelRepositoryAggregate, private userRepository: IUserRepository, private guildRepository: IGuildRepositoryAggregate, - private packService: PackService, private embedService: EmbedService, storageService: IStorageService, attachmentUploadTraceRepository: AttachmentUploadTraceRepository, @@ -136,18 +134,8 @@ export class MessagePersistenceService { virusScanService, snowflakeService, ); - this.contentService = new MessageContentService( - this.userRepository, - guildRepository, - this.packService, - limitConfigService, - ); - this.stickerService = new MessageStickerService( - this.userRepository, - guildRepository, - this.packService, - limitConfigService, - ); + this.contentService = new MessageContentService(this.userRepository, guildRepository, limitConfigService); + this.stickerService = new MessageStickerService(this.userRepository, guildRepository, limitConfigService); this.embedAttachmentResolver = new MessageEmbedAttachmentResolver(); this.attachmentDecayService = new AttachmentDecayService(); } diff --git a/fluxer_api/src/api/channel/services/message/MessageStickerService.ts b/fluxer_api/src/api/channel/services/message/MessageStickerService.ts index fbc0a3d85..da878c769 100644 --- a/fluxer_api/src/api/channel/services/message/MessageStickerService.ts +++ b/fluxer_api/src/api/channel/services/message/MessageStickerService.ts @@ -11,14 +11,12 @@ import type {IGuildRepositoryAggregate} from '../../../guild/repositories/IGuild import type {LimitConfigService} from '../../../limits/LimitConfigService'; import {resolveLimitSafe} from '../../../limits/LimitConfigUtils'; import {createLimitMatchContext} from '../../../limits/LimitMatchContextBuilder'; -import type {PackService} from '../../../pack/PackService'; import type {IUserRepository} from '../../../user/IUserRepository'; export class MessageStickerService { constructor( private userRepository: IUserRepository, private guildRepository: IGuildRepositoryAggregate, - private packService: PackService, private readonly limitConfigService: LimitConfigService, ) {} @@ -30,10 +28,6 @@ export class MessageStickerService { isNSFWAllowed?: boolean; }): Promise> { const {stickerIds, userId, guildId, hasPermission, isNSFWAllowed = true} = params; - const packResolver = await this.packService.createPackExpressionAccessResolver({ - userId, - type: 'sticker', - }); let hasGlobalExpressions = 0; if (userId) { const user = await this.userRepository.findUnique(userId); @@ -55,10 +49,6 @@ export class MessageStickerService { if (!stickerFromAnyGuild) { throw InputValidationError.fromCode('sticker', ValidationErrorCodes.CUSTOM_STICKER_NOT_FOUND); } - const packAccess = await packResolver.resolve(stickerFromAnyGuild.guildId); - if (packAccess === 'not-accessible') { - throw InputValidationError.fromCode('sticker', ValidationErrorCodes.CUSTOM_STICKER_NOT_FOUND); - } if (!isNSFWAllowed && stickerFromAnyGuild.isNsfw) { throw new NsfwEmojiStickerBlockedError(); } @@ -97,10 +87,6 @@ export class MessageStickerService { throw new MissingPermissionsError(); } } - const packAccess = await packResolver.resolve(stickerFromOtherGuild.guildId); - if (packAccess === 'not-accessible') { - throw InputValidationError.fromCode('sticker', ValidationErrorCodes.CUSTOM_STICKER_NOT_FOUND); - } if (!isNSFWAllowed && stickerFromOtherGuild.isNsfw) { throw new NsfwEmojiStickerBlockedError(); } diff --git a/fluxer_api/src/api/database/types/UserTypes.ts b/fluxer_api/src/api/database/types/UserTypes.ts index c711b7c87..e642923b4 100644 --- a/fluxer_api/src/api/database/types/UserTypes.ts +++ b/fluxer_api/src/api/database/types/UserTypes.ts @@ -311,24 +311,6 @@ export interface UserGuildSettingsRow { version: number; } -export interface ExpressionPackRow { - pack_id: GuildID; - pack_type: string; - creator_id: UserID; - name: string; - description: Nullish; - created_at: Date; - updated_at: Date; - version: number; -} - -export interface PackInstallationRow { - user_id: UserID; - pack_id: GuildID; - pack_type: string; - installed_at: Date; -} - export interface SavedMessageRow { user_id: UserID; channel_id: ChannelID; @@ -515,16 +497,6 @@ export const USER_SETTINGS_COLUMNS = [ 'default_share_voice_activity', 'version', ] as const satisfies ReadonlyArray; -export const EXPRESSION_PACK_COLUMNS = [ - 'pack_id', - 'pack_type', - 'creator_id', - 'name', - 'description', - 'created_at', - 'updated_at', - 'version', -] as const satisfies ReadonlyArray; export const USER_GUILD_SETTINGS_COLUMNS = [ 'user_id', 'guild_id', diff --git a/fluxer_api/src/api/invite/InviteController.ts b/fluxer_api/src/api/invite/InviteController.ts index 480f69c43..fcd909d13 100644 --- a/fluxer_api/src/api/invite/InviteController.ts +++ b/fluxer_api/src/api/invite/InviteController.ts @@ -1,16 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import { - ChannelIdParam, - GuildIdParam, - InviteCodeParam, - PackIdParam, -} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; +import {ChannelIdParam, GuildIdParam, InviteCodeParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; import { ChannelInviteCreateRequest, InviteMetadataResponseSchema, InviteResponseSchema, - PackInviteCreateRequest, } from '@fluxer/schema/src/domains/invite/InviteSchemas'; import {z} from 'zod'; import {createChannelID, createGuildID, createInviteCode} from '../BrandedTypes'; @@ -30,7 +24,7 @@ export function InviteController(app: HonoApp) { operationId: 'get_invite', summary: 'Get invite information', description: - 'Fetches detailed information about an invite using its code, including the guild, channel, or pack it belongs to and metadata such as expiration and usage limits. This endpoint does not require authentication and does not consume the invite.', + 'Fetches detailed information about an invite using its code, including the guild or channel it belongs to and metadata such as expiration and usage limits. This endpoint does not require authentication and does not consume the invite.', responseSchema: InviteResponseSchema, statusCode: 200, security: [], @@ -53,7 +47,7 @@ export function InviteController(app: HonoApp) { operationId: 'accept_invite', summary: 'Accept invite', description: - 'Accepts an invite using its code, adding the authenticated user to the corresponding guild, pack, or other entity. The invite usage count is incremented, and if it reaches its maximum usage limit or expiration, the invite is automatically revoked. Returns the accepted invite details.', + 'Accepts an invite using its code, adding the authenticated user to the corresponding guild or other entity. The invite usage count is incremented, and if it reaches its maximum usage limit or expiration, the invite is automatically revoked. Returns the accepted invite details.', responseSchema: InviteResponseSchema, statusCode: 200, security: ['bearerToken', 'sessionToken'], @@ -77,7 +71,7 @@ export function InviteController(app: HonoApp) { operationId: 'delete_invite', summary: 'Delete invite', description: - 'Permanently deletes an invite by its code, preventing any further usage. The authenticated user must have permission to manage invites for the guild, channel, or pack associated with the invite. This action can be logged in the audit log if an X-Audit-Log-Reason header is provided.', + 'Permanently deletes an invite by its code, preventing any further usage. The authenticated user must have permission to manage invites for the guild or channel associated with the invite. This action can be logged in the audit log if an X-Audit-Log-Reason header is provided.', responseSchema: null, statusCode: 204, security: ['botToken', 'bearerToken', 'sessionToken'], @@ -171,60 +165,4 @@ export function InviteController(app: HonoApp) { return ctx.json(await inviteRequestService.listGuildInvites({userId, guildId, requestCache})); }, ); - app.get( - '/packs/:pack_id/invites', - RateLimitMiddleware(RateLimitConfigs.PACKS_INVITES_LIST), - LoginRequired, - DefaultUserOnly, - Validator('param', PackIdParam), - OpenAPI({ - operationId: 'list_pack_invites', - summary: 'List pack invites', - description: - 'Retrieves all currently active invites for the specified pack, including invite codes, creators, expiration times, and usage statistics. The authenticated user must have permission to manage invites for the pack and must be a default (non-bot) user. Returns an array of invite metadata objects.', - responseSchema: z.array(InviteMetadataResponseSchema), - statusCode: 200, - security: ['bearerToken', 'sessionToken'], - tags: ['Invites'], - }), - async (ctx) => { - const userId = ctx.get('user').id; - const packId = createGuildID(ctx.req.valid('param').pack_id); - const inviteRequestService = ctx.get('inviteRequestService'); - const requestCache = ctx.get('requestCache'); - return ctx.json(await inviteRequestService.listPackInvites({userId, packId, requestCache})); - }, - ); - app.post( - '/packs/:pack_id/invites', - RateLimitMiddleware(RateLimitConfigs.PACKS_INVITES_CREATE), - LoginRequired, - DefaultUserOnly, - Validator('param', PackIdParam), - Validator('json', PackInviteCreateRequest), - OpenAPI({ - operationId: 'create_pack_invite', - summary: 'Create pack invite', - description: - 'Creates a new invite for the specified pack with optional parameters such as maximum age and maximum uses. The authenticated user must have permission to create invites for the pack and must be a default (non-bot) user. Returns the created invite with full metadata including usage statistics.', - responseSchema: InviteMetadataResponseSchema, - statusCode: 200, - security: ['bearerToken', 'sessionToken'], - tags: ['Invites'], - }), - async (ctx) => { - const userId = ctx.get('user').id; - const packId = createGuildID(ctx.req.valid('param').pack_id); - const inviteRequestService = ctx.get('inviteRequestService'); - const requestCache = ctx.get('requestCache'); - return ctx.json( - await inviteRequestService.createPackInvite({ - inviterId: userId, - packId, - requestCache, - data: ctx.req.valid('json'), - }), - ); - }, - ); } diff --git a/fluxer_api/src/api/invite/InviteModel.ts b/fluxer_api/src/api/invite/InviteModel.ts index 64cc3d27d..a9a901880 100644 --- a/fluxer_api/src/api/invite/InviteModel.ts +++ b/fluxer_api/src/api/invite/InviteModel.ts @@ -2,7 +2,6 @@ import {InviteTypes} from '@fluxer/constants/src/ChannelConstants'; import {UnknownInviteError} from '@fluxer/errors/src/domains/invite/UnknownInviteError'; -import {UnknownPackError} from '@fluxer/errors/src/domains/pack/UnknownPackError'; import type {ChannelPartialResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas'; import type {GuildPartialResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; import type { @@ -10,8 +9,6 @@ import type { GroupDmInviteResponse, GuildInviteMetadataResponse, GuildInviteResponse, - PackInviteMetadataResponse, - PackInviteResponse, } from '@fluxer/schema/src/domains/invite/InviteSchemas'; import type {z} from 'zod'; import type {ChannelID, GuildID} from '../BrandedTypes'; @@ -20,8 +17,6 @@ import type {UserCacheService} from '../infrastructure/UserCacheService'; import type {RequestCache} from '../middleware/RequestCacheMiddleware'; import type {Channel} from '../models/Channel'; import type {Invite} from '../models/Invite'; -import {mapPackToSummary} from '../pack/PackModel'; -import type {PackRepository} from '../pack/PackRepository'; import {getCachedUserPartialResponse, getCachedUserPartialResponses} from '../user/UserCacheHelpers'; interface MapInviteToGuildInviteResponseParams { @@ -216,67 +211,3 @@ export async function mapInviteToGroupDmInviteMetadataResponse({ max_uses: invite.maxUses, }; } - -interface MapInviteToPackInviteResponseParams { - invite: Invite; - userCacheService: UserCacheService; - requestCache: RequestCache; - packRepository: PackRepository; -} - -const buildPackInviteBase = async ({ - invite, - userCacheService, - requestCache, - packRepository, -}: MapInviteToPackInviteResponseParams): Promise> => { - if (!invite.guildId) { - throw new UnknownPackError(); - } - const pack = await packRepository.getPack(invite.guildId); - if (!pack) { - throw new UnknownPackError(); - } - const creator = await getCachedUserPartialResponse({ - userId: pack.creatorId, - userCacheService, - requestCache, - }); - const inviter = invite.inviterId - ? await getCachedUserPartialResponse({ - userId: invite.inviterId, - userCacheService, - requestCache, - }) - : null; - const expiresAt = invite.maxAge > 0 ? new Date(invite.createdAt.getTime() + invite.maxAge * 1000) : null; - return { - code: invite.code, - type: invite.type as typeof InviteTypes.EMOJI_PACK | typeof InviteTypes.STICKER_PACK, - pack: { - ...mapPackToSummary(pack), - creator, - }, - inviter, - expires_at: expiresAt?.toISOString() ?? null, - temporary: invite.temporary, - }; -}; - -export async function mapInviteToPackInviteResponse( - params: MapInviteToPackInviteResponseParams, -): Promise> { - return buildPackInviteBase(params); -} - -export async function mapInviteToPackInviteMetadataResponse( - params: MapInviteToPackInviteResponseParams, -): Promise> { - const baseResponse = await buildPackInviteBase(params); - return { - ...baseResponse, - created_at: params.invite.createdAt.toISOString(), - uses: params.invite.uses, - max_uses: params.invite.maxUses, - }; -} diff --git a/fluxer_api/src/api/invite/InviteRequestService.ts b/fluxer_api/src/api/invite/InviteRequestService.ts index f64601ef5..6a885ce28 100644 --- a/fluxer_api/src/api/invite/InviteRequestService.ts +++ b/fluxer_api/src/api/invite/InviteRequestService.ts @@ -1,14 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {InviteTypes} from '@fluxer/constants/src/ChannelConstants'; -import {UnknownPackError} from '@fluxer/errors/src/domains/pack/UnknownPackError'; import type {ChannelPartialResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas'; import type {GuildPartialResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; import type { ChannelInviteCreateRequest, InviteMetadataResponseSchema, InviteResponseSchema, - PackInviteCreateRequest, } from '@fluxer/schema/src/domains/invite/InviteSchemas'; import type {ChannelID, GuildID, InviteCode, UserID} from '../BrandedTypes'; import {mapChannelToPartialResponse} from '../channel/ChannelMappers'; @@ -19,14 +17,11 @@ import type {UserCacheService} from '../infrastructure/UserCacheService'; import type {RequestCache} from '../middleware/RequestCacheMiddleware'; import type {Channel} from '../models/Channel'; import type {Invite} from '../models/Invite'; -import type {PackRepository} from '../pack/PackRepository'; import { mapInviteToGroupDmInviteMetadataResponse, mapInviteToGroupDmInviteResponse, mapInviteToGuildInviteMetadataResponse, mapInviteToGuildInviteResponse, - mapInviteToPackInviteMetadataResponse, - mapInviteToPackInviteResponse, } from './InviteModel'; import type {InviteService} from './InviteService'; @@ -41,7 +36,6 @@ interface MappingHelpers { memberCount: number; presenceCount: number; }>; - packRepository: PackRepository; gatewayService: IGatewayService; } @@ -51,7 +45,6 @@ export class InviteRequestService { private readonly channelService: ChannelService, private readonly guildService: GuildService, private readonly gatewayService: IGatewayService, - private readonly packRepository: PackRepository, private readonly userCacheService: UserCacheService, ) {} @@ -127,43 +120,6 @@ export class InviteRequestService { return this.mapInviteList(invites, params.requestCache); } - async listPackInvites(params: { - userId: UserID; - packId: GuildID; - requestCache: RequestCache; - }): Promise> { - const invites = await this.inviteService.getPackInvitesSorted({ - userId: params.userId, - packId: params.packId, - }); - return this.mapInviteList(invites, params.requestCache); - } - - async createPackInvite(params: { - inviterId: UserID; - packId: GuildID; - requestCache: RequestCache; - data: PackInviteCreateRequest; - }): Promise { - const pack = await this.packRepository.getPack(params.packId); - if (!pack) { - throw new UnknownPackError(); - } - const {invite, isNew} = await this.inviteService.createPackInvite({ - inviterId: params.inviterId, - packId: params.packId, - packType: pack.type, - maxUses: params.data.max_uses ?? 0, - maxAge: params.data.max_age ?? 0, - unique: params.data.unique ?? false, - }); - const inviteData = await this.mapInviteMetadataResponse(invite, params.requestCache); - if (isNew) { - await this.inviteService.dispatchInviteCreate(invite, inviteData); - } - return inviteData; - } - private createMappingHelpers(requestCache: RequestCache): MappingHelpers { return { userCacheService: this.userCacheService, @@ -176,7 +132,6 @@ export class InviteRequestService { await this.channelService.channelData.operations.getChannelMemberCount(channelId), getGuildResponse: async (guildId: GuildID) => await this.guildService.data.getPublicGuildData(guildId), getGuildCounts: async (guildId: GuildID) => await this.gatewayService.getGuildCounts(guildId), - packRepository: this.packRepository, gatewayService: this.gatewayService, }; } @@ -186,9 +141,6 @@ export class InviteRequestService { if (invite.type === InviteTypes.GROUP_DM) { return mapInviteToGroupDmInviteResponse({invite, ...helpers}); } - if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) { - return mapInviteToPackInviteResponse({invite, ...helpers}); - } return mapInviteToGuildInviteResponse({invite, ...helpers}); } @@ -200,9 +152,6 @@ export class InviteRequestService { if (invite.type === InviteTypes.GROUP_DM) { return mapInviteToGroupDmInviteMetadataResponse({invite, ...helpers}); } - if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) { - return mapInviteToPackInviteMetadataResponse({invite, ...helpers}); - } return mapInviteToGuildInviteMetadataResponse({invite, ...helpers}); } diff --git a/fluxer_api/src/api/invite/InviteService.ts b/fluxer_api/src/api/invite/InviteService.ts index f58d1f78d..548b5526b 100644 --- a/fluxer_api/src/api/invite/InviteService.ts +++ b/fluxer_api/src/api/invite/InviteService.ts @@ -12,12 +12,9 @@ import {MaxGuildInvitesError} from '@fluxer/errors/src/domains/guild/MaxGuildInv import {InvitesDisabledError} from '@fluxer/errors/src/domains/invite/InvitesDisabledError'; import {TemporaryInviteRequiresPresenceError} from '@fluxer/errors/src/domains/invite/TemporaryInviteRequiresPresenceError'; import {UnknownInviteError} from '@fluxer/errors/src/domains/invite/UnknownInviteError'; -import {PackAccessDeniedError} from '@fluxer/errors/src/domains/pack/PackAccessDeniedError'; -import {UnknownPackError} from '@fluxer/errors/src/domains/pack/UnknownPackError'; import type { GroupDmInviteMetadataResponse, GuildInviteMetadataResponse, - PackInviteMetadataResponse, } from '@fluxer/schema/src/domains/invite/InviteSchemas'; import type {ApiContext} from '../ApiContext'; import type {ChannelID, GuildID, InviteCode, UserID} from '../BrandedTypes'; @@ -32,8 +29,6 @@ import {createLimitMatchContext} from '../limits/LimitMatchContextBuilder'; import type {RequestCache} from '../middleware/RequestCacheMiddleware'; import type {Channel} from '../models/Channel'; import {Invite} from '../models/Invite'; -import type {PackRepository, PackType} from '../pack/PackRepository'; -import type {PackService} from '../pack/PackService'; import * as RandomUtils from '../utils/RandomUtils'; import type {IInviteRepository} from './IInviteRepository'; @@ -56,15 +51,6 @@ interface CreateInviteParams { temporary?: boolean; } -interface CreatePackInviteParams { - inviterId: UserID; - packId: GuildID; - packType: PackType; - maxUses: number; - maxAge: number; - unique: boolean; -} - interface AcceptInviteParams { userId: UserID; inviteCode: InviteCode; @@ -108,11 +94,6 @@ interface ReusableInviteCriteria { type?: number; } -const PACK_TYPE_TO_INVITE_TYPE: Record = { - emoji: InviteTypes.EMOJI_PACK, - sticker: InviteTypes.STICKER_PACK, -}; - export class InviteService { constructor( private readonly apiContext: ApiContext, @@ -120,8 +101,6 @@ export class InviteService { private guildService: GuildService, private channelService: ChannelService, private readonly guildAuditLogService: GuildAuditLogService, - private readonly packRepository: PackRepository, - private readonly packService: PackService, private readonly limitConfigService: LimitConfigService, ) {} @@ -255,51 +234,6 @@ export class InviteService { return {invite: newInvite, isNew: true}; } - async createPackInvite({inviterId, packId, packType, maxUses, maxAge, unique}: CreatePackInviteParams): Promise<{ - invite: Invite; - isNew: boolean; - }> { - const pack = await this.packRepository.getPack(packId); - if (!pack) { - throw new UnknownPackError(); - } - if (pack.creatorId !== inviterId) { - throw new PackAccessDeniedError(); - } - if (pack.type !== packType) { - throw new PackAccessDeniedError(); - } - const allInvites = await this.inviteRepository.listGuildInvites(packId); - const inviteType = PACK_TYPE_TO_INVITE_TYPE[packType]; - if (!unique) { - const existingInvite = this.findReusableInvite(allInvites, { - inviterId, - maxUses, - maxAge, - type: inviteType, - }); - if (existingInvite) { - return {invite: existingInvite, isNew: false}; - } - } - const packInviteLimit = this.resolveInviteLimit(null); - if (allInvites.length >= packInviteLimit) { - throw new MaxGuildInvitesError(packInviteLimit); - } - const newInvite = await this.inviteRepository.create({ - code: this.createRandomInviteCode(), - type: inviteType, - guild_id: packId, - channel_id: null, - inviter_id: inviterId, - uses: 0, - max_uses: maxUses, - max_age: maxAge, - temporary: false, - }); - return {invite: newInvite, isNew: true}; - } - async acceptInvite({userId, inviteCode, requestCache}: AcceptInviteParams): Promise { const invite = await this.findInviteWithLowercaseFallback(inviteCode); if (!invite) throw new UnknownInviteError(); @@ -334,11 +268,6 @@ export class InviteService { }); return this.incrementInviteUses(invite, {deleteWhenExhausted: true}); } - if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) { - if (!invite.guildId) throw new UnknownInviteError(); - await this.packService.installPack(userId, invite.guildId); - return this.incrementInviteUses(invite, {deleteWhenExhausted: true}); - } if (!invite.guildId) throw new UnknownInviteError(); const guild = await this.guildService.data.getGuildSystem(invite.guildId); if ((guild.disabledOperations & GuildOperations.INSTANT_INVITES) !== 0) { @@ -433,18 +362,6 @@ export class InviteService { async deleteInvite({userId, inviteCode}: DeleteInviteParams, auditLogReason?: string | null): Promise { const invite = await this.findInviteWithLowercaseFallback(inviteCode); if (!invite) throw new UnknownInviteError(); - if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) { - if (!invite.guildId) throw new UnknownInviteError(); - const pack = await this.packRepository.getPack(invite.guildId); - if (!pack) { - throw new UnknownPackError(); - } - if (pack.creatorId !== userId) { - throw new PackAccessDeniedError(); - } - await this.inviteRepository.delete(invite.code); - return; - } if (invite.type === InviteTypes.GROUP_DM) { if (!invite.channelId) throw new UnknownInviteError(); const channel = await this.channelService.channelData.operations.getChannel({ @@ -497,25 +414,9 @@ export class InviteService { return invites.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); } - async getPackInvitesSorted(params: {userId: UserID; packId: GuildID}): Promise> { - const {userId, packId} = params; - const pack = await this.packRepository.getPack(packId); - if (!pack) { - throw new UnknownPackError(); - } - if (pack.creatorId !== userId) { - throw new PackAccessDeniedError(); - } - const invites = await this.inviteRepository.listGuildInvites(packId); - const inviteType = PACK_TYPE_TO_INVITE_TYPE[pack.type]; - return invites - .filter((invite) => invite.type === inviteType) - .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); - } - async dispatchInviteCreate( invite: Invite, - inviteData: GuildInviteMetadataResponse | GroupDmInviteMetadataResponse | PackInviteMetadataResponse, + inviteData: GuildInviteMetadataResponse | GroupDmInviteMetadataResponse, ): Promise { if (invite.guildId && invite.type === InviteTypes.GUILD) { await this.apiContext.services.gateway.dispatchGuild({ diff --git a/fluxer_api/src/api/middleware/GuildStackServiceFactory.ts b/fluxer_api/src/api/middleware/GuildStackServiceFactory.ts index beb90a0ad..b5d79f203 100644 --- a/fluxer_api/src/api/middleware/GuildStackServiceFactory.ts +++ b/fluxer_api/src/api/middleware/GuildStackServiceFactory.ts @@ -9,7 +9,6 @@ import {ChannelService} from '../channel/services/ChannelService'; import type {IFavoriteMemeRepository} from '../favorite_meme/IFavoriteMemeRepository'; import type {GuildAuditLogService} from '../guild/GuildAuditLogService'; import type {IGuildRepositoryAggregate} from '../guild/repositories/IGuildRepositoryAggregate'; -import type {ExpressionAssetPurger} from '../guild/services/content/ExpressionAssetPurger'; import {GuildService} from '../guild/services/GuildService'; import type {AvatarService} from '../infrastructure/AvatarService'; import type {IPurgeQueue} from '../infrastructure/BunnyPurgeQueue'; @@ -23,8 +22,6 @@ import type {UserCacheService} from '../infrastructure/UserCacheService'; import type {InviteRepository} from '../invite/InviteRepository'; import {InviteService} from '../invite/InviteService'; import type {LimitConfigService} from '../limits/LimitConfigService'; -import type {PackRepository} from '../pack/PackRepository'; -import {PackService} from '../pack/PackService'; import type {ReadStateService} from '../read_state/ReadStateService'; import type {IUserRepository} from '../user/IUserRepository'; import type {VoiceAvailabilityService} from '../voice/VoiceAvailabilityService'; @@ -32,7 +29,6 @@ import type {IWebhookRepository} from '../webhook/IWebhookRepository'; interface GuildStackServiceFactoryDependencies { apiContext: ApiContext; - packRepository: PackRepository; channelRepository: IChannelRepository; userRepository: IUserRepository; guildRepository: IGuildRepositoryAggregate; @@ -42,7 +38,6 @@ interface GuildStackServiceFactoryDependencies { avatarService: AvatarService; entityAssetService: EntityAssetService; assetDeletionQueue: IAssetDeletionQueue; - expressionAssetPurger: ExpressionAssetPurger; userCacheService: UserCacheService; limitConfigService: LimitConfigService; embedService: EmbedService; @@ -59,40 +54,24 @@ interface GuildStackServiceFactoryDependencies { } export interface GuildStackServices { - packService: PackService; channelService: ChannelService; guildService: GuildService; inviteService: InviteService; } class LazyGuildStackServices implements GuildStackServices { - private cachedPackService: PackService | undefined; private cachedChannelService: ChannelService | undefined; private cachedGuildService: GuildService | undefined; private cachedInviteService: InviteService | undefined; constructor(private readonly dependencies: GuildStackServiceFactoryDependencies) {} - get packService(): PackService { - this.cachedPackService ??= new PackService( - this.dependencies.apiContext, - this.dependencies.packRepository, - this.dependencies.guildRepository, - this.dependencies.avatarService, - this.dependencies.expressionAssetPurger, - this.dependencies.userCacheService, - this.dependencies.limitConfigService, - ); - return this.cachedPackService; - } - get channelService(): ChannelService { this.cachedChannelService ??= new ChannelService( this.dependencies.apiContext, this.dependencies.channelRepository, this.dependencies.userRepository, this.dependencies.guildRepository, - this.packService, this.dependencies.userCacheService, this.dependencies.embedService, this.dependencies.readStateService, @@ -139,8 +118,6 @@ class LazyGuildStackServices implements GuildStackServices { this.guildService, this.channelService, this.dependencies.guildAuditLogService, - this.dependencies.packRepository, - this.packService, this.dependencies.limitConfigService, ); return this.cachedInviteService; diff --git a/fluxer_api/src/api/middleware/ServiceMiddleware.ts b/fluxer_api/src/api/middleware/ServiceMiddleware.ts index e54837797..2ffb1bd03 100644 --- a/fluxer_api/src/api/middleware/ServiceMiddleware.ts +++ b/fluxer_api/src/api/middleware/ServiceMiddleware.ts @@ -124,7 +124,6 @@ import { getEntranceSoundPlayService, getEntranceSoundService, getErrorI18nService, - getExpressionAssetPurger, getFavoriteMemeRepository, getGatewayRequestService, getGifService, @@ -139,7 +138,6 @@ import { getLimitConfigService, getNcmecSubmissionService, getOAuth2TokenRepository, - getPackRepository, getPasswordChangeRepository, getPremiumStateReconciliationQueueService, getPurgeQueue, @@ -444,7 +442,6 @@ class RequestServices implements RequestScopedServices { private get guildStack(): GuildStackServices { this.cachedGuildStack ??= createGuildStackServices({ apiContext: this.context, - packRepository: getPackRepository(), channelRepository: this.channelRepository, userRepository: getUserRepository(), guildRepository: this.requestGuildRepository, @@ -454,7 +451,6 @@ class RequestServices implements RequestScopedServices { avatarService: getAvatarService(), entityAssetService: getEntityAssetService(), assetDeletionQueue: getAssetDeletionQueue(), - expressionAssetPurger: getExpressionAssetPurger(), userCacheService: getUserCacheService(), limitConfigService: getLimitConfigService(), embedService: getEmbedService(), @@ -472,10 +468,6 @@ class RequestServices implements RequestScopedServices { return this.cachedGuildStack; } - get packService() { - return this.guildStack.packService; - } - get channelService() { return this.guildStack.channelService; } @@ -583,10 +575,6 @@ class RequestServices implements RequestScopedServices { return getOAuth2TokenRepository(); } - get packRepository() { - return getPackRepository(); - } - get rateLimitService() { return getRateLimitService(); } @@ -822,7 +810,6 @@ class RequestServices implements RequestScopedServices { this.channelService, this.guildService, this.gatewayService, - getPackRepository(), getUserCacheService(), ); return this.cachedInviteRequestService; diff --git a/fluxer_api/src/api/middleware/ServiceSingletons.ts b/fluxer_api/src/api/middleware/ServiceSingletons.ts index b8862d379..465e46b6b 100644 --- a/fluxer_api/src/api/middleware/ServiceSingletons.ts +++ b/fluxer_api/src/api/middleware/ServiceSingletons.ts @@ -43,7 +43,6 @@ import {createNatsGifProvider} from '../gif/NatsGifProvider'; import {GuildAuditLogService} from '../guild/GuildAuditLogService'; import {GuildDiscoveryRepository} from '../guild/repositories/GuildDiscoveryRepository'; import {GuildRepository} from '../guild/repositories/GuildRepository'; -import {ExpressionAssetPurger} from '../guild/services/content/ExpressionAssetPurger'; import {GuildDiscoveryService} from '../guild/services/GuildDiscoveryService'; import {AssetDeletionQueue} from '../infrastructure/AssetDeletionQueue'; import {AvatarService} from '../infrastructure/AvatarService'; @@ -75,7 +74,6 @@ import {BotAuthService} from '../oauth/BotAuthService'; import {BotMfaMirrorService} from '../oauth/BotMfaMirrorService'; import {ApplicationRepository} from '../oauth/repositories/ApplicationRepository'; import {OAuth2TokenRepository} from '../oauth/repositories/OAuth2TokenRepository'; -import {PackRepository} from '../pack/PackRepository'; import {ReadStateRepository} from '../read_state/ReadStateRepository'; import {ReadStateRequestService} from '../read_state/ReadStateRequestService'; import {ReadStateService} from '../read_state/ReadStateService'; @@ -119,7 +117,6 @@ export const getAdminArchiveRepository = singleton(() => new AdminArchiveReposit export const getVoiceRepository = singleton(() => new VoiceRepository()); export const getApplicationRepository = singleton(() => new ApplicationRepository()); export const getOAuth2TokenRepository = singleton(() => new OAuth2TokenRepository()); -export const getPackRepository = singleton(() => new PackRepository()); export const getGuildDiscoveryRepository = singleton(() => new GuildDiscoveryRepository()); export const getEmailChangeRepository = singleton(() => new EmailChangeRepository()); export const getPasswordChangeRepository = singleton(() => new PasswordChangeRepository()); @@ -398,7 +395,6 @@ export const getGifService = singleton(() => { createNatsGifProvider(async () => (await instanceConfigRepository.getEffectiveGifConfig()).klipy_api_key), ); }); -export const getExpressionAssetPurger = singleton(() => new ExpressionAssetPurger(getAssetDeletionQueue())); export const getGuildAuditLogService = singleton( () => new GuildAuditLogService(getGuildRepository(), getSnowflakeService(), getWorkerService(), getGatewayService()), ); diff --git a/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts b/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts index 099d14d3c..4a27c96b0 100644 --- a/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts +++ b/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts @@ -54,8 +54,6 @@ const REQUEST_SERVICE_VARIABLES: ReadonlyArray = [ 'oauth2RequestService', 'oauth2Service', 'oauth2TokenRepository', - 'packRepository', - 'packService', 'passwordChangeService', 'rateLimitService', 'readStateRequestService', diff --git a/fluxer_api/src/api/models/ExpressionPack.ts b/fluxer_api/src/api/models/ExpressionPack.ts deleted file mode 100644 index 044a74c58..000000000 --- a/fluxer_api/src/api/models/ExpressionPack.ts +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, UserID} from '../BrandedTypes'; -import type {ExpressionPackRow} from '../database/types/UserTypes'; - -type ExpressionPackType = 'emoji' | 'sticker'; - -export class ExpressionPack { - readonly id: GuildID; - readonly type: ExpressionPackType; - readonly creatorId: UserID; - readonly name: string; - readonly description: string | null; - readonly createdAt: Date; - readonly updatedAt: Date; - readonly version: number; - - constructor(row: ExpressionPackRow) { - this.id = row.pack_id; - this.type = row.pack_type as ExpressionPackType; - this.creatorId = row.creator_id; - this.name = row.name; - this.description = row.description ?? null; - this.createdAt = row.created_at; - this.updatedAt = row.updated_at; - this.version = row.version; - } - - toRow(): ExpressionPackRow { - return { - pack_id: this.id, - pack_type: this.type, - creator_id: this.creatorId, - name: this.name, - description: this.description, - created_at: this.createdAt, - updated_at: this.updatedAt, - version: this.version, - }; - } -} diff --git a/fluxer_api/src/api/openapi/openapi.json b/fluxer_api/src/api/openapi/openapi.json index 15ce4b3b1..1fe96f8f0 100644 --- a/fluxer_api/src/api/openapi/openapi.json +++ b/fluxer_api/src/api/openapi/openapi.json @@ -10220,7 +10220,7 @@ } }, "x-mint": {"metadata": {"title": "Get invite information"}}, - "description": "Fetches detailed information about an invite using its code, including the guild, channel, or pack it belongs to and metadata such as expiration and usage limits. This endpoint does not require authentication and does not consume the invite.", + "description": "Fetches detailed information about an invite using its code, including the guild or channel it belongs to and metadata such as expiration and usage limits. This endpoint does not require authentication and does not consume the invite.", "parameters": [ { "name": "invite_code", @@ -10293,7 +10293,7 @@ } }, "x-mint": {"metadata": {"title": "Accept invite"}}, - "description": "Accepts an invite using its code, adding the authenticated user to the corresponding guild, pack, or other entity. The invite usage count is incremented, and if it reaches its maximum usage limit or expiration, the invite is automatically revoked. Returns the accepted invite details.", + "description": "Accepts an invite using its code, adding the authenticated user to the corresponding guild or other entity. The invite usage count is incremented, and if it reaches its maximum usage limit or expiration, the invite is automatically revoked. Returns the accepted invite details.", "security": [{"sessionToken": []}], "parameters": [ { @@ -10364,7 +10364,7 @@ } }, "x-mint": {"metadata": {"title": "Delete invite"}}, - "description": "Permanently deletes an invite by its code, preventing any further usage. The authenticated user must have permission to manage invites for the guild, channel, or pack associated with the invite. This action can be logged in the audit log if an X-Audit-Log-Reason header is provided.", + "description": "Permanently deletes an invite by its code, preventing any further usage. The authenticated user must have permission to manage invites for the guild or channel associated with the invite. This action can be logged in the audit log if an X-Audit-Log-Reason header is provided.", "security": [{"botToken": []}, {"sessionToken": []}], "parameters": [ { @@ -11659,1418 +11659,6 @@ "security": [{"oauth2Token": ["identify"]}] } }, - "/packs": { - "get": { - "operationId": "list_user_packs", - "summary": "List user packs", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PackDashboardResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "List user packs"}}, - "description": "Returns a dashboard view containing all emoji and sticker packs created by or owned by the authenticated user. This includes pack metadata such as name, description, type, and cover image.", - "security": [{"sessionToken": []}] - } - }, - "/packs/emojis/{pack_id}": { - "post": { - "operationId": "create_pack_emoji", - "summary": "Create pack emoji", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildEmojiResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Create pack emoji"}}, - "description": "Creates a new emoji within the specified pack. Requires the pack ID in the path and emoji metadata (name and image data) in the request body. Returns the newly created emoji with its generated ID.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildEmojiCreateRequest"}}} - } - }, - "get": { - "operationId": "list_pack_emojis", - "summary": "List pack emojis", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildEmojiWithUserListResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "List pack emojis"}}, - "description": "Returns a list of all emojis contained within the specified pack, including emoji metadata and creator information. Results include emoji ID, name, image URL, and the user who created each emoji.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ] - } - }, - "/packs/emojis/{pack_id}/bulk": { - "post": { - "operationId": "bulk_create_pack_emojis", - "summary": "Bulk create pack emojis", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildEmojiBulkCreateResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Bulk create pack emojis"}}, - "description": "Creates multiple emojis within the specified pack in a single bulk operation. Accepts an array of emoji definitions, each containing name and image data. Returns a response containing all successfully created emojis.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildEmojiBulkCreateRequest"}}} - } - } - }, - "/packs/emojis/{pack_id}/{emoji_id}": { - "patch": { - "operationId": "update_pack_emoji", - "summary": "Update pack emoji", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildEmojiResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Update pack emoji"}}, - "description": "Updates the name of an existing emoji within the specified pack. Requires both pack ID and emoji ID in the path parameters. Returns the updated emoji with its new name and all existing metadata.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - }, - { - "name": "emoji_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the emoji" - } - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildEmojiUpdateRequest"}}} - } - }, - "delete": { - "operationId": "delete_pack_emoji", - "summary": "Delete pack emoji", - "tags": ["Packs"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Delete pack emoji"}}, - "description": "Permanently deletes an emoji from the specified pack. Requires both pack ID and emoji ID in the path parameters. Accepts an optional \"purge\" query parameter to control whether associated assets are immediately deleted.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - }, - { - "name": "emoji_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the emoji" - }, - { - "name": "purge", - "in": "query", - "required": false, - "schema": {"type": "string", "description": "Whether to also purge the asset from storage"} - } - ] - } - }, - "/packs/stickers/{pack_id}": { - "post": { - "operationId": "create_pack_sticker", - "summary": "Create pack sticker", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildStickerResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Create pack sticker"}}, - "description": "Creates a new sticker within the specified pack. Requires the pack ID in the path and sticker metadata (name, description, tags, and image data) in the request body. Returns the newly created sticker with its generated ID.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildStickerCreateRequest"}}} - } - }, - "get": { - "operationId": "list_pack_stickers", - "summary": "List pack stickers", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": {"schema": {"$ref": "#/components/schemas/GuildStickerWithUserListResponse"}} - } - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "List pack stickers"}}, - "description": "Returns a list of all stickers contained within the specified pack, including sticker metadata and creator information. Results include sticker ID, name, description, tags, image URL, and the user who created each sticker.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ] - } - }, - "/packs/stickers/{pack_id}/bulk": { - "post": { - "operationId": "bulk_create_pack_stickers", - "summary": "Bulk create pack stickers", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildStickerBulkCreateResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Bulk create pack stickers"}}, - "description": "Creates multiple stickers within the specified pack in a single bulk operation. Accepts an array of sticker definitions, each containing name, description, tags, and image data. Returns a response containing all successfully created stickers.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildStickerBulkCreateRequest"}}} - } - } - }, - "/packs/stickers/{pack_id}/{sticker_id}": { - "patch": { - "operationId": "update_pack_sticker", - "summary": "Update pack sticker", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildStickerResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Update pack sticker"}}, - "description": "Updates the name, description, or tags of an existing sticker within the specified pack. Requires both pack ID and sticker ID in the path parameters. Returns the updated sticker with its new metadata and all existing fields.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - }, - { - "name": "sticker_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the sticker" - } - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GuildStickerUpdateRequest"}}} - } - }, - "delete": { - "operationId": "delete_pack_sticker", - "summary": "Delete pack sticker", - "tags": ["Packs"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Delete pack sticker"}}, - "description": "Permanently deletes a sticker from the specified pack. Requires both pack ID and sticker ID in the path parameters. Accepts an optional \"purge\" query parameter to control whether associated assets are immediately deleted.", - "security": [{"botToken": []}, {"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - }, - { - "name": "sticker_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the sticker" - }, - { - "name": "purge", - "in": "query", - "required": false, - "schema": {"type": "string", "description": "Whether to also purge the asset from storage"} - } - ] - } - }, - "/packs/{pack_id}": { - "patch": { - "operationId": "update_pack", - "summary": "Update pack", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PackSummaryResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Update pack"}}, - "description": "Updates the metadata for an existing pack owned by the authenticated user. Allowed modifications include name, description, and cover image. Returns the updated pack with all current metadata.", - "security": [{"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PackUpdateRequest"}}} - } - }, - "delete": { - "operationId": "delete_pack", - "summary": "Delete pack", - "tags": ["Packs"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Delete pack"}}, - "description": "Permanently deletes a pack owned by the authenticated user along with all emojis or stickers contained within it. This action cannot be undone and will remove all associated assets.", - "security": [{"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ] - } - }, - "/packs/{pack_id}/install": { - "post": { - "operationId": "install_pack", - "summary": "Install pack", - "tags": ["Packs"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Install pack"}}, - "description": "Installs a pack to the authenticated user's collection, making its emojis or stickers available for use. The pack must be publicly accessible or owned by the user.", - "security": [{"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ] - }, - "delete": { - "operationId": "uninstall_pack", - "summary": "Uninstall pack", - "tags": ["Packs"], - "responses": { - "204": {"description": "No Content"}, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Uninstall pack"}}, - "description": "Uninstalls a pack from the authenticated user's collection, removing access to its emojis or stickers. This does not delete the pack itself, only removes it from the user's installed list.", - "security": [{"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ] - } - }, - "/packs/{pack_id}/invites": { - "get": { - "operationId": "list_pack_invites", - "summary": "List pack invites", - "tags": ["Invites"], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": {"type": "array", "items": {"$ref": "#/components/schemas/InviteMetadataResponseSchema"}} - } - } - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "List pack invites"}}, - "description": "Retrieves all currently active invites for the specified pack, including invite codes, creators, expiration times, and usage statistics. The authenticated user must have permission to manage invites for the pack and must be a default (non-bot) user. Returns an array of invite metadata objects.", - "security": [{"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ] - }, - "post": { - "operationId": "create_pack_invite", - "summary": "Create pack invite", - "tags": ["Invites"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/InviteMetadataResponseSchema"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Create pack invite"}}, - "description": "Creates a new invite for the specified pack with optional parameters such as maximum age and maximum uses. The authenticated user must have permission to create invites for the pack and must be a default (non-bot) user. Returns the created invite with full metadata including usage statistics.", - "security": [{"sessionToken": []}], - "parameters": [ - { - "name": "pack_id", - "in": "path", - "required": true, - "schema": {"$ref": "#/components/schemas/SnowflakeType"}, - "description": "The ID of the pack" - } - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PackInviteCreateRequest"}}} - } - } - }, - "/packs/{pack_type}": { - "post": { - "operationId": "create_pack", - "summary": "Create pack", - "tags": ["Packs"], - "responses": { - "200": { - "description": "Success", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PackSummaryResponse"}}} - }, - "400": { - "description": "Bad Request - The request was malformed or contained invalid data", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "401": { - "description": "Unauthorized - Authentication is required or the token is invalid", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "403": { - "description": "Forbidden - You do not have permission to perform this action", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - }, - "429": { - "description": "Too Many Requests - You are being rate limited", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": {"type": "string", "enum": ["RATE_LIMITED"]}, - "message": {"type": "string"}, - "retry_after": {"type": "number", "description": "Seconds to wait before retrying"}, - "global": {"type": "boolean", "description": "Whether this is a global rate limit"} - }, - "required": ["code", "message", "retry_after"] - } - } - }, - "headers": { - "Retry-After": { - "description": "Number of seconds to wait before retrying (only on 429)", - "schema": {"type": "integer"} - }, - "X-RateLimit-Limit": { - "description": "The number of requests that can be made in the current window", - "schema": {"type": "integer"} - }, - "X-RateLimit-Remaining": { - "description": "The number of remaining requests that can be made", - "schema": {"type": "integer"} - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp when the rate limit resets", - "schema": {"type": "integer"} - } - } - }, - "500": { - "description": "Internal Server Error - An unexpected error occurred", - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}} - } - }, - "x-mint": {"metadata": {"title": "Create pack"}}, - "description": "Creates a new emoji or sticker pack owned by the authenticated user. The pack type is specified in the path parameter and can be either \"emoji\" or \"sticker\". Returns the newly created pack with its metadata.", - "security": [{"sessionToken": []}], - "parameters": [ - { - "name": "pack_type", - "in": "path", - "required": true, - "schema": {"type": "string"}, - "description": "The pack type" - } - ], - "requestBody": { - "required": true, - "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PackCreateRequest"}}} - } - } - }, "/premium/cancel-pending-subscription-change": { "post": { "operationId": "cancel_pending_subscription_change", @@ -24531,7 +23119,6 @@ "INVALID_FORM_BODY", "INVALID_GRANT", "INVALID_HANDOFF_CODE", - "INVALID_PACK_TYPE", "INVALID_PERMISSIONS_INTEGER", "INVALID_PERMISSIONS_NEGATIVE", "INVALID_PHONE_NUMBER", @@ -24574,8 +23161,6 @@ "MAX_GUILDS", "NEW_ACCOUNT_GUILD_JOIN_RATE_LIMITED", "MAX_INVITES", - "MAX_PACK_EXPRESSIONS", - "MAX_PACKS", "MAX_PINS_PER_CHANNEL", "MESSAGE_TOTAL_ATTACHMENT_SIZE_TOO_LARGE", "MAX_REACTIONS", @@ -24610,7 +23195,6 @@ "NOT_OWNER_OF_ADMIN_API_KEY", "NSFW_CONTENT_AGE_RESTRICTED", "NSFW_EMOJI_STICKER_BLOCKED", - "PACK_ACCESS_DENIED", "PASSKEY_AUTHENTICATION_FAILED", "PASSKEYS_DISABLED", "PHONE_ADD_NOT_ELIGIBLE", @@ -24689,7 +23273,6 @@ "UNKNOWN_INVITE", "UNKNOWN_MEMBER", "UNKNOWN_MESSAGE", - "UNKNOWN_PACK", "UNKNOWN_REPORT", "UNKNOWN_ROLE", "UNKNOWN_STICKER", @@ -24845,7 +23428,6 @@ "Invalid form body.", "The authorization grant is invalid, expired, or has been revoked.", "Invalid handoff code.", - "Invalid pack type.", "Permissions must be a valid integer.", "Permissions must be non-negative.", "Invalid phone number.", @@ -24888,8 +23470,6 @@ "You've reached the maximum of {count, plural, one {# community} other {# communities}}.", "New accounts are limited to joining 10 communities in the first 24 hours. Please try again later.", "You've reached the maximum of {count, plural, one {# invite} other {# invites}}.", - "You've reached the maximum of {count, plural, one {# pack expression} other {# pack expressions}}.", - "You've reached the maximum of {count, plural, one {# pack} other {# packs}}.", "You've reached the maximum of {count, plural, one {# pin} other {# pins}} per channel.", "File size is too large. Maximum file size is {maxSize}.", "You've reached the maximum of {count, plural, one {# reaction} other {# reactions}}.", @@ -24924,7 +23504,6 @@ "You don't have permission to modify this admin API key.", "NSFW content is age restricted.", "This emoji or sticker is classified as NSFW and cannot be used in this context.", - "You don't have permission to access this pack.", "Passkey authentication failed.", "Passkeys are disabled.", "You are not eligible to add a phone number to your account.", @@ -25003,7 +23582,6 @@ "Invite wasn't found or is no longer valid.", "Member wasn't found in this community.", "Message wasn't found.", - "Pack wasn't found.", "Unknown report.", "Role wasn't found.", "Unknown sticker.", @@ -27397,8 +25975,7 @@ "InviteMetadataResponseSchema": { "oneOf": [ {"$ref": "#/components/schemas/GuildInviteMetadataResponse"}, - {"$ref": "#/components/schemas/GroupDmInviteMetadataResponse"}, - {"$ref": "#/components/schemas/PackInviteMetadataResponse"} + {"$ref": "#/components/schemas/GroupDmInviteMetadataResponse"} ] }, "GuildInviteMetadataResponse": { @@ -27668,80 +26245,6 @@ }, "required": ["code", "type", "channel", "member_count", "temporary", "created_at", "uses", "max_uses"] }, - "PackInviteMetadataResponse": { - "type": "object", - "properties": { - "code": {"type": "string", "description": "The unique invite code"}, - "type": {"type": "integer", "enum": [2, 3], "description": "The type of pack invite (emoji or sticker pack)"}, - "pack": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^(0|[1-9][0-9]*)$", - "description": "The unique identifier for the pack" - }, - "name": {"type": "string", "description": "The display name of the pack"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "description": "The description of the pack" - }, - "type": { - "type": "string", - "enum": ["emoji", "sticker"], - "description": "The type of pack (emoji or sticker)" - }, - "creator_id": { - "type": "string", - "pattern": "^(0|[1-9][0-9]*)$", - "description": "The ID of the user who created the pack" - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO8601 timestamp of when the pack was created" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "ISO8601 timestamp of when the pack was last updated" - }, - "creator": {"$ref": "#/components/schemas/UserPartialResponse"} - }, - "required": ["id", "name", "type", "creator_id", "created_at", "updated_at", "creator"], - "description": "The pack this invite is for" - }, - "inviter": { - "anyOf": [{"$ref": "#/components/schemas/UserPartialResponse"}, {"type": "null"}], - "description": "The user who created the invite" - }, - "expires_at": { - "anyOf": [{"type": "string", "format": "date-time"}, {"type": "null"}], - "description": "ISO8601 timestamp of when the invite expires" - }, - "temporary": {"type": "boolean", "description": "Whether the invite grants temporary access"}, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO8601 timestamp of when the invite was created" - }, - "uses": { - "type": "integer", - "minimum": 0, - "maximum": 2147483647, - "format": "int32", - "description": "The number of times this invite has been used" - }, - "max_uses": { - "type": "integer", - "minimum": 0, - "maximum": 2147483647, - "format": "int32", - "description": "The maximum number of times this invite can be used" - } - }, - "required": ["code", "type", "pack", "temporary", "created_at", "uses", "max_uses"] - }, "ChannelInviteCreateRequest": { "type": "object", "properties": { @@ -30683,8 +29186,7 @@ "InviteResponseSchema": { "oneOf": [ {"$ref": "#/components/schemas/GuildInviteResponse"}, - {"$ref": "#/components/schemas/GroupDmInviteResponse"}, - {"$ref": "#/components/schemas/PackInviteResponse"} + {"$ref": "#/components/schemas/GroupDmInviteResponse"} ] }, "GuildInviteResponse": { @@ -30804,61 +29306,6 @@ }, "required": ["code", "type", "channel", "member_count", "temporary"] }, - "PackInviteResponse": { - "type": "object", - "properties": { - "code": {"type": "string", "description": "The unique invite code"}, - "type": {"type": "integer", "enum": [2, 3], "description": "The type of pack invite (emoji or sticker pack)"}, - "pack": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^(0|[1-9][0-9]*)$", - "description": "The unique identifier for the pack" - }, - "name": {"type": "string", "description": "The display name of the pack"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "description": "The description of the pack" - }, - "type": { - "type": "string", - "enum": ["emoji", "sticker"], - "description": "The type of pack (emoji or sticker)" - }, - "creator_id": { - "type": "string", - "pattern": "^(0|[1-9][0-9]*)$", - "description": "The ID of the user who created the pack" - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO8601 timestamp of when the pack was created" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "ISO8601 timestamp of when the pack was last updated" - }, - "creator": {"$ref": "#/components/schemas/UserPartialResponse"} - }, - "required": ["id", "name", "type", "creator_id", "created_at", "updated_at", "creator"], - "description": "The pack this invite is for" - }, - "inviter": { - "anyOf": [{"$ref": "#/components/schemas/UserPartialResponse"}, {"type": "null"}], - "description": "The user who created the invite" - }, - "expires_at": { - "anyOf": [{"type": "string", "format": "date-time"}, {"type": "null"}], - "description": "ISO8601 timestamp of when the invite expires" - }, - "temporary": {"type": "boolean", "description": "Whether the invite grants temporary access"} - }, - "required": ["code", "type", "pack", "temporary"] - }, "GeolocationResponse": { "type": "object", "properties": { @@ -31409,150 +29856,6 @@ }, "required": ["sub", "id", "username", "discriminator", "global_name", "avatar"] }, - "PackDashboardResponse": { - "type": "object", - "properties": { - "emoji": { - "type": "object", - "properties": { - "installed_limit": { - "type": "integer", - "minimum": 0, - "maximum": 2147483647, - "format": "int32", - "description": "Maximum number of packs the user can install" - }, - "created_limit": { - "type": "integer", - "minimum": 0, - "maximum": 2147483647, - "format": "int32", - "description": "Maximum number of packs the user can create" - }, - "installed": { - "type": "array", - "items": {"$ref": "#/components/schemas/PackSummaryResponse"}, - "description": "List of packs the user has installed" - }, - "created": { - "type": "array", - "items": {"$ref": "#/components/schemas/PackSummaryResponse"}, - "description": "List of packs the user has created" - } - }, - "required": ["installed_limit", "created_limit", "installed", "created"], - "description": "Dashboard section for emoji packs" - }, - "sticker": { - "type": "object", - "properties": { - "installed_limit": { - "type": "integer", - "minimum": 0, - "maximum": 2147483647, - "format": "int32", - "description": "Maximum number of packs the user can install" - }, - "created_limit": { - "type": "integer", - "minimum": 0, - "maximum": 2147483647, - "format": "int32", - "description": "Maximum number of packs the user can create" - }, - "installed": { - "type": "array", - "items": {"$ref": "#/components/schemas/PackSummaryResponse"}, - "description": "List of packs the user has installed" - }, - "created": { - "type": "array", - "items": {"$ref": "#/components/schemas/PackSummaryResponse"}, - "description": "List of packs the user has created" - } - }, - "required": ["installed_limit", "created_limit", "installed", "created"], - "description": "Dashboard section for sticker packs" - } - }, - "required": ["emoji", "sticker"] - }, - "PackSummaryResponse": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^(0|[1-9][0-9]*)$", - "description": "The unique identifier (snowflake) for the pack" - }, - "name": {"type": "string", "description": "The display name of the pack"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "description": "The description of the pack" - }, - "type": { - "type": "string", - "enum": ["emoji", "sticker"], - "description": "The type of expression pack (emoji or sticker)" - }, - "creator_id": { - "type": "string", - "pattern": "^(0|[1-9][0-9]*)$", - "description": "The ID of the user who created the pack" - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO8601 timestamp of when the pack was created" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "ISO8601 timestamp of when the pack was last updated" - }, - "installed_at": { - "type": "string", - "format": "date-time", - "description": "ISO8601 timestamp of when the pack was installed by the user" - } - }, - "required": ["id", "name", "description", "type", "creator_id", "created_at", "updated_at"] - }, - "PackUpdateRequest": { - "type": "object", - "properties": { - "name": {"type": "string", "description": "The new name of the pack"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "description": "The new description of the pack" - } - } - }, - "PackInviteCreateRequest": { - "type": "object", - "properties": { - "max_uses": { - "anyOf": [{"type": "integer", "minimum": 0, "maximum": 100, "format": "int32"}, {"type": "null"}], - "description": "Maximum number of times this invite can be used (0 for unlimited)" - }, - "max_age": { - "anyOf": [{"type": "integer", "minimum": 0, "maximum": 604800, "format": "int32"}, {"type": "null"}], - "description": "Duration in seconds before the invite expires (0 for never)" - }, - "unique": { - "anyOf": [{"type": "boolean"}, {"type": "null"}], - "description": "Whether to create a new unique invite or reuse an existing one" - } - } - }, - "PackCreateRequest": { - "type": "object", - "properties": { - "name": {"type": "string", "description": "The name of the pack"}, - "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The description of the pack"} - }, - "required": ["name"] - }, "ChangeSubscriptionRequest": { "type": "object", "properties": { @@ -37129,7 +35432,6 @@ {"name": "Guilds", "description": "Guild management"}, {"name": "Channels", "description": "Channel management and messaging"}, {"name": "Invites", "description": "Guild invitations"}, - {"name": "Packs", "description": "Sticker and emoji packs"}, {"name": "Webhooks", "description": "Webhook management"}, {"name": "OAuth2", "description": "OAuth2 applications and authorization"}, {"name": "Gateway", "description": "WebSocket gateway information"}, diff --git a/fluxer_api/src/api/pack/PackExpressionAccessResolver.ts b/fluxer_api/src/api/pack/PackExpressionAccessResolver.ts deleted file mode 100644 index c081594db..000000000 --- a/fluxer_api/src/api/pack/PackExpressionAccessResolver.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, UserID} from '../BrandedTypes'; -import type {PackType} from './PackRepository'; - -export type PackExpressionAccessResolution = 'accessible' | 'not-accessible' | 'not-pack'; - -export interface PackExpressionAccessResolver { - resolve(packId: GuildID): Promise; -} - -export interface PackExpressionAccessResolverParams { - userId: UserID | null; - type: PackType; -} diff --git a/fluxer_api/src/api/pack/PackModel.ts b/fluxer_api/src/api/pack/PackModel.ts deleted file mode 100644 index 5f81169da..000000000 --- a/fluxer_api/src/api/pack/PackModel.ts +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {PackSummaryResponse} from '@fluxer/schema/src/domains/pack/PackSchemas'; -import type {ExpressionPack} from '../models/ExpressionPack'; - -export function mapPackToSummary(pack: ExpressionPack, installedAt?: Date | null): PackSummaryResponse { - const summary: PackSummaryResponse = { - id: pack.id.toString(), - name: pack.name, - description: pack.description, - type: pack.type, - creator_id: pack.creatorId.toString(), - created_at: pack.createdAt.toISOString(), - updated_at: pack.updatedAt.toISOString(), - }; - if (installedAt) { - summary.installed_at = installedAt.toISOString(); - } - return summary; -} diff --git a/fluxer_api/src/api/pack/PackRepository.ts b/fluxer_api/src/api/pack/PackRepository.ts deleted file mode 100644 index 8b552e24f..000000000 --- a/fluxer_api/src/api/pack/PackRepository.ts +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {GuildID, UserID} from '../BrandedTypes'; -import {BatchBuilder, fetchMany, fetchOne, upsertOne} from '../database/CassandraQueryExecution'; -import {nextVersion} from '../database/CassandraTypes'; -import {buildPatchFromData, executeVersionedUpdate} from '../database/CassandraVersionedUpdate'; -import type {ExpressionPackRow, PackInstallationRow} from '../database/types/UserTypes'; -import {EXPRESSION_PACK_COLUMNS} from '../database/types/UserTypes'; -import {ExpressionPack} from '../models/ExpressionPack'; -import {ExpressionPacks, ExpressionPacksByCreator, PackInstallations} from '../Tables'; - -export type PackType = ExpressionPack['type']; - -const FETCH_EXPRESSION_PACK_BY_ID_QUERY = ExpressionPacks.select({ - where: ExpressionPacks.where.eq('pack_id'), - limit: 1, -}); -const FETCH_EXPRESSION_PACKS_BY_CREATOR_QUERY = ExpressionPacksByCreator.select({ - where: ExpressionPacksByCreator.where.eq('creator_id'), -}); -const FETCH_PACK_INSTALLATIONS_BY_USER_QUERY = PackInstallations.select({ - where: PackInstallations.where.eq('user_id'), -}); -const FETCH_PACK_INSTALLATION_QUERY = PackInstallations.select({ - where: [PackInstallations.where.eq('user_id'), PackInstallations.where.eq('pack_id')], - limit: 1, -}); - -export class PackRepository { - async getPack(packId: GuildID): Promise { - const row = await fetchOne(FETCH_EXPRESSION_PACK_BY_ID_QUERY.bind({pack_id: packId})); - return row ? new ExpressionPack(row) : null; - } - - async listPacksByCreator(creatorId: UserID, packType?: PackType): Promise> { - const rows = await fetchMany( - FETCH_EXPRESSION_PACKS_BY_CREATOR_QUERY.bind({creator_id: creatorId}), - ); - return rows.filter((row) => (packType ? row.pack_type === packType : true)).map((row) => new ExpressionPack(row)); - } - - async countPacksByCreator(creatorId: UserID, packType: PackType): Promise { - const rows = await fetchMany( - FETCH_EXPRESSION_PACKS_BY_CREATOR_QUERY.bind({creator_id: creatorId}), - ); - return rows.filter((row) => row.pack_type === packType).length; - } - - async upsertPack(data: ExpressionPackRow): Promise { - const packId = data.pack_id; - const previousPack = await this.getPack(packId); - const result = await executeVersionedUpdate( - async () => { - const existing = await fetchOne(FETCH_EXPRESSION_PACK_BY_ID_QUERY.bind({pack_id: packId})); - return existing ?? null; - }, - (current) => ({ - pk: {pack_id: packId}, - patch: buildPatchFromData(data, current, EXPRESSION_PACK_COLUMNS, ['pack_id']), - }), - ExpressionPacks, - ); - const batch = new BatchBuilder(); - if (previousPack && previousPack.creatorId !== data.creator_id) { - batch.addPrepared( - ExpressionPacksByCreator.deleteByPk({ - creator_id: previousPack.creatorId, - pack_id: packId, - }), - ); - } - const finalPack = new ExpressionPack({...data, version: result.finalVersion ?? nextVersion(previousPack?.version)}); - batch.addPrepared(ExpressionPacksByCreator.insert(finalPack.toRow())); - await batch.execute(); - return finalPack; - } - - async deletePack(packId: GuildID): Promise { - const pack = await this.getPack(packId); - const batch = new BatchBuilder().addPrepared(ExpressionPacks.deleteByPk({pack_id: packId})); - if (pack) { - batch.addPrepared( - ExpressionPacksByCreator.deleteByPk({ - creator_id: pack.creatorId, - pack_id: packId, - }), - ); - } - await batch.execute(); - } - - async listInstallations(userId: UserID): Promise> { - return await fetchMany(FETCH_PACK_INSTALLATIONS_BY_USER_QUERY.bind({user_id: userId})); - } - - async addInstallation(data: PackInstallationRow): Promise { - await upsertOne(PackInstallations.insert(data)); - } - - async removeInstallation(userId: UserID, packId: GuildID): Promise { - await PackInstallations.deleteByPk({ - user_id: userId, - pack_id: packId, - }); - } - - async hasInstallation(userId: UserID, packId: GuildID): Promise { - const row = await fetchOne( - FETCH_PACK_INSTALLATION_QUERY.bind({ - user_id: userId, - pack_id: packId, - }), - ); - return row !== null; - } -} diff --git a/fluxer_api/src/api/pack/PackService.ts b/fluxer_api/src/api/pack/PackService.ts deleted file mode 100644 index 932842b07..000000000 --- a/fluxer_api/src/api/pack/PackService.ts +++ /dev/null @@ -1,642 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; -import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata'; -import { - MAX_CREATED_PACKS_NON_PREMIUM, - MAX_INSTALLED_PACKS_NON_PREMIUM, - MAX_PACK_EXPRESSIONS, -} from '@fluxer/constants/src/LimitConstants'; -import {UserFlags} from '@fluxer/constants/src/UserConstants'; -import {FeatureAccessError} from '@fluxer/errors/src/domains/core/FeatureAccessError'; -import {FeatureTemporarilyDisabledError} from '@fluxer/errors/src/domains/core/FeatureTemporarilyDisabledError'; -import {UnknownGuildEmojiError} from '@fluxer/errors/src/domains/guild/UnknownGuildEmojiError'; -import {UnknownGuildStickerError} from '@fluxer/errors/src/domains/guild/UnknownGuildStickerError'; -import {InvalidPackTypeError} from '@fluxer/errors/src/domains/pack/InvalidPackTypeError'; -import {MaxPackExpressionsError} from '@fluxer/errors/src/domains/pack/MaxPackExpressionsError'; -import {MaxPackLimitError} from '@fluxer/errors/src/domains/pack/MaxPackLimitError'; -import {PackAccessDeniedError} from '@fluxer/errors/src/domains/pack/PackAccessDeniedError'; -import {UnknownPackError} from '@fluxer/errors/src/domains/pack/UnknownPackError'; -import {FluxerError} from '@fluxer/errors/src/FluxerError'; -import {getErrorMessageUnsafe} from '@fluxer/errors/src/i18n/ErrorI18n'; -import type { - GuildEmojiResponse, - GuildEmojiWithUserResponse, - GuildStickerResponse, - GuildStickerWithUserResponse, -} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas'; -import type {PackDashboardResponse, PackDashboardSectionResponse} from '@fluxer/schema/src/domains/pack/PackSchemas'; -import type {ApiContext} from '../ApiContext'; -import type {EmojiID, GuildID, StickerID, UserID} from '../BrandedTypes'; -import {createEmojiID, createGuildID, createStickerID} from '../BrandedTypes'; -import { - mapGuildEmojisWithUsersToResponse, - mapGuildEmojiToResponse, - mapGuildStickersWithUsersToResponse, - mapGuildStickerToResponse, -} from '../guild/GuildModel'; -import type {IGuildRepositoryAggregate} from '../guild/repositories/IGuildRepositoryAggregate'; -import type {ExpressionAssetPurger} from '../guild/services/content/ExpressionAssetPurger'; -import type {AvatarService} from '../infrastructure/AvatarService'; -import type {UserCacheService} from '../infrastructure/UserCacheService'; -import type {LimitConfigService} from '../limits/LimitConfigService'; -import {resolveLimitSafe} from '../limits/LimitConfigUtils'; -import {createLimitMatchContext} from '../limits/LimitMatchContextBuilder'; -import type {RequestCache} from '../middleware/RequestCacheMiddleware'; -import {ExpressionPack} from '../models/ExpressionPack'; -import type {User} from '../models/User'; -import type { - PackExpressionAccessResolution, - PackExpressionAccessResolver, - PackExpressionAccessResolverParams, -} from './PackExpressionAccessResolver'; -import {mapPackToSummary} from './PackModel'; -import type {PackRepository, PackType} from './PackRepository'; - -export class PackService { - constructor( - private readonly apiContext: ApiContext, - private readonly packRepository: PackRepository, - private readonly guildRepository: IGuildRepositoryAggregate, - private readonly avatarService: AvatarService, - private readonly assetPurger: ExpressionAssetPurger, - private readonly userCacheService: UserCacheService, - private readonly limitConfigService: LimitConfigService, - ) {} - - private getLocalizedPackExpressionLimitMessage(locale: string | null | undefined, count: number): string { - return getErrorMessageUnsafe(APIErrorCodes.MAX_PACK_EXPRESSIONS, locale, {count}); - } - - private getLocalizedBulkErrorMessage(error: unknown, locale: string | null | undefined): string { - if (error instanceof FluxerError) { - return getErrorMessageUnsafe(error.code, locale, error.messageVariables, error.message); - } - return getErrorMessageUnsafe(APIErrorCodes.GENERAL_ERROR, locale); - } - - private async requireExpressionPackAccess(userId: UserID): Promise { - const user = await this.apiContext.services.users.findUnique(userId); - if (!user || (user.flags & UserFlags.STAFF) === 0n) { - throw new FeatureTemporarilyDisabledError(); - } - } - - private async hasFeatureAccess(userId: UserID, limitKey: LimitKey): Promise { - const user = await this.apiContext.services.users.findUnique(userId); - if (!user) { - return false; - } - const ctx = createLimitMatchContext({user}); - const value = resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, limitKey, 0); - return value > 0; - } - - private async ensurePackOwner(userId: UserID, packId: GuildID): Promise { - const pack = await this.packRepository.getPack(packId); - if (!pack) { - throw new UnknownPackError(); - } - if (pack.creatorId !== userId) { - throw new PackAccessDeniedError(); - } - return pack; - } - - private async collectInstalledPacks(userId: UserID): Promise< - Array<{ - pack: ExpressionPack; - installedAt: Date; - }> - > { - const installations = await this.packRepository.listInstallations(userId); - const results: Array<{ - pack: ExpressionPack; - installedAt: Date; - }> = []; - for (const row of installations) { - const pack = await this.packRepository.getPack(row.pack_id); - if (!pack) { - await this.packRepository.removeInstallation(userId, row.pack_id); - continue; - } - results.push({pack, installedAt: row.installed_at}); - } - return results; - } - - private async requireFeature(userId: UserID, limitKey: LimitKey): Promise { - if (!(await this.hasFeatureAccess(userId, limitKey))) { - throw new FeatureAccessError(); - } - } - - private async getInstalledPackIdsByType(userId: UserID, packType: PackType): Promise> { - const installations = await this.packRepository.listInstallations(userId); - return new Set(installations.filter((row) => row.pack_type === packType).map((row) => row.pack_id)); - } - - private buildPackExpressionAccessResolution( - userId: UserID, - packType: PackType, - pack: ExpressionPack | null, - ): PackExpressionAccessResolution { - if (!pack) { - return 'not-pack'; - } - if (pack.type !== packType) { - return 'not-pack'; - } - return pack.creatorId === userId ? 'accessible' : 'not-accessible'; - } - - async createPackExpressionAccessResolver( - params: PackExpressionAccessResolverParams, - ): Promise { - const {userId, type} = params; - if (!userId) { - return { - resolve: async () => 'not-pack', - }; - } - const installedPackIds = await this.getInstalledPackIdsByType(userId, type); - const resolutionCache = new Map(); - return { - resolve: async (packId: GuildID) => { - if (installedPackIds.has(packId)) { - resolutionCache.set(packId, 'accessible'); - return 'accessible'; - } - const cached = resolutionCache.get(packId); - if (cached) { - return cached; - } - const pack = await this.packRepository.getPack(packId); - const resolution = this.buildPackExpressionAccessResolution(userId, type, pack); - resolutionCache.set(packId, resolution); - return resolution; - }, - }; - } - - async listUserPacks(userId: UserID): Promise { - await this.requireExpressionPackAccess(userId); - const user = await this.apiContext.services.users.findUnique(userId); - const ctx = createLimitMatchContext({user}); - resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, 'feature_global_expressions', 0); - const createdEmoji = await this.packRepository.listPacksByCreator(userId, 'emoji'); - const createdSticker = await this.packRepository.listPacksByCreator(userId, 'sticker'); - const installations = await this.collectInstalledPacks(userId); - const installedEmoji = installations - .filter((entry) => entry.pack.type === 'emoji') - .map((entry) => mapPackToSummary(entry.pack, entry.installedAt)); - const installedSticker = installations - .filter((entry) => entry.pack.type === 'sticker') - .map((entry) => mapPackToSummary(entry.pack, entry.installedAt)); - const fallbackCreatedLimit = MAX_CREATED_PACKS_NON_PREMIUM; - const fallbackInstalledLimit = MAX_INSTALLED_PACKS_NON_PREMIUM; - const createdLimit = this.resolveLimitForUser(user ?? null, 'max_created_packs', fallbackCreatedLimit); - const installedLimit = this.resolveLimitForUser(user ?? null, 'max_installed_packs', fallbackInstalledLimit); - const emojiSection: PackDashboardSectionResponse = { - installed_limit: installedLimit, - created_limit: createdLimit, - installed: installedEmoji, - created: createdEmoji.map((pack) => mapPackToSummary(pack)), - }; - const stickerSection: PackDashboardSectionResponse = { - installed_limit: installedLimit, - created_limit: createdLimit, - installed: installedSticker, - created: createdSticker.map((pack) => mapPackToSummary(pack)), - }; - return { - emoji: emojiSection, - sticker: stickerSection, - }; - } - - async createPack(params: { - user: User; - type: PackType; - name: string; - description?: string | null; - }): Promise { - await this.requireExpressionPackAccess(params.user.id); - await this.requireFeature(params.user.id, 'feature_global_expressions'); - const createdCount = await this.packRepository.countPacksByCreator(params.user.id, params.type); - const fallbackLimit = MAX_CREATED_PACKS_NON_PREMIUM; - const limit = this.resolveLimitForUser(params.user, 'max_created_packs', fallbackLimit); - if (createdCount >= limit) { - throw new MaxPackLimitError(params.type, limit, 'create'); - } - const now = new Date(); - const packId = createGuildID(await this.apiContext.services.snowflake.generate()); - return await this.packRepository.upsertPack({ - pack_id: packId, - pack_type: params.type, - creator_id: params.user.id, - name: params.name, - description: params.description ?? null, - created_at: now, - updated_at: now, - version: 1, - }); - } - - async updatePack(params: { - userId: UserID; - packId: GuildID; - name?: string; - description?: string | null; - }): Promise { - await this.requireExpressionPackAccess(params.userId); - const pack = await this.ensurePackOwner(params.userId, params.packId); - const now = new Date(); - const updatedPack = new ExpressionPack({ - ...pack.toRow(), - name: params.name ?? pack.name, - description: params.description === undefined ? pack.description : params.description, - updated_at: now, - }); - return await this.packRepository.upsertPack(updatedPack.toRow()); - } - - async deletePack(userId: UserID, packId: GuildID): Promise { - await this.requireExpressionPackAccess(userId); - await this.ensurePackOwner(userId, packId); - await this.packRepository.deletePack(packId); - } - - async installPack(userId: UserID, packId: GuildID): Promise { - await this.requireExpressionPackAccess(userId); - const pack = await this.packRepository.getPack(packId); - if (!pack) { - throw new UnknownPackError(); - } - const alreadyInstalled = await this.packRepository.hasInstallation(userId, packId); - if (alreadyInstalled) { - return; - } - await this.requireFeature(userId, 'feature_global_expressions'); - const user = await this.apiContext.services.users.findUnique(userId); - const fallbackLimit = MAX_INSTALLED_PACKS_NON_PREMIUM; - const limit = this.resolveLimitForUser(user ?? null, 'max_installed_packs', fallbackLimit); - const installations = await this.collectInstalledPacks(userId); - const typeCount = installations.filter((entry) => entry.pack.type === pack.type).length; - if (typeCount >= limit) { - throw new MaxPackLimitError(pack.type, limit, 'install'); - } - await this.packRepository.addInstallation({ - user_id: userId, - pack_id: packId, - pack_type: pack.type, - installed_at: new Date(), - }); - } - - async uninstallPack(userId: UserID, packId: GuildID): Promise { - await this.requireExpressionPackAccess(userId); - await this.packRepository.removeInstallation(userId, packId); - } - - async getInstalledPackIds(userId: UserID): Promise> { - await this.requireExpressionPackAccess(userId); - const installations = await this.collectInstalledPacks(userId); - return new Set(installations.map((entry) => entry.pack.id)); - } - - async getPackEmojis(params: { - userId: UserID; - packId: GuildID; - requestCache: RequestCache; - }): Promise> { - await this.requireExpressionPackAccess(params.userId); - const pack = await this.ensurePackOwner(params.userId, params.packId); - if (pack.type !== 'emoji') { - throw new InvalidPackTypeError('emoji'); - } - const emojis = await this.guildRepository.listEmojis(pack.id); - return await mapGuildEmojisWithUsersToResponse(emojis, this.userCacheService, params.requestCache); - } - - async getPackStickers(params: { - userId: UserID; - packId: GuildID; - requestCache: RequestCache; - }): Promise> { - await this.requireExpressionPackAccess(params.userId); - const pack = await this.ensurePackOwner(params.userId, params.packId); - if (pack.type !== 'sticker') { - throw new InvalidPackTypeError('sticker'); - } - const stickers = await this.guildRepository.listStickers(pack.id); - return await mapGuildStickersWithUsersToResponse(stickers, this.userCacheService, params.requestCache); - } - - async createPackEmoji(params: { - user: User; - packId: GuildID; - name: string; - image: string; - }): Promise { - await this.requireExpressionPackAccess(params.user.id); - await this.requireFeature(params.user.id, 'feature_global_expressions'); - const pack = await this.ensurePackOwner(params.user.id, params.packId); - if (pack.type !== 'emoji') { - throw new InvalidPackTypeError('emoji'); - } - const emojiCount = await this.guildRepository.countEmojis(pack.id); - const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS); - if (emojiCount >= expressionLimit) { - throw new MaxPackExpressionsError(expressionLimit); - } - const {animated, imageBuffer, contentType} = await this.avatarService.processEmoji({ - errorPath: 'image', - base64Image: params.image, - }); - const emojiId = createEmojiID(await this.apiContext.services.snowflake.generate()); - await this.avatarService.uploadEmoji({ - prefix: 'emojis', - emojiId, - imageBuffer, - contentType, - }); - const emoji = await this.guildRepository.upsertEmoji({ - guild_id: pack.id, - emoji_id: emojiId, - name: params.name, - creator_id: params.user.id, - animated, - nsfw: null, - version: 1, - }); - return mapGuildEmojiToResponse(emoji); - } - - async bulkCreatePackEmojis(params: { - user: User; - packId: GuildID; - emojis: Array<{ - name: string; - image: string; - }>; - }): Promise<{ - success: Array; - failed: Array<{ - name: string; - error: string; - }>; - }> { - await this.requireExpressionPackAccess(params.user.id); - await this.requireFeature(params.user.id, 'feature_global_expressions'); - const pack = await this.ensurePackOwner(params.user.id, params.packId); - if (pack.type !== 'emoji') { - throw new InvalidPackTypeError('emoji'); - } - let emojiCount = await this.guildRepository.countEmojis(pack.id); - const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS); - const success: Array = []; - const failed: Array<{ - name: string; - error: string; - }> = []; - for (const emojiData of params.emojis) { - if (emojiCount >= expressionLimit) { - failed.push({ - name: emojiData.name, - error: this.getLocalizedPackExpressionLimitMessage(params.user.locale, expressionLimit), - }); - continue; - } - try { - const {animated, imageBuffer, contentType} = await this.avatarService.processEmoji({ - errorPath: `emojis[${success.length + failed.length}].image`, - base64Image: emojiData.image, - }); - const emojiId = createEmojiID(await this.apiContext.services.snowflake.generate()); - await this.avatarService.uploadEmoji({ - prefix: 'emojis', - emojiId, - imageBuffer, - contentType, - }); - const emoji = await this.guildRepository.upsertEmoji({ - guild_id: pack.id, - emoji_id: emojiId, - name: emojiData.name, - creator_id: params.user.id, - animated, - nsfw: null, - version: 1, - }); - success.push(mapGuildEmojiToResponse(emoji)); - emojiCount += 1; - } catch (error) { - failed.push({ - name: emojiData.name, - error: this.getLocalizedBulkErrorMessage(error, params.user.locale), - }); - } - } - return {success, failed}; - } - - async updatePackEmoji(params: { - userId: UserID; - packId: GuildID; - emojiId: EmojiID; - name: string; - }): Promise { - await this.requireExpressionPackAccess(params.userId); - const pack = await this.ensurePackOwner(params.userId, params.packId); - if (pack.type !== 'emoji') { - throw new InvalidPackTypeError('emoji'); - } - const emoji = await this.guildRepository.getEmoji(params.emojiId, pack.id); - if (!emoji) { - throw new UnknownGuildEmojiError(); - } - const updatedEmoji = await this.guildRepository.upsertEmoji({ - ...emoji.toRow(), - name: params.name, - }); - return mapGuildEmojiToResponse(updatedEmoji); - } - - async deletePackEmoji(params: {userId: UserID; packId: GuildID; emojiId: EmojiID; purge?: boolean}): Promise { - await this.requireExpressionPackAccess(params.userId); - const pack = await this.ensurePackOwner(params.userId, params.packId); - if (pack.type !== 'emoji') { - throw new InvalidPackTypeError('emoji'); - } - const emoji = await this.guildRepository.getEmoji(params.emojiId, pack.id); - if (!emoji) { - throw new UnknownGuildEmojiError(); - } - await this.guildRepository.deleteEmoji(pack.id, params.emojiId); - if (params.purge) { - await this.assetPurger.purgeEmoji(emoji.id.toString()); - } - } - - async createPackSticker(params: { - user: User; - packId: GuildID; - name: string; - description?: string | null; - tags: Array; - image: string; - }): Promise { - await this.requireExpressionPackAccess(params.user.id); - await this.requireFeature(params.user.id, 'feature_global_expressions'); - const pack = await this.ensurePackOwner(params.user.id, params.packId); - if (pack.type !== 'sticker') { - throw new InvalidPackTypeError('sticker'); - } - const stickerCount = await this.guildRepository.countStickers(pack.id); - const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS); - if (stickerCount >= expressionLimit) { - throw new MaxPackExpressionsError(expressionLimit); - } - const {animated, imageBuffer} = await this.avatarService.processSticker({ - errorPath: 'image', - base64Image: params.image, - }); - const stickerId = createStickerID(await this.apiContext.services.snowflake.generate()); - await this.avatarService.uploadSticker({prefix: 'stickers', stickerId, imageBuffer}); - const sticker = await this.guildRepository.upsertSticker({ - guild_id: pack.id, - sticker_id: stickerId, - name: params.name, - description: params.description ?? null, - tags: params.tags, - animated, - nsfw: null, - creator_id: params.user.id, - version: 1, - }); - const response = mapGuildStickerToResponse(sticker); - return response; - } - - async bulkCreatePackStickers(params: { - user: User; - packId: GuildID; - stickers: Array<{ - name: string; - description?: string | null; - tags: Array; - image: string; - }>; - }): Promise<{ - success: Array; - failed: Array<{ - name: string; - error: string; - }>; - }> { - await this.requireExpressionPackAccess(params.user.id); - await this.requireFeature(params.user.id, 'feature_global_expressions'); - const pack = await this.ensurePackOwner(params.user.id, params.packId); - if (pack.type !== 'sticker') { - throw new InvalidPackTypeError('sticker'); - } - let stickerCount = await this.guildRepository.countStickers(pack.id); - const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS); - const success: Array = []; - const failed: Array<{ - name: string; - error: string; - }> = []; - for (const stickerData of params.stickers) { - if (stickerCount >= expressionLimit) { - failed.push({ - name: stickerData.name, - error: this.getLocalizedPackExpressionLimitMessage(params.user.locale, expressionLimit), - }); - continue; - } - try { - const {animated, imageBuffer} = await this.avatarService.processSticker({ - errorPath: `stickers[${success.length + failed.length}].image`, - base64Image: stickerData.image, - }); - const stickerId = createStickerID(await this.apiContext.services.snowflake.generate()); - await this.avatarService.uploadSticker({prefix: 'stickers', stickerId, imageBuffer}); - const sticker = await this.guildRepository.upsertSticker({ - guild_id: pack.id, - sticker_id: stickerId, - name: stickerData.name, - description: stickerData.description ?? null, - tags: stickerData.tags, - animated, - nsfw: null, - creator_id: params.user.id, - version: 1, - }); - success.push(mapGuildStickerToResponse(sticker)); - stickerCount += 1; - } catch (error) { - failed.push({ - name: stickerData.name, - error: this.getLocalizedBulkErrorMessage(error, params.user.locale), - }); - } - } - for (const _ of success) { - } - return {success, failed}; - } - - async updatePackSticker(params: { - userId: UserID; - packId: GuildID; - stickerId: StickerID; - name: string; - description?: string | null; - tags: Array; - }): Promise { - await this.requireExpressionPackAccess(params.userId); - const pack = await this.ensurePackOwner(params.userId, params.packId); - if (pack.type !== 'sticker') { - throw new InvalidPackTypeError('sticker'); - } - const sticker = await this.guildRepository.getSticker(params.stickerId, pack.id); - if (!sticker) { - throw new UnknownGuildStickerError(); - } - const updatedSticker = await this.guildRepository.upsertSticker({ - ...sticker.toRow(), - name: params.name, - description: params.description ?? null, - tags: params.tags, - }); - return mapGuildStickerToResponse(updatedSticker); - } - - async deletePackSticker(params: { - userId: UserID; - packId: GuildID; - stickerId: StickerID; - purge?: boolean; - }): Promise { - await this.requireExpressionPackAccess(params.userId); - const pack = await this.ensurePackOwner(params.userId, params.packId); - if (pack.type !== 'sticker') { - throw new InvalidPackTypeError('sticker'); - } - const sticker = await this.guildRepository.getSticker(params.stickerId, pack.id); - if (!sticker) { - throw new UnknownGuildStickerError(); - } - await this.guildRepository.deleteSticker(pack.id, params.stickerId); - if (params.purge) { - await this.assetPurger.purgeSticker(sticker.id.toString()); - } - } - - private resolveLimitForUser(user: User | null, key: LimitKey, fallback: number): number { - const ctx = createLimitMatchContext({user}); - return resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, key, fallback); - } -} diff --git a/fluxer_api/src/api/pack/controllers/PackController.ts b/fluxer_api/src/api/pack/controllers/PackController.ts deleted file mode 100644 index 65a55b229..000000000 --- a/fluxer_api/src/api/pack/controllers/PackController.ts +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {PackIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; -import { - PackCreateRequest, - PackDashboardResponse, - PackSummaryResponse, - PackTypeParam, - PackUpdateRequest, -} from '@fluxer/schema/src/domains/pack/PackSchemas'; -import {createGuildID} from '../../BrandedTypes'; -import {DefaultUserOnly, LoginRequired} from '../../middleware/AuthMiddleware'; -import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware'; -import {OpenAPI} from '../../middleware/ResponseTypeMiddleware'; -import {RateLimitConfigs} from '../../RateLimitConfig'; -import type {HonoApp} from '../../types/HonoEnv'; -import {Validator} from '../../Validator'; -import {mapPackToSummary} from '../PackModel'; - -export function PackController(app: HonoApp) { - app.get( - '/packs', - RateLimitMiddleware(RateLimitConfigs.PACKS_LIST), - LoginRequired, - DefaultUserOnly, - OpenAPI({ - operationId: 'list_user_packs', - summary: 'List user packs', - description: - 'Returns a dashboard view containing all emoji and sticker packs created by or owned by the authenticated user. This includes pack metadata such as name, description, type, and cover image.', - responseSchema: PackDashboardResponse, - statusCode: 200, - security: ['bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const response = await ctx.get('packService').listUserPacks(ctx.get('user').id); - return ctx.json(response); - }, - ); - app.post( - '/packs/:pack_type', - RateLimitMiddleware(RateLimitConfigs.PACKS_CREATE), - LoginRequired, - DefaultUserOnly, - Validator('param', PackTypeParam), - Validator('json', PackCreateRequest), - OpenAPI({ - operationId: 'create_pack', - summary: 'Create pack', - description: - 'Creates a new emoji or sticker pack owned by the authenticated user. The pack type is specified in the path parameter and can be either "emoji" or "sticker". Returns the newly created pack with its metadata.', - responseSchema: PackSummaryResponse, - statusCode: 200, - security: ['bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const user = ctx.get('user'); - const data = ctx.req.valid('json'); - const pack = await ctx.get('packService').createPack({ - user, - type: ctx.req.valid('param').pack_type as 'emoji' | 'sticker', - name: data.name, - description: data.description ?? null, - }); - return ctx.json(mapPackToSummary(pack)); - }, - ); - app.patch( - '/packs/:pack_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_UPDATE), - LoginRequired, - DefaultUserOnly, - Validator('param', PackIdParam), - Validator('json', PackUpdateRequest), - OpenAPI({ - operationId: 'update_pack', - summary: 'Update pack', - description: - 'Updates the metadata for an existing pack owned by the authenticated user. Allowed modifications include name, description, and cover image. Returns the updated pack with all current metadata.', - responseSchema: PackSummaryResponse, - statusCode: 200, - security: ['bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const data = ctx.req.valid('json'); - const updated = await ctx.get('packService').updatePack({ - userId: ctx.get('user').id, - packId: createGuildID(ctx.req.valid('param').pack_id), - name: data.name, - description: data.description, - }); - return ctx.json(mapPackToSummary(updated)); - }, - ); - app.delete( - '/packs/:pack_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_DELETE), - LoginRequired, - DefaultUserOnly, - Validator('param', PackIdParam), - OpenAPI({ - operationId: 'delete_pack', - summary: 'Delete pack', - description: - 'Permanently deletes a pack owned by the authenticated user along with all emojis or stickers contained within it. This action cannot be undone and will remove all associated assets.', - responseSchema: null, - statusCode: 204, - security: ['bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - await ctx.get('packService').deletePack(ctx.get('user').id, createGuildID(ctx.req.valid('param').pack_id)); - return ctx.body(null, 204); - }, - ); - app.post( - '/packs/:pack_id/install', - RateLimitMiddleware(RateLimitConfigs.PACKS_INSTALL), - LoginRequired, - DefaultUserOnly, - Validator('param', PackIdParam), - OpenAPI({ - operationId: 'install_pack', - summary: 'Install pack', - description: - "Installs a pack to the authenticated user's collection, making its emojis or stickers available for use. The pack must be publicly accessible or owned by the user.", - responseSchema: null, - statusCode: 204, - security: ['bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - await ctx.get('packService').installPack(ctx.get('user').id, createGuildID(ctx.req.valid('param').pack_id)); - return ctx.body(null, 204); - }, - ); - app.delete( - '/packs/:pack_id/install', - RateLimitMiddleware(RateLimitConfigs.PACKS_INSTALL), - LoginRequired, - DefaultUserOnly, - Validator('param', PackIdParam), - OpenAPI({ - operationId: 'uninstall_pack', - summary: 'Uninstall pack', - description: - "Uninstalls a pack from the authenticated user's collection, removing access to its emojis or stickers. This does not delete the pack itself, only removes it from the user's installed list.", - responseSchema: null, - statusCode: 204, - security: ['bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - await ctx.get('packService').uninstallPack(ctx.get('user').id, createGuildID(ctx.req.valid('param').pack_id)); - return ctx.body(null, 204); - }, - ); -} diff --git a/fluxer_api/src/api/pack/controllers/PackEmojiController.ts b/fluxer_api/src/api/pack/controllers/PackEmojiController.ts deleted file mode 100644 index c54bd5a6c..000000000 --- a/fluxer_api/src/api/pack/controllers/PackEmojiController.ts +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {PackIdEmojiIdParam, PackIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; -import {PurgeQuery} from '@fluxer/schema/src/domains/common/CommonQuerySchemas'; -import { - GuildEmojiBulkCreateResponse, - GuildEmojiResponse, - GuildEmojiWithUserListResponse, -} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas'; -import { - GuildEmojiBulkCreateRequest, - GuildEmojiCreateRequest, - GuildEmojiUpdateRequest, -} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas'; -import {createEmojiID, createGuildID} from '../../BrandedTypes'; -import {LoginRequired} from '../../middleware/AuthMiddleware'; -import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware'; -import {OpenAPI} from '../../middleware/ResponseTypeMiddleware'; -import {RateLimitConfigs} from '../../RateLimitConfig'; -import type {HonoApp} from '../../types/HonoEnv'; -import {Validator} from '../../Validator'; - -export function PackEmojiController(app: HonoApp) { - app.post( - '/packs/emojis/:pack_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJI_CREATE), - LoginRequired, - Validator('param', PackIdParam), - Validator('json', GuildEmojiCreateRequest), - OpenAPI({ - operationId: 'create_pack_emoji', - summary: 'Create pack emoji', - description: - 'Creates a new emoji within the specified pack. Requires the pack ID in the path and emoji metadata (name and image data) in the request body. Returns the newly created emoji with its generated ID.', - responseSchema: GuildEmojiResponse, - statusCode: 200, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const packId = createGuildID(ctx.req.valid('param').pack_id); - const {name, image} = ctx.req.valid('json'); - const user = ctx.get('user'); - return ctx.json(await ctx.get('packService').createPackEmoji({user, packId, name, image})); - }, - ); - app.post( - '/packs/emojis/:pack_id/bulk', - RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJI_BULK_CREATE), - LoginRequired, - Validator('param', PackIdParam), - Validator('json', GuildEmojiBulkCreateRequest), - OpenAPI({ - operationId: 'bulk_create_pack_emojis', - summary: 'Bulk create pack emojis', - description: - 'Creates multiple emojis within the specified pack in a single bulk operation. Accepts an array of emoji definitions, each containing name and image data. Returns a response containing all successfully created emojis.', - responseSchema: GuildEmojiBulkCreateResponse, - statusCode: 200, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const packId = createGuildID(ctx.req.valid('param').pack_id); - const {emojis} = ctx.req.valid('json'); - const user = ctx.get('user'); - return ctx.json(await ctx.get('packService').bulkCreatePackEmojis({user, packId, emojis})); - }, - ); - app.get( - '/packs/emojis/:pack_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJIS_LIST), - LoginRequired, - Validator('param', PackIdParam), - OpenAPI({ - operationId: 'list_pack_emojis', - summary: 'List pack emojis', - description: - 'Returns a list of all emojis contained within the specified pack, including emoji metadata and creator information. Results include emoji ID, name, image URL, and the user who created each emoji.', - responseSchema: GuildEmojiWithUserListResponse, - statusCode: 200, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const packId = createGuildID(ctx.req.valid('param').pack_id); - const userId = ctx.get('user').id; - const requestCache = ctx.get('requestCache'); - return ctx.json(await ctx.get('packService').getPackEmojis({userId, packId, requestCache})); - }, - ); - app.patch( - '/packs/emojis/:pack_id/:emoji_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJI_UPDATE), - LoginRequired, - Validator('param', PackIdEmojiIdParam), - Validator('json', GuildEmojiUpdateRequest), - OpenAPI({ - operationId: 'update_pack_emoji', - summary: 'Update pack emoji', - description: - 'Updates the name of an existing emoji within the specified pack. Requires both pack ID and emoji ID in the path parameters. Returns the updated emoji with its new name and all existing metadata.', - responseSchema: GuildEmojiResponse, - statusCode: 200, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const {pack_id, emoji_id} = ctx.req.valid('param'); - const packId = createGuildID(pack_id); - const emojiId = createEmojiID(emoji_id); - const {name} = ctx.req.valid('json'); - return ctx.json( - await ctx.get('packService').updatePackEmoji({userId: ctx.get('user').id, packId, emojiId, name}), - ); - }, - ); - app.delete( - '/packs/emojis/:pack_id/:emoji_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_EMOJI_DELETE), - LoginRequired, - Validator('param', PackIdEmojiIdParam), - Validator('query', PurgeQuery), - OpenAPI({ - operationId: 'delete_pack_emoji', - summary: 'Delete pack emoji', - description: - 'Permanently deletes an emoji from the specified pack. Requires both pack ID and emoji ID in the path parameters. Accepts an optional "purge" query parameter to control whether associated assets are immediately deleted.', - responseSchema: null, - statusCode: 204, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const {pack_id, emoji_id} = ctx.req.valid('param'); - const packId = createGuildID(pack_id); - const emojiId = createEmojiID(emoji_id); - const purge = ctx.req.valid('query').purge ?? false; - await ctx.get('packService').deletePackEmoji({ - userId: ctx.get('user').id, - packId, - emojiId, - purge, - }); - return ctx.body(null, 204); - }, - ); -} diff --git a/fluxer_api/src/api/pack/controllers/PackStickerController.ts b/fluxer_api/src/api/pack/controllers/PackStickerController.ts deleted file mode 100644 index 3cd034878..000000000 --- a/fluxer_api/src/api/pack/controllers/PackStickerController.ts +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {PackIdParam, PackIdStickerIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; -import {PurgeQuery} from '@fluxer/schema/src/domains/common/CommonQuerySchemas'; -import { - GuildStickerBulkCreateResponse, - GuildStickerResponse, - GuildStickerWithUserListResponse, -} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas'; -import { - GuildStickerBulkCreateRequest, - GuildStickerCreateRequest, - GuildStickerUpdateRequest, -} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas'; -import {createGuildID, createStickerID} from '../../BrandedTypes'; -import {LoginRequired} from '../../middleware/AuthMiddleware'; -import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware'; -import {OpenAPI} from '../../middleware/ResponseTypeMiddleware'; -import {RateLimitConfigs} from '../../RateLimitConfig'; -import type {HonoApp} from '../../types/HonoEnv'; -import {Validator} from '../../Validator'; - -export function PackStickerController(app: HonoApp) { - app.post( - '/packs/stickers/:pack_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_STICKER_CREATE), - LoginRequired, - Validator('param', PackIdParam), - Validator('json', GuildStickerCreateRequest), - OpenAPI({ - operationId: 'create_pack_sticker', - summary: 'Create pack sticker', - description: - 'Creates a new sticker within the specified pack. Requires the pack ID in the path and sticker metadata (name, description, tags, and image data) in the request body. Returns the newly created sticker with its generated ID.', - responseSchema: GuildStickerResponse, - statusCode: 200, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const packId = createGuildID(ctx.req.valid('param').pack_id); - const {name, description, tags, image} = ctx.req.valid('json'); - const user = ctx.get('user'); - const sticker = await ctx.get('packService').createPackSticker({user, packId, name, description, tags, image}); - return ctx.json(sticker); - }, - ); - app.post( - '/packs/stickers/:pack_id/bulk', - RateLimitMiddleware(RateLimitConfigs.PACKS_STICKER_BULK_CREATE), - LoginRequired, - Validator('param', PackIdParam), - Validator('json', GuildStickerBulkCreateRequest), - OpenAPI({ - operationId: 'bulk_create_pack_stickers', - summary: 'Bulk create pack stickers', - description: - 'Creates multiple stickers within the specified pack in a single bulk operation. Accepts an array of sticker definitions, each containing name, description, tags, and image data. Returns a response containing all successfully created stickers.', - responseSchema: GuildStickerBulkCreateResponse, - statusCode: 200, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const packId = createGuildID(ctx.req.valid('param').pack_id); - const {stickers} = ctx.req.valid('json'); - const user = ctx.get('user'); - const result = await ctx.get('packService').bulkCreatePackStickers({user, packId, stickers}); - return ctx.json(result); - }, - ); - app.get( - '/packs/stickers/:pack_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_STICKERS_LIST), - LoginRequired, - Validator('param', PackIdParam), - OpenAPI({ - operationId: 'list_pack_stickers', - summary: 'List pack stickers', - description: - 'Returns a list of all stickers contained within the specified pack, including sticker metadata and creator information. Results include sticker ID, name, description, tags, image URL, and the user who created each sticker.', - responseSchema: GuildStickerWithUserListResponse, - statusCode: 200, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const packId = createGuildID(ctx.req.valid('param').pack_id); - const userId = ctx.get('user').id; - const requestCache = ctx.get('requestCache'); - return ctx.json(await ctx.get('packService').getPackStickers({userId, packId, requestCache})); - }, - ); - app.patch( - '/packs/stickers/:pack_id/:sticker_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_STICKER_UPDATE), - LoginRequired, - Validator('param', PackIdStickerIdParam), - Validator('json', GuildStickerUpdateRequest), - OpenAPI({ - operationId: 'update_pack_sticker', - summary: 'Update pack sticker', - description: - 'Updates the name, description, or tags of an existing sticker within the specified pack. Requires both pack ID and sticker ID in the path parameters. Returns the updated sticker with its new metadata and all existing fields.', - responseSchema: GuildStickerResponse, - statusCode: 200, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const {pack_id, sticker_id} = ctx.req.valid('param'); - const packId = createGuildID(pack_id); - const stickerId = createStickerID(sticker_id); - const {name, description, tags} = ctx.req.valid('json'); - return ctx.json( - await ctx - .get('packService') - .updatePackSticker({userId: ctx.get('user').id, packId, stickerId, name, description, tags}), - ); - }, - ); - app.delete( - '/packs/stickers/:pack_id/:sticker_id', - RateLimitMiddleware(RateLimitConfigs.PACKS_STICKER_DELETE), - LoginRequired, - Validator('param', PackIdStickerIdParam), - Validator('query', PurgeQuery), - OpenAPI({ - operationId: 'delete_pack_sticker', - summary: 'Delete pack sticker', - description: - 'Permanently deletes a sticker from the specified pack. Requires both pack ID and sticker ID in the path parameters. Accepts an optional "purge" query parameter to control whether associated assets are immediately deleted.', - responseSchema: null, - statusCode: 204, - security: ['botToken', 'bearerToken', 'sessionToken'], - tags: ['Packs'], - }), - async (ctx) => { - const {pack_id, sticker_id} = ctx.req.valid('param'); - const packId = createGuildID(pack_id); - const stickerId = createStickerID(sticker_id); - const {purge = false} = ctx.req.valid('query'); - await ctx.get('packService').deletePackSticker({ - userId: ctx.get('user').id, - packId, - stickerId, - purge, - }); - return ctx.body(null, 204); - }, - ); -} diff --git a/fluxer_api/src/api/pack/controllers/index.ts b/fluxer_api/src/api/pack/controllers/index.ts deleted file mode 100644 index b7847a69f..000000000 --- a/fluxer_api/src/api/pack/controllers/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {HonoApp} from '../../types/HonoEnv'; -import {PackController} from './PackController'; -import {PackEmojiController} from './PackEmojiController'; -import {PackStickerController} from './PackStickerController'; - -export function registerPackControllers(app: HonoApp) { - PackController(app); - PackEmojiController(app); - PackStickerController(app); -} diff --git a/fluxer_api/src/api/pack/tests/PackInviteFlow.test.ts b/fluxer_api/src/api/pack/tests/PackInviteFlow.test.ts deleted file mode 100644 index 1ad7ff1ac..000000000 --- a/fluxer_api/src/api/pack/tests/PackInviteFlow.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {afterEach, beforeEach, describe, expect, test} from 'vitest'; -import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; -import {HTTP_STATUS} from '../../test/TestConstants'; -import {createBuilder} from '../../test/TestRequestBuilder'; -import { - createPack, - createPackEmoji, - createPackSticker, - deletePack, - deletePackEmoji, - deletePackSticker, - getPackEmojis, - getPackStickers, - installPack, - listPacks, - setupPackTestAccount, - uninstallPack, - updatePack, -} from './PackTestUtils'; - -describe('Pack Invite Flow', () => { - let harness: ApiTestHarness; - beforeEach(async () => { - harness = await createApiTestHarness(); - }); - afterEach(async () => { - await harness?.shutdown(); - }); - test('user can create and list emoji pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'emoji', { - name: 'My Emoji Pack', - description: 'A collection of emojis', - }); - const dashboard = await listPacks(harness, account.token); - const createdPack = dashboard.emoji.created.find((p) => p.id === pack.id); - expect(createdPack).toBeTruthy(); - expect(createdPack?.name).toBe('My Emoji Pack'); - }); - test('user can update pack name and description', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'emoji', {name: 'Original Name'}); - const updated = await updatePack(harness, account.token, pack.id, { - name: 'Updated Name', - description: 'New description', - }); - expect(updated.name).toBe('Updated Name'); - }); - test('user can delete own pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'emoji', {name: 'To Delete'}); - await deletePack(harness, account.token, pack.id); - const dashboard = await listPacks(harness, account.token); - const deletedPack = dashboard.emoji.created.find((p) => p.id === pack.id); - expect(deletedPack).toBeUndefined(); - }); - test('user cannot update another users pack', async () => { - const {account: owner} = await setupPackTestAccount(harness); - const pack = await createPack(harness, owner.token, 'emoji', {name: 'Owners Pack'}); - const {account: other} = await setupPackTestAccount(harness); - await createBuilder(harness, other.token) - .patch(`/packs/${pack.id}`) - .body({name: 'Stolen Name'}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); - test('user cannot delete another users pack', async () => { - const {account: owner} = await setupPackTestAccount(harness); - const pack = await createPack(harness, owner.token, 'emoji', {name: 'Protected Pack'}); - const {account: other} = await setupPackTestAccount(harness); - await createBuilder(harness, other.token).delete(`/packs/${pack.id}`).expect(HTTP_STATUS.FORBIDDEN).execute(); - }); - test('user can install pack from another user', async () => { - const {account: owner} = await setupPackTestAccount(harness); - const pack = await createPack(harness, owner.token, 'emoji', {name: 'Shareable Pack'}); - const {account: installer} = await setupPackTestAccount(harness); - await installPack(harness, installer.token, pack.id); - const dashboard = await listPacks(harness, installer.token); - expect(dashboard.emoji.installed.some((p) => p.id === pack.id)).toBe(true); - }); - test('uninstall pack returns success', async () => { - const {account: owner} = await setupPackTestAccount(harness); - const pack = await createPack(harness, owner.token, 'emoji', {name: 'Uninstall Test Pack'}); - const {account: installer} = await setupPackTestAccount(harness); - await installPack(harness, installer.token, pack.id); - await uninstallPack(harness, installer.token, pack.id); - }); - test('installing pack is idempotent', async () => { - const {account: owner} = await setupPackTestAccount(harness); - const pack = await createPack(harness, owner.token, 'emoji', {name: 'Idempotent Pack'}); - const {account: installer} = await setupPackTestAccount(harness); - await installPack(harness, installer.token, pack.id); - await installPack(harness, installer.token, pack.id); - const dashboard = await listPacks(harness, installer.token); - const installedCount = dashboard.emoji.installed.filter((p) => p.id === pack.id).length; - expect(installedCount).toBe(1); - }); - test('cannot install non-existent pack', async () => { - const {account} = await setupPackTestAccount(harness); - await createBuilder(harness, account.token) - .post('/packs/999999999999999999/install') - .body({}) - .expect(HTTP_STATUS.NOT_FOUND) - .execute(); - }); - test('can add emoji to pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'emoji', {name: 'Emoji Pack'}); - const emoji = await createPackEmoji(harness, account.token, pack.id, 'test_emoji'); - expect(emoji.id).toBeTruthy(); - expect(emoji.name).toBe('test_emoji'); - const emojis = await getPackEmojis(harness, account.token, pack.id); - expect(emojis.some((e) => e.id === emoji.id)).toBe(true); - }); - test('can add sticker to pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'sticker', {name: 'Sticker Pack'}); - const sticker = await createPackSticker(harness, account.token, pack.id, 'test_sticker', ['happy', 'fun']); - expect(sticker.id).toBeTruthy(); - expect(sticker.name).toBe('test_sticker'); - const stickers = await getPackStickers(harness, account.token, pack.id); - expect(stickers.some((s) => s.id === sticker.id)).toBe(true); - }); - test('cannot add emoji to sticker pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'sticker', {name: 'Wrong Type Pack'}); - await createBuilder(harness, account.token) - .post(`/packs/emojis/${pack.id}`) - .body({name: 'wrong_emoji', image: 'data:image/png;base64,iVBORw0KGgo='}) - .expect(HTTP_STATUS.BAD_REQUEST) - .execute(); - }); - test('cannot add sticker to emoji pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'emoji', {name: 'Wrong Type Pack'}); - await createBuilder(harness, account.token) - .post(`/packs/stickers/${pack.id}`) - .body({name: 'wrong_sticker', tags: ['test'], image: 'data:image/png;base64,iVBORw0KGgo='}) - .expect(HTTP_STATUS.BAD_REQUEST) - .execute(); - }); - test('can delete emoji from pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'emoji', {name: 'Delete Emoji Pack'}); - const emoji = await createPackEmoji(harness, account.token, pack.id, 'to_delete'); - await deletePackEmoji(harness, account.token, pack.id, emoji.id); - const emojis = await getPackEmojis(harness, account.token, pack.id); - expect(emojis.some((e) => e.id === emoji.id)).toBe(false); - }); - test('can delete sticker from pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'sticker', {name: 'Delete Sticker Pack'}); - const sticker = await createPackSticker(harness, account.token, pack.id, 'to_delete', ['bye']); - await deletePackSticker(harness, account.token, pack.id, sticker.id); - const stickers = await getPackStickers(harness, account.token, pack.id); - expect(stickers.some((s) => s.id === sticker.id)).toBe(false); - }); - test('cannot add emoji to another users pack', async () => { - const {account: owner} = await setupPackTestAccount(harness); - const pack = await createPack(harness, owner.token, 'emoji', {name: 'Protected Emoji Pack'}); - const {account: other} = await setupPackTestAccount(harness); - await createBuilder(harness, other.token) - .post(`/packs/emojis/${pack.id}`) - .body({name: 'unauthorized', image: 'data:image/png;base64,iVBORw0KGgo='}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); - test('pack creator shows in created packs list', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'emoji', {name: 'My Created Pack'}); - const dashboard = await listPacks(harness, account.token); - expect(dashboard.emoji.created.some((p) => p.id === pack.id)).toBe(true); - expect(dashboard.emoji.installed.some((p) => p.id === pack.id)).toBe(false); - }); - test('multiple packs can be created and managed', async () => { - const {account} = await setupPackTestAccount(harness); - const emojiPack1 = await createPack(harness, account.token, 'emoji', {name: 'Emoji Pack 1'}); - const emojiPack2 = await createPack(harness, account.token, 'emoji', {name: 'Emoji Pack 2'}); - const stickerPack = await createPack(harness, account.token, 'sticker', {name: 'Sticker Pack 1'}); - const dashboard = await listPacks(harness, account.token); - expect(dashboard.emoji.created.length).toBe(2); - expect(dashboard.sticker.created.length).toBe(1); - expect(dashboard.emoji.created.some((p) => p.id === emojiPack1.id)).toBe(true); - expect(dashboard.emoji.created.some((p) => p.id === emojiPack2.id)).toBe(true); - expect(dashboard.sticker.created.some((p) => p.id === stickerPack.id)).toBe(true); - }); - test('installed pack shows installed_at timestamp', async () => { - const {account: owner} = await setupPackTestAccount(harness); - const pack = await createPack(harness, owner.token, 'emoji', {name: 'Timestamped Pack'}); - const {account: installer} = await setupPackTestAccount(harness); - await installPack(harness, installer.token, pack.id); - const dashboard = await listPacks(harness, installer.token); - const installedPack = dashboard.emoji.installed.find((p) => p.id === pack.id); - expect(installedPack).toBeTruthy(); - expect(installedPack?.installed_at).toBeTruthy(); - }); -}); diff --git a/fluxer_api/src/api/pack/tests/PackPremiumRequirements.test.ts b/fluxer_api/src/api/pack/tests/PackPremiumRequirements.test.ts deleted file mode 100644 index f71351f43..000000000 --- a/fluxer_api/src/api/pack/tests/PackPremiumRequirements.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {afterEach, beforeEach, describe, expect, test} from 'vitest'; -import {createTestAccount} from '../../auth/tests/AuthTestUtils'; -import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; -import {HTTP_STATUS} from '../../test/TestConstants'; -import {createBuilder} from '../../test/TestRequestBuilder'; -import { - createPack, - grantPremium, - grantStaffAccess, - installPack, - listPacks, - revokePremium, - setupNonPremiumPackTestAccount, - setupPackTestAccount, -} from './PackTestUtils'; - -describe('Pack Premium Requirements', () => { - let harness: ApiTestHarness; - beforeEach(async () => { - harness = await createApiTestHarness(); - }); - afterEach(async () => { - await harness?.shutdown(); - }); - test('user without staff flag cannot list packs', async () => { - const account = await createTestAccount(harness); - await createBuilder(harness, account.token).get('/packs').expect(HTTP_STATUS.FORBIDDEN).execute(); - }); - test('user with staff flag but no premium cannot create pack', async () => { - const {account} = await setupNonPremiumPackTestAccount(harness); - await createBuilder(harness, account.token) - .post('/packs/emoji') - .body({name: 'Test Pack'}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); - test('premium user with staff flag can create emoji pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'emoji', {name: 'My Emoji Pack'}); - expect(pack.id).toBeTruthy(); - expect(pack.name).toBe('My Emoji Pack'); - expect(pack.type).toBe('emoji'); - }); - test('premium user with staff flag can create sticker pack', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'sticker', {name: 'My Sticker Pack'}); - expect(pack.id).toBeTruthy(); - expect(pack.name).toBe('My Sticker Pack'); - expect(pack.type).toBe('sticker'); - }); - test('non-premium user cannot create pack emoji', async () => { - const {account} = await setupPackTestAccount(harness); - const pack = await createPack(harness, account.token, 'emoji', {name: 'Emoji Pack'}); - await revokePremium(harness, account.userId); - await createBuilder(harness, account.token) - .post(`/packs/emojis/${pack.id}`) - .body({name: 'emoji1', image: 'data:image/png;base64,iVBORw0KGgo='}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); - test('non-premium user cannot install pack', async () => { - const {account: owner} = await setupPackTestAccount(harness); - const pack = await createPack(harness, owner.token, 'emoji', {name: 'Shared Pack'}); - const {account: installer} = await setupNonPremiumPackTestAccount(harness); - await createBuilder(harness, installer.token) - .post(`/packs/${pack.id}/install`) - .body({}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); - test('premium user can install pack', async () => { - const {account: owner} = await setupPackTestAccount(harness); - const pack = await createPack(harness, owner.token, 'emoji', {name: 'Installable Pack'}); - const {account: installer} = await setupPackTestAccount(harness); - await installPack(harness, installer.token, pack.id); - const dashboard = await listPacks(harness, installer.token); - const installed = dashboard.emoji.installed.find((p) => p.id === pack.id); - expect(installed).toBeTruthy(); - }); - test('user without staff flag cannot access pack endpoints', async () => { - const account = await createTestAccount(harness); - await createBuilder(harness, account.token).get('/packs').expect(HTTP_STATUS.FORBIDDEN).execute(); - await createBuilder(harness, account.token) - .post('/packs/emoji') - .body({name: 'Unauthorized Pack'}) - .expect(HTTP_STATUS.FORBIDDEN) - .execute(); - }); - test('user gains staff flag and can access expression packs', async () => { - const owner = await createTestAccount(harness); - await grantStaffAccess(harness, owner.userId); - await grantPremium(harness, owner.userId); - const dashboard = await listPacks(harness, owner.token); - expect(dashboard.emoji).toBeTruthy(); - expect(dashboard.sticker).toBeTruthy(); - }); - test('pack limits show correct values for premium user', async () => { - const {account} = await setupPackTestAccount(harness); - const dashboard = await listPacks(harness, account.token); - expect(dashboard.emoji.created_limit).toBeGreaterThan(0); - expect(dashboard.emoji.installed_limit).toBeGreaterThan(0); - expect(dashboard.sticker.created_limit).toBeGreaterThan(0); - expect(dashboard.sticker.installed_limit).toBeGreaterThan(0); - }); - test('pack limits show zero for non-premium user with staff flag', async () => { - const {account} = await setupNonPremiumPackTestAccount(harness); - const dashboard = await listPacks(harness, account.token); - expect(dashboard.emoji.created_limit).toBe(0); - expect(dashboard.emoji.installed_limit).toBe(0); - }); -}); diff --git a/fluxer_api/src/api/pack/tests/PackTestUtils.ts b/fluxer_api/src/api/pack/tests/PackTestUtils.ts deleted file mode 100644 index 94e7db5db..000000000 --- a/fluxer_api/src/api/pack/tests/PackTestUtils.ts +++ /dev/null @@ -1,206 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {readFileSync} from 'node:fs'; -import {join} from 'node:path'; -import type { - GuildEmojiResponse, - GuildEmojiWithUserResponse, - GuildStickerResponse, - GuildStickerWithUserResponse, -} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas'; -import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; -import type {PackDashboardResponse, PackSummaryResponse} from '@fluxer/schema/src/domains/pack/PackSchemas'; -import {createTestAccount, type TestAccount} from '../../auth/tests/AuthTestUtils'; -import type {ApiTestHarness} from '../../test/ApiTestHarness'; -import {HTTP_STATUS} from '../../test/TestConstants'; -import {createBuilder, createBuilderWithoutAuth} from '../../test/TestRequestBuilder'; - -interface PackCreateRequest { - name: string; - description?: string | null; -} - -interface PackUpdateRequest { - name?: string; - description?: string | null; -} - -type PackType = 'emoji' | 'sticker'; - -function loadPackFixture(filename: string): Buffer { - const fixturesPath = join(import.meta.dirname, '..', '..', 'test', 'fixtures', filename); - return readFileSync(fixturesPath); -} - -async function createGuild(harness: ApiTestHarness, token: string, name: string): Promise { - return createBuilder(harness, token).post('/guilds').body({name}).expect(HTTP_STATUS.OK).execute(); -} - -export async function grantStaffAccess(harness: ApiTestHarness, userId: string): Promise { - await createBuilderWithoutAuth(harness).patch(`/test/users/${userId}/flags`).body({flags: 1}).execute(); -} - -export async function grantPremium(harness: ApiTestHarness, userId: string): Promise { - const premiumUntil = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(); - await createBuilderWithoutAuth(harness) - .post(`/test/users/${userId}/premium`) - .body({ - premium_type: 2, - premium_until: premiumUntil, - }) - .execute(); -} - -export async function revokePremium(harness: ApiTestHarness, userId: string): Promise { - await createBuilderWithoutAuth(harness) - .post(`/test/users/${userId}/premium`) - .body({ - premium_type: null, - premium_until: null, - }) - .execute(); -} - -export async function setupPackTestAccount(harness: ApiTestHarness): Promise<{ - account: TestAccount; - guild: GuildResponse; -}> { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Pack Test Guild'); - await grantStaffAccess(harness, account.userId); - await grantPremium(harness, account.userId); - return {account, guild}; -} - -export async function setupNonPremiumPackTestAccount(harness: ApiTestHarness): Promise<{ - account: TestAccount; - guild: GuildResponse; -}> { - const account = await createTestAccount(harness); - const guild = await createGuild(harness, account.token, 'Pack Test Guild'); - await grantStaffAccess(harness, account.userId); - return {account, guild}; -} - -export async function listPacks(harness: ApiTestHarness, token: string): Promise { - return createBuilder(harness, token).get('/packs').expect(HTTP_STATUS.OK).execute(); -} - -export async function createPack( - harness: ApiTestHarness, - token: string, - packType: PackType, - data: PackCreateRequest, -): Promise { - return createBuilder(harness, token) - .post(`/packs/${packType}`) - .body(data) - .expect(HTTP_STATUS.OK) - .execute(); -} - -export async function updatePack( - harness: ApiTestHarness, - token: string, - packId: string, - data: PackUpdateRequest, -): Promise { - return createBuilder(harness, token) - .patch(`/packs/${packId}`) - .body(data) - .expect(HTTP_STATUS.OK) - .execute(); -} - -export async function deletePack(harness: ApiTestHarness, token: string, packId: string): Promise { - await createBuilder(harness, token).delete(`/packs/${packId}`).expect(HTTP_STATUS.NO_CONTENT).execute(); -} - -export async function installPack(harness: ApiTestHarness, token: string, packId: string): Promise { - await createBuilder(harness, token) - .post(`/packs/${packId}/install`) - .body({}) - .expect(HTTP_STATUS.NO_CONTENT) - .execute(); -} - -export async function uninstallPack(harness: ApiTestHarness, token: string, packId: string): Promise { - await createBuilder(harness, token).delete(`/packs/${packId}/install`).expect(HTTP_STATUS.NO_CONTENT).execute(); -} - -export async function getPackEmojis( - harness: ApiTestHarness, - token: string, - packId: string, -): Promise> { - return createBuilder>(harness, token) - .get(`/packs/emojis/${packId}`) - .expect(HTTP_STATUS.OK) - .execute(); -} - -export async function getPackStickers( - harness: ApiTestHarness, - token: string, - packId: string, -): Promise> { - return createBuilder>(harness, token) - .get(`/packs/stickers/${packId}`) - .expect(HTTP_STATUS.OK) - .execute(); -} - -export async function createPackEmoji( - harness: ApiTestHarness, - token: string, - packId: string, - name: string, - imageBase64?: string, -): Promise { - const image = imageBase64 ?? loadPackFixture('yeah.png').toString('base64'); - return createBuilder(harness, token) - .post(`/packs/emojis/${packId}`) - .body({name, image: `data:image/png;base64,${image}`}) - .expect(HTTP_STATUS.OK) - .execute(); -} - -export async function deletePackEmoji( - harness: ApiTestHarness, - token: string, - packId: string, - emojiId: string, -): Promise { - await createBuilder(harness, token) - .delete(`/packs/emojis/${packId}/${emojiId}`) - .expect(HTTP_STATUS.NO_CONTENT) - .execute(); -} - -export async function createPackSticker( - harness: ApiTestHarness, - token: string, - packId: string, - name: string, - tags: Array, - imageBase64?: string, -): Promise { - const image = imageBase64 ?? loadPackFixture('sticker.png').toString('base64'); - return createBuilder(harness, token) - .post(`/packs/stickers/${packId}`) - .body({name, tags, image: `data:image/png;base64,${image}`}) - .expect(HTTP_STATUS.OK) - .execute(); -} - -export async function deletePackSticker( - harness: ApiTestHarness, - token: string, - packId: string, - stickerId: string, -): Promise { - await createBuilder(harness, token) - .delete(`/packs/stickers/${packId}/${stickerId}`) - .expect(HTTP_STATUS.NO_CONTENT) - .execute(); -} diff --git a/fluxer_api/src/api/rate_limit_configs/PackRateLimitConfig.ts b/fluxer_api/src/api/rate_limit_configs/PackRateLimitConfig.ts deleted file mode 100644 index 48d650d27..000000000 --- a/fluxer_api/src/api/rate_limit_configs/PackRateLimitConfig.ts +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {ms} from 'itty-time'; -import type {RouteRateLimitConfig} from '../middleware/RateLimitMiddleware'; - -export const PackRateLimitConfigs = { - PACKS_LIST: { - bucket: 'packs:list', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_CREATE: { - bucket: 'packs:create', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_UPDATE: { - bucket: 'packs:update::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_DELETE: { - bucket: 'packs:delete::pack_id', - config: {limit: 10, windowMs: ms('1 minute')}, - } as RouteRateLimitConfig, - PACKS_INSTALL: { - bucket: 'packs:install::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_INVITES_LIST: { - bucket: 'packs:invite:list::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_INVITES_CREATE: { - bucket: 'packs:invite:create::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_EMOJIS_LIST: { - bucket: 'packs:emoji:list::pack_id', - config: {limit: 60, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_EMOJI_CREATE: { - bucket: 'packs:emoji:create::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_EMOJI_BULK_CREATE: { - bucket: 'packs:emoji:bulk_create::pack_id', - config: {limit: 6, windowMs: ms('1 minute')}, - } as RouteRateLimitConfig, - PACKS_EMOJI_UPDATE: { - bucket: 'packs:emoji:update::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_EMOJI_DELETE: { - bucket: 'packs:emoji:delete::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_STICKERS_LIST: { - bucket: 'packs:sticker:list::pack_id', - config: {limit: 60, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_STICKER_CREATE: { - bucket: 'packs:sticker:create::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_STICKER_BULK_CREATE: { - bucket: 'packs:sticker:bulk_create::pack_id', - config: {limit: 6, windowMs: ms('1 minute')}, - } as RouteRateLimitConfig, - PACKS_STICKER_UPDATE: { - bucket: 'packs:sticker:update::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, - PACKS_STICKER_DELETE: { - bucket: 'packs:sticker:delete::pack_id', - config: {limit: 20, windowMs: ms('10 seconds')}, - } as RouteRateLimitConfig, -} as const; diff --git a/fluxer_api/src/api/types/HonoEnv.ts b/fluxer_api/src/api/types/HonoEnv.ts index f4bde28db..988ac2b0f 100644 --- a/fluxer_api/src/api/types/HonoEnv.ts +++ b/fluxer_api/src/api/types/HonoEnv.ts @@ -56,8 +56,6 @@ import type {OAuth2RequestService} from '../oauth/OAuth2RequestService'; import type {OAuth2Service} from '../oauth/OAuth2Service'; import type {IApplicationRepository} from '../oauth/repositories/IApplicationRepository'; import type {IOAuth2TokenRepository} from '../oauth/repositories/IOAuth2TokenRepository'; -import type {PackRepository} from '../pack/PackRepository'; -import type {PackService} from '../pack/PackService'; import type {ReadStateRequestService} from '../read_state/ReadStateRequestService'; import type {ReadStateService} from '../read_state/ReadStateService'; import type {ReportRequestService} from '../report/ReportRequestService'; @@ -142,8 +140,6 @@ export interface HonoEnv { gatewayRequestService: GatewayRequestService; discoveryService: IGuildDiscoveryService; guildService: GuildService; - packService: PackService; - packRepository: PackRepository; inviteService: InviteService; inviteRequestService: InviteRequestService; liveKitWebhookService?: LiveKitWebhookService; diff --git a/fluxer_api/src/api/user/tests/UserCustomStatusEmojiPremium.test.ts b/fluxer_api/src/api/user/tests/UserCustomStatusEmojiPremium.test.ts index a19e76367..19c700438 100644 --- a/fluxer_api/src/api/user/tests/UserCustomStatusEmojiPremium.test.ts +++ b/fluxer_api/src/api/user/tests/UserCustomStatusEmojiPremium.test.ts @@ -87,7 +87,7 @@ describe('User custom status emoji premium', () => { expect(settings.custom_status?.emoji_id).toBeUndefined(); expect(settings.custom_status?.emoji_animated).toBe(false); }); - it('allows custom emoji status for premium users without guild or installed pack access', async () => { + it('allows custom emoji status for premium users without guild access', async () => { const owner = await createTestAccount(harness); const premiumAccount = await createTestAccount(harness); await grantPremium(harness, premiumAccount.userId, UserPremiumTypes.SUBSCRIPTION); diff --git a/fluxer_api/src/api/utils/EmojiUtils.ts b/fluxer_api/src/api/utils/EmojiUtils.ts index 93fef7d5c..bcded86e8 100644 --- a/fluxer_api/src/api/utils/EmojiUtils.ts +++ b/fluxer_api/src/api/utils/EmojiUtils.ts @@ -8,7 +8,6 @@ import type {LimitConfigService} from '../limits/LimitConfigService'; import {resolveLimitSafe} from '../limits/LimitConfigUtils'; import {createLimitMatchContext} from '../limits/LimitMatchContextBuilder'; import type {GuildEmoji} from '../models/GuildEmoji'; -import type {PackExpressionAccessResolution, PackExpressionAccessResolver} from '../pack/PackExpressionAccessResolver'; import type {IUserAccountRepository} from '../user/repositories/IUserAccountRepository'; type EmojiGuildRepository = Pick; @@ -26,7 +25,6 @@ interface SanitizeCustomEmojisParams { guildRepository: EmojiGuildRepository; limitConfigService: LimitConfigService; hasPermission?: (permission: bigint) => Promise; - packResolver?: PackExpressionAccessResolver; } interface EmojiMatch { @@ -43,17 +41,8 @@ interface CodeBlock { } export async function sanitizeCustomEmojis(params: SanitizeCustomEmojisParams): Promise { - const { - content, - userId, - webhookId, - guildId, - userRepository, - guildRepository, - limitConfigService, - hasPermission, - packResolver, - } = params; + const {content, userId, webhookId, guildId, userRepository, guildRepository, limitConfigService, hasPermission} = + params; const escapedContexts = parseEscapedContexts(content); const isInEscapedContext = (index: number): boolean => escapedContexts.some((ctx) => index >= ctx.start && index < ctx.end); @@ -80,7 +69,6 @@ export async function sanitizeCustomEmojis(params: SanitizeCustomEmojisParams): isWebhook, hasGlobalExpressions, canUseExternalEmojis, - packResolver, }); return applyReplacements(content, replacements); } @@ -171,7 +159,6 @@ async function determineReplacements(params: { isWebhook: boolean; hasGlobalExpressions: number; canUseExternalEmojis: boolean | null; - packResolver?: PackExpressionAccessResolver; }): Promise> { const {emojiMatches, emojiLookups, guildId, isWebhook, hasGlobalExpressions, canUseExternalEmojis} = params; const replacements: Array = []; @@ -184,7 +171,6 @@ async function determineReplacements(params: { isWebhook, hasGlobalExpressions, canUseExternalEmojis, - packResolver: params.packResolver, }); if (shouldReplace) { replacements.push({ @@ -203,9 +189,8 @@ async function shouldReplaceEmoji(params: { isWebhook: boolean; hasGlobalExpressions: number; canUseExternalEmojis: boolean | null; - packResolver?: PackExpressionAccessResolver; }): Promise { - const {lookup, guildId, isWebhook, hasGlobalExpressions, canUseExternalEmojis, packResolver} = params; + const {lookup, guildId, isWebhook, hasGlobalExpressions, canUseExternalEmojis} = params; if (!guildId) { if (!lookup.globalEmoji) return true; if (!isWebhook && hasGlobalExpressions === 0) return true; @@ -215,23 +200,11 @@ async function shouldReplaceEmoji(params: { return false; } if (!lookup.globalEmoji) return true; - const packAccess = await resolvePackAccessStatus(lookup.globalEmoji.guildId, packResolver); - if (packAccess === 'not-accessible') { - return true; - } if (!isWebhook && hasGlobalExpressions === 0) return true; if (hasGlobalExpressions > 0 && canUseExternalEmojis === false) return true; return false; } -async function resolvePackAccessStatus( - packId: GuildID, - packResolver?: PackExpressionAccessResolver, -): Promise { - if (!packResolver) return 'not-pack'; - return await packResolver.resolve(packId); -} - function applyReplacements(content: string, replacements: Array): string { if (replacements.length === 0) return content; const sorted = [...replacements].sort((a, b) => b.start - a.start); diff --git a/fluxer_api/src/api/worker/WorkerDependencies.ts b/fluxer_api/src/api/worker/WorkerDependencies.ts index f30ca8caf..f1db6af71 100644 --- a/fluxer_api/src/api/worker/WorkerDependencies.ts +++ b/fluxer_api/src/api/worker/WorkerDependencies.ts @@ -77,7 +77,6 @@ import { getEmailService, getEmbedService, getEntityAssetService, - getExpressionAssetPurger, getFavoriteMemeRepository, getGuildAuditLogService, getGuildRepository, @@ -89,7 +88,6 @@ import { getLimitConfigService, getNcmecSubmissionService, getOAuth2TokenRepository, - getPackRepository, getPremiumStateReconciliationQueueService, getPurgeQueue, getRateLimitService, @@ -215,7 +213,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS await ensureVirusScanInitialized(); const virusScanService = getVirusScanServiceInstance(); const rateLimitService = getRateLimitService(); - const packRepository = getPackRepository(); const emailService = getEmailService(); const workerService = getWorkerService(); const guildAuditLogService = getGuildAuditLogService(); @@ -261,7 +258,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS const apiContext = createApiContext(); const {channelService, guildService, inviteService} = createGuildStackServices({ apiContext, - packRepository, channelRepository, userRepository, guildRepository, @@ -271,7 +267,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS avatarService, entityAssetService, assetDeletionQueue, - expressionAssetPurger: getExpressionAssetPurger(), userCacheService, limitConfigService, embedService, diff --git a/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx b/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx index ecddb752d..3fc50aebb 100644 --- a/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx +++ b/fluxer_app/src/features/app/components/dialogs/components/AllSettingsRenderer.tsx @@ -14,7 +14,6 @@ import {AppearanceInlineContent} from '@app/features/user/components/modals/tabs import ApplicationsTab from '@app/features/user/components/modals/tabs/applications_tab'; import {ChatSettingsInlineContent} from '@app/features/user/components/modals/tabs/chat_settings_tab/ChatSettingsTabInline'; import DesktopSettingsTab from '@app/features/user/components/modals/tabs/DesktopSettingsTab'; -import ExpressionPacksTab from '@app/features/user/components/modals/tabs/ExpressionPacksTab'; import GiftInventoryTab from '@app/features/user/components/modals/tabs/GiftInventoryTab'; import KeybindsTab from '@app/features/user/components/modals/tabs/KeybindsTab'; import LanguageTab from '@app/features/user/components/modals/tabs/LanguageTab'; @@ -65,7 +64,6 @@ const INLINE_TAB_COMPONENTS: Partial { message_character_limit: i18n._(MESSAGE_CHARACTER_LIMIT_DESCRIPTOR), bookmarked_messages: i18n._(BOOKMARKED_MESSAGES_DESCRIPTOR), file_upload_size: i18n._(FILE_UPLOAD_SIZE_DESCRIPTOR), - emoji_sticker_packs: i18n._(EMOJI_STICKER_PACKS_DESCRIPTOR), saved_media: i18n._(SAVED_MEDIA_DESCRIPTOR), use_animated_emojis: i18n._(USE_ANIMATED_EMOJIS_DESCRIPTOR), global_emoji_sticker_access: i18n._(GLOBAL_EMOJI_STICKER_ACCESS_DESCRIPTOR), diff --git a/fluxer_app/src/features/app/constants/Endpoints.ts b/fluxer_app/src/features/app/constants/Endpoints.ts index b48f33076..0e9ddbc42 100644 --- a/fluxer_app/src/features/app/constants/Endpoints.ts +++ b/fluxer_app/src/features/app/constants/Endpoints.ts @@ -222,17 +222,6 @@ export const Endpoints = { USER_PUSH_ROTATE: '/users/@me/push/rotate', USER_PUSH_SUBSCRIPTIONS: '/users/@me/push/subscriptions', USER_PUSH_SUBSCRIPTION: (subscriptionId: string) => `/users/@me/push/subscriptions/${subscriptionId}`, - PACKS: '/packs', - PACK: (packId: string) => `/packs/${packId}`, - PACK_CREATE: (packType: 'emoji' | 'sticker') => `/packs/${packType}`, - PACK_INSTALL: (packId: string) => `/packs/${packId}/install`, - PACK_EMOJIS: (packId: string) => `/packs/emojis/${packId}`, - PACK_EMOJI: (packId: string, emojiId: string) => `/packs/emojis/${packId}/${emojiId}`, - PACK_EMOJI_BULK: (packId: string) => `/packs/emojis/${packId}/bulk`, - PACK_STICKERS: (packId: string) => `/packs/stickers/${packId}`, - PACK_STICKER: (packId: string, stickerId: string) => `/packs/stickers/${packId}/${stickerId}`, - PACK_STICKERS_BULK: (packId: string) => `/packs/stickers/${packId}/bulk`, - PACK_INVITES: (packId: string) => `/packs/${packId}/invites`, WEBHOOK: (webhookId: string) => `/webhooks/${webhookId}`, REPORT_MESSAGE: '/reports/message', REPORT_USER: '/reports/user', diff --git a/fluxer_app/src/features/auth/flow/AuthPageStyles.module.css b/fluxer_app/src/features/auth/flow/AuthPageStyles.module.css index bc5528f35..40d9dff21 100644 --- a/fluxer_app/src/features/auth/flow/AuthPageStyles.module.css +++ b/fluxer_app/src/features/auth/flow/AuthPageStyles.module.css @@ -79,15 +79,6 @@ gap: 0.5rem; } -.packBadge { - background: var(--background-modifier-accent); - border-radius: 62.4375rem; - padding: 0.15rem 0.6rem; - font-size: 0.75rem; - color: var(--text-primary); - font-weight: 600; -} - .entityTitle { font-weight: 700; font-size: 1.25rem; @@ -107,24 +98,6 @@ gap: 1rem; } -.packDescription { - font-size: 0.95rem; - color: var(--text-secondary); - line-height: 1.4; - margin: 0.25rem 0; -} - -.packMeta { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.packMetaText { - font-size: 0.78rem; - color: var(--text-tertiary); -} - .entityStat { display: flex; align-items: center; diff --git a/fluxer_app/src/features/auth/flow/InviteHeader.tsx b/fluxer_app/src/features/auth/flow/InviteHeader.tsx index 6d1d86e28..f15c4f720 100644 --- a/fluxer_app/src/features/auth/flow/InviteHeader.tsx +++ b/fluxer_app/src/features/auth/flow/InviteHeader.tsx @@ -3,39 +3,16 @@ import styles from '@app/features/auth/flow/AuthPageStyles.module.css'; import {GuildBadge} from '@app/features/guild/components/GuildBadge'; import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon'; -import {NO_DESCRIPTION_PROVIDED_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; -import {isGroupDmInvite, isGuildInvite, isPackInvite} from '@app/features/invite/types/InviteTypes'; -import {Avatar} from '@app/features/ui/components/Avatar'; +import {isGroupDmInvite, isGuildInvite} from '@app/features/invite/types/InviteTypes'; import {BaseAvatar} from '@app/features/ui/components/BaseAvatar'; -import {User} from '@app/features/user/models/User'; import * as AvatarUtils from '@app/features/user/utils/AvatarUtils'; import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; import * as NicknameUtils from '@app/features/user/utils/NicknameUtils'; -import type {GroupDmInvite, GuildInvite, Invite, PackInvite} from '@fluxer/schema/src/domains/invite/InviteSchemas'; -import {msg} from '@lingui/core/macro'; -import {Plural, Trans, useLingui} from '@lingui/react/macro'; +import type {GroupDmInvite, GuildInvite, Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas'; +import {Plural, Trans} from '@lingui/react/macro'; import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting'; import {observer} from 'mobx-react-lite'; -import {useEffect, useMemo, useState} from 'react'; - -const EMOJI_PACK_DESCRIPTOR = msg({ - message: 'Emoji pack', - comment: 'Short label in the authentication invite header. Keep the tone plain and specific.', -}); -const STICKER_PACK_DESCRIPTOR = msg({ - message: 'Sticker pack', - comment: 'Short label in the authentication invite header. Keep the tone plain and specific.', -}); -const CREATED_BY_DESCRIPTOR = msg({ - message: 'Created by {userName}', - comment: - 'Short label in the authentication invite header. Preserve {userName}; it is inserted by code. Keep the tone plain and specific.', -}); -const INVITED_BY_DESCRIPTOR = msg({ - message: 'Invited by {inviterTag}', - comment: - 'Short label in the authentication invite header. Preserve {inviterTag}; it is inserted by code. Keep the tone plain and specific.', -}); +import {useEffect, useState} from 'react'; interface InviteHeaderProps { invite: Invite; @@ -49,10 +26,6 @@ interface GroupDMInviteHeaderProps { invite: GroupDmInvite; } -interface PackInviteHeaderProps { - invite: PackInvite; -} - interface PreviewGuildInviteHeaderProps { guildId: string; guildName: string; @@ -175,69 +148,10 @@ export const GroupDMInviteHeader = observer(function GroupDMInviteHeader({invite ); }); -export const PackInviteHeader = observer(function PackInviteHeader({invite}: PackInviteHeaderProps) { - const {i18n} = useLingui(); - const pack = invite.pack; - const creatorRecord = useMemo(() => new User(pack.creator), [pack.creator]); - const creatorDisplayName = NicknameUtils.getDisplayName(creatorRecord); - const packKindLabel = pack.type === 'emoji' ? i18n._(EMOJI_PACK_DESCRIPTOR) : i18n._(STICKER_PACK_DESCRIPTOR); - const inviterTag = invite.inviter ? `${invite.inviter.username}#${invite.inviter.discriminator}` : null; - return ( -
-
- -
-
-

- You've been invited to install -

-
-

- {pack.name} -

- - {packKindLabel} - -
-

- {pack.description || i18n._(NO_DESCRIPTION_PROVIDED_DESCRIPTOR)} -

-
- - {i18n._(CREATED_BY_DESCRIPTOR, {userName: creatorDisplayName})} - - {inviterTag ? ( - - {i18n._(INVITED_BY_DESCRIPTOR, {inviterTag})} - - ) : null} -
-
-
- ); -}); - export function InviteHeader({invite}: InviteHeaderProps) { if (isGroupDmInvite(invite)) { return ; } - if (isPackInvite(invite)) { - return ; - } if (isGuildInvite(invite)) { return ; } diff --git a/fluxer_app/src/features/channel/components/InviteEmbed.module.css b/fluxer_app/src/features/channel/components/InviteEmbed.module.css index 5976c1073..836c80378 100644 --- a/fluxer_app/src/features/channel/components/InviteEmbed.module.css +++ b/fluxer_app/src/features/channel/components/InviteEmbed.module.css @@ -105,45 +105,3 @@ line-height: 1.2; white-space: nowrap; } - -.packTitleRow { - display: flex; - align-items: center; - justify-content: space-between; -} - -.packBadge { - background: var(--background-modifier-accent); - color: var(--text-primary); - font-size: 0.75em; - line-height: 1.25; - padding: 0.15rem 0.5rem; - border-radius: 0.75rem; -} - -.packBody { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.packDescription { - color: var(--text-secondary); - font-size: 0.875em; - line-height: 1.4; - margin: 0; -} - -.packMeta { - display: flex; - flex-direction: column; - gap: 0.25rem; - font-size: 0.78em; - color: var(--text-tertiary); -} - -.packNote { - margin: 0; - font-size: 0.75em; - color: var(--text-tertiary-secondary); -} diff --git a/fluxer_app/src/features/channel/components/InviteEmbed.tsx b/fluxer_app/src/features/channel/components/InviteEmbed.tsx index c52938a39..80facf478 100644 --- a/fluxer_app/src/features/channel/components/InviteEmbed.tsx +++ b/fluxer_app/src/features/channel/components/InviteEmbed.tsx @@ -16,18 +16,11 @@ import {GuildBadge} from '@app/features/guild/components/GuildBadge'; import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon'; import GuildCount from '@app/features/guild/state/GuildCount'; import Guilds from '@app/features/guild/state/Guilds'; -import { - JOIN_COMMUNITY_DESCRIPTOR, - NO_DESCRIPTION_PROVIDED_DESCRIPTOR, -} from '@app/features/i18n/utils/CommonMessageDescriptors'; +import {JOIN_COMMUNITY_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils'; import * as InviteCommands from '@app/features/invite/commands/InviteCommands'; import Invites from '@app/features/invite/state/Invites'; -import { - isGroupDmInvite, - isGuildInvite, - isPackInvite as isPackInviteGuard, -} from '@app/features/invite/types/InviteTypes'; +import {isGroupDmInvite, isGuildInvite} from '@app/features/invite/types/InviteTypes'; import {getGroupDmInviteCounts} from '@app/features/invite/utils/GroupDmInviteCounts'; import { GuildInvitePrimaryAction, @@ -56,11 +49,9 @@ import {Button} from '@app/features/ui/button/Button'; import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands'; import {Avatar} from '@app/features/ui/components/Avatar'; import FocusRing from '@app/features/ui/focus_ring/FocusRing'; -import {User} from '@app/features/user/models/User'; import Users from '@app/features/user/state/Users'; import * as AvatarUtils from '@app/features/user/utils/AvatarUtils'; import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; -import * as NicknameUtils from '@app/features/user/utils/NicknameUtils'; import {msg} from '@lingui/core/macro'; import {Plural, Trans, useLingui} from '@lingui/react/macro'; import {QuestionIcon} from '@phosphor-icons/react'; @@ -81,26 +72,6 @@ const JOIN_GROUP_DESCRIPTOR = msg({ message: 'Join group', comment: 'Button label on a group DM invite embed that accepts the invite.', }); -const EMOJI_PACK_DESCRIPTOR = msg({ - message: 'Emoji pack', - comment: 'Kind label on an emoji pack invite embed.', -}); -const STICKER_PACK_DESCRIPTOR = msg({ - message: 'Sticker pack', - comment: 'Kind label on a sticker pack invite embed.', -}); -const INSTALL_EMOJI_PACK_DESCRIPTOR = msg({ - message: 'Install emoji pack', - comment: 'Button label on an emoji pack invite embed that installs the pack.', -}); -const INSTALL_STICKER_PACK_DESCRIPTOR = msg({ - message: 'Install sticker pack', - comment: 'Button label on a sticker pack invite embed that installs the pack.', -}); -const ACCEPTING_THIS_INVITE_INSTALLS_THE_PACK_AUTOMATICALLY_DESCRIPTOR = msg({ - message: 'Accepting this invite installs the pack automatically.', - comment: 'Helper text on a pack invite embed explaining that accepting installs the pack.', -}); const INVITES_DISABLED_DESCRIPTOR = msg({ message: 'Invites disabled', comment: 'Status label on a community invite embed when invites are paused or anti-raid mode is active.', @@ -185,12 +156,7 @@ const InviteEmbedInner = observer(function InviteEmbedInner({ const inviteState = Invites.invites.get(code) ?? null; const shouldForceSkeleton = useEmbedSkeletonOverride(); const invite = inviteState?.data ?? null; - const isPackInvite = invite != null && isPackInviteGuard(invite); const isGuildInviteType = invite != null && isGuildInvite(invite); - const packCreatorRecord = useMemo(() => { - if (!isPackInvite || !invite) return null; - return new User(invite.pack.creator); - }, [invite, isPackInvite]); const guildFromInvite = isGuildInviteType ? invite!.guild : null; const guild = Guilds.getGuild(guildFromInvite?.id ?? '') || guildFromInvite; const embedSplash = guild != null ? ('embedSplash' in guild ? guild.embedSplash : guild.embed_splash) : undefined; @@ -302,71 +268,6 @@ const InviteEmbedInner = observer(function InviteEmbedInner({ data-flx="channel.invite-embed.embed-card" /> ); - } else if (isPackInviteGuard(invite)) { - const pack = invite.pack; - const packCreator = packCreatorRecord ?? new User(pack.creator); - const packKindLabel = pack.type === 'emoji' ? i18n._(EMOJI_PACK_DESCRIPTOR) : i18n._(STICKER_PACK_DESCRIPTOR); - const packActionLabel = - pack.type === 'emoji' ? i18n._(INSTALL_EMOJI_PACK_DESCRIPTOR) : i18n._(INSTALL_STICKER_PACK_DESCRIPTOR); - const inviterTag = invite.inviter - ? NicknameUtils.formatTagForStreamerMode(`${invite.inviter.username}#${invite.inviter.discriminator}`) - : null; - const handleAcceptInvite = () => InviteCommands.acceptAndTransitionToChannel(invite.code, i18n); - content = ( - } - title={ -
-

- {pack.name} -

- - {packKindLabel} - -
- } - body={ -
-

- {pack.description || i18n._(NO_DESCRIPTION_PROVIDED_DESCRIPTOR)} -

-
- - Created by {NicknameUtils.getDisplayName(packCreator)} - - {inviterTag ? ( - - Invited by {inviterTag} - - ) : null} -
-

- {i18n._(ACCEPTING_THIS_INVITE_INSTALLS_THE_PACK_AUTOMATICALLY_DESCRIPTOR)} -

-
- } - footer={ - - } - data-flx="channel.invite-embed.embed-card--2" - /> - ); } else if (!guild || !isGuildInvite(invite)) { content = ; } else { diff --git a/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/ResetOptions.ts b/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/ResetOptions.ts index aa536459b..dbcca78aa 100644 --- a/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/ResetOptions.ts +++ b/fluxer_app/src/features/channel/components/channel_header_components/developer_tools/ResetOptions.ts @@ -31,7 +31,6 @@ export const DEFAULT_DEVELOPER_OPTIONS = { selfHostedModeOverride: false, forceShowVanityURLDisclaimer: false, forceShowVoiceConnection: false, - showExpressionPacksSettings: false, showProfileTimezoneSettings: false, premiumScenarioOverride: null, premiumTypeOverride: null, diff --git a/fluxer_app/src/features/devtools/components/DeveloperOptionsToggleGroups.ts b/fluxer_app/src/features/devtools/components/DeveloperOptionsToggleGroups.ts index 964b16940..14fa971ff 100644 --- a/fluxer_app/src/features/devtools/components/DeveloperOptionsToggleGroups.ts +++ b/fluxer_app/src/features/devtools/components/DeveloperOptionsToggleGroups.ts @@ -122,15 +122,6 @@ const FORCE_SHOW_VOICE_CONNECTION_DESCRIPTOR = msg({ message: 'Force show voice connection', comment: 'Developer option label for always showing the voice connection status bar.', }); -const SHOW_EXPRESSION_PACKS_SETTINGS_DESCRIPTOR = msg({ - message: 'Show expression packs settings', - comment: 'Developer option label for exposing the staff-only Expression packs page in user settings.', -}); -const SHOW_EXPRESSION_PACKS_SETTINGS_DESC_DESCRIPTOR = msg({ - message: 'Expose the staff-only Expression packs page in user settings.', - comment: - 'Developer / debug surface — keep terse and technical. Tooltip / description for the Expression packs settings toggle.', -}); const SHOW_PROFILE_TIMEZONE_SETTINGS_DESCRIPTOR = msg({ message: 'Show profile timezone settings', comment: 'Developer option label for exposing the staff-only profile timezone section in profile settings.', @@ -254,11 +245,6 @@ export const getToggleGroups = (): Array => [ label: FORCE_SHOW_VOICE_CONNECTION_DESCRIPTOR, description: ALWAYS_DISPLAY_THE_VOICE_CONNECTION_STATUS_BAR_IN_DESCRIPTOR, }, - { - key: 'showExpressionPacksSettings', - label: SHOW_EXPRESSION_PACKS_SETTINGS_DESCRIPTOR, - description: SHOW_EXPRESSION_PACKS_SETTINGS_DESC_DESCRIPTOR, - }, { key: 'showProfileTimezoneSettings', label: SHOW_PROFILE_TIMEZONE_SETTINGS_DESCRIPTOR, diff --git a/fluxer_app/src/features/devtools/state/DeveloperOptions.ts b/fluxer_app/src/features/devtools/state/DeveloperOptions.ts index 9d2a5bd83..25c5a0670 100644 --- a/fluxer_app/src/features/devtools/state/DeveloperOptions.ts +++ b/fluxer_app/src/features/devtools/state/DeveloperOptions.ts @@ -46,7 +46,6 @@ export type DeveloperOptionsState = Readonly<{ selfHostedModeOverride: boolean; forceShowVanityURLDisclaimer: boolean; forceShowVoiceConnection: boolean; - showExpressionPacksSettings: boolean; showProfileTimezoneSettings: boolean; premiumScenarioOverride: PremiumScenarioOverride | null; premiumTypeOverride: number | null; @@ -129,7 +128,6 @@ class DeveloperOptions implements DeveloperOptionsState { selfHostedModeOverride = false; forceShowVanityURLDisclaimer = false; forceShowVoiceConnection = false; - showExpressionPacksSettings = false; showProfileTimezoneSettings = false; premiumScenarioOverride: PremiumScenarioOverride | null = null; premiumTypeOverride: number | null = null; @@ -215,7 +213,6 @@ class DeveloperOptions implements DeveloperOptionsState { 'selfHostedModeOverride', 'forceShowVanityURLDisclaimer', 'forceShowVoiceConnection', - 'showExpressionPacksSettings', 'showProfileTimezoneSettings', 'premiumScenarioOverride', 'premiumTypeOverride', diff --git a/fluxer_app/src/features/expressions/commands/PackCommands.ts b/fluxer_app/src/features/expressions/commands/PackCommands.ts deleted file mode 100644 index d61860120..000000000 --- a/fluxer_app/src/features/expressions/commands/PackCommands.ts +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {Endpoints} from '@app/features/app/constants/Endpoints'; -import {http} from '@app/features/platform/transport/RestTransport'; -import {Logger} from '@app/features/platform/utils/AppLogger'; -import type {PackDashboardResponse, PackSummaryResponse} from '@fluxer/schema/src/domains/pack/PackSchemas'; - -const logger = new Logger('Packs'); - -type PackType = 'emoji' | 'sticker'; - -interface PackCreateRequest { - name: string; - description: string | null; -} - -interface PackUpdateRequest { - name?: string; - description?: string | null; -} - -function packCreateRequest(name: string, description?: string | null): PackCreateRequest { - return {name, description: description ?? null}; -} - -export async function list(): Promise { - try { - logger.debug('Requesting pack dashboard'); - const response = await http.get(Endpoints.PACKS); - return response.body; - } catch (error) { - logger.error('Failed to fetch pack dashboard:', error); - throw error; - } -} - -export async function create(type: PackType, name: string, description?: string | null): Promise { - try { - logger.debug(`Creating ${type} pack ${name}`); - const response = await http.post(Endpoints.PACK_CREATE(type), { - body: packCreateRequest(name, description), - }); - return response.body; - } catch (error) { - logger.error(`Failed to create ${type} pack:`, error); - throw error; - } -} - -export async function update(packId: string, data: PackUpdateRequest): Promise { - try { - logger.debug(`Updating pack ${packId}`); - const response = await http.patch(Endpoints.PACK(packId), {body: data}); - return response.body; - } catch (error) { - logger.error(`Failed to update pack ${packId}:`, error); - throw error; - } -} - -export async function remove(packId: string): Promise { - try { - logger.debug(`Deleting pack ${packId}`); - await http.delete(Endpoints.PACK(packId)); - } catch (error) { - logger.error(`Failed to delete pack ${packId}:`, error); - throw error; - } -} - -export async function install(packId: string): Promise { - try { - logger.debug(`Installing pack ${packId}`); - await http.post(Endpoints.PACK_INSTALL(packId)); - } catch (error) { - logger.error(`Failed to install pack ${packId}:`, error); - throw error; - } -} - -export async function uninstall(packId: string): Promise { - try { - logger.debug(`Uninstalling pack ${packId}`); - await http.delete(Endpoints.PACK_INSTALL(packId)); - } catch (error) { - logger.error(`Failed to uninstall pack ${packId}:`, error); - throw error; - } -} diff --git a/fluxer_app/src/features/expressions/components/modals/CreatePackModal.module.css b/fluxer_app/src/features/expressions/components/modals/CreatePackModal.module.css deleted file mode 100644 index c6f4c9aca..000000000 --- a/fluxer_app/src/features/expressions/components/modals/CreatePackModal.module.css +++ /dev/null @@ -1,16 +0,0 @@ -/* SPDX-License-Identifier: AGPL-3.0-or-later */ - -.description { - margin-bottom: 1rem; - color: var(--text-secondary); -} - -.form { - margin: 0; -} - -.formFields { - display: flex; - flex-direction: column; - gap: 1rem; -} diff --git a/fluxer_app/src/features/expressions/components/modals/CreatePackModal.tsx b/fluxer_app/src/features/expressions/components/modals/CreatePackModal.tsx deleted file mode 100644 index 4749b5e45..000000000 --- a/fluxer_app/src/features/expressions/components/modals/CreatePackModal.tsx +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import * as Modal from '@app/features/app/components/dialogs/Modal'; -import {useFormSubmit} from '@app/features/app/hooks/useFormSubmit'; -import styles from '@app/features/expressions/components/modals/CreatePackModal.module.css'; -import Packs from '@app/features/expressions/state/ExpressionsPacks'; -import {CREATE_DESCRIPTOR, DESCRIPTION_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; -import {Button} from '@app/features/ui/button/Button'; -import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; -import {Form} from '@app/features/ui/components/form/Form'; -import {Input, Textarea} from '@app/features/ui/components/form/FormInput'; -import {msg} from '@lingui/core/macro'; -import {Trans, useLingui} from '@lingui/react/macro'; -import {observer} from 'mobx-react-lite'; -import {useCallback} from 'react'; -import {useForm} from 'react-hook-form'; - -const CREATE_EMOJI_PACK_DESCRIPTOR = msg({ - message: 'Create emoji pack', - comment: 'Action that opens the create-emoji-pack modal.', -}); -const CREATE_STICKER_PACK_DESCRIPTOR = msg({ - message: 'Create sticker pack', - comment: 'Action that opens the create-sticker-pack modal.', -}); -const PACK_NAME_DESCRIPTOR = msg({ - message: 'Pack name', - comment: 'Form field label for the name of an expression pack.', -}); -const PACK_NAME_IS_REQUIRED_DESCRIPTOR = msg({ - message: 'Pack name is required', - comment: 'Form validation error shown when the pack name field is empty.', -}); -const PACK_NAME_MUST_BE_AT_LEAST_2_CHARACTERS_DESCRIPTOR = msg({ - message: 'Pack name must be at least 2 characters', - comment: 'Form validation error for a pack name that is too short.', -}); -const PACK_NAME_MUST_BE_AT_MOST_64_CHARACTERS_DESCRIPTOR = msg({ - message: 'Pack name must be at most 64 characters', - comment: 'Form validation error for a pack name that is too long.', -}); -const MY_SUPER_PACK_DESCRIPTOR = msg({ - message: 'My super pack', - comment: 'Form placeholder example for a pack name input.', -}); -const MAXIMUM_256_CHARACTERS_DESCRIPTOR = msg({ - message: 'Maximum 256 characters', - comment: 'Form helper text describing the maximum length of a description field.', -}); -const DESCRIBE_WHAT_EXPRESSIONS_ARE_INSIDE_THIS_PACK_DESCRIPTOR = msg({ - message: "What's in this pack?", - comment: 'Form helper text for an expression pack description input.', -}); - -interface FormInputs { - name: string; - description: string; -} - -interface CreatePackModalProps { - type: 'emoji' | 'sticker'; - onSuccess?: () => void; -} - -export const CreatePackModal = observer(({type, onSuccess}: CreatePackModalProps) => { - const {i18n} = useLingui(); - const form = useForm({ - defaultValues: { - name: '', - description: '', - }, - }); - const title = type === 'emoji' ? i18n._(CREATE_EMOJI_PACK_DESCRIPTOR) : i18n._(CREATE_STICKER_PACK_DESCRIPTOR); - const submitHandler = useCallback( - async (data: FormInputs) => { - await Packs.createPack(type, data.name.trim(), data.description.trim() || null); - onSuccess?.(); - ModalCommands.pop(); - }, - [type, onSuccess], - ); - const {handleSubmit, isSubmitting} = useFormSubmit({ - form, - onSubmit: submitHandler, - defaultErrorField: 'name', - }); - return ( - ModalCommands.pop()} data-flx="expressions.create-pack-modal.modal-root"> - - -

- {type === 'emoji' ? ( - Start curating a custom emoji pack that you can share and install. - ) : ( - Bundle your favorite stickers into a pack you can distribute. - )} -

-
-
- -