refactor progress
This commit is contained in:
31
packages/api/src/pack/PackExpressionAccessResolver.tsx
Normal file
31
packages/api/src/pack/PackExpressionAccessResolver.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildID, UserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {PackType} from '@fluxer/api/src/pack/PackRepository';
|
||||
|
||||
export type PackExpressionAccessResolution = 'accessible' | 'not-accessible' | 'not-pack';
|
||||
export interface PackExpressionAccessResolver {
|
||||
resolve(packId: GuildID): Promise<PackExpressionAccessResolution>;
|
||||
}
|
||||
|
||||
export interface PackExpressionAccessResolverParams {
|
||||
userId: UserID | null;
|
||||
type: PackType;
|
||||
}
|
||||
37
packages/api/src/pack/PackModel.tsx
Normal file
37
packages/api/src/pack/PackModel.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {ExpressionPack} from '@fluxer/api/src/models/ExpressionPack';
|
||||
import type {PackSummaryResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
|
||||
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;
|
||||
}
|
||||
145
packages/api/src/pack/PackRepository.tsx
Normal file
145
packages/api/src/pack/PackRepository.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildID, UserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {
|
||||
BatchBuilder,
|
||||
buildPatchFromData,
|
||||
executeVersionedUpdate,
|
||||
fetchMany,
|
||||
fetchOne,
|
||||
nextVersion,
|
||||
} from '@fluxer/api/src/database/Cassandra';
|
||||
import type {ExpressionPackRow, PackInstallationRow} from '@fluxer/api/src/database/types/UserTypes';
|
||||
import {EXPRESSION_PACK_COLUMNS} from '@fluxer/api/src/database/types/UserTypes';
|
||||
import {ExpressionPack} from '@fluxer/api/src/models/ExpressionPack';
|
||||
import {ExpressionPacks, ExpressionPacksByCreator, PackInstallations} from '@fluxer/api/src/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<ExpressionPack | null> {
|
||||
const row = await fetchOne<ExpressionPackRow>(FETCH_EXPRESSION_PACK_BY_ID_QUERY.bind({pack_id: packId}));
|
||||
return row ? new ExpressionPack(row) : null;
|
||||
}
|
||||
|
||||
async listPacksByCreator(creatorId: UserID, packType?: PackType): Promise<Array<ExpressionPack>> {
|
||||
const rows = await fetchMany<ExpressionPackRow>(
|
||||
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<number> {
|
||||
const rows = await fetchMany<ExpressionPackRow>(
|
||||
FETCH_EXPRESSION_PACKS_BY_CREATOR_QUERY.bind({creator_id: creatorId}),
|
||||
);
|
||||
return rows.filter((row) => row.pack_type === packType).length;
|
||||
}
|
||||
|
||||
async upsertPack(data: ExpressionPackRow): Promise<ExpressionPack> {
|
||||
const packId = data.pack_id;
|
||||
const previousPack = await this.getPack(packId);
|
||||
const result = await executeVersionedUpdate<ExpressionPackRow, 'pack_id'>(
|
||||
async () => {
|
||||
const existing = await fetchOne<ExpressionPackRow>(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<void> {
|
||||
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<Array<PackInstallationRow>> {
|
||||
return await fetchMany<PackInstallationRow>(FETCH_PACK_INSTALLATIONS_BY_USER_QUERY.bind({user_id: userId}));
|
||||
}
|
||||
|
||||
async addInstallation(data: PackInstallationRow): Promise<void> {
|
||||
await fetchOne(PackInstallations.insert(data));
|
||||
}
|
||||
|
||||
async removeInstallation(userId: UserID, packId: GuildID): Promise<void> {
|
||||
await PackInstallations.deleteByPk({
|
||||
user_id: userId,
|
||||
pack_id: packId,
|
||||
});
|
||||
}
|
||||
|
||||
async hasInstallation(userId: UserID, packId: GuildID): Promise<boolean> {
|
||||
const row = await fetchOne<PackInstallationRow>(
|
||||
FETCH_PACK_INSTALLATION_QUERY.bind({
|
||||
user_id: userId,
|
||||
pack_id: packId,
|
||||
}),
|
||||
);
|
||||
return row !== null;
|
||||
}
|
||||
}
|
||||
91
packages/api/src/pack/PackRequestService.tsx
Normal file
91
packages/api/src/pack/PackRequestService.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {GuildID, UserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import {mapPackToSummary} from '@fluxer/api/src/pack/PackModel';
|
||||
import type {PackService} from '@fluxer/api/src/pack/PackService';
|
||||
import type {
|
||||
PackCreateRequest,
|
||||
PackDashboardResponse,
|
||||
PackSummaryResponse,
|
||||
PackType,
|
||||
PackUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
|
||||
interface PackListParams {
|
||||
userId: UserID;
|
||||
}
|
||||
|
||||
interface PackCreateParams {
|
||||
user: User;
|
||||
type: PackType;
|
||||
data: PackCreateRequest;
|
||||
}
|
||||
|
||||
interface PackUpdateParams {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
data: PackUpdateRequest;
|
||||
}
|
||||
|
||||
interface PackDeleteParams {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
}
|
||||
|
||||
export class PackRequestService {
|
||||
constructor(private readonly packService: PackService) {}
|
||||
|
||||
async listUserPacks(params: PackListParams): Promise<PackDashboardResponse> {
|
||||
return this.packService.listUserPacks(params.userId);
|
||||
}
|
||||
|
||||
async createPack(params: PackCreateParams): Promise<PackSummaryResponse> {
|
||||
const pack = await this.packService.createPack({
|
||||
user: params.user,
|
||||
type: params.type as 'emoji' | 'sticker',
|
||||
name: params.data.name,
|
||||
description: params.data.description ?? null,
|
||||
});
|
||||
return mapPackToSummary(pack);
|
||||
}
|
||||
|
||||
async updatePack(params: PackUpdateParams): Promise<PackSummaryResponse> {
|
||||
const updated = await this.packService.updatePack({
|
||||
userId: params.userId,
|
||||
packId: params.packId,
|
||||
name: params.data.name,
|
||||
description: params.data.description,
|
||||
});
|
||||
return mapPackToSummary(updated);
|
||||
}
|
||||
|
||||
async deletePack(params: PackDeleteParams): Promise<void> {
|
||||
await this.packService.deletePack(params.userId, params.packId);
|
||||
}
|
||||
|
||||
async installPack(params: PackDeleteParams): Promise<void> {
|
||||
await this.packService.installPack(params.userId, params.packId);
|
||||
}
|
||||
|
||||
async uninstallPack(params: PackDeleteParams): Promise<void> {
|
||||
await this.packService.uninstallPack(params.userId, params.packId);
|
||||
}
|
||||
}
|
||||
659
packages/api/src/pack/PackService.tsx
Normal file
659
packages/api/src/pack/PackService.tsx
Normal file
@@ -0,0 +1,659 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {EmojiID, GuildID, StickerID, UserID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {createEmojiID, createGuildID, createStickerID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {
|
||||
mapGuildEmojisWithUsersToResponse,
|
||||
mapGuildEmojiToResponse,
|
||||
mapGuildStickersWithUsersToResponse,
|
||||
mapGuildStickerToResponse,
|
||||
} from '@fluxer/api/src/guild/GuildModel';
|
||||
import type {IGuildRepositoryAggregate} from '@fluxer/api/src/guild/repositories/IGuildRepositoryAggregate';
|
||||
import type {ExpressionAssetPurger} from '@fluxer/api/src/guild/services/content/ExpressionAssetPurger';
|
||||
import type {AvatarService} from '@fluxer/api/src/infrastructure/AvatarService';
|
||||
import {getMetricsService} from '@fluxer/api/src/infrastructure/MetricsService';
|
||||
import type {SnowflakeService} from '@fluxer/api/src/infrastructure/SnowflakeService';
|
||||
import type {UserCacheService} from '@fluxer/api/src/infrastructure/UserCacheService';
|
||||
import type {LimitConfigService} from '@fluxer/api/src/limits/LimitConfigService';
|
||||
import {resolveLimitSafe} from '@fluxer/api/src/limits/LimitConfigUtils';
|
||||
import {createLimitMatchContext} from '@fluxer/api/src/limits/LimitMatchContextBuilder';
|
||||
import type {RequestCache} from '@fluxer/api/src/middleware/RequestCacheMiddleware';
|
||||
import {ExpressionPack} from '@fluxer/api/src/models/ExpressionPack';
|
||||
import type {User} from '@fluxer/api/src/models/User';
|
||||
import type {
|
||||
PackExpressionAccessResolution,
|
||||
PackExpressionAccessResolver,
|
||||
PackExpressionAccessResolverParams,
|
||||
} from '@fluxer/api/src/pack/PackExpressionAccessResolver';
|
||||
import {mapPackToSummary} from '@fluxer/api/src/pack/PackModel';
|
||||
import type {PackRepository, PackType} from '@fluxer/api/src/pack/PackRepository';
|
||||
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
|
||||
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 {ManagedTraits} from '@fluxer/constants/src/ManagedTraits';
|
||||
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 type {
|
||||
GuildEmojiResponse,
|
||||
GuildEmojiWithUserResponse,
|
||||
GuildStickerResponse,
|
||||
GuildStickerWithUserResponse,
|
||||
} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
|
||||
import type {PackDashboardResponse, PackDashboardSectionResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
|
||||
export class PackService {
|
||||
constructor(
|
||||
private readonly packRepository: PackRepository,
|
||||
private readonly guildRepository: IGuildRepositoryAggregate,
|
||||
private readonly avatarService: AvatarService,
|
||||
private readonly snowflakeService: SnowflakeService,
|
||||
private readonly assetPurger: ExpressionAssetPurger,
|
||||
private readonly userRepository: IUserRepository,
|
||||
private readonly userCacheService: UserCacheService,
|
||||
private readonly limitConfigService: LimitConfigService,
|
||||
) {}
|
||||
|
||||
private async requireExpressionPackAccess(userId: UserID): Promise<void> {
|
||||
const user = await this.userRepository.findUnique(userId);
|
||||
if (!user || !user.traits.has(ManagedTraits.EXPRESSION_PACKS)) {
|
||||
throw new FeatureTemporarilyDisabledError();
|
||||
}
|
||||
}
|
||||
|
||||
private async hasFeatureAccess(userId: UserID, limitKey: LimitKey): Promise<boolean> {
|
||||
const user = await this.userRepository.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<ExpressionPack> {
|
||||
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<void> {
|
||||
if (!(await this.hasFeatureAccess(userId, limitKey))) {
|
||||
throw new FeatureAccessError();
|
||||
}
|
||||
}
|
||||
|
||||
private async getInstalledPackIdsByType(userId: UserID, packType: PackType): Promise<Set<GuildID>> {
|
||||
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<PackExpressionAccessResolver> {
|
||||
const {userId, type} = params;
|
||||
|
||||
if (!userId) {
|
||||
return {
|
||||
resolve: async () => 'not-pack',
|
||||
};
|
||||
}
|
||||
|
||||
const installedPackIds = await this.getInstalledPackIdsByType(userId, type);
|
||||
const resolutionCache = new Map<GuildID, PackExpressionAccessResolution>();
|
||||
|
||||
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<PackDashboardResponse> {
|
||||
await this.requireExpressionPackAccess(userId);
|
||||
const user = await this.userRepository.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<ExpressionPack> {
|
||||
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.snowflakeService.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<ExpressionPack> {
|
||||
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<void> {
|
||||
await this.requireExpressionPackAccess(userId);
|
||||
await this.ensurePackOwner(userId, packId);
|
||||
await this.packRepository.deletePack(packId);
|
||||
}
|
||||
|
||||
async installPack(userId: UserID, packId: GuildID): Promise<void> {
|
||||
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.userRepository.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<void> {
|
||||
await this.requireExpressionPackAccess(userId);
|
||||
await this.packRepository.removeInstallation(userId, packId);
|
||||
}
|
||||
|
||||
async getInstalledPackIds(userId: UserID): Promise<Set<GuildID>> {
|
||||
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<Array<GuildEmojiWithUserResponse>> {
|
||||
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<Array<GuildStickerWithUserResponse>> {
|
||||
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<GuildEmojiResponse> {
|
||||
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.snowflakeService.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,
|
||||
version: 1,
|
||||
});
|
||||
|
||||
return mapGuildEmojiToResponse(emoji);
|
||||
}
|
||||
|
||||
async bulkCreatePackEmojis(params: {
|
||||
user: User;
|
||||
packId: GuildID;
|
||||
emojis: Array<{name: string; image: string}>;
|
||||
}): Promise<{success: Array<GuildEmojiResponse>; 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<GuildEmojiResponse> = [];
|
||||
const failed: Array<{name: string; error: string}> = [];
|
||||
|
||||
for (const emojiData of params.emojis) {
|
||||
if (emojiCount >= expressionLimit) {
|
||||
failed.push({name: emojiData.name, error: 'Pack expression limit reached'});
|
||||
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.snowflakeService.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,
|
||||
version: 1,
|
||||
});
|
||||
|
||||
success.push(mapGuildEmojiToResponse(emoji));
|
||||
emojiCount += 1;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
failed.push({name: emojiData.name, error: message});
|
||||
}
|
||||
}
|
||||
|
||||
return {success, failed};
|
||||
}
|
||||
|
||||
async updatePackEmoji(params: {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
emojiId: EmojiID;
|
||||
name: string;
|
||||
}): Promise<GuildEmojiResponse> {
|
||||
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<void> {
|
||||
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<string>;
|
||||
image: string;
|
||||
}): Promise<GuildStickerResponse> {
|
||||
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.snowflakeService.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,
|
||||
creator_id: params.user.id,
|
||||
version: 1,
|
||||
});
|
||||
|
||||
const response = mapGuildStickerToResponse(sticker);
|
||||
getMetricsService().counter({name: 'fluxer.stickers.created', value: 1});
|
||||
return response;
|
||||
}
|
||||
|
||||
async bulkCreatePackStickers(params: {
|
||||
user: User;
|
||||
packId: GuildID;
|
||||
stickers: Array<{name: string; description?: string | null; tags: Array<string>; image: string}>;
|
||||
}): Promise<{success: Array<GuildStickerResponse>; 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<GuildStickerResponse> = [];
|
||||
const failed: Array<{name: string; error: string}> = [];
|
||||
|
||||
for (const stickerData of params.stickers) {
|
||||
if (stickerCount >= expressionLimit) {
|
||||
failed.push({name: stickerData.name, error: 'Pack expression limit reached'});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const {animated, imageBuffer} = await this.avatarService.processSticker({
|
||||
errorPath: `stickers[${success.length + failed.length}].image`,
|
||||
base64Image: stickerData.image,
|
||||
});
|
||||
|
||||
const stickerId = createStickerID(await this.snowflakeService.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,
|
||||
creator_id: params.user.id,
|
||||
version: 1,
|
||||
});
|
||||
|
||||
success.push(mapGuildStickerToResponse(sticker));
|
||||
stickerCount += 1;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
failed.push({name: stickerData.name, error: message});
|
||||
}
|
||||
}
|
||||
|
||||
for (const _ of success) {
|
||||
getMetricsService().counter({name: 'fluxer.stickers.created', value: 1});
|
||||
}
|
||||
|
||||
return {success, failed};
|
||||
}
|
||||
|
||||
async updatePackSticker(params: {
|
||||
userId: UserID;
|
||||
packId: GuildID;
|
||||
stickerId: StickerID;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
tags: Array<string>;
|
||||
}): Promise<GuildStickerResponse> {
|
||||
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<void> {
|
||||
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());
|
||||
}
|
||||
|
||||
getMetricsService().counter({name: 'fluxer.stickers.deleted', value: 1});
|
||||
}
|
||||
|
||||
private resolveLimitForUser(user: User | null, key: LimitKey, fallback: number): number {
|
||||
const ctx = createLimitMatchContext({user});
|
||||
return resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, key, fallback);
|
||||
}
|
||||
}
|
||||
188
packages/api/src/pack/controllers/PackController.tsx
Normal file
188
packages/api/src/pack/controllers/PackController.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createGuildID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {DefaultUserOnly, LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {Validator} from '@fluxer/api/src/Validator';
|
||||
import {PackIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {
|
||||
PackCreateRequest,
|
||||
PackDashboardResponse,
|
||||
PackSummaryResponse,
|
||||
PackTypeParam,
|
||||
PackUpdateRequest,
|
||||
} from '@fluxer/schema/src/domains/pack/PackSchemas';
|
||||
|
||||
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: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const response = await ctx.get('packRequestService').listUserPacks({
|
||||
userId: 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: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const response = await ctx.get('packRequestService').createPack({
|
||||
user: ctx.get('user'),
|
||||
type: ctx.req.valid('param').pack_type,
|
||||
data: ctx.req.valid('json'),
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
|
||||
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: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
const response = await ctx.get('packRequestService').updatePack({
|
||||
userId: ctx.get('user').id,
|
||||
packId: createGuildID(ctx.req.valid('param').pack_id),
|
||||
data: ctx.req.valid('json'),
|
||||
});
|
||||
return ctx.json(response);
|
||||
},
|
||||
);
|
||||
|
||||
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: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
await ctx.get('packRequestService').deletePack({
|
||||
userId: ctx.get('user').id,
|
||||
packId: 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: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
await ctx.get('packRequestService').installPack({
|
||||
userId: ctx.get('user').id,
|
||||
packId: 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: ['botToken', 'bearerToken', 'sessionToken'],
|
||||
tags: ['Packs'],
|
||||
}),
|
||||
async (ctx) => {
|
||||
await ctx.get('packRequestService').uninstallPack({
|
||||
userId: ctx.get('user').id,
|
||||
packId: createGuildID(ctx.req.valid('param').pack_id),
|
||||
});
|
||||
return ctx.body(null, 204);
|
||||
},
|
||||
);
|
||||
}
|
||||
169
packages/api/src/pack/controllers/PackEmojiController.tsx
Normal file
169
packages/api/src/pack/controllers/PackEmojiController.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createEmojiID, createGuildID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {Validator} from '@fluxer/api/src/Validator';
|
||||
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';
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
176
packages/api/src/pack/controllers/PackStickerController.tsx
Normal file
176
packages/api/src/pack/controllers/PackStickerController.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createGuildID, createStickerID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
|
||||
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
|
||||
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
|
||||
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
import {Validator} from '@fluxer/api/src/Validator';
|
||||
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';
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
29
packages/api/src/pack/controllers/index.tsx
Normal file
29
packages/api/src/pack/controllers/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {PackController} from '@fluxer/api/src/pack/controllers/PackController';
|
||||
import {PackEmojiController} from '@fluxer/api/src/pack/controllers/PackEmojiController';
|
||||
import {PackStickerController} from '@fluxer/api/src/pack/controllers/PackStickerController';
|
||||
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
|
||||
|
||||
export function registerPackControllers(app: HonoApp) {
|
||||
PackController(app);
|
||||
PackEmojiController(app);
|
||||
PackStickerController(app);
|
||||
}
|
||||
279
packages/api/src/pack/tests/PackInviteFlow.test.tsx
Normal file
279
packages/api/src/pack/tests/PackInviteFlow.test.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
createPack,
|
||||
createPackEmoji,
|
||||
createPackSticker,
|
||||
deletePack,
|
||||
deletePackEmoji,
|
||||
deletePackSticker,
|
||||
getPackEmojis,
|
||||
getPackStickers,
|
||||
installPack,
|
||||
listPacks,
|
||||
setupPackTestAccount,
|
||||
uninstallPack,
|
||||
updatePack,
|
||||
} from '@fluxer/api/src/pack/tests/PackTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
166
packages/api/src/pack/tests/PackPremiumRequirements.test.tsx
Normal file
166
packages/api/src/pack/tests/PackPremiumRequirements.test.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createGuild,
|
||||
createPack,
|
||||
enableExpressionPacksForGuild,
|
||||
grantPremium,
|
||||
installPack,
|
||||
listPacks,
|
||||
revokePremium,
|
||||
setupNonPremiumPackTestAccount,
|
||||
setupPackTestAccount,
|
||||
} from '@fluxer/api/src/pack/tests/PackTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
describe('Pack Premium Requirements', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
test('user without expression_packs trait cannot list packs', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, account.token).get('/packs').expect(HTTP_STATUS.FORBIDDEN).execute();
|
||||
});
|
||||
|
||||
test('user with trait 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 trait 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 trait 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 trait 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 trait when joining guild with expression_packs feature', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Feature Guild');
|
||||
await enableExpressionPacksForGuild(harness, guild.id);
|
||||
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 trait', 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);
|
||||
});
|
||||
});
|
||||
229
packages/api/src/pack/tests/PackTestUtils.tsx
Normal file
229
packages/api/src/pack/tests/PackTestUtils.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {readFileSync} from 'node:fs';
|
||||
import {join} from 'node:path';
|
||||
import {createTestAccount, type TestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {ManagedTraits} from '@fluxer/constants/src/ManagedTraits';
|
||||
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';
|
||||
|
||||
export interface PackCreateRequest {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface PackUpdateRequest {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export type PackType = 'emoji' | 'sticker';
|
||||
|
||||
export function loadPackFixture(filename: string): Buffer {
|
||||
const fixturesPath = join(import.meta.dirname, '..', '..', 'test', 'fixtures', filename);
|
||||
return readFileSync(fixturesPath);
|
||||
}
|
||||
|
||||
export async function createGuild(harness: ApiTestHarness, token: string, name: string): Promise<GuildResponse> {
|
||||
return createBuilder<GuildResponse>(harness, token).post('/guilds').body({name}).expect(HTTP_STATUS.OK).execute();
|
||||
}
|
||||
|
||||
export async function enableExpressionPacksForGuild(harness: ApiTestHarness, guildId: string): Promise<void> {
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post(`/test/guilds/${guildId}/features`)
|
||||
.body({
|
||||
add_features: [ManagedTraits.EXPRESSION_PACKS],
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function grantPremium(harness: ApiTestHarness, userId: string): Promise<void> {
|
||||
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<void> {
|
||||
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 enableExpressionPacksForGuild(harness, guild.id);
|
||||
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 enableExpressionPacksForGuild(harness, guild.id);
|
||||
return {account, guild};
|
||||
}
|
||||
|
||||
export async function listPacks(harness: ApiTestHarness, token: string): Promise<PackDashboardResponse> {
|
||||
return createBuilder<PackDashboardResponse>(harness, token).get('/packs').expect(HTTP_STATUS.OK).execute();
|
||||
}
|
||||
|
||||
export async function createPack(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packType: PackType,
|
||||
data: PackCreateRequest,
|
||||
): Promise<PackSummaryResponse> {
|
||||
return createBuilder<PackSummaryResponse>(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<PackSummaryResponse> {
|
||||
return createBuilder<PackSummaryResponse>(harness, token)
|
||||
.patch(`/packs/${packId}`)
|
||||
.body(data)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function deletePack(harness: ApiTestHarness, token: string, packId: string): Promise<void> {
|
||||
await createBuilder<void>(harness, token).delete(`/packs/${packId}`).expect(HTTP_STATUS.NO_CONTENT).execute();
|
||||
}
|
||||
|
||||
export async function installPack(harness: ApiTestHarness, token: string, packId: string): Promise<void> {
|
||||
await createBuilder<void>(harness, token)
|
||||
.post(`/packs/${packId}/install`)
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function uninstallPack(harness: ApiTestHarness, token: string, packId: string): Promise<void> {
|
||||
await createBuilder<void>(harness, token).delete(`/packs/${packId}/install`).expect(HTTP_STATUS.NO_CONTENT).execute();
|
||||
}
|
||||
|
||||
export async function getPackEmojis(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packId: string,
|
||||
): Promise<Array<GuildEmojiWithUserResponse>> {
|
||||
return createBuilder<Array<GuildEmojiWithUserResponse>>(harness, token)
|
||||
.get(`/packs/emojis/${packId}`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function getPackStickers(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
packId: string,
|
||||
): Promise<Array<GuildStickerWithUserResponse>> {
|
||||
return createBuilder<Array<GuildStickerWithUserResponse>>(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<GuildEmojiResponse> {
|
||||
const image = imageBase64 ?? loadPackFixture('yeah.png').toString('base64');
|
||||
return createBuilder<GuildEmojiResponse>(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<void> {
|
||||
await createBuilder<void>(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<string>,
|
||||
imageBase64?: string,
|
||||
): Promise<GuildStickerResponse> {
|
||||
const image = imageBase64 ?? loadPackFixture('sticker.png').toString('base64');
|
||||
return createBuilder<GuildStickerResponse>(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<void> {
|
||||
await createBuilder<void>(harness, token)
|
||||
.delete(`/packs/stickers/${packId}/${stickerId}`)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
}
|
||||
Reference in New Issue
Block a user