153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
import { TRPCError } from '@trpc/server';
|
|
import { isCanonicalIsoTimestamp, type AccountIconProjection } from '@sammo-ts/common';
|
|
|
|
import type { GameApiContext } from '../context.js';
|
|
import { ConflictingTurnDaemonCommandError } from '../daemon/databaseTransport.js';
|
|
import type { AccountIconSource } from '../auth/accountIconSource.js';
|
|
import type { GatewayUserFlushEvent } from '../auth/flushStore.js';
|
|
import type { TurnDaemonTransport } from '../daemon/transport.js';
|
|
|
|
export const loadAuthoritativeAccountIcon = async (
|
|
ctx: GameApiContext,
|
|
userId: string
|
|
): Promise<AccountIconProjection> => {
|
|
if (!ctx.accountIconSource) {
|
|
throw new TRPCError({
|
|
code: 'PRECONDITION_FAILED',
|
|
message: 'Gateway 계정 아이콘 원장이 구성되지 않았습니다.',
|
|
});
|
|
}
|
|
try {
|
|
const projection = await ctx.accountIconSource.get(userId);
|
|
if (!projection) {
|
|
throw new TRPCError({
|
|
code: 'PRECONDITION_FAILED',
|
|
message: 'Gateway에서 계정 정보를 찾을 수 없습니다.',
|
|
});
|
|
}
|
|
return projection;
|
|
} catch (error) {
|
|
if (error instanceof TRPCError) {
|
|
throw error;
|
|
}
|
|
throw new TRPCError({
|
|
code: 'PRECONDITION_FAILED',
|
|
message: 'Gateway 계정 아이콘 정보를 확인할 수 없습니다.',
|
|
});
|
|
}
|
|
};
|
|
|
|
export const adjustAccountIconForUser = async (
|
|
ctx: GameApiContext,
|
|
userId: string,
|
|
selected?: AccountIconProjection,
|
|
enforceCooldown = true,
|
|
requestKey?: string
|
|
): Promise<{
|
|
ok: true;
|
|
generalId: number | null;
|
|
updated: boolean;
|
|
}> => {
|
|
const projection = selected ?? (await loadAuthoritativeAccountIcon(ctx, userId));
|
|
const requestId = selected
|
|
? `general:adjustIcon:${userId}:manual:${requestKey ?? `${projection.revision}:${encodeURIComponent(projection.picture)}`}`
|
|
: `general:adjustIcon:${userId}:${projection.revision}`;
|
|
try {
|
|
const result = await ctx.turnDaemon.requestCommand({
|
|
type: 'adjustGeneralIcon',
|
|
requestId,
|
|
userId,
|
|
picture: projection.picture,
|
|
imageServer: projection.imageServer,
|
|
iconRevision: projection.revision,
|
|
enforceCooldown,
|
|
});
|
|
if (!result) {
|
|
throw new TRPCError({
|
|
code: 'TIMEOUT',
|
|
message: '요청은 접수됐지만 처리 결과를 아직 확인하지 못했습니다. 같은 요청으로 다시 시도해 주세요.',
|
|
});
|
|
}
|
|
if (result.type !== 'adjustGeneralIcon') {
|
|
throw new TRPCError({
|
|
code: 'INTERNAL_SERVER_ERROR',
|
|
message: '턴 데몬이 올바르지 않은 아이콘 적용 결과를 반환했습니다.',
|
|
});
|
|
}
|
|
if (!result.ok) {
|
|
throw new TRPCError({
|
|
code: result.code,
|
|
message: result.reason,
|
|
});
|
|
}
|
|
return {
|
|
ok: true,
|
|
generalId: result.generalId,
|
|
updated: result.updated,
|
|
};
|
|
} catch (error) {
|
|
if (
|
|
error instanceof ConflictingTurnDaemonCommandError ||
|
|
(error instanceof Error && error.name === 'ConflictingTurnDaemonCommandError')
|
|
) {
|
|
throw new TRPCError({
|
|
code: 'CONFLICT',
|
|
message: '이미 접수된 아이콘 적용 요청과 최신 계정 정보가 다릅니다.',
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const enqueueProfileIconResetForUser = async (
|
|
ctx: GameApiContext,
|
|
userId: string,
|
|
expectedResetRevision: string
|
|
): Promise<boolean> => {
|
|
const projection = await loadAuthoritativeAccountIcon(ctx, userId);
|
|
if (
|
|
projection.revision !== expectedResetRevision ||
|
|
projection.picture !== 'default.jpg' ||
|
|
projection.imageServer !== 0
|
|
) {
|
|
return false;
|
|
}
|
|
await ctx.turnDaemon.sendCommand({
|
|
type: 'adjustGeneralIcon',
|
|
requestId: `general:adjustIcon:${userId}:${projection.revision}`,
|
|
userId,
|
|
picture: projection.picture,
|
|
imageServer: projection.imageServer,
|
|
iconRevision: projection.revision,
|
|
});
|
|
return true;
|
|
};
|
|
|
|
export const createAdminProfileIconResetFlushHandler =
|
|
(source: AccountIconSource, turnDaemon: TurnDaemonTransport) =>
|
|
async (event: GatewayUserFlushEvent): Promise<void> => {
|
|
if (event.reason !== 'admin-profile-icon-reset') {
|
|
return;
|
|
}
|
|
if (!event.iconRevision || !isCanonicalIsoTimestamp(event.iconRevision)) {
|
|
return;
|
|
}
|
|
const projection = await source.get(event.userId);
|
|
if (
|
|
!projection ||
|
|
projection.revision !== event.iconRevision ||
|
|
projection.picture !== 'default.jpg' ||
|
|
projection.imageServer !== 0
|
|
) {
|
|
return;
|
|
}
|
|
await turnDaemon.sendCommand({
|
|
type: 'adjustGeneralIcon',
|
|
requestId: `general:adjustIcon:${event.userId}:${projection.revision}`,
|
|
userId: event.userId,
|
|
picture: projection.picture,
|
|
imageServer: projection.imageServer,
|
|
iconRevision: projection.revision,
|
|
});
|
|
};
|