/*
* 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 .
*/
import {RTC_REGION_ID_MAX_LENGTH, RTC_REGION_ID_MIN_LENGTH} from '@fluxer/constants/src/LimitConstants';
import {ChannelResponse, RtcRegionResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
import {GuildEmojiResponse, GuildStickerResponse} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
import {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
import {GuildRoleResponse} from '@fluxer/schema/src/domains/guild/GuildRoleSchemas';
import {FavoriteMemeResponse} from '@fluxer/schema/src/domains/meme/MemeSchemas';
import {CustomStatusPayload} from '@fluxer/schema/src/domains/user/UserRequestSchemas';
import {
CustomStatusResponse,
RelationshipResponse,
UserGuildSettingsResponse,
UserPrivateResponse,
UserSettingsResponse,
} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {createStringType, SnowflakeStringType, SnowflakeType} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {z} from 'zod';
export const RpcGuildCollectionType = z.enum(['guild', 'roles', 'channels', 'emojis', 'stickers', 'members']);
export type RpcGuildCollectionType = z.infer;
export const ReadStateResponse = z.object({
id: SnowflakeStringType.describe('The channel ID for this read state'),
mention_count: z.number().describe('Number of unread mentions in the channel'),
last_message_id: SnowflakeStringType.nullish().describe('ID of the last read message'),
last_pin_timestamp: z.string().nullish().describe('Timestamp of the last pinned message'),
});
export type ReadStateResponse = z.infer;
export const RpcRequest = z.discriminatedUnion('type', [
z.object({
type: z.literal('session').describe('Request type for session initialization'),
token: createStringType().describe('Authentication token for the session'),
version: z.literal(1).describe('RPC protocol version'),
ip: createStringType(1, 45).optional().describe('Client IP address'),
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'),
collection: RpcGuildCollectionType.describe('Guild collection to fetch'),
limit: z.number().int().min(1).max(1000).optional().describe('Maximum number of items to return'),
after_user_id: SnowflakeType.optional().describe('Cursor for member collection pagination'),
}),
z.object({
type: z.literal('log_guild_crash').describe('Request type for logging guild crashes'),
guild_id: SnowflakeType.describe('ID of the guild that crashed'),
stacktrace: z.string().describe('Error stacktrace from the crash'),
}),
z.object({
type: z.literal('get_user_guild_settings').describe('Request type for fetching user guild settings'),
user_ids: z.array(SnowflakeType).describe('IDs of users to fetch settings for'),
guild_id: SnowflakeType.describe('ID of the guild'),
}),
z.object({
type: z.literal('get_push_subscriptions').describe('Request type for fetching push notification subscriptions'),
user_ids: z.array(SnowflakeType).describe('IDs of users to fetch subscriptions for'),
}),
z.object({
type: z.literal('get_badge_counts').describe('Request type for fetching notification badge counts'),
user_ids: z.array(SnowflakeType).describe('IDs of users to fetch badge counts for'),
}),
z.object({
type: z.literal('geoip_lookup').describe('Request type for IP geolocation lookup'),
ip: createStringType(1, 45).describe('IP address to lookup'),
}),
z.object({
type: z.literal('delete_push_subscriptions').describe('Request type for deleting push notification subscriptions'),
subscriptions: z
.array(
z.object({
user_id: SnowflakeType.describe('ID of the user'),
subscription_id: createStringType().describe('ID of the subscription to delete'),
}),
)
.describe('List of subscriptions to delete'),
}),
z.object({
type: z.literal('get_user_blocked_ids').describe('Request type for fetching blocked user IDs'),
user_ids: z.array(SnowflakeType).describe('IDs of users to fetch blocked lists for'),
}),
z.object({
type: z.literal('voice_get_token').describe('Request type for getting voice connection token'),
guild_id: SnowflakeType.optional().describe('ID of the guild for the voice channel'),
channel_id: SnowflakeType.describe('ID of the voice channel'),
user_id: SnowflakeType.describe('ID of the user joining voice'),
connection_id: createStringType().optional().describe('Existing connection ID for reconnection'),
rtc_region: createStringType(RTC_REGION_ID_MIN_LENGTH, RTC_REGION_ID_MAX_LENGTH)
.optional()
.describe(
`Preferred voice region for the connection (${RTC_REGION_ID_MIN_LENGTH}-${RTC_REGION_ID_MAX_LENGTH} characters)`,
),
latitude: createStringType(1, 32).optional().describe('Client latitude for region selection'),
longitude: createStringType(1, 32).optional().describe('Client longitude for region selection'),
can_speak: z.boolean().optional().describe('Whether the user can speak in the channel'),
can_stream: z.boolean().optional().describe('Whether the user can stream in the channel'),
can_video: z.boolean().optional().describe('Whether the user can use video in the channel'),
token_nonce: createStringType(1, 64).optional().describe('Token nonce for replay prevention'),
}),
z.object({
type: z.literal('kick_temporary_member').describe('Request type for kicking temporary guild members'),
user_id: SnowflakeType.describe('ID of the user to kick'),
guild_ids: z.array(SnowflakeType).describe('IDs of guilds to kick the user from'),
}),
z.object({
type: z
.literal('voice_force_disconnect_participant')
.describe('Request type for force disconnecting a voice participant'),
guild_id: SnowflakeType.optional().describe('ID of the guild'),
channel_id: SnowflakeType.describe('ID of the voice channel'),
user_id: SnowflakeType.describe('ID of the user to disconnect'),
connection_id: createStringType().describe('Connection ID of the user'),
}),
z.object({
type: z.literal('voice_update_participant').describe('Request type for updating voice participant state'),
guild_id: SnowflakeType.optional().describe('ID of the guild'),
channel_id: SnowflakeType.describe('ID of the voice channel'),
user_id: SnowflakeType.describe('ID of the user to update'),
mute: z.boolean().describe('Whether the user is muted'),
deaf: z.boolean().describe('Whether the user is deafened'),
}),
z.object({
type: z
.literal('voice_force_disconnect_channel')
.describe('Request type for force disconnecting all participants from a channel'),
guild_id: SnowflakeType.optional().describe('ID of the guild'),
channel_id: SnowflakeType.describe('ID of the voice channel to clear'),
}),
z.object({
type: z
.literal('voice_update_participant_permissions')
.describe('Request type for updating voice participant permissions'),
guild_id: SnowflakeType.optional().describe('ID of the guild'),
channel_id: SnowflakeType.describe('ID of the voice channel'),
user_id: SnowflakeType.describe('ID of the user to update'),
connection_id: createStringType().describe('Connection ID of the user'),
can_speak: z.boolean().describe('Whether the user can speak'),
can_stream: z.boolean().describe('Whether the user can stream'),
can_video: z.boolean().describe('Whether the user can use video'),
}),
z.object({
type: z.literal('call_ended').describe('Request type for notifying that a call has ended'),
channel_id: SnowflakeType.describe('ID of the channel where the call ended'),
message_id: SnowflakeType.describe('ID of the call start message'),
participants: z.array(SnowflakeType).describe('IDs of users who participated in the call'),
ended_timestamp: z.number().describe('Unix timestamp when the call ended'),
}),
z.object({
type: z.literal('get_dm_channel').describe('Request type for fetching a DM channel'),
channel_id: SnowflakeType.describe('ID of the DM channel'),
user_id: SnowflakeType.describe('ID of the user requesting the channel'),
}),
z.object({
type: z.literal('validate_custom_status').describe('Request type for validating a custom status'),
user_id: SnowflakeType.describe('ID of the user'),
custom_status: CustomStatusPayload.nullish().describe('Custom status data to validate'),
}),
]);
export type RpcRequest = z.infer;
export const RpcResponseSessionData = z.object({
auth_session_id_hash: z.string().nullish().describe('Hash of the authentication session ID'),
user: UserPrivateResponse.describe('Private user data for the authenticated user'),
user_settings: UserSettingsResponse.nullish().describe('User settings configuration'),
user_guild_settings: z.array(UserGuildSettingsResponse).describe('Per-guild settings for the user'),
notes: z.record(SnowflakeStringType, z.string()).describe('User notes keyed by user ID'),
read_states: z.array(ReadStateResponse).describe('Read state for each channel'),
private_channels: z.array(ChannelResponse).describe('List of DM and group DM channels'),
relationships: z.array(RelationshipResponse).describe('User relationships (friends, blocked, etc.)'),
favorite_memes: z.array(FavoriteMemeResponse).describe('List of user favorite memes'),
guild_ids: z.array(SnowflakeStringType).describe('IDs of guilds the user is a member of'),
pinned_dms: z.array(SnowflakeStringType).describe('IDs of pinned DM channels'),
country_code: z.string().describe('Two-letter country code from IP geolocation'),
rtc_regions: z.array(RtcRegionResponse).describe('Available voice server regions'),
version: z.number().int().describe('Session data version for cache invalidation'),
});
export type RpcResponseSessionData = z.infer;
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;
export const RpcResponseGuildCollectionData = z.object({
collection: RpcGuildCollectionType.describe('Guild collection returned in this response'),
guild: GuildResponse.nullish().describe('Guild information'),
roles: z.array(GuildRoleResponse).nullish().describe('List of roles in the guild'),
channels: z.array(ChannelResponse).nullish().describe('List of channels in the guild'),
emojis: z.array(GuildEmojiResponse).nullish().describe('List of custom emojis in the guild'),
stickers: z.array(GuildStickerResponse).nullish().describe('List of custom stickers in the guild'),
members: z.array(GuildMemberResponse).nullish().describe('List of guild members in this chunk'),
has_more: z.boolean().describe('Whether more data is available for this collection'),
next_after_user_id: SnowflakeStringType.nullish().describe('Cursor for the next member chunk'),
});
export type RpcResponseGuildCollectionData = z.infer;
export const RpcResponseValidateCustomStatus = z.object({
custom_status: CustomStatusResponse.nullish().describe('Validated custom status or null if invalid'),
});
export type RpcResponseValidateCustomStatus = z.infer;
export const RpcResponse = z.discriminatedUnion('type', [
z.object({
type: z.literal('session').describe('Response type for session initialization'),
data: RpcResponseSessionData.describe('Session initialization data'),
}),
z.object({
type: z.literal('log_guild_crash').describe('Response type for guild crash logging'),
data: z
.object({
success: z.boolean().describe('Whether the crash was logged successfully'),
})
.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'),
}),
z.object({
type: z.literal('get_user_guild_settings').describe('Response type for user guild settings'),
data: z
.object({
user_guild_settings: z
.array(UserGuildSettingsResponse.nullable())
.describe('Guild settings for each requested user'),
})
.describe('User guild settings data'),
}),
z.object({
type: z.literal('get_push_subscriptions').describe('Response type for push subscriptions'),
data: z
.record(
SnowflakeStringType,
z.array(
z.object({
subscription_id: z.string().describe('Unique identifier for the subscription'),
endpoint: z.string().describe('Push notification endpoint URL'),
p256dh_key: z.string().describe('P-256 Diffie-Hellman public key'),
auth_key: z.string().describe('Authentication secret key'),
}),
),
)
.describe('Push subscriptions keyed by user ID'),
}),
z.object({
type: z.literal('delete_push_subscriptions').describe('Response type for push subscription deletion'),
data: z.object({success: z.boolean().describe('Whether the deletion was successful')}).describe('Deletion result'),
}),
z.object({
type: z.literal('get_user_blocked_ids').describe('Response type for blocked user IDs'),
data: z.record(SnowflakeStringType, z.array(SnowflakeStringType)).describe('Blocked user IDs keyed by user ID'),
}),
z.object({
type: z.literal('voice_get_token').describe('Response type for voice connection token'),
data: z
.object({
token: z.string().describe('Voice server authentication token'),
endpoint: z.string().describe('Voice server endpoint URL'),
connectionId: z.string().describe('Unique connection identifier'),
tokenNonce: z.string().describe('Token nonce for webhook confirmation'),
})
.describe('Voice connection credentials'),
}),
z.object({
type: z.literal('kick_temporary_member').describe('Response type for temporary member kick'),
data: z
.object({
success: z.boolean().describe('Whether the kick was successful'),
})
.describe('Kick result'),
}),
z.object({
type: z.literal('get_badge_counts').describe('Response type for badge counts'),
data: z
.object({
badge_counts: z.record(SnowflakeStringType, z.number().int().min(0)).describe('Badge counts keyed by user ID'),
})
.describe('Badge count data'),
}),
z.object({
type: z.literal('voice_force_disconnect_participant').describe('Response type for force disconnect participant'),
data: z
.object({
success: z.boolean().describe('Whether the disconnect was successful'),
})
.describe('Disconnect result'),
}),
z.object({
type: z.literal('voice_update_participant').describe('Response type for voice participant update'),
data: z
.object({
success: z.boolean().describe('Whether the update was successful'),
})
.describe('Update result'),
}),
z.object({
type: z.literal('voice_force_disconnect_channel').describe('Response type for force disconnect channel'),
data: z
.object({
success: z.boolean().describe('Whether the operation was successful'),
disconnected_count: z.number().optional().describe('Number of participants disconnected'),
message: z.string().optional().describe('Additional status message'),
})
.describe('Channel disconnect result'),
}),
z.object({
type: z.literal('voice_update_participant_permissions').describe('Response type for voice permissions update'),
data: z
.object({
success: z.boolean().describe('Whether the permissions update was successful'),
})
.describe('Permissions update result'),
}),
z.object({
type: z.literal('call_ended').describe('Response type for call ended notification'),
data: z
.object({
success: z.boolean().describe('Whether the notification was processed successfully'),
})
.describe('Call ended result'),
}),
z.object({
type: z.literal('validate_custom_status').describe('Response type for custom status validation'),
data: RpcResponseValidateCustomStatus.describe('Custom status validation result'),
}),
z.object({
type: z.literal('geoip_lookup').describe('Response type for IP geolocation lookup'),
data: z.object({country_code: z.string().describe('Two-letter country code')}).describe('Geolocation result'),
}),
z.object({
type: z.literal('get_dm_channel').describe('Response type for DM channel fetch'),
data: z
.object({
channel: ChannelResponse.nullish().describe('The DM channel or null if not found'),
})
.describe('DM channel result'),
}),
]);
export type RpcResponse = z.infer;