chore: cleanup guild rpc
This commit is contained in:
parent
528e4e0d7f
commit
cf06cadcfc
@ -40,6 +40,10 @@ const FETCH_GUILD_MEMBERS_BY_GUILD_ID_QUERY = GuildMembers.selectCql({
|
||||
where: GuildMembers.where.eq('guild_id'),
|
||||
});
|
||||
|
||||
const COUNT_GUILD_MEMBERS_BY_GUILD_ID_QUERY = GuildMembers.selectCountCql({
|
||||
where: GuildMembers.where.eq('guild_id'),
|
||||
});
|
||||
|
||||
function createPaginatedFirstPageQuery(limit: number) {
|
||||
return GuildMembers.selectCql({
|
||||
where: GuildMembers.where.eq('guild_id'),
|
||||
@ -70,6 +74,13 @@ export class GuildMemberRepository extends IGuildMemberRepository {
|
||||
return members.map((member) => new GuildMember(member));
|
||||
}
|
||||
|
||||
async countMembers(guildId: GuildID): Promise<number> {
|
||||
const result = await fetchOne<{count: bigint}>(COUNT_GUILD_MEMBERS_BY_GUILD_ID_QUERY, {
|
||||
guild_id: guildId,
|
||||
});
|
||||
return result ? Number(result.count) : 0;
|
||||
}
|
||||
|
||||
async upsertMember(data: GuildMemberRow, oldData?: GuildMemberRow | null): Promise<GuildMember> {
|
||||
const guildId = data.guild_id;
|
||||
const userId = data.user_id;
|
||||
|
||||
@ -126,6 +126,10 @@ export class GuildRepository implements IGuildRepositoryAggregate {
|
||||
return await this.memberRepo.listMembers(guildId);
|
||||
}
|
||||
|
||||
async countMembers(guildId: GuildID): Promise<number> {
|
||||
return await this.memberRepo.countMembers(guildId);
|
||||
}
|
||||
|
||||
async upsertMember(data: GuildMemberRow): Promise<GuildMember> {
|
||||
return await this.memberRepo.upsertMember(data);
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ import type {GuildMember} from '@fluxer/api/src/models/GuildMember';
|
||||
export abstract class IGuildMemberRepository {
|
||||
abstract getMember(guildId: GuildID, userId: UserID): Promise<GuildMember | null>;
|
||||
abstract listMembers(guildId: GuildID): Promise<Array<GuildMember>>;
|
||||
abstract countMembers(guildId: GuildID): Promise<number>;
|
||||
abstract upsertMember(data: GuildMemberRow): Promise<GuildMember>;
|
||||
abstract deleteMember(guildId: GuildID, userId: UserID): Promise<void>;
|
||||
abstract listMembersPaginated(guildId: GuildID, limit: number, afterUserId?: UserID): Promise<Array<GuildMember>>;
|
||||
|
||||
@ -59,9 +59,7 @@ import type {AuthSession} from '@fluxer/api/src/models/AuthSession';
|
||||
import type {Channel} from '@fluxer/api/src/models/Channel';
|
||||
import type {FavoriteMeme} from '@fluxer/api/src/models/FavoriteMeme';
|
||||
import type {Guild} from '@fluxer/api/src/models/Guild';
|
||||
import type {GuildEmoji} from '@fluxer/api/src/models/GuildEmoji';
|
||||
import type {GuildMember} from '@fluxer/api/src/models/GuildMember';
|
||||
import type {GuildRole} from '@fluxer/api/src/models/GuildRole';
|
||||
import type {GuildSticker} from '@fluxer/api/src/models/GuildSticker';
|
||||
import type {ReadState} from '@fluxer/api/src/models/ReadState';
|
||||
import type {Relationship} from '@fluxer/api/src/models/Relationship';
|
||||
@ -110,7 +108,6 @@ import type {
|
||||
RpcRequest,
|
||||
RpcResponse,
|
||||
RpcResponseGuildCollectionData,
|
||||
RpcResponseGuildData,
|
||||
RpcResponseSessionData,
|
||||
} from '@fluxer/schema/src/domains/rpc/RpcSchemas';
|
||||
import type {RelationshipResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
@ -132,11 +129,6 @@ interface HandleSessionRequestParams {
|
||||
longitude?: string;
|
||||
}
|
||||
|
||||
interface HandleGuildRequestParams {
|
||||
guildId: GuildID;
|
||||
requestCache: RequestCache;
|
||||
}
|
||||
|
||||
interface HandleGuildCollectionRequestParams {
|
||||
guildId: GuildID;
|
||||
collection: RpcGuildCollectionType;
|
||||
@ -145,24 +137,11 @@ interface HandleGuildCollectionRequestParams {
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
interface GetGuildDataParams {
|
||||
guildId: GuildID;
|
||||
}
|
||||
|
||||
interface GetUserDataParams {
|
||||
userId: UserID;
|
||||
includePrivateChannels?: boolean;
|
||||
}
|
||||
|
||||
interface GuildData {
|
||||
guild: Guild;
|
||||
channels: Array<Channel>;
|
||||
emojis: Array<GuildEmoji>;
|
||||
stickers: Array<GuildSticker>;
|
||||
members: Array<GuildMember>;
|
||||
roles: Array<GuildRole>;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
user: User;
|
||||
settings: UserSettings | null;
|
||||
@ -419,14 +398,6 @@ export class RpcService {
|
||||
data: {success: true},
|
||||
};
|
||||
}
|
||||
case 'guild':
|
||||
return {
|
||||
type: 'guild',
|
||||
data: await this.handleGuildRequest({
|
||||
guildId: createGuildID(request.guild_id),
|
||||
requestCache,
|
||||
}),
|
||||
};
|
||||
case 'guild_collection':
|
||||
return {
|
||||
type: 'guild_collection',
|
||||
@ -1207,37 +1178,6 @@ export class RpcService {
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGuildRequest({guildId, requestCache}: HandleGuildRequestParams): Promise<RpcResponseGuildData> {
|
||||
const guildData = await this.getGuildData({guildId});
|
||||
|
||||
if (!guildData) {
|
||||
throw new UnknownGuildError();
|
||||
}
|
||||
|
||||
const [channels, members] = await Promise.all([
|
||||
Promise.all(
|
||||
guildData.channels.map((channel) =>
|
||||
mapChannelToResponse({
|
||||
channel,
|
||||
currentUserId: null,
|
||||
userCacheService: this.userCacheService,
|
||||
requestCache,
|
||||
}),
|
||||
),
|
||||
),
|
||||
this.mapRpcGuildMembers({guildId, members: guildData.members, requestCache}),
|
||||
]);
|
||||
|
||||
return {
|
||||
guild: mapGuildToGuildResponse(guildData.guild),
|
||||
roles: guildData.roles.map(mapGuildRoleToResponse),
|
||||
channels,
|
||||
emojis: guildData.emojis.map(mapGuildEmojiToResponse),
|
||||
stickers: guildData.stickers.map(mapGuildStickerToResponse),
|
||||
members,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleGuildCollectionRequest({
|
||||
guildId,
|
||||
collection,
|
||||
@ -1300,7 +1240,9 @@ export class RpcService {
|
||||
guildId: GuildID;
|
||||
}): Promise<RpcResponseGuildCollectionData> {
|
||||
const guild = await this.getGuildOrThrow(guildId);
|
||||
const repairedBannerGuild = await this.repairGuildBannerHeight(guild);
|
||||
const memberCount = await this.guildRepository.countMembers(guildId);
|
||||
const repairedMemberCountGuild = await this.updateGuildMemberCount(guild, memberCount);
|
||||
const repairedBannerGuild = await this.repairGuildBannerHeight(repairedMemberCountGuild);
|
||||
const repairedSplashGuild = await this.repairGuildSplashDimensions(repairedBannerGuild);
|
||||
const repairedEmbedSplashGuild = await this.repairGuildEmbedSplashDimensions(repairedSplashGuild);
|
||||
return {
|
||||
@ -1427,39 +1369,6 @@ export class RpcService {
|
||||
});
|
||||
}
|
||||
|
||||
private async getGuildData({guildId}: GetGuildDataParams): Promise<GuildData | null> {
|
||||
const [guildResult, channels, emojis, stickers, members, roles] = await Promise.all([
|
||||
this.guildRepository.findUnique(guildId),
|
||||
this.channelRepository.listGuildChannels(guildId),
|
||||
this.guildRepository.listEmojis(guildId),
|
||||
this.guildRepository.listStickers(guildId),
|
||||
this.guildRepository.listMembers(guildId),
|
||||
this.guildRepository.listRoles(guildId),
|
||||
]);
|
||||
if (!guildResult) return null;
|
||||
|
||||
const migratedStickers = await this.migrateGuildStickersForRpc(guildId, stickers);
|
||||
|
||||
const repairedChannelRefsGuild = await this.repairDanglingChannelReferences({guild: guildResult, channels});
|
||||
const repairedBannerGuild = await this.repairGuildBannerHeight(repairedChannelRefsGuild);
|
||||
const repairedSplashGuild = await this.repairGuildSplashDimensions(repairedBannerGuild);
|
||||
const repairedEmbedSplashGuild = await this.repairGuildEmbedSplashDimensions(repairedSplashGuild);
|
||||
const updatedGuild = await this.updateGuildMemberCount(repairedEmbedSplashGuild, members.length);
|
||||
|
||||
this.repairOrphanedInvitesAndWebhooks({guild: updatedGuild, channels}).catch((error) => {
|
||||
Logger.warn({guildId: guildId.toString(), error}, 'Failed to repair orphaned invites/webhooks');
|
||||
});
|
||||
|
||||
return {
|
||||
guild: updatedGuild,
|
||||
channels,
|
||||
emojis,
|
||||
stickers: migratedStickers,
|
||||
members,
|
||||
roles,
|
||||
};
|
||||
}
|
||||
|
||||
private async getUserData({userId, includePrivateChannels = true}: GetUserDataParams): Promise<UserData | null> {
|
||||
const user = await this.userRepository.findUnique(userId);
|
||||
if (!user) return null;
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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 {createGuildID} from '@fluxer/api/src/BrandedTypes';
|
||||
import {GuildRepository} from '@fluxer/api/src/guild/repositories/GuildRepository';
|
||||
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
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';
|
||||
|
||||
interface RpcGuildCollectionResponse {
|
||||
type: 'guild_collection';
|
||||
data: {
|
||||
collection: 'guild';
|
||||
guild: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
async function setGuildMemberCount(harness: ApiTestHarness, guildId: string, memberCount: number): Promise<void> {
|
||||
await createBuilder(harness, '')
|
||||
.post(`/test/guilds/${guildId}/member-count`)
|
||||
.body({member_count: memberCount})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
describe('RpcService guild member count repair', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
test('repairs guild member_count from guild_members count when fetching guild data', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'RPC Guild Member Count Repair');
|
||||
const guildId = createGuildID(BigInt(guild.id));
|
||||
const guildRepository = new GuildRepository();
|
||||
|
||||
await setGuildMemberCount(harness, guild.id, 999);
|
||||
|
||||
const staleGuild = await guildRepository.findUnique(guildId);
|
||||
expect(staleGuild).toBeTruthy();
|
||||
if (!staleGuild) {
|
||||
throw new Error('Expected guild to exist before RPC member_count repair');
|
||||
}
|
||||
expect(staleGuild.memberCount).toBe(999);
|
||||
|
||||
const rpcResponse = await createBuilder<RpcGuildCollectionResponse>(harness, '')
|
||||
.post('/test/rpc-session-init')
|
||||
.body({
|
||||
type: 'guild_collection',
|
||||
guild_id: guild.id,
|
||||
collection: 'guild',
|
||||
})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
expect(rpcResponse.type).toBe('guild_collection');
|
||||
expect(rpcResponse.data.collection).toBe('guild');
|
||||
expect(rpcResponse.data.guild.id).toBe(guild.id);
|
||||
|
||||
const repairedGuild = await guildRepository.findUnique(guildId);
|
||||
expect(repairedGuild).toBeTruthy();
|
||||
if (!repairedGuild) {
|
||||
throw new Error('Expected guild to exist after RPC member_count repair');
|
||||
}
|
||||
|
||||
const actualMemberCount = await guildRepository.countMembers(guildId);
|
||||
expect(actualMemberCount).toBe(1);
|
||||
expect(repairedGuild.memberCount).toBe(actualMemberCount);
|
||||
});
|
||||
});
|
||||
@ -102,6 +102,7 @@ import {UnknownHarvestError} from '@fluxer/errors/src/domains/moderation/Unknown
|
||||
import {InvalidBotFlagError} from '@fluxer/errors/src/domains/oauth/InvalidBotFlagError';
|
||||
import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';
|
||||
import {UnknownUserFlagError} from '@fluxer/errors/src/domains/user/UnknownUserFlagError';
|
||||
import {RpcRequest} from '@fluxer/schema/src/domains/rpc/RpcSchemas';
|
||||
import {createSnowflakeFromTimestamp, snowflakeToDate} from '@fluxer/snowflake/src/Snowflake';
|
||||
import * as BucketUtils from '@fluxer/snowflake/src/SnowflakeBuckets';
|
||||
import type {Context} from 'hono';
|
||||
@ -3149,7 +3150,7 @@ export function TestHarnessController(app: HonoApp) {
|
||||
});
|
||||
|
||||
app.post('/test/rpc-session-init', async (ctx) => {
|
||||
const request = await ctx.req.json();
|
||||
const request = RpcRequest.parse(await ctx.req.json());
|
||||
const response = await ctx.get('rpcService').handleRpcRequest({request, requestCache: ctx.get('requestCache')});
|
||||
return ctx.json(response);
|
||||
});
|
||||
|
||||
@ -57,10 +57,6 @@ export const RpcRequest = z.discriminatedUnion('type', [
|
||||
latitude: createStringType(1, 32).optional().describe('Client latitude for region selection'),
|
||||
longitude: createStringType(1, 32).optional().describe('Client longitude for region selection'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('guild').describe('Request type for fetching guild data'),
|
||||
guild_id: SnowflakeType.describe('ID of the guild to fetch'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('guild_collection').describe('Request type for fetching a single guild collection chunk'),
|
||||
guild_id: SnowflakeType.describe('ID of the guild to fetch'),
|
||||
@ -204,17 +200,6 @@ export const RpcResponseSessionData = z.object({
|
||||
|
||||
export type RpcResponseSessionData = z.infer<typeof RpcResponseSessionData>;
|
||||
|
||||
export const RpcResponseGuildData = z.object({
|
||||
guild: GuildResponse.describe('Guild information'),
|
||||
roles: z.array(GuildRoleResponse).describe('List of roles in the guild'),
|
||||
channels: z.array(ChannelResponse).describe('List of channels in the guild'),
|
||||
emojis: z.array(GuildEmojiResponse).describe('List of custom emojis in the guild'),
|
||||
stickers: z.array(GuildStickerResponse).describe('List of custom stickers in the guild'),
|
||||
members: z.array(GuildMemberResponse).describe('List of guild members'),
|
||||
});
|
||||
|
||||
export type RpcResponseGuildData = z.infer<typeof RpcResponseGuildData>;
|
||||
|
||||
export const RpcResponseGuildCollectionData = z.object({
|
||||
collection: RpcGuildCollectionType.describe('Guild collection returned in this response'),
|
||||
guild: GuildResponse.nullish().describe('Guild information'),
|
||||
@ -248,10 +233,6 @@ export const RpcResponse = z.discriminatedUnion('type', [
|
||||
})
|
||||
.describe('Crash logging result'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('guild').describe('Response type for guild data'),
|
||||
data: RpcResponseGuildData.describe('Guild data'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('guild_collection').describe('Response type for guild collection chunks'),
|
||||
data: RpcResponseGuildCollectionData.describe('Guild collection chunk data'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user