chore: cleanup guild rpc

This commit is contained in:
Hampus Kraft
2026-02-19 01:22:26 +00:00
parent 528e4e0d7f
commit cf06cadcfc
7 changed files with 118 additions and 114 deletions

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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>>;

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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);
});