initial commit
This commit is contained in:
470
fluxer_app/src/records/GuildRecord.tsx
Normal file
470
fluxer_app/src/records/GuildRecord.tsx
Normal file
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
* 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 {GuildSplashCardAlignmentValue} from '~/Constants';
|
||||
import {
|
||||
GuildFeatures,
|
||||
GuildSplashCardAlignment,
|
||||
LARGE_GUILD_THRESHOLD,
|
||||
MAX_GUILD_EMOJIS_ANIMATED,
|
||||
MAX_GUILD_EMOJIS_ANIMATED_MORE_EMOJI,
|
||||
MAX_GUILD_EMOJIS_STATIC,
|
||||
MAX_GUILD_EMOJIS_STATIC_MORE_EMOJI,
|
||||
MAX_GUILD_STICKERS,
|
||||
MAX_GUILD_STICKERS_MORE_STICKERS,
|
||||
MessageNotifications,
|
||||
} from '~/Constants';
|
||||
import type {Channel} from '~/records/ChannelRecord';
|
||||
import type {GuildEmoji} from '~/records/GuildEmojiRecord';
|
||||
import type {GuildMember} from '~/records/GuildMemberRecord';
|
||||
import type {GuildRole} from '~/records/GuildRoleRecord';
|
||||
import {GuildRoleRecord} from '~/records/GuildRoleRecord';
|
||||
import type {GuildSticker} from '~/records/GuildStickerRecord';
|
||||
import type {Presence} from '~/stores/PresenceStore';
|
||||
import type {VoiceState} from '~/stores/voice/MediaEngineFacade';
|
||||
import * as SnowflakeUtils from '~/utils/SnowflakeUtils';
|
||||
|
||||
export type Guild = Readonly<{
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
banner?: string | null;
|
||||
banner_width?: number | null;
|
||||
banner_height?: number | null;
|
||||
splash?: string | null;
|
||||
splash_width?: number | null;
|
||||
splash_height?: number | null;
|
||||
splash_card_alignment?: GuildSplashCardAlignmentValue;
|
||||
embed_splash?: string | null;
|
||||
embed_splash_width?: number | null;
|
||||
embed_splash_height?: number | null;
|
||||
vanity_url_code: string | null;
|
||||
owner_id: string;
|
||||
system_channel_id: string | null;
|
||||
system_channel_flags?: number;
|
||||
rules_channel_id?: string | null;
|
||||
afk_channel_id?: string | null;
|
||||
afk_timeout?: number;
|
||||
features: ReadonlyArray<string>;
|
||||
verification_level?: number;
|
||||
mfa_level?: number;
|
||||
nsfw_level?: number;
|
||||
explicit_content_filter?: number;
|
||||
default_message_notifications?: number;
|
||||
disabled_operations?: number;
|
||||
joined_at?: string;
|
||||
unavailable?: boolean;
|
||||
member_count?: number;
|
||||
}>;
|
||||
|
||||
export type GuildReadyData = Readonly<{
|
||||
id: string;
|
||||
properties: Omit<Guild, 'roles'>;
|
||||
channels: ReadonlyArray<Channel>;
|
||||
emojis: ReadonlyArray<GuildEmoji>;
|
||||
stickers?: ReadonlyArray<GuildSticker>;
|
||||
members: ReadonlyArray<GuildMember>;
|
||||
member_count: number;
|
||||
presences?: ReadonlyArray<Presence>;
|
||||
voice_states?: ReadonlyArray<VoiceState>;
|
||||
roles: ReadonlyArray<GuildRole>;
|
||||
joined_at: string;
|
||||
unavailable?: boolean;
|
||||
}>;
|
||||
|
||||
type GuildInput = Guild | GuildRecord;
|
||||
|
||||
export class GuildRecord {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly icon: string | null;
|
||||
readonly banner: string | null;
|
||||
readonly bannerWidth: number | null;
|
||||
readonly bannerHeight: number | null;
|
||||
readonly splash: string | null;
|
||||
readonly splashWidth: number | null;
|
||||
readonly splashHeight: number | null;
|
||||
readonly splashCardAlignment: GuildSplashCardAlignmentValue;
|
||||
readonly embedSplash: string | null;
|
||||
readonly embedSplashWidth: number | null;
|
||||
readonly embedSplashHeight: number | null;
|
||||
readonly features: ReadonlySet<string>;
|
||||
readonly vanityURLCode: string | null;
|
||||
readonly ownerId: string;
|
||||
readonly systemChannelId: string | null;
|
||||
readonly systemChannelFlags: number;
|
||||
readonly rulesChannelId: string | null;
|
||||
readonly afkChannelId: string | null;
|
||||
readonly afkTimeout: number;
|
||||
readonly roles: Readonly<Record<string, GuildRoleRecord>>;
|
||||
readonly verificationLevel: number;
|
||||
readonly mfaLevel: number;
|
||||
readonly nsfwLevel: number;
|
||||
readonly explicitContentFilter: number;
|
||||
readonly defaultMessageNotifications: number;
|
||||
private readonly _disabledOperations: number;
|
||||
readonly joinedAt: string | null;
|
||||
readonly unavailable: boolean;
|
||||
readonly memberCount: number;
|
||||
|
||||
constructor(guild: GuildInput) {
|
||||
this.id = guild.id;
|
||||
this.name = guild.name;
|
||||
this.icon = guild.icon;
|
||||
this.banner = this.normalizeBanner(guild);
|
||||
this.bannerWidth = this.normalizeBannerWidth(guild);
|
||||
this.bannerHeight = this.normalizeBannerHeight(guild);
|
||||
this.splash = this.normalizeSplash(guild);
|
||||
this.splashWidth = this.normalizeSplashWidth(guild);
|
||||
this.splashHeight = this.normalizeSplashHeight(guild);
|
||||
this.splashCardAlignment = this.normalizeSplashCardAlignment(guild);
|
||||
this.embedSplash = this.normalizeEmbedSplash(guild);
|
||||
this.embedSplashWidth = this.normalizeEmbedSplashWidth(guild);
|
||||
this.embedSplashHeight = this.normalizeEmbedSplashHeight(guild);
|
||||
this.features = new Set(guild.features);
|
||||
this.vanityURLCode = this.normalizeVanityUrlCode(guild);
|
||||
this.ownerId = this.normalizeOwnerId(guild);
|
||||
this.systemChannelId = this.normalizeSystemChannelId(guild);
|
||||
this.systemChannelFlags = this.normalizeSystemChannelFlags(guild);
|
||||
this.rulesChannelId = this.normalizeRulesChannelId(guild);
|
||||
this.afkChannelId = this.normalizeAfkChannelId(guild);
|
||||
this.afkTimeout = this.normalizeAfkTimeout(guild);
|
||||
this.roles = this.normalizeRoles(guild);
|
||||
this.verificationLevel = this.normalizeVerificationLevel(guild);
|
||||
this.mfaLevel = this.normalizeMfaLevel(guild);
|
||||
this.nsfwLevel = this.normalizeNsfwLevel(guild);
|
||||
this.explicitContentFilter = this.normalizeExplicitContentFilter(guild);
|
||||
this.defaultMessageNotifications = this.normalizeDefaultMessageNotifications(guild);
|
||||
this._disabledOperations = this.normalizeDisabledOperations(guild);
|
||||
this.joinedAt = this.normalizeJoinedAt(guild);
|
||||
this.unavailable = guild.unavailable ?? false;
|
||||
this.memberCount = this.normalizeMemberCount(guild);
|
||||
}
|
||||
|
||||
private normalizeField<T>(guild: GuildInput, snakeCase: keyof Guild, camelCase: keyof GuildRecord): T {
|
||||
const value = this.isGuildInput(guild) ? guild[snakeCase] : guild[camelCase];
|
||||
return (value === undefined ? null : value) as T;
|
||||
}
|
||||
|
||||
private normalizeFieldWithDefault<T>(
|
||||
guild: GuildInput,
|
||||
snakeCase: keyof Guild,
|
||||
camelCase: keyof GuildRecord,
|
||||
defaultValue: T,
|
||||
): T {
|
||||
return this.isGuildInput(guild) ? ((guild[snakeCase] ?? defaultValue) as T) : (guild[camelCase] as T);
|
||||
}
|
||||
|
||||
private normalizeBanner(guild: GuildInput): string | null {
|
||||
return this.normalizeField(guild, 'banner', 'banner');
|
||||
}
|
||||
|
||||
private normalizeBannerWidth(guild: GuildInput): number | null {
|
||||
return this.normalizeField(guild, 'banner_width', 'bannerWidth');
|
||||
}
|
||||
|
||||
private normalizeBannerHeight(guild: GuildInput): number | null {
|
||||
return this.normalizeField(guild, 'banner_height', 'bannerHeight');
|
||||
}
|
||||
|
||||
private normalizeSplash(guild: GuildInput): string | null {
|
||||
return this.normalizeField(guild, 'splash', 'splash');
|
||||
}
|
||||
|
||||
private normalizeSplashWidth(guild: GuildInput): number | null {
|
||||
return this.normalizeField(guild, 'splash_width', 'splashWidth');
|
||||
}
|
||||
|
||||
private normalizeSplashHeight(guild: GuildInput): number | null {
|
||||
return this.normalizeField(guild, 'splash_height', 'splashHeight');
|
||||
}
|
||||
|
||||
private normalizeSplashCardAlignment(guild: GuildInput): GuildSplashCardAlignmentValue {
|
||||
if (this.isGuildInput(guild)) {
|
||||
return guild.splash_card_alignment ?? GuildSplashCardAlignment.CENTER;
|
||||
}
|
||||
return guild.splashCardAlignment ?? GuildSplashCardAlignment.CENTER;
|
||||
}
|
||||
|
||||
private normalizeEmbedSplash(guild: GuildInput): string | null {
|
||||
return this.normalizeField(guild, 'embed_splash', 'embedSplash');
|
||||
}
|
||||
|
||||
private normalizeEmbedSplashWidth(guild: GuildInput): number | null {
|
||||
return this.normalizeField(guild, 'embed_splash_width', 'embedSplashWidth');
|
||||
}
|
||||
|
||||
private normalizeEmbedSplashHeight(guild: GuildInput): number | null {
|
||||
return this.normalizeField(guild, 'embed_splash_height', 'embedSplashHeight');
|
||||
}
|
||||
|
||||
private normalizeVanityUrlCode(guild: GuildInput): string | null {
|
||||
return this.normalizeField(guild, 'vanity_url_code', 'vanityURLCode');
|
||||
}
|
||||
|
||||
private normalizeOwnerId(guild: GuildInput): string {
|
||||
return this.normalizeField(guild, 'owner_id', 'ownerId');
|
||||
}
|
||||
|
||||
private normalizeSystemChannelId(guild: GuildInput): string | null {
|
||||
return this.normalizeField(guild, 'system_channel_id', 'systemChannelId');
|
||||
}
|
||||
|
||||
private normalizeSystemChannelFlags(guild: GuildInput): number {
|
||||
return this.normalizeFieldWithDefault(guild, 'system_channel_flags', 'systemChannelFlags', 0);
|
||||
}
|
||||
|
||||
private normalizeRulesChannelId(guild: GuildInput): string | null {
|
||||
return this.normalizeField(guild, 'rules_channel_id', 'rulesChannelId');
|
||||
}
|
||||
|
||||
private normalizeAfkChannelId(guild: GuildInput): string | null {
|
||||
return this.normalizeField(guild, 'afk_channel_id', 'afkChannelId');
|
||||
}
|
||||
|
||||
private normalizeAfkTimeout(guild: GuildInput): number {
|
||||
return this.normalizeFieldWithDefault(guild, 'afk_timeout', 'afkTimeout', 0);
|
||||
}
|
||||
|
||||
private normalizeRoles(guild: GuildInput): Readonly<Record<string, GuildRoleRecord>> {
|
||||
return Object.freeze('roles' in guild ? {...guild.roles} : {});
|
||||
}
|
||||
|
||||
private normalizeVerificationLevel(guild: GuildInput): number {
|
||||
return this.normalizeFieldWithDefault(guild, 'verification_level', 'verificationLevel', 0);
|
||||
}
|
||||
|
||||
private normalizeMfaLevel(guild: GuildInput): number {
|
||||
return this.normalizeFieldWithDefault(guild, 'mfa_level', 'mfaLevel', 0);
|
||||
}
|
||||
|
||||
private normalizeNsfwLevel(guild: GuildInput): number {
|
||||
return this.normalizeFieldWithDefault(guild, 'nsfw_level', 'nsfwLevel', 0);
|
||||
}
|
||||
|
||||
private normalizeExplicitContentFilter(guild: GuildInput): number {
|
||||
return this.normalizeFieldWithDefault(guild, 'explicit_content_filter', 'explicitContentFilter', 0);
|
||||
}
|
||||
|
||||
private normalizeDefaultMessageNotifications(guild: GuildInput): number {
|
||||
return this.normalizeFieldWithDefault(guild, 'default_message_notifications', 'defaultMessageNotifications', 0);
|
||||
}
|
||||
|
||||
private normalizeDisabledOperations(guild: GuildInput): number {
|
||||
return this.normalizeFieldWithDefault(guild, 'disabled_operations', 'disabledOperations', 0);
|
||||
}
|
||||
|
||||
private normalizeJoinedAt(guild: GuildInput): string | null {
|
||||
return this.normalizeField(guild, 'joined_at', 'joinedAt');
|
||||
}
|
||||
|
||||
private normalizeMemberCount(guild: GuildInput): number {
|
||||
if (this.isGuildInput(guild)) {
|
||||
const value = (guild as Guild).member_count;
|
||||
return typeof value === 'number' ? value : 0;
|
||||
}
|
||||
return (guild as GuildRecord).memberCount ?? 0;
|
||||
}
|
||||
|
||||
private isGuildInput(guild: GuildInput): guild is Guild {
|
||||
return 'vanity_url_code' in guild;
|
||||
}
|
||||
|
||||
get disabledOperations(): number {
|
||||
return this._disabledOperations;
|
||||
}
|
||||
|
||||
static fromGuildReadyData(guildData: GuildReadyData): GuildRecord {
|
||||
const roles = Object.freeze(
|
||||
guildData.roles.reduce<Record<string, GuildRoleRecord>>(
|
||||
(acc, role) => ({
|
||||
// biome-ignore lint/performance/noAccumulatingSpread: acceptable for guild roles - manageable dataset size and immutability required
|
||||
...acc,
|
||||
[role.id]: new GuildRoleRecord(guildData.properties.id, role),
|
||||
}),
|
||||
{},
|
||||
),
|
||||
);
|
||||
|
||||
return new GuildRecord({
|
||||
...guildData.properties,
|
||||
roles,
|
||||
joined_at: guildData.joined_at,
|
||||
unavailable: guildData.unavailable,
|
||||
});
|
||||
}
|
||||
|
||||
toJSON(): Guild & {
|
||||
roles: Readonly<Record<string, GuildRoleRecord>>;
|
||||
} {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
icon: this.icon,
|
||||
banner: this.banner,
|
||||
banner_width: this.bannerWidth,
|
||||
banner_height: this.bannerHeight,
|
||||
splash: this.splash,
|
||||
splash_width: this.splashWidth,
|
||||
splash_height: this.splashHeight,
|
||||
splash_card_alignment: this.splashCardAlignment,
|
||||
embed_splash: this.embedSplash,
|
||||
embed_splash_width: this.embedSplashWidth,
|
||||
embed_splash_height: this.embedSplashHeight,
|
||||
features: [...this.features],
|
||||
vanity_url_code: this.vanityURLCode,
|
||||
owner_id: this.ownerId,
|
||||
system_channel_id: this.systemChannelId,
|
||||
system_channel_flags: this.systemChannelFlags,
|
||||
rules_channel_id: this.rulesChannelId,
|
||||
afk_channel_id: this.afkChannelId,
|
||||
afk_timeout: this.afkTimeout,
|
||||
verification_level: this.verificationLevel,
|
||||
mfa_level: this.mfaLevel,
|
||||
nsfw_level: this.nsfwLevel,
|
||||
explicit_content_filter: this.explicitContentFilter,
|
||||
default_message_notifications: this.defaultMessageNotifications,
|
||||
disabled_operations: this._disabledOperations,
|
||||
joined_at: this.joinedAt ?? undefined,
|
||||
unavailable: this.unavailable,
|
||||
member_count: this.memberCount,
|
||||
roles: this.roles,
|
||||
};
|
||||
}
|
||||
|
||||
withUpdates(guild: Partial<Guild>): GuildRecord {
|
||||
return new GuildRecord({
|
||||
...this,
|
||||
name: guild.name ?? this.name,
|
||||
icon: guild.icon ?? this.icon,
|
||||
banner: guild.banner ?? this.banner,
|
||||
bannerWidth: guild.banner_width ?? this.bannerWidth,
|
||||
bannerHeight: guild.banner_height ?? this.bannerHeight,
|
||||
splash: guild.splash ?? this.splash,
|
||||
splashWidth: guild.splash_width ?? this.splashWidth,
|
||||
splashHeight: guild.splash_height ?? this.splashHeight,
|
||||
splashCardAlignment: guild.splash_card_alignment ?? this.splashCardAlignment,
|
||||
embedSplash: guild.embed_splash ?? this.embedSplash,
|
||||
embedSplashWidth: guild.embed_splash_width ?? this.embedSplashWidth,
|
||||
embedSplashHeight: guild.embed_splash_height ?? this.embedSplashHeight,
|
||||
features: guild.features ? new Set(guild.features) : this.features,
|
||||
vanityURLCode: guild.vanity_url_code ?? this.vanityURLCode,
|
||||
ownerId: guild.owner_id ?? this.ownerId,
|
||||
systemChannelId: guild.system_channel_id ?? this.systemChannelId,
|
||||
systemChannelFlags: guild.system_channel_flags ?? this.systemChannelFlags,
|
||||
rulesChannelId: guild.rules_channel_id ?? this.rulesChannelId,
|
||||
afkChannelId: guild.afk_channel_id ?? this.afkChannelId,
|
||||
afkTimeout: guild.afk_timeout ?? this.afkTimeout,
|
||||
verificationLevel: guild.verification_level ?? this.verificationLevel,
|
||||
mfaLevel: guild.mfa_level ?? this.mfaLevel,
|
||||
nsfwLevel: guild.nsfw_level ?? this.nsfwLevel,
|
||||
explicitContentFilter: guild.explicit_content_filter ?? this.explicitContentFilter,
|
||||
defaultMessageNotifications: guild.default_message_notifications ?? this.defaultMessageNotifications,
|
||||
disabledOperations: guild.disabled_operations ?? this.disabledOperations,
|
||||
unavailable: guild.unavailable ?? this.unavailable,
|
||||
memberCount: guild.member_count ?? this.memberCount,
|
||||
});
|
||||
}
|
||||
|
||||
withRoles(roles: Record<string, GuildRoleRecord>): GuildRecord {
|
||||
return new GuildRecord({
|
||||
...this,
|
||||
roles: Object.freeze({...roles}),
|
||||
});
|
||||
}
|
||||
|
||||
addRole(role: GuildRoleRecord): GuildRecord {
|
||||
return this.withRoles({
|
||||
...this.roles,
|
||||
[role.id]: role,
|
||||
});
|
||||
}
|
||||
|
||||
removeRole(roleId: string): GuildRecord {
|
||||
const {[roleId]: _, ...remainingRoles} = this.roles;
|
||||
return this.withRoles(remainingRoles);
|
||||
}
|
||||
|
||||
updateRole(role: GuildRoleRecord): GuildRecord {
|
||||
if (!this.roles[role.id]) {
|
||||
return this;
|
||||
}
|
||||
return this.addRole(role);
|
||||
}
|
||||
|
||||
getRole(roleId: string): GuildRoleRecord | undefined {
|
||||
return this.roles[roleId];
|
||||
}
|
||||
|
||||
get createdAt(): Date {
|
||||
return new Date(SnowflakeUtils.extractTimestamp(this.id));
|
||||
}
|
||||
|
||||
isOwner(userId?: string | null): boolean {
|
||||
return userId != null && this.ownerId === userId;
|
||||
}
|
||||
|
||||
get maxStaticEmojis(): number {
|
||||
if (this.features.has(GuildFeatures.MORE_EMOJI)) {
|
||||
return MAX_GUILD_EMOJIS_STATIC_MORE_EMOJI;
|
||||
}
|
||||
if (this.features.has(GuildFeatures.UNLIMITED_EMOJI)) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return MAX_GUILD_EMOJIS_STATIC;
|
||||
}
|
||||
|
||||
get maxAnimatedEmojis(): number {
|
||||
if (this.features.has(GuildFeatures.MORE_EMOJI)) {
|
||||
return MAX_GUILD_EMOJIS_ANIMATED_MORE_EMOJI;
|
||||
}
|
||||
if (this.features.has(GuildFeatures.UNLIMITED_EMOJI)) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return MAX_GUILD_EMOJIS_ANIMATED;
|
||||
}
|
||||
|
||||
get maxStickers(): number {
|
||||
if (this.features.has(GuildFeatures.MORE_STICKERS)) {
|
||||
return MAX_GUILD_STICKERS_MORE_STICKERS;
|
||||
}
|
||||
if (this.features.has(GuildFeatures.UNLIMITED_STICKERS)) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return MAX_GUILD_STICKERS;
|
||||
}
|
||||
|
||||
get isLargeGuild(): boolean {
|
||||
return this.features.has(GuildFeatures.LARGE_GUILD_OVERRIDE) || this.memberCount > LARGE_GUILD_THRESHOLD;
|
||||
}
|
||||
|
||||
get effectiveMessageNotifications(): number {
|
||||
if (this.memberCount === undefined || this.memberCount === null || this.memberCount < 0) {
|
||||
return this.defaultMessageNotifications;
|
||||
}
|
||||
if (this.isLargeGuild) {
|
||||
return MessageNotifications.ONLY_MENTIONS;
|
||||
}
|
||||
return this.defaultMessageNotifications;
|
||||
}
|
||||
|
||||
get isNotificationOverrideActive(): boolean {
|
||||
return this.isLargeGuild && this.defaultMessageNotifications === MessageNotifications.ALL_MESSAGES;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user