initial commit
This commit is contained in:
56
fluxer_app/src/records/AuthSessionRecord.tsx
Normal file
56
fluxer_app/src/records/AuthSessionRecord.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
export type AuthSession = Readonly<{
|
||||
id: string;
|
||||
approx_last_used_at: string | null;
|
||||
client_os: string;
|
||||
client_platform: string;
|
||||
client_location: string;
|
||||
}>;
|
||||
|
||||
export class AuthSessionRecord {
|
||||
readonly id: string;
|
||||
readonly approxLastUsedAt: Date | null;
|
||||
readonly clientOs: string;
|
||||
readonly clientPlatform: string;
|
||||
readonly clientLocation: string;
|
||||
|
||||
constructor(data: AuthSession) {
|
||||
this.id = data.id;
|
||||
this.approxLastUsedAt = data.approx_last_used_at ? new Date(data.approx_last_used_at) : null;
|
||||
this.clientOs = data.client_os;
|
||||
this.clientPlatform = data.client_platform;
|
||||
this.clientLocation = data.client_location;
|
||||
}
|
||||
|
||||
toJSON(): AuthSession {
|
||||
return {
|
||||
id: this.id,
|
||||
approx_last_used_at: this.approxLastUsedAt?.toISOString() ?? null,
|
||||
client_os: this.clientOs,
|
||||
client_platform: this.clientPlatform,
|
||||
client_location: this.clientLocation,
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: AuthSessionRecord): boolean {
|
||||
return JSON.stringify(this) === JSON.stringify(other);
|
||||
}
|
||||
}
|
||||
59
fluxer_app/src/records/BetaCodeRecord.tsx
Normal file
59
fluxer_app/src/records/BetaCodeRecord.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 UserPartial, UserRecord} from '~/records/UserRecord';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
|
||||
export type BetaCode = Readonly<{
|
||||
code: string;
|
||||
created_at: string;
|
||||
redeemed_at: string | null;
|
||||
redeemer: UserPartial | null;
|
||||
}>;
|
||||
|
||||
export class BetaCodeRecord {
|
||||
readonly code: string;
|
||||
readonly createdAt: Date;
|
||||
readonly redeemedAt: Date | null;
|
||||
readonly redeemer: UserRecord | null;
|
||||
|
||||
constructor(data: BetaCode) {
|
||||
this.code = data.code;
|
||||
this.createdAt = new Date(data.created_at);
|
||||
this.redeemedAt = data.redeemed_at ? new Date(data.redeemed_at) : null;
|
||||
this.redeemer = data.redeemer ? new UserRecord(data.redeemer) : null;
|
||||
|
||||
if (this.redeemer != null) {
|
||||
UserStore.cacheUsers([this.redeemer.toJSON()]);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): BetaCode {
|
||||
return {
|
||||
code: this.code,
|
||||
created_at: this.createdAt.toISOString(),
|
||||
redeemed_at: this.redeemedAt?.toISOString() ?? null,
|
||||
redeemer: this.redeemer ? this.redeemer.toJSON() : null,
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: BetaCodeRecord): boolean {
|
||||
return JSON.stringify(this) === JSON.stringify(other);
|
||||
}
|
||||
}
|
||||
376
fluxer_app/src/records/ChannelRecord.tsx
Normal file
376
fluxer_app/src/records/ChannelRecord.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
* 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 {ChannelTypes} from '~/Constants';
|
||||
import type {UserPartial} from '~/records/UserRecord';
|
||||
import UserPinnedDMStore from '~/stores/UserPinnedDMStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import * as SnowflakeUtils from '~/utils/SnowflakeUtils';
|
||||
|
||||
type ChannelOverwrite = Readonly<{
|
||||
id: string;
|
||||
type: number;
|
||||
allow: string;
|
||||
deny: string;
|
||||
}>;
|
||||
|
||||
export class ChannelOverwriteRecord {
|
||||
readonly id: string;
|
||||
readonly type: number;
|
||||
readonly allow: bigint;
|
||||
readonly deny: bigint;
|
||||
|
||||
constructor(overwrite: ChannelOverwrite) {
|
||||
this.id = overwrite.id;
|
||||
this.type = overwrite.type;
|
||||
this.allow = BigInt(overwrite.allow);
|
||||
this.deny = BigInt(overwrite.deny);
|
||||
}
|
||||
|
||||
withUpdates(overwrite: Partial<ChannelOverwrite>): ChannelOverwriteRecord {
|
||||
return new ChannelOverwriteRecord({
|
||||
id: this.id,
|
||||
type: overwrite.type ?? this.type,
|
||||
allow: overwrite.allow ?? this.allow.toString(),
|
||||
deny: overwrite.deny ?? this.deny.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
equals(other: ChannelOverwriteRecord): boolean {
|
||||
return this.id === other.id && this.type === other.type && this.allow === other.allow && this.deny === other.deny;
|
||||
}
|
||||
|
||||
toJSON(): ChannelOverwrite {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
allow: this.allow.toString(),
|
||||
deny: this.deny.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultReactionEmoji = Readonly<{
|
||||
emoji_id: string | null;
|
||||
emoji_name: string | null;
|
||||
}>;
|
||||
|
||||
export type Channel = Readonly<{
|
||||
id: string;
|
||||
guild_id?: string;
|
||||
name?: string;
|
||||
topic?: string | null;
|
||||
url?: string | null;
|
||||
icon?: string | null;
|
||||
owner_id?: string | null;
|
||||
type: number;
|
||||
position?: number;
|
||||
parent_id?: string | null;
|
||||
bitrate?: number | null;
|
||||
user_limit?: number | null;
|
||||
rtc_region?: string | null;
|
||||
last_message_id?: string | null;
|
||||
last_pin_timestamp?: string | null;
|
||||
permission_overwrites?: ReadonlyArray<ChannelOverwrite>;
|
||||
recipients?: ReadonlyArray<UserPartial>;
|
||||
nsfw?: boolean;
|
||||
rate_limit_per_user?: number;
|
||||
nicks?: Readonly<Record<string, string>>;
|
||||
flags?: number;
|
||||
member_count?: number;
|
||||
message_count?: number;
|
||||
total_message_sent?: number;
|
||||
default_reaction_emoji?: DefaultReactionEmoji | null;
|
||||
}>;
|
||||
|
||||
export class ChannelRecord {
|
||||
readonly id: string;
|
||||
readonly guildId?: string;
|
||||
readonly name?: string;
|
||||
readonly topic: string | null;
|
||||
readonly url: string | null;
|
||||
readonly icon: string | null;
|
||||
readonly ownerId: string | null;
|
||||
readonly type: number;
|
||||
readonly position?: number;
|
||||
readonly parentId: string | null;
|
||||
readonly bitrate: number | null;
|
||||
readonly userLimit: number | null;
|
||||
readonly rtcRegion: string | null;
|
||||
readonly lastMessageId: string | null;
|
||||
readonly lastPinTimestamp: Date | null;
|
||||
readonly permissionOverwrites: Readonly<Record<string, ChannelOverwriteRecord>>;
|
||||
readonly recipientIds: ReadonlyArray<string>;
|
||||
readonly nsfw: boolean;
|
||||
readonly rateLimitPerUser: number;
|
||||
readonly nicks: Readonly<Record<string, string>>;
|
||||
readonly flags: number;
|
||||
readonly memberCount?: number;
|
||||
readonly messageCount?: number;
|
||||
readonly totalMessageSent?: number;
|
||||
readonly defaultReactionEmoji?: DefaultReactionEmoji | null;
|
||||
|
||||
constructor(channel: Channel) {
|
||||
this.id = channel.id;
|
||||
this.guildId = channel.guild_id;
|
||||
this.name = channel.name;
|
||||
this.topic = channel.topic ?? null;
|
||||
this.url = channel.url ?? null;
|
||||
this.icon = channel.icon ?? null;
|
||||
this.ownerId = channel.owner_id ?? null;
|
||||
this.type = channel.type;
|
||||
this.position = channel.position;
|
||||
this.parentId = channel.parent_id ?? null;
|
||||
this.bitrate = channel.bitrate ?? null;
|
||||
this.userLimit = channel.user_limit ?? null;
|
||||
this.rtcRegion = channel.rtc_region ?? null;
|
||||
this.lastMessageId = channel.last_message_id ?? null;
|
||||
this.lastPinTimestamp = channel.last_pin_timestamp ? new Date(channel.last_pin_timestamp) : null;
|
||||
this.nsfw = channel.nsfw ?? false;
|
||||
this.rateLimitPerUser = channel.rate_limit_per_user ?? 0;
|
||||
this.flags = channel.flags ?? 0;
|
||||
this.nicks = channel.nicks ?? {};
|
||||
|
||||
this.memberCount = channel.member_count;
|
||||
this.messageCount = channel.message_count;
|
||||
this.totalMessageSent = channel.total_message_sent;
|
||||
|
||||
this.defaultReactionEmoji = channel.default_reaction_emoji;
|
||||
|
||||
if ((this.type === ChannelTypes.DM || this.type === ChannelTypes.GROUP_DM) && channel.recipients) {
|
||||
UserStore.cacheUsers(Array.from(channel.recipients));
|
||||
}
|
||||
|
||||
if (this.type === ChannelTypes.DM_PERSONAL_NOTES) {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
this.recipientIds = currentUser ? [currentUser.id] : [];
|
||||
} else if ((this.type === ChannelTypes.DM || this.type === ChannelTypes.GROUP_DM) && channel.recipients) {
|
||||
this.recipientIds = channel.recipients.map((user) => user.id);
|
||||
} else {
|
||||
this.recipientIds = [];
|
||||
}
|
||||
|
||||
this.permissionOverwrites =
|
||||
!this.isPrivate() && channel.permission_overwrites
|
||||
? channel.permission_overwrites.reduce(
|
||||
(acc, overwrite) => {
|
||||
acc[overwrite.id] = new ChannelOverwriteRecord(overwrite);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ChannelOverwriteRecord>,
|
||||
)
|
||||
: {};
|
||||
}
|
||||
|
||||
hasFlag(flag: number): boolean {
|
||||
return (this.flags & flag) === flag;
|
||||
}
|
||||
|
||||
get isPinned(): boolean {
|
||||
return UserPinnedDMStore.pinnedDMs.includes(this.id);
|
||||
}
|
||||
|
||||
isPrivate(): boolean {
|
||||
return (
|
||||
this.type === ChannelTypes.DM ||
|
||||
this.type === ChannelTypes.GROUP_DM ||
|
||||
this.type === ChannelTypes.DM_PERSONAL_NOTES
|
||||
);
|
||||
}
|
||||
|
||||
isDM(): boolean {
|
||||
return this.type === ChannelTypes.DM;
|
||||
}
|
||||
|
||||
isGroupDM(): boolean {
|
||||
return this.type === ChannelTypes.GROUP_DM;
|
||||
}
|
||||
|
||||
isPersonalNotes(): boolean {
|
||||
return this.type === ChannelTypes.DM_PERSONAL_NOTES;
|
||||
}
|
||||
|
||||
isGuildText(): boolean {
|
||||
return this.type === ChannelTypes.GUILD_TEXT;
|
||||
}
|
||||
|
||||
isGuildVoice(): boolean {
|
||||
return this.type === ChannelTypes.GUILD_VOICE;
|
||||
}
|
||||
|
||||
isGuildCategory(): boolean {
|
||||
return this.type === ChannelTypes.GUILD_CATEGORY;
|
||||
}
|
||||
|
||||
isVoice(): boolean {
|
||||
return this.type === ChannelTypes.GUILD_VOICE;
|
||||
}
|
||||
|
||||
isText(): boolean {
|
||||
return this.type === ChannelTypes.GUILD_TEXT;
|
||||
}
|
||||
|
||||
isNSFW(): boolean {
|
||||
return this.nsfw;
|
||||
}
|
||||
|
||||
getRecipientId(): string | undefined {
|
||||
if (this.type !== ChannelTypes.DM) return undefined;
|
||||
return this.recipientIds[0];
|
||||
}
|
||||
|
||||
get createdAt(): Date {
|
||||
return new Date(SnowflakeUtils.extractTimestamp(this.id));
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<Channel>): ChannelRecord {
|
||||
let newRecipients: Array<UserPartial> = [];
|
||||
|
||||
if (
|
||||
updates.type === ChannelTypes.DM_PERSONAL_NOTES ||
|
||||
(this.type === ChannelTypes.DM_PERSONAL_NOTES && updates.type === undefined)
|
||||
) {
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
if (currentUser) {
|
||||
newRecipients = [currentUser.toJSON()];
|
||||
}
|
||||
} else if ((this.type === ChannelTypes.DM || this.type === ChannelTypes.GROUP_DM) && updates.recipients) {
|
||||
newRecipients = Array.from(updates.recipients);
|
||||
UserStore.cacheUsers(newRecipients);
|
||||
} else if (this.type === ChannelTypes.DM || this.type === ChannelTypes.GROUP_DM) {
|
||||
newRecipients = this.recipientIds.map((id) => UserStore.getUser(id)!.toJSON());
|
||||
}
|
||||
|
||||
return new ChannelRecord({
|
||||
id: this.id,
|
||||
guild_id: updates.guild_id ?? this.guildId,
|
||||
name: updates.name ?? this.name,
|
||||
topic: updates.topic !== undefined ? updates.topic : this.topic,
|
||||
url: updates.url !== undefined ? updates.url : this.url,
|
||||
icon: updates.icon !== undefined ? updates.icon : this.icon,
|
||||
owner_id: updates.owner_id !== undefined ? updates.owner_id : this.ownerId,
|
||||
type: updates.type ?? this.type,
|
||||
position: updates.position ?? this.position,
|
||||
parent_id: updates.parent_id !== undefined ? updates.parent_id : this.parentId,
|
||||
bitrate: updates.bitrate !== undefined ? updates.bitrate : this.bitrate,
|
||||
user_limit: updates.user_limit !== undefined ? updates.user_limit : this.userLimit,
|
||||
rtc_region: updates.rtc_region !== undefined ? updates.rtc_region : this.rtcRegion,
|
||||
last_message_id: updates.last_message_id !== undefined ? updates.last_message_id : this.lastMessageId,
|
||||
last_pin_timestamp: updates.last_pin_timestamp ?? this.lastPinTimestamp?.toISOString() ?? undefined,
|
||||
permission_overwrites: !this.isPrivate()
|
||||
? (updates.permission_overwrites ?? Object.values(this.permissionOverwrites).map((o) => o.toJSON()))
|
||||
: undefined,
|
||||
recipients: newRecipients.length > 0 ? newRecipients : undefined,
|
||||
nsfw: updates.nsfw ?? this.nsfw,
|
||||
rate_limit_per_user: updates.rate_limit_per_user ?? this.rateLimitPerUser,
|
||||
nicks: updates.nicks ?? this.nicks,
|
||||
flags: updates.flags ?? this.flags,
|
||||
member_count: updates.member_count ?? this.memberCount,
|
||||
message_count: updates.message_count ?? this.messageCount,
|
||||
total_message_sent: updates.total_message_sent ?? this.totalMessageSent,
|
||||
default_reaction_emoji: updates.default_reaction_emoji ?? this.defaultReactionEmoji,
|
||||
});
|
||||
}
|
||||
|
||||
withOverwrite(overwrite: ChannelOverwriteRecord): ChannelRecord {
|
||||
if (this.isPrivate()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return new ChannelRecord({
|
||||
...this.toJSON(),
|
||||
permission_overwrites: Object.values({
|
||||
...this.permissionOverwrites,
|
||||
[overwrite.id]: overwrite,
|
||||
}).map((o) => o.toJSON()),
|
||||
});
|
||||
}
|
||||
|
||||
equals(other: ChannelRecord): boolean {
|
||||
if (this === other) return true;
|
||||
|
||||
if (this.id !== other.id) return false;
|
||||
if (this.guildId !== other.guildId) return false;
|
||||
if (this.name !== other.name) return false;
|
||||
if (this.topic !== other.topic) return false;
|
||||
if (this.url !== other.url) return false;
|
||||
if (this.icon !== other.icon) return false;
|
||||
if (this.ownerId !== other.ownerId) return false;
|
||||
if (this.type !== other.type) return false;
|
||||
if (this.position !== other.position) return false;
|
||||
if (this.parentId !== other.parentId) return false;
|
||||
if (this.bitrate !== other.bitrate) return false;
|
||||
if (this.userLimit !== other.userLimit) return false;
|
||||
if (this.rtcRegion !== other.rtcRegion) return false;
|
||||
if (this.lastMessageId !== other.lastMessageId) return false;
|
||||
if (this.lastPinTimestamp?.getTime() !== other.lastPinTimestamp?.getTime()) return false;
|
||||
if (this.nsfw !== other.nsfw) return false;
|
||||
if (this.rateLimitPerUser !== other.rateLimitPerUser) return false;
|
||||
if (this.flags !== other.flags) return false;
|
||||
|
||||
if (this.recipientIds.length !== other.recipientIds.length) return false;
|
||||
for (let i = 0; i < this.recipientIds.length; i++) {
|
||||
if (this.recipientIds[i] !== other.recipientIds[i]) return false;
|
||||
}
|
||||
|
||||
const thisOverwrites = Object.keys(this.permissionOverwrites);
|
||||
const otherOverwrites = Object.keys(other.permissionOverwrites);
|
||||
if (thisOverwrites.length !== otherOverwrites.length) return false;
|
||||
for (const key of thisOverwrites) {
|
||||
if (!this.permissionOverwrites[key].equals(other.permissionOverwrites[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
toJSON(): Channel {
|
||||
return {
|
||||
id: this.id,
|
||||
guild_id: this.guildId,
|
||||
name: this.name,
|
||||
topic: this.topic,
|
||||
url: this.url,
|
||||
icon: this.icon,
|
||||
owner_id: this.ownerId,
|
||||
type: this.type,
|
||||
position: this.position,
|
||||
parent_id: this.parentId,
|
||||
bitrate: this.bitrate,
|
||||
user_limit: this.userLimit,
|
||||
rtc_region: this.rtcRegion,
|
||||
last_message_id: this.lastMessageId,
|
||||
last_pin_timestamp: this.lastPinTimestamp?.toISOString() ?? undefined,
|
||||
permission_overwrites: Object.values(this.permissionOverwrites).map((o) => o.toJSON()),
|
||||
recipients:
|
||||
this.type === ChannelTypes.DM || this.type === ChannelTypes.GROUP_DM
|
||||
? this.recipientIds.map((id) => UserStore.getUser(id)!.toJSON())
|
||||
: undefined,
|
||||
nsfw: this.nsfw,
|
||||
rate_limit_per_user: this.rateLimitPerUser,
|
||||
nicks: this.nicks,
|
||||
flags: this.flags,
|
||||
member_count: this.memberCount,
|
||||
message_count: this.messageCount,
|
||||
total_message_sent: this.totalMessageSent,
|
||||
default_reaction_emoji: this.defaultReactionEmoji,
|
||||
};
|
||||
}
|
||||
}
|
||||
95
fluxer_app/src/records/DeveloperApplicationRecord.tsx
Normal file
95
fluxer_app/src/records/DeveloperApplicationRecord.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
export interface DeveloperApplicationBot {
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
avatar: string | null;
|
||||
bio?: string | null;
|
||||
token?: string;
|
||||
banner?: string | null;
|
||||
}
|
||||
|
||||
export interface DeveloperApplication {
|
||||
id: string;
|
||||
name: string;
|
||||
redirect_uris: Array<string>;
|
||||
bot_public: boolean;
|
||||
bot_require_code_grant: boolean;
|
||||
client_secret?: string;
|
||||
bot?: DeveloperApplicationBot;
|
||||
}
|
||||
|
||||
export class DeveloperApplicationRecord implements DeveloperApplication {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly redirect_uris: Array<string>;
|
||||
readonly bot_public: boolean;
|
||||
readonly bot_require_code_grant: boolean;
|
||||
readonly client_secret?: string;
|
||||
readonly bot?: DeveloperApplicationBot;
|
||||
|
||||
constructor(application: DeveloperApplication) {
|
||||
this.id = application.id;
|
||||
this.name = application.name;
|
||||
this.redirect_uris = application.redirect_uris ? [...application.redirect_uris] : [];
|
||||
this.bot_public = application.bot_public;
|
||||
this.bot_require_code_grant = application.bot_require_code_grant;
|
||||
if ('client_secret' in application) {
|
||||
this.client_secret = application.client_secret;
|
||||
}
|
||||
if (application.bot) {
|
||||
this.bot = {
|
||||
id: application.bot.id,
|
||||
username: application.bot.username,
|
||||
discriminator: application.bot.discriminator,
|
||||
avatar: application.bot.avatar,
|
||||
bio: application.bot.bio ?? null,
|
||||
token: application.bot.token,
|
||||
banner: application.bot.banner ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static from(application: DeveloperApplication): DeveloperApplicationRecord {
|
||||
return new DeveloperApplicationRecord(application);
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<DeveloperApplication>): DeveloperApplicationRecord {
|
||||
return new DeveloperApplicationRecord({
|
||||
...this.toObject(),
|
||||
...updates,
|
||||
redirect_uris: updates.redirect_uris ?? this.redirect_uris,
|
||||
bot: updates.bot ?? this.bot,
|
||||
});
|
||||
}
|
||||
|
||||
toObject(): DeveloperApplication {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
redirect_uris: [...this.redirect_uris],
|
||||
bot_public: this.bot_public,
|
||||
bot_require_code_grant: this.bot_require_code_grant,
|
||||
client_secret: this.client_secret,
|
||||
bot: this.bot ? {...this.bot} : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
147
fluxer_app/src/records/FavoriteMemeRecord.tsx
Normal file
147
fluxer_app/src/records/FavoriteMemeRecord.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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 * as SnowflakeUtils from '~/utils/SnowflakeUtils';
|
||||
|
||||
export type FavoriteMeme = Readonly<{
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
alt_text: string | null;
|
||||
tags: Array<string>;
|
||||
attachment_id: string;
|
||||
filename: string;
|
||||
content_type: string;
|
||||
content_hash: string | null;
|
||||
size: number;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
duration: number | null;
|
||||
is_gifv: boolean;
|
||||
url: string;
|
||||
tenor_id: string | null;
|
||||
}>;
|
||||
|
||||
export class FavoriteMemeRecord {
|
||||
readonly id: string;
|
||||
readonly userId: string;
|
||||
readonly name: string;
|
||||
readonly altText: string | null;
|
||||
readonly tags: Array<string>;
|
||||
readonly attachmentId: string;
|
||||
readonly filename: string;
|
||||
readonly contentType: string;
|
||||
readonly contentHash: string | null;
|
||||
readonly size: number;
|
||||
readonly width: number | null;
|
||||
readonly height: number | null;
|
||||
readonly duration: number | null;
|
||||
readonly isGifv: boolean;
|
||||
readonly url: string;
|
||||
readonly tenorId: string | null;
|
||||
|
||||
constructor(meme: FavoriteMeme) {
|
||||
this.id = meme.id;
|
||||
this.userId = meme.user_id;
|
||||
this.name = meme.name;
|
||||
this.altText = meme.alt_text;
|
||||
this.tags = meme.tags;
|
||||
this.attachmentId = meme.attachment_id;
|
||||
this.filename = meme.filename;
|
||||
this.contentType = meme.content_type;
|
||||
this.contentHash = meme.content_hash;
|
||||
this.size = meme.size;
|
||||
this.width = meme.width;
|
||||
this.height = meme.height;
|
||||
this.duration = meme.duration;
|
||||
this.isGifv = meme.is_gifv;
|
||||
this.url = meme.url;
|
||||
this.tenorId = meme.tenor_id;
|
||||
}
|
||||
|
||||
get createdAtTimestamp(): number {
|
||||
return SnowflakeUtils.extractTimestamp(this.id);
|
||||
}
|
||||
|
||||
get createdAt(): Date {
|
||||
return new Date(this.createdAtTimestamp);
|
||||
}
|
||||
|
||||
isImage(): boolean {
|
||||
return this.contentType.startsWith('image/');
|
||||
}
|
||||
|
||||
isVideo(): boolean {
|
||||
return this.contentType.startsWith('video/');
|
||||
}
|
||||
|
||||
isAudio(): boolean {
|
||||
return this.contentType.startsWith('audio/');
|
||||
}
|
||||
|
||||
getMediaType(): 'image' | 'gifv' | 'video' | 'audio' | 'unknown' {
|
||||
if (this.isGifv) return 'gifv';
|
||||
if (this.isImage()) return 'image';
|
||||
if (this.isVideo()) return 'video';
|
||||
if (this.isAudio()) return 'audio';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
equals(other: FavoriteMemeRecord): boolean {
|
||||
return (
|
||||
this.id === other.id &&
|
||||
this.userId === other.userId &&
|
||||
this.name === other.name &&
|
||||
this.altText === other.altText &&
|
||||
JSON.stringify(this.tags) === JSON.stringify(other.tags) &&
|
||||
this.attachmentId === other.attachmentId &&
|
||||
this.filename === other.filename &&
|
||||
this.contentType === other.contentType &&
|
||||
this.contentHash === other.contentHash &&
|
||||
this.size === other.size &&
|
||||
this.width === other.width &&
|
||||
this.height === other.height &&
|
||||
this.duration === other.duration &&
|
||||
this.isGifv === other.isGifv &&
|
||||
this.url === other.url &&
|
||||
this.tenorId === other.tenorId
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): FavoriteMeme {
|
||||
return {
|
||||
id: this.id,
|
||||
user_id: this.userId,
|
||||
name: this.name,
|
||||
alt_text: this.altText,
|
||||
tags: this.tags,
|
||||
attachment_id: this.attachmentId,
|
||||
filename: this.filename,
|
||||
content_type: this.contentType,
|
||||
content_hash: this.contentHash,
|
||||
size: this.size,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
duration: this.duration,
|
||||
is_gifv: this.isGifv,
|
||||
url: this.url,
|
||||
tenor_id: this.tenorId,
|
||||
};
|
||||
}
|
||||
}
|
||||
89
fluxer_app/src/records/GuildEmojiRecord.tsx
Normal file
89
fluxer_app/src/records/GuildEmojiRecord.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 {UserPartial} from '~/records/UserRecord';
|
||||
import * as AvatarUtils from '~/utils/AvatarUtils';
|
||||
|
||||
export type GuildEmoji = Readonly<{
|
||||
id: string;
|
||||
name: string;
|
||||
animated: boolean;
|
||||
user?: UserPartial;
|
||||
}>;
|
||||
|
||||
export interface GuildEmojiWithUser extends GuildEmoji {
|
||||
user?: UserPartial;
|
||||
}
|
||||
|
||||
export class GuildEmojiRecord {
|
||||
readonly id: string;
|
||||
readonly guildId: string;
|
||||
readonly name: string;
|
||||
readonly uniqueName: string;
|
||||
readonly allNamesString: string;
|
||||
readonly url: string;
|
||||
readonly animated: boolean;
|
||||
readonly user?: UserPartial;
|
||||
|
||||
constructor(guildId: string, data: GuildEmoji) {
|
||||
this.id = data.id;
|
||||
this.guildId = guildId;
|
||||
this.name = data.name;
|
||||
this.uniqueName = data.name;
|
||||
this.allNamesString = `:${data.name}:`;
|
||||
this.url = AvatarUtils.getEmojiURL({
|
||||
id: data.id,
|
||||
animated: data.animated,
|
||||
});
|
||||
this.animated = data.animated;
|
||||
this.user = data.user;
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<GuildEmoji>): GuildEmojiRecord {
|
||||
return new GuildEmojiRecord(this.guildId, {
|
||||
id: updates.id ?? this.id,
|
||||
name: updates.name ?? this.name,
|
||||
animated: updates.animated ?? this.animated,
|
||||
user: updates.user ?? this.user,
|
||||
});
|
||||
}
|
||||
|
||||
equals(other: GuildEmojiRecord): boolean {
|
||||
return (
|
||||
this.id === other.id &&
|
||||
this.guildId === other.guildId &&
|
||||
this.name === other.name &&
|
||||
this.animated === other.animated &&
|
||||
this.user?.id === other.user?.id
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): GuildEmoji {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
animated: this.animated,
|
||||
user: this.user,
|
||||
};
|
||||
}
|
||||
|
||||
static create(guildId: string, data: GuildEmoji): GuildEmojiRecord {
|
||||
return new GuildEmojiRecord(guildId, data);
|
||||
}
|
||||
}
|
||||
191
fluxer_app/src/records/GuildMemberRecord.tsx
Normal file
191
fluxer_app/src/records/GuildMemberRecord.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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 {GuildMemberProfileFlags} from '~/Constants';
|
||||
import type {GuildRoleRecord} from '~/records/GuildRoleRecord';
|
||||
import type {UserPartial} from '~/records/UserRecord';
|
||||
import {UserRecord} from '~/records/UserRecord';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import GuildStore from '~/stores/GuildStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import * as ColorUtils from '~/utils/ColorUtils';
|
||||
|
||||
export type GuildMember = Readonly<{
|
||||
user: UserPartial;
|
||||
nick?: string | null;
|
||||
avatar?: string | null;
|
||||
banner?: string | null;
|
||||
accent_color?: string | null;
|
||||
roles: ReadonlyArray<string>;
|
||||
joined_at: string;
|
||||
join_source_type?: number | null;
|
||||
source_invite_code?: string | null;
|
||||
inviter_id?: string | null;
|
||||
mute?: boolean;
|
||||
deaf?: boolean;
|
||||
communication_disabled_until?: string | null;
|
||||
profile_flags?: number | null;
|
||||
}>;
|
||||
|
||||
export class GuildMemberRecord {
|
||||
readonly guildId: string;
|
||||
readonly user: UserRecord;
|
||||
readonly nick: string | null;
|
||||
readonly avatar: string | null;
|
||||
readonly banner: string | null;
|
||||
readonly accentColor: string | null;
|
||||
readonly roles: ReadonlySet<string>;
|
||||
readonly joinedAt: Date;
|
||||
readonly joinSourceType: number | null;
|
||||
readonly sourceInviteCode: string | null;
|
||||
readonly inviterId: string | null;
|
||||
readonly mute: boolean;
|
||||
readonly deaf: boolean;
|
||||
readonly communicationDisabledUntil: Date | null;
|
||||
readonly profileFlags: number;
|
||||
|
||||
constructor(guildId: string, guildMember: GuildMember) {
|
||||
this.guildId = guildId;
|
||||
|
||||
const cachedUser = UserStore.getUser(guildMember.user.id);
|
||||
if (cachedUser) {
|
||||
this.user = cachedUser;
|
||||
} else {
|
||||
this.user = new UserRecord(guildMember.user);
|
||||
UserStore.cacheUsers([this.user.toJSON()]);
|
||||
}
|
||||
|
||||
this.nick = guildMember.nick ?? null;
|
||||
this.avatar = guildMember.avatar ?? null;
|
||||
this.banner = guildMember.banner ?? null;
|
||||
this.accentColor = guildMember.accent_color ?? null;
|
||||
this.roles = new Set(guildMember.roles);
|
||||
this.joinedAt = new Date(guildMember.joined_at);
|
||||
this.joinSourceType = guildMember.join_source_type ?? null;
|
||||
this.sourceInviteCode = guildMember.source_invite_code ?? null;
|
||||
this.inviterId = guildMember.inviter_id ?? null;
|
||||
this.mute = guildMember.mute ?? false;
|
||||
this.deaf = guildMember.deaf ?? false;
|
||||
this.communicationDisabledUntil = guildMember.communication_disabled_until
|
||||
? new Date(guildMember.communication_disabled_until)
|
||||
: null;
|
||||
this.profileFlags = guildMember.profile_flags ?? 0;
|
||||
}
|
||||
|
||||
isAvatarUnset(): boolean {
|
||||
return (this.profileFlags & GuildMemberProfileFlags.AVATAR_UNSET) !== 0;
|
||||
}
|
||||
|
||||
isBannerUnset(): boolean {
|
||||
return (this.profileFlags & GuildMemberProfileFlags.BANNER_UNSET) !== 0;
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<GuildMember>): GuildMemberRecord {
|
||||
return new GuildMemberRecord(this.guildId, {
|
||||
user: updates.user ?? this.user.toJSON(),
|
||||
nick: updates.nick ?? this.nick,
|
||||
avatar: updates.avatar ?? this.avatar,
|
||||
banner: updates.banner ?? this.banner,
|
||||
accent_color: updates.accent_color ?? this.accentColor,
|
||||
roles: updates.roles ?? Array.from(this.roles),
|
||||
joined_at: updates.joined_at ?? this.joinedAt.toISOString(),
|
||||
join_source_type: updates.join_source_type ?? this.joinSourceType,
|
||||
source_invite_code: updates.source_invite_code ?? this.sourceInviteCode,
|
||||
inviter_id: updates.inviter_id ?? this.inviterId,
|
||||
mute: updates.mute ?? this.mute,
|
||||
deaf: updates.deaf ?? this.deaf,
|
||||
communication_disabled_until:
|
||||
updates.communication_disabled_until ?? this.communicationDisabledUntil?.toISOString() ?? null,
|
||||
profile_flags: updates.profile_flags ?? this.profileFlags,
|
||||
});
|
||||
}
|
||||
|
||||
withRoles(roles: Iterable<string>): GuildMemberRecord {
|
||||
return new GuildMemberRecord(this.guildId, {
|
||||
...this.toJSON(),
|
||||
roles: Array.from(roles),
|
||||
});
|
||||
}
|
||||
|
||||
getSortedRoles(): ReadonlyArray<GuildRoleRecord> {
|
||||
const guild = GuildStore.getGuild(this.guildId);
|
||||
if (!guild) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(this.roles)
|
||||
.map((roleId) => guild.roles[roleId])
|
||||
.filter((role): role is GuildRoleRecord => role !== undefined)
|
||||
.sort((a, b) => {
|
||||
if (b.position !== a.position) {
|
||||
return b.position - a.position;
|
||||
}
|
||||
return BigInt(a.id) < BigInt(b.id) ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
getColorString(): string | undefined {
|
||||
const sortedRoles = this.getSortedRoles();
|
||||
for (const role of sortedRoles) {
|
||||
if (role.color) {
|
||||
return ColorUtils.int2rgb(role.color);
|
||||
}
|
||||
}
|
||||
|
||||
const guild = GuildStore.getGuild(this.guildId);
|
||||
if (guild) {
|
||||
const everyoneRole = guild.roles[this.guildId];
|
||||
if (everyoneRole?.color) {
|
||||
return ColorUtils.int2rgb(everyoneRole.color);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
isCurrentUser(): boolean {
|
||||
return this.user.id === AuthenticationStore.currentUserId;
|
||||
}
|
||||
|
||||
isTimedOut(): boolean {
|
||||
if (!this.communicationDisabledUntil) {
|
||||
return false;
|
||||
}
|
||||
return this.communicationDisabledUntil.getTime() > Date.now();
|
||||
}
|
||||
|
||||
toJSON(): GuildMember {
|
||||
return {
|
||||
user: this.user.toJSON(),
|
||||
nick: this.nick,
|
||||
avatar: this.avatar,
|
||||
banner: this.banner,
|
||||
accent_color: this.accentColor,
|
||||
roles: Array.from(this.roles),
|
||||
joined_at: this.joinedAt.toISOString(),
|
||||
join_source_type: this.joinSourceType,
|
||||
source_invite_code: this.sourceInviteCode,
|
||||
inviter_id: this.inviterId,
|
||||
mute: this.mute,
|
||||
deaf: this.deaf,
|
||||
communication_disabled_until: this.communicationDisabledUntil?.toISOString() ?? null,
|
||||
profile_flags: this.profileFlags,
|
||||
};
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
101
fluxer_app/src/records/GuildRoleRecord.tsx
Normal file
101
fluxer_app/src/records/GuildRoleRecord.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
export interface GuildRole {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly color: number;
|
||||
readonly position: number;
|
||||
readonly hoist_position?: number | null;
|
||||
readonly permissions: string;
|
||||
readonly hoist: boolean;
|
||||
readonly mentionable: boolean;
|
||||
}
|
||||
|
||||
export class GuildRoleRecord {
|
||||
readonly id: string;
|
||||
readonly guildId: string;
|
||||
readonly name: string;
|
||||
readonly color: number;
|
||||
readonly position: number;
|
||||
readonly hoistPosition: number | null;
|
||||
readonly permissions: bigint;
|
||||
readonly hoist: boolean;
|
||||
readonly mentionable: boolean;
|
||||
|
||||
constructor(guildId: string, guildRole: GuildRole) {
|
||||
this.id = guildRole.id;
|
||||
this.guildId = guildId;
|
||||
this.name = guildRole.name;
|
||||
this.color = guildRole.color;
|
||||
this.position = guildRole.position;
|
||||
this.hoistPosition = guildRole.hoist_position ?? null;
|
||||
this.permissions = BigInt(guildRole.permissions);
|
||||
this.hoist = guildRole.hoist;
|
||||
this.mentionable = guildRole.mentionable;
|
||||
}
|
||||
|
||||
get effectiveHoistPosition(): number {
|
||||
return this.hoistPosition ?? this.position;
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<GuildRole>): GuildRoleRecord {
|
||||
return new GuildRoleRecord(this.guildId, {
|
||||
id: this.id,
|
||||
name: updates.name ?? this.name,
|
||||
color: updates.color ?? this.color,
|
||||
position: updates.position ?? this.position,
|
||||
hoist_position: updates.hoist_position !== undefined ? updates.hoist_position : this.hoistPosition,
|
||||
permissions: updates.permissions ?? this.permissions.toString(),
|
||||
hoist: updates.hoist ?? this.hoist,
|
||||
mentionable: updates.mentionable ?? this.mentionable,
|
||||
});
|
||||
}
|
||||
|
||||
get isEveryone(): boolean {
|
||||
return this.id === this.guildId;
|
||||
}
|
||||
|
||||
equals(other: GuildRoleRecord): boolean {
|
||||
return (
|
||||
this.id === other.id &&
|
||||
this.guildId === other.guildId &&
|
||||
this.name === other.name &&
|
||||
this.color === other.color &&
|
||||
this.position === other.position &&
|
||||
this.hoistPosition === other.hoistPosition &&
|
||||
this.permissions === other.permissions &&
|
||||
this.hoist === other.hoist &&
|
||||
this.mentionable === other.mentionable
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): GuildRole {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
color: this.color,
|
||||
position: this.position,
|
||||
hoist_position: this.hoistPosition,
|
||||
permissions: this.permissions.toString(),
|
||||
hoist: this.hoist,
|
||||
mentionable: this.mentionable,
|
||||
};
|
||||
}
|
||||
}
|
||||
107
fluxer_app/src/records/GuildStickerRecord.tsx
Normal file
107
fluxer_app/src/records/GuildStickerRecord.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 {StickerFormatTypes} from '~/Constants';
|
||||
import type {UserPartial} from '~/records/UserRecord';
|
||||
import * as AvatarUtils from '~/utils/AvatarUtils';
|
||||
|
||||
export type GuildSticker = Readonly<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: Array<string>;
|
||||
format_type: number;
|
||||
user?: UserPartial;
|
||||
}>;
|
||||
|
||||
export interface GuildStickerWithUser extends GuildSticker {
|
||||
user?: UserPartial;
|
||||
}
|
||||
|
||||
export function isStickerAnimated(sticker: GuildSticker) {
|
||||
return sticker.format_type === StickerFormatTypes.GIF;
|
||||
}
|
||||
|
||||
export class GuildStickerRecord {
|
||||
readonly id: string;
|
||||
readonly guildId: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly tags: ReadonlyArray<string>;
|
||||
readonly url: string;
|
||||
readonly formatType: number;
|
||||
readonly user?: UserPartial;
|
||||
|
||||
constructor(guildId: string, data: GuildSticker) {
|
||||
this.id = data.id;
|
||||
this.guildId = guildId;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
this.tags = Object.freeze([...data.tags]);
|
||||
this.url = AvatarUtils.getStickerURL({
|
||||
id: data.id,
|
||||
animated: isStickerAnimated(data),
|
||||
size: 320,
|
||||
});
|
||||
this.formatType = data.format_type;
|
||||
this.user = data.user;
|
||||
}
|
||||
|
||||
isAnimated() {
|
||||
return isStickerAnimated(this.toJSON());
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<GuildSticker>): GuildStickerRecord {
|
||||
return new GuildStickerRecord(this.guildId, {
|
||||
id: updates.id ?? this.id,
|
||||
name: updates.name ?? this.name,
|
||||
description: updates.description ?? this.description,
|
||||
tags: updates.tags ?? [...this.tags],
|
||||
format_type: updates.format_type ?? this.formatType,
|
||||
user: updates.user ?? this.user,
|
||||
});
|
||||
}
|
||||
|
||||
equals(other: GuildStickerRecord): boolean {
|
||||
return (
|
||||
this.id === other.id &&
|
||||
this.guildId === other.guildId &&
|
||||
this.name === other.name &&
|
||||
this.description === other.description &&
|
||||
JSON.stringify(this.tags) === JSON.stringify(other.tags) &&
|
||||
this.formatType === other.formatType &&
|
||||
this.user?.id === other.user?.id
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): GuildSticker {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
tags: [...this.tags],
|
||||
format_type: this.formatType,
|
||||
user: this.user,
|
||||
};
|
||||
}
|
||||
|
||||
static create(guildId: string, data: GuildSticker): GuildStickerRecord {
|
||||
return new GuildStickerRecord(guildId, data);
|
||||
}
|
||||
}
|
||||
704
fluxer_app/src/records/MessageRecord.tsx
Normal file
704
fluxer_app/src/records/MessageRecord.tsx
Normal file
@@ -0,0 +1,704 @@
|
||||
/*
|
||||
* 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 {MessageFlags, MessageStates, MessageTypes} from '~/Constants';
|
||||
import type {GuildMember} from '~/records/GuildMemberRecord';
|
||||
import type {UserPartial} from '~/records/UserRecord';
|
||||
import {UserRecord} from '~/records/UserRecord';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import GuildMemberStore from '~/stores/GuildMemberStore';
|
||||
import GuildStore from '~/stores/GuildStore';
|
||||
import RelationshipStore from '~/stores/RelationshipStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import type {Invite as InviteType} from '~/types/InviteTypes';
|
||||
import * as GiftCodeUtils from '~/utils/giftCodeUtils';
|
||||
import * as InviteUtils from '~/utils/InviteUtils';
|
||||
import {emojiEquals, type ReactionEmoji} from '~/utils/ReactionUtils';
|
||||
import * as ThemeUtils from '~/utils/ThemeUtils';
|
||||
|
||||
export type Invite = InviteType;
|
||||
|
||||
export interface EmbedAuthor {
|
||||
name: string;
|
||||
url?: string;
|
||||
icon_url?: string;
|
||||
proxy_icon_url?: string;
|
||||
}
|
||||
|
||||
export interface EmbedFooter {
|
||||
text: string;
|
||||
icon_url?: string;
|
||||
proxy_icon_url?: string;
|
||||
}
|
||||
|
||||
export interface EmbedMedia {
|
||||
url: string;
|
||||
proxy_url?: string;
|
||||
content_type?: string;
|
||||
content_hash?: string | null;
|
||||
width?: number;
|
||||
height?: number;
|
||||
placeholder?: string;
|
||||
flags: number;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
nsfw?: boolean;
|
||||
}
|
||||
|
||||
export interface EmbedField {
|
||||
name: string;
|
||||
value: string;
|
||||
inline: boolean;
|
||||
}
|
||||
|
||||
export interface MessageEmbed {
|
||||
id: string;
|
||||
type: string;
|
||||
url?: string;
|
||||
title?: string;
|
||||
color?: number;
|
||||
timestamp?: string;
|
||||
description?: string;
|
||||
author?: EmbedAuthor;
|
||||
image?: EmbedMedia;
|
||||
thumbnail?: EmbedMedia;
|
||||
footer?: EmbedFooter;
|
||||
fields?: ReadonlyArray<EmbedField>;
|
||||
provider?: EmbedAuthor;
|
||||
video?: EmbedMedia;
|
||||
audio?: EmbedMedia;
|
||||
flags?: number;
|
||||
}
|
||||
|
||||
export interface MessageReference {
|
||||
message_id: string;
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
type?: number;
|
||||
}
|
||||
|
||||
export interface MessageReaction {
|
||||
emoji: ReactionEmoji;
|
||||
count: number;
|
||||
me?: true;
|
||||
me_burst?: boolean;
|
||||
count_details?: {
|
||||
burst: number;
|
||||
normal: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageAttachment {
|
||||
id: string;
|
||||
filename: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
caption?: string;
|
||||
content_type?: string;
|
||||
size: number;
|
||||
url: string | null;
|
||||
proxy_url: string | null;
|
||||
width?: number;
|
||||
height?: number;
|
||||
placeholder?: string;
|
||||
placeholder_version?: number;
|
||||
flags: number;
|
||||
duration_secs?: number;
|
||||
duration?: number;
|
||||
waveform?: string;
|
||||
content_hash?: string | null;
|
||||
nsfw?: boolean;
|
||||
expires_at?: string | null;
|
||||
expired?: boolean;
|
||||
}
|
||||
|
||||
export interface MessageCall {
|
||||
participants: Array<string>;
|
||||
ended_timestamp?: string | null;
|
||||
}
|
||||
|
||||
export interface MessageSnapshot {
|
||||
type: number;
|
||||
content: string;
|
||||
embeds?: ReadonlyArray<MessageEmbed>;
|
||||
attachments?: ReadonlyArray<MessageAttachment>;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface MessageStickerItem {
|
||||
id: string;
|
||||
name: string;
|
||||
format_type: number;
|
||||
}
|
||||
|
||||
export interface ChannelMention {
|
||||
id: string;
|
||||
guild_id: string;
|
||||
type: number;
|
||||
name: string;
|
||||
parent_id?: string | null;
|
||||
}
|
||||
|
||||
export interface AllowedMentions {
|
||||
parse?: ReadonlyArray<'roles' | 'users' | 'everyone'>;
|
||||
roles?: ReadonlyArray<string>;
|
||||
users?: ReadonlyArray<string>;
|
||||
replied_user?: boolean;
|
||||
}
|
||||
|
||||
export interface MessageMention extends UserPartial {
|
||||
member?: Omit<GuildMember, 'user'>;
|
||||
}
|
||||
|
||||
export type Message = Readonly<{
|
||||
id: string;
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
author: UserPartial;
|
||||
member?: Omit<GuildMember, 'user'>;
|
||||
webhook_id?: string;
|
||||
application_id?: string;
|
||||
type: number;
|
||||
flags: number;
|
||||
pinned: boolean;
|
||||
tts?: boolean;
|
||||
mention_everyone: boolean;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
edited_timestamp?: string;
|
||||
mentions?: ReadonlyArray<MessageMention>;
|
||||
mention_roles?: ReadonlyArray<string>;
|
||||
mention_channels?: ReadonlyArray<ChannelMention>;
|
||||
embeds?: ReadonlyArray<MessageEmbed>;
|
||||
attachments?: ReadonlyArray<MessageAttachment>;
|
||||
stickers?: ReadonlyArray<MessageStickerItem>;
|
||||
reactions?: ReadonlyArray<MessageReaction>;
|
||||
message_reference?: MessageReference;
|
||||
referenced_message?: Message | null;
|
||||
message_snapshots?: ReadonlyArray<MessageSnapshot>;
|
||||
call?: MessageCall | null;
|
||||
state?: string;
|
||||
nonce?: string;
|
||||
blocked?: boolean;
|
||||
loggingName?: string;
|
||||
_allowedMentions?: AllowedMentions;
|
||||
_favoriteMemeId?: string;
|
||||
}>;
|
||||
|
||||
interface TransformedMessageCall {
|
||||
participants: Array<string>;
|
||||
endedTimestamp: Date | null;
|
||||
}
|
||||
|
||||
function transformMessageCall(call?: MessageCall | null): TransformedMessageCall | null {
|
||||
if (call != null) {
|
||||
return {
|
||||
participants: call.participants,
|
||||
endedTimestamp: call.ended_timestamp != null ? new Date(call.ended_timestamp) : null,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let embedIdCounter = 0;
|
||||
|
||||
const generateEmbedId = (): string => {
|
||||
return `embed_${embedIdCounter++}`;
|
||||
};
|
||||
|
||||
const areEmbedsEqual = (embed1: MessageEmbed, embed2: MessageEmbed): boolean => {
|
||||
const {id: _id1, ...embed1WithoutId} = embed1;
|
||||
const {id: _id2, ...embed2WithoutId} = embed2;
|
||||
return JSON.stringify(embed1WithoutId) === JSON.stringify(embed2WithoutId);
|
||||
};
|
||||
|
||||
const embedCache: Array<{embed: MessageEmbed; id: string}> = [];
|
||||
|
||||
const getOrCreateEmbedId = (embed: Omit<MessageEmbed, 'id'>): string => {
|
||||
const existingEmbed = embedCache.find((cached) => areEmbedsEqual(cached.embed, embed as MessageEmbed));
|
||||
|
||||
if (existingEmbed) {
|
||||
return existingEmbed.id;
|
||||
}
|
||||
|
||||
const newId = generateEmbedId();
|
||||
embedCache.push({
|
||||
embed: {...embed, id: newId} as MessageEmbed,
|
||||
id: newId,
|
||||
});
|
||||
|
||||
return newId;
|
||||
};
|
||||
|
||||
interface MessageRecordOptions {
|
||||
skipUserCache?: boolean;
|
||||
}
|
||||
|
||||
export class MessageRecord {
|
||||
readonly id: string;
|
||||
readonly channelId: string;
|
||||
readonly guildId?: string;
|
||||
readonly author: UserRecord;
|
||||
readonly webhookId?: string;
|
||||
readonly applicationId?: string;
|
||||
readonly type: number;
|
||||
readonly flags: number;
|
||||
readonly pinned: boolean;
|
||||
readonly mentionEveryone: boolean;
|
||||
readonly content: string;
|
||||
readonly timestamp: Date;
|
||||
readonly editedTimestamp: Date | null;
|
||||
readonly mentions: ReadonlyArray<UserRecord>;
|
||||
readonly mentionRoles: ReadonlyArray<string>;
|
||||
readonly mentionChannels: ReadonlyArray<ChannelMention>;
|
||||
readonly embeds: ReadonlyArray<MessageEmbed>;
|
||||
readonly attachments: ReadonlyArray<MessageAttachment>;
|
||||
readonly stickerItems: ReadonlyArray<MessageStickerItem>;
|
||||
readonly reactions: ReadonlyArray<MessageReaction>;
|
||||
readonly messageReference?: MessageReference;
|
||||
readonly referencedMessage?: MessageRecord | null;
|
||||
readonly messageSnapshots?: ReadonlyArray<MessageSnapshot>;
|
||||
readonly call: TransformedMessageCall | null;
|
||||
readonly state: string;
|
||||
readonly nonce?: string;
|
||||
readonly blocked: boolean;
|
||||
readonly loggingName?: string;
|
||||
|
||||
readonly invites: ReadonlyArray<string>;
|
||||
readonly gifts: ReadonlyArray<string>;
|
||||
readonly themes: ReadonlyArray<string>;
|
||||
|
||||
readonly _allowedMentions?: AllowedMentions;
|
||||
readonly _favoriteMemeId?: string;
|
||||
readonly stickers?: ReadonlyArray<MessageStickerItem>;
|
||||
|
||||
constructor(message: Message, options?: MessageRecordOptions) {
|
||||
if (!options?.skipUserCache) {
|
||||
UserStore.cacheUsers([message.author, ...(message.mentions ?? [])]);
|
||||
}
|
||||
|
||||
const isBlocked = RelationshipStore.isBlocked(message.author.id);
|
||||
|
||||
if (message.webhook_id) {
|
||||
this.author = new UserRecord(message.author);
|
||||
} else {
|
||||
this.author = UserStore.getUser(message.author.id) || new UserRecord(message.author);
|
||||
}
|
||||
|
||||
this.id = message.id;
|
||||
this.channelId = message.channel_id;
|
||||
this.guildId = message.guild_id;
|
||||
this.webhookId = message.webhook_id;
|
||||
this.applicationId = message.application_id;
|
||||
this.type = message.type;
|
||||
this.flags = message.flags;
|
||||
this.pinned = message.pinned;
|
||||
this.mentionEveryone = message.mention_everyone;
|
||||
this.content = message.content;
|
||||
this.timestamp = new Date(message.timestamp);
|
||||
this.editedTimestamp = message.edited_timestamp ? new Date(message.edited_timestamp) : null;
|
||||
this.state = message.state ?? MessageStates.SENT;
|
||||
this.nonce = message.nonce;
|
||||
this.blocked = message.blocked ?? isBlocked;
|
||||
this.loggingName = message.loggingName;
|
||||
|
||||
this.mentions = Object.freeze((message.mentions ?? []).map((user) => new UserRecord(user)));
|
||||
this.mentionRoles = Object.freeze(message.mention_roles ?? []);
|
||||
this.mentionChannels = Object.freeze(message.mention_channels ?? []);
|
||||
this.embeds = Object.freeze(
|
||||
(message.embeds ?? []).map((embed) => ({
|
||||
...embed,
|
||||
id: getOrCreateEmbedId(embed),
|
||||
})),
|
||||
);
|
||||
this.attachments = Object.freeze(message.attachments ?? []);
|
||||
this.stickerItems = Object.freeze(message.stickers ?? []);
|
||||
this.reactions = Object.freeze(message.reactions ?? []);
|
||||
|
||||
this.messageReference = message.message_reference;
|
||||
this.referencedMessage = message.referenced_message
|
||||
? new MessageRecord(message.referenced_message, {skipUserCache: true})
|
||||
: undefined;
|
||||
this.messageSnapshots = message.message_snapshots ? Object.freeze(message.message_snapshots) : undefined;
|
||||
|
||||
this.call = transformMessageCall(message.call);
|
||||
|
||||
this.invites = Object.freeze(InviteUtils.findInvites(message.content));
|
||||
this.gifts = Object.freeze(GiftCodeUtils.findGifts(message.content));
|
||||
this.themes = Object.freeze(ThemeUtils.findThemes(message.content));
|
||||
|
||||
this._allowedMentions = message._allowedMentions;
|
||||
this._favoriteMemeId = message._favoriteMemeId;
|
||||
this.stickers = message.stickers ? Object.freeze(message.stickers) : undefined;
|
||||
}
|
||||
|
||||
hasFlag(flag: number): boolean {
|
||||
return (this.flags & flag) === flag;
|
||||
}
|
||||
|
||||
get suppressEmbeds(): boolean {
|
||||
return this.hasFlag(MessageFlags.SUPPRESS_EMBEDS);
|
||||
}
|
||||
|
||||
get suppressNotifications(): boolean {
|
||||
return this.hasFlag(MessageFlags.SUPPRESS_NOTIFICATIONS);
|
||||
}
|
||||
|
||||
get isSilent(): boolean {
|
||||
return this.hasFlag(MessageFlags.SUPPRESS_NOTIFICATIONS);
|
||||
}
|
||||
|
||||
isUserMessage(): boolean {
|
||||
return (
|
||||
this.type === MessageTypes.DEFAULT || this.type === MessageTypes.REPLY || this.type === MessageTypes.CLIENT_SYSTEM
|
||||
);
|
||||
}
|
||||
|
||||
isSystemMessage(): boolean {
|
||||
return !this.isUserMessage();
|
||||
}
|
||||
|
||||
isAuthor(userId?: string | null): boolean {
|
||||
return userId != null && this.author.id === userId;
|
||||
}
|
||||
|
||||
isCurrentUserAuthor(): boolean {
|
||||
return this.isAuthor(AuthenticationStore.currentUserId);
|
||||
}
|
||||
|
||||
isMentioned(): boolean {
|
||||
return messageMentionsCurrentUser(this.toJSON());
|
||||
}
|
||||
|
||||
get isSending(): boolean {
|
||||
return this.state === MessageStates.SENDING;
|
||||
}
|
||||
|
||||
get isSent(): boolean {
|
||||
return this.state === MessageStates.SENT;
|
||||
}
|
||||
|
||||
get hasFailed(): boolean {
|
||||
return this.state === MessageStates.FAILED;
|
||||
}
|
||||
|
||||
get isEditing(): boolean {
|
||||
return this.state === MessageStates.EDITING;
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<Message>): MessageRecord {
|
||||
return new MessageRecord(
|
||||
{
|
||||
id: this.id,
|
||||
channel_id: this.channelId,
|
||||
guild_id: updates.guild_id ?? this.guildId,
|
||||
author: updates.author ?? this.author.toJSON(),
|
||||
webhook_id: updates.webhook_id ?? this.webhookId,
|
||||
application_id: updates.application_id ?? this.applicationId,
|
||||
type: updates.type ?? this.type,
|
||||
flags: updates.flags ?? this.flags,
|
||||
pinned: updates.pinned ?? this.pinned,
|
||||
mention_everyone: 'mention_everyone' in updates ? (updates.mention_everyone ?? false) : this.mentionEveryone,
|
||||
content: updates.content ?? this.content,
|
||||
timestamp: this.timestamp.toISOString(),
|
||||
edited_timestamp: updates.edited_timestamp ?? this.editedTimestamp?.toISOString(),
|
||||
mentions: 'mentions' in updates ? updates.mentions : this.mentions.map((m) => m.toJSON()),
|
||||
mention_roles: 'mention_roles' in updates ? updates.mention_roles : this.mentionRoles,
|
||||
mention_channels: updates.mention_channels ?? this.mentionChannels,
|
||||
embeds: updates.embeds ?? this.embeds,
|
||||
attachments: updates.attachments ?? this.attachments,
|
||||
stickers: updates.stickers ?? this.stickerItems,
|
||||
reactions: updates.reactions ?? this.reactions,
|
||||
message_reference: updates.message_reference ?? this.messageReference,
|
||||
referenced_message: updates.referenced_message ?? this.referencedMessage?.toJSON(),
|
||||
message_snapshots: updates.message_snapshots ?? this.messageSnapshots,
|
||||
call: updates.call ?? this.call,
|
||||
state: updates.state ?? this.state,
|
||||
nonce: updates.nonce ?? this.nonce,
|
||||
blocked: updates.blocked ?? this.blocked,
|
||||
loggingName: updates.loggingName ?? this.loggingName,
|
||||
},
|
||||
{skipUserCache: true},
|
||||
);
|
||||
}
|
||||
|
||||
withReaction(emoji: ReactionEmoji, add = true, me = false): MessageRecord {
|
||||
const existingReaction = this.getReaction(emoji);
|
||||
|
||||
if (!existingReaction && !add) {
|
||||
return this;
|
||||
}
|
||||
|
||||
let newReactions: Array<MessageReaction>;
|
||||
|
||||
if (existingReaction) {
|
||||
if (add) {
|
||||
newReactions = this.reactions.map((reaction) =>
|
||||
emojiEquals(reaction.emoji, emoji)
|
||||
? {
|
||||
...reaction,
|
||||
count: me && reaction.me ? reaction.count : reaction.count + 1,
|
||||
me: me || reaction.me ? true : undefined,
|
||||
}
|
||||
: reaction,
|
||||
);
|
||||
} else {
|
||||
const updatedCount = existingReaction.count - (me && !existingReaction.me ? 0 : 1);
|
||||
if (updatedCount <= 0) {
|
||||
newReactions = this.reactions.filter((reaction) => !emojiEquals(reaction.emoji, emoji));
|
||||
} else {
|
||||
newReactions = this.reactions.map((reaction) =>
|
||||
emojiEquals(reaction.emoji, emoji)
|
||||
? {
|
||||
...reaction,
|
||||
count: updatedCount,
|
||||
me: me ? undefined : reaction.me,
|
||||
}
|
||||
: reaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newReactions = [
|
||||
...this.reactions,
|
||||
{
|
||||
emoji,
|
||||
count: 1,
|
||||
me: me ? true : undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return this.withUpdates({reactions: newReactions});
|
||||
}
|
||||
|
||||
withoutReactionEmoji(emoji: ReactionEmoji): MessageRecord {
|
||||
return this.withUpdates({
|
||||
reactions: this.reactions.filter((reaction) => !emojiEquals(reaction.emoji, emoji)),
|
||||
});
|
||||
}
|
||||
|
||||
getReaction(emoji: ReactionEmoji): MessageReaction | undefined {
|
||||
return this.reactions.find((r) => emojiEquals(r.emoji, emoji));
|
||||
}
|
||||
|
||||
equals(other: MessageRecord): boolean {
|
||||
if (this === other) return true;
|
||||
|
||||
if (this.id !== other.id) return false;
|
||||
if (this.channelId !== other.channelId) return false;
|
||||
if (this.guildId !== other.guildId) return false;
|
||||
if (this.type !== other.type) return false;
|
||||
if (this.flags !== other.flags) return false;
|
||||
if (this.pinned !== other.pinned) return false;
|
||||
if (this.mentionEveryone !== other.mentionEveryone) return false;
|
||||
if (this.content !== other.content) return false;
|
||||
if (this.state !== other.state) return false;
|
||||
if (this.nonce !== other.nonce) return false;
|
||||
if (this.blocked !== other.blocked) return false;
|
||||
if (this.webhookId !== other.webhookId) return false;
|
||||
if (this.applicationId !== other.applicationId) return false;
|
||||
if (this.loggingName !== other.loggingName) return false;
|
||||
|
||||
if (this.timestamp.getTime() !== other.timestamp.getTime()) return false;
|
||||
if (this.editedTimestamp?.getTime() !== other.editedTimestamp?.getTime()) return false;
|
||||
|
||||
if (!this.author.equals(other.author)) return false;
|
||||
|
||||
if (this.mentions.length !== other.mentions.length) return false;
|
||||
if (this.mentionRoles.length !== other.mentionRoles.length) return false;
|
||||
if (this.mentionChannels.length !== other.mentionChannels.length) return false;
|
||||
if (this.embeds.length !== other.embeds.length) return false;
|
||||
if (this.attachments.length !== other.attachments.length) return false;
|
||||
if (this.stickerItems.length !== other.stickerItems.length) return false;
|
||||
if (this.reactions.length !== other.reactions.length) return false;
|
||||
if (this.invites.length !== other.invites.length) return false;
|
||||
if (this.gifts.length !== other.gifts.length) return false;
|
||||
if (this.themes.length !== other.themes.length) return false;
|
||||
|
||||
for (let i = 0; i < this.mentions.length; i++) {
|
||||
if (!this.mentions[i].equals(other.mentions[i])) return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.mentionRoles.length; i++) {
|
||||
if (this.mentionRoles[i] !== other.mentionRoles[i]) return false;
|
||||
}
|
||||
|
||||
if (this.mentionChannels.length > 0) {
|
||||
for (let i = 0; i < this.mentionChannels.length; i++) {
|
||||
if (JSON.stringify(this.mentionChannels[i]) !== JSON.stringify(other.mentionChannels[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.embeds.length; i++) {
|
||||
if (this.embeds[i].id !== other.embeds[i].id) return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.attachments.length; i++) {
|
||||
const a1 = this.attachments[i];
|
||||
const a2 = other.attachments[i];
|
||||
if (
|
||||
a1.id !== a2.id ||
|
||||
a1.filename !== a2.filename ||
|
||||
a1.size !== a2.size ||
|
||||
a1.url !== a2.url ||
|
||||
a1.proxy_url !== a2.proxy_url ||
|
||||
a1.width !== a2.width ||
|
||||
a1.height !== a2.height ||
|
||||
a1.content_type !== a2.content_type ||
|
||||
a1.flags !== a2.flags
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.stickerItems.length; i++) {
|
||||
const s1 = this.stickerItems[i];
|
||||
const s2 = other.stickerItems[i];
|
||||
if (s1.id !== s2.id || s1.name !== s2.name || s1.format_type !== s2.format_type) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.reactions.length; i++) {
|
||||
const r1 = this.reactions[i];
|
||||
const r2 = other.reactions[i];
|
||||
if (!emojiEquals(r1.emoji, r2.emoji) || r1.count !== r2.count || r1.me !== r2.me || r1.me_burst !== r2.me_burst) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.invites.length; i++) {
|
||||
if (this.invites[i] !== other.invites[i]) return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.gifts.length; i++) {
|
||||
if (this.gifts[i] !== other.gifts[i]) return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.themes.length; i++) {
|
||||
if (this.themes[i] !== other.themes[i]) return false;
|
||||
}
|
||||
|
||||
if (this.messageReference !== other.messageReference) {
|
||||
if (!this.messageReference || !other.messageReference) return false;
|
||||
if (
|
||||
this.messageReference.message_id !== other.messageReference.message_id ||
|
||||
this.messageReference.channel_id !== other.messageReference.channel_id ||
|
||||
this.messageReference.guild_id !== other.messageReference.guild_id ||
|
||||
this.messageReference.type !== other.messageReference.type
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.referencedMessage !== other.referencedMessage) {
|
||||
if (!this.referencedMessage || !other.referencedMessage) return false;
|
||||
if (!this.referencedMessage.equals(other.referencedMessage)) return false;
|
||||
}
|
||||
|
||||
if (this.messageSnapshots !== other.messageSnapshots) {
|
||||
if (!this.messageSnapshots || !other.messageSnapshots) return false;
|
||||
if (this.messageSnapshots.length !== other.messageSnapshots.length) return false;
|
||||
for (let i = 0; i < this.messageSnapshots.length; i++) {
|
||||
if (JSON.stringify(this.messageSnapshots[i]) !== JSON.stringify(other.messageSnapshots[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.call !== other.call) {
|
||||
if (!this.call || !other.call) return false;
|
||||
if (
|
||||
this.call.participants.length !== other.call.participants.length ||
|
||||
this.call.endedTimestamp?.getTime() !== other.call.endedTimestamp?.getTime()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < this.call.participants.length; i++) {
|
||||
if (this.call.participants[i] !== other.call.participants[i]) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static hasRenderChanges(prev: MessageRecord | undefined, next: MessageRecord | undefined): boolean {
|
||||
if (!prev && !next) return false;
|
||||
if (!prev || !next) return true;
|
||||
return !prev.equals(next);
|
||||
}
|
||||
|
||||
toJSON(): Message {
|
||||
return {
|
||||
id: this.id,
|
||||
channel_id: this.channelId,
|
||||
guild_id: this.guildId,
|
||||
author: this.author.toJSON(),
|
||||
webhook_id: this.webhookId,
|
||||
application_id: this.applicationId,
|
||||
type: this.type,
|
||||
flags: this.flags,
|
||||
pinned: this.pinned,
|
||||
mention_everyone: this.mentionEveryone,
|
||||
content: this.content,
|
||||
timestamp: this.timestamp.toISOString(),
|
||||
edited_timestamp: this.editedTimestamp?.toISOString(),
|
||||
mentions: this.mentions.map((user) => user.toJSON()),
|
||||
mention_roles: this.mentionRoles,
|
||||
mention_channels: this.mentionChannels,
|
||||
embeds: this.embeds,
|
||||
attachments: this.attachments,
|
||||
stickers: this.stickerItems,
|
||||
reactions: this.reactions,
|
||||
message_reference: this.messageReference,
|
||||
referenced_message: this.referencedMessage?.toJSON(),
|
||||
message_snapshots: this.messageSnapshots,
|
||||
call: this.call,
|
||||
state: this.state,
|
||||
nonce: this.nonce,
|
||||
blocked: this.blocked,
|
||||
loggingName: this.loggingName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const messageMentionsCurrentUser = (message: Message): boolean => {
|
||||
const channel = ChannelStore.getChannel(message.channel_id);
|
||||
if (!channel) return false;
|
||||
|
||||
if (message.mention_everyone) return true;
|
||||
|
||||
if (message.mentions?.some((user) => user.id === AuthenticationStore.currentUserId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!channel.guildId) return false;
|
||||
|
||||
const guild = GuildStore.getGuild(channel.guildId);
|
||||
if (!guild) return false;
|
||||
|
||||
const guildMember = GuildMemberStore.getMember(guild.id, AuthenticationStore.currentUserId);
|
||||
if (!guildMember) return false;
|
||||
|
||||
return message.mention_roles?.some((roleId) => guildMember.roles.has(roleId)) ?? false;
|
||||
};
|
||||
148
fluxer_app/src/records/ProfileRecord.tsx
Normal file
148
fluxer_app/src/records/ProfileRecord.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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 {GuildMember, GuildMemberRecord} from '~/records/GuildMemberRecord';
|
||||
import type {GuildRecord} from '~/records/GuildRecord';
|
||||
import type {UserPartial, UserProfile} from '~/records/UserRecord';
|
||||
import GuildMemberStore from '~/stores/GuildMemberStore';
|
||||
import GuildStore from '~/stores/GuildStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
|
||||
export type Profile = Readonly<{
|
||||
user: UserPartial;
|
||||
user_profile: UserProfile;
|
||||
guild_member_profile?: UserProfile | null;
|
||||
timezone_offset: number | null;
|
||||
guild_member?: GuildMember;
|
||||
premium_type?: number;
|
||||
premium_since?: string;
|
||||
premium_lifetime_sequence?: number;
|
||||
mutual_friends?: Array<UserPartial>;
|
||||
}>;
|
||||
|
||||
export class ProfileRecord {
|
||||
readonly userId: string;
|
||||
readonly guildId: string | null;
|
||||
readonly userProfile: Readonly<UserProfile>;
|
||||
readonly guildMemberProfile: Readonly<UserProfile> | null;
|
||||
readonly timezoneOffset: number | null;
|
||||
readonly premiumType: number | null;
|
||||
readonly premiumSince: Date | null;
|
||||
readonly premiumLifetimeSequence: number | null;
|
||||
readonly mutualFriends: ReadonlyArray<UserPartial> | null;
|
||||
|
||||
constructor(profile: Profile, guildId?: string) {
|
||||
this.userId = profile.user.id;
|
||||
this.guildId = guildId ?? null;
|
||||
this.userProfile = Object.freeze({...profile.user_profile});
|
||||
this.guildMemberProfile = profile.guild_member_profile ? Object.freeze({...profile.guild_member_profile}) : null;
|
||||
this.timezoneOffset = profile.timezone_offset;
|
||||
this.premiumType = profile.premium_type ?? null;
|
||||
this.premiumSince = profile.premium_since ? new Date(profile.premium_since) : null;
|
||||
this.premiumLifetimeSequence = profile.premium_lifetime_sequence ?? null;
|
||||
this.mutualFriends = profile.mutual_friends ? Object.freeze([...profile.mutual_friends]) : null;
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<Profile>): ProfileRecord {
|
||||
return new ProfileRecord(
|
||||
{
|
||||
user: {...this.toJSON().user, ...(updates.user ?? {})},
|
||||
user_profile: updates.user_profile ?? this.userProfile,
|
||||
guild_member_profile:
|
||||
updates.guild_member_profile === undefined ? this.guildMemberProfile : (updates.guild_member_profile ?? null),
|
||||
timezone_offset: updates.timezone_offset ?? this.timezoneOffset,
|
||||
guild_member: updates.guild_member,
|
||||
premium_type: updates.premium_type !== undefined ? updates.premium_type : (this.premiumType ?? undefined),
|
||||
premium_since:
|
||||
updates.premium_since !== undefined ? updates.premium_since : (this.premiumSince?.toISOString() ?? undefined),
|
||||
premium_lifetime_sequence:
|
||||
updates.premium_lifetime_sequence !== undefined
|
||||
? updates.premium_lifetime_sequence
|
||||
: (this.premiumLifetimeSequence ?? undefined),
|
||||
mutual_friends: updates.mutual_friends ?? (this.mutualFriends ? [...this.mutualFriends] : undefined),
|
||||
},
|
||||
this.guildId ?? undefined,
|
||||
);
|
||||
}
|
||||
|
||||
withGuildId(guildId: string | null): ProfileRecord {
|
||||
return new ProfileRecord(this.toJSON(), guildId ?? undefined);
|
||||
}
|
||||
|
||||
get guild(): GuildRecord | null {
|
||||
if (!this.guildId) return null;
|
||||
return GuildStore.getGuild(this.guildId) ?? null;
|
||||
}
|
||||
|
||||
get guildMember(): GuildMemberRecord | null {
|
||||
if (!this.guildId) return null;
|
||||
return GuildMemberStore.getMember(this.guildId, this.userId) ?? null;
|
||||
}
|
||||
|
||||
getGuildMemberProfile(): Readonly<UserProfile> | null {
|
||||
return this.guildMemberProfile;
|
||||
}
|
||||
|
||||
getEffectiveProfile(): Readonly<UserProfile> {
|
||||
if (!this.guildMemberProfile) {
|
||||
return this.userProfile;
|
||||
}
|
||||
|
||||
const guildMember = this.guildMember;
|
||||
const isBannerUnset = guildMember?.isBannerUnset() ?? false;
|
||||
const bannerColor = isBannerUnset
|
||||
? null
|
||||
: (this.guildMemberProfile?.banner_color ?? this.userProfile.banner_color ?? null);
|
||||
|
||||
return {
|
||||
bio: this.guildMemberProfile.bio ?? this.userProfile.bio,
|
||||
banner: isBannerUnset ? null : (this.guildMemberProfile.banner ?? this.userProfile.banner),
|
||||
banner_color: bannerColor,
|
||||
pronouns: this.guildMemberProfile.pronouns ?? this.userProfile.pronouns,
|
||||
accent_color: this.guildMemberProfile.accent_color ?? this.userProfile.accent_color,
|
||||
};
|
||||
}
|
||||
|
||||
equals(other: ProfileRecord): boolean {
|
||||
return (
|
||||
this.userId === other.userId &&
|
||||
this.guildId === other.guildId &&
|
||||
JSON.stringify(this.userProfile) === JSON.stringify(other.userProfile) &&
|
||||
JSON.stringify(this.guildMemberProfile) === JSON.stringify(other.guildMemberProfile) &&
|
||||
this.timezoneOffset === other.timezoneOffset &&
|
||||
this.premiumType === other.premiumType &&
|
||||
this.premiumSince === other.premiumSince &&
|
||||
this.premiumLifetimeSequence === other.premiumLifetimeSequence &&
|
||||
JSON.stringify(this.mutualFriends) === JSON.stringify(other.mutualFriends)
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): Profile {
|
||||
return {
|
||||
user: UserStore.getUser(this.userId)!,
|
||||
user_profile: {...this.userProfile},
|
||||
guild_member_profile: this.guildMemberProfile ? {...this.guildMemberProfile} : undefined,
|
||||
timezone_offset: this.timezoneOffset,
|
||||
premium_type: this.premiumType ?? undefined,
|
||||
premium_since: this.premiumSince?.toISOString() ?? undefined,
|
||||
premium_lifetime_sequence: this.premiumLifetimeSequence ?? undefined,
|
||||
mutual_friends: this.mutualFriends ? [...this.mutualFriends] : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
71
fluxer_app/src/records/RelationshipRecord.tsx
Normal file
71
fluxer_app/src/records/RelationshipRecord.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 {UserPartial, UserRecord} from '~/records/UserRecord';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
|
||||
export type Relationship = Readonly<{
|
||||
id: string;
|
||||
type: number;
|
||||
user?: UserPartial;
|
||||
since: string;
|
||||
nickname?: string | null;
|
||||
}>;
|
||||
|
||||
export class RelationshipRecord {
|
||||
readonly id: string;
|
||||
readonly type: number;
|
||||
readonly userId: string;
|
||||
readonly since: Date;
|
||||
readonly nickname: string | null;
|
||||
|
||||
constructor(relationship: Relationship) {
|
||||
if (relationship.user) {
|
||||
UserStore.cacheUsers([relationship.user]);
|
||||
this.userId = relationship.user.id;
|
||||
} else {
|
||||
this.userId = relationship.id;
|
||||
}
|
||||
this.id = relationship.id;
|
||||
this.type = relationship.type;
|
||||
this.since = new Date(relationship.since);
|
||||
this.nickname = relationship.nickname ?? null;
|
||||
}
|
||||
|
||||
get user(): UserRecord {
|
||||
return UserStore.getUser(this.userId)!;
|
||||
}
|
||||
|
||||
withUpdates(relationship: Relationship): RelationshipRecord {
|
||||
const mergedUser = relationship.user
|
||||
? {
|
||||
...this.user?.toJSON(),
|
||||
...relationship.user,
|
||||
}
|
||||
: this.user?.toJSON();
|
||||
|
||||
return new RelationshipRecord({
|
||||
id: relationship.id ?? this.id,
|
||||
type: relationship.type ?? this.type,
|
||||
since: relationship.since ?? this.since.toISOString(),
|
||||
nickname: relationship.nickname ?? this.nickname,
|
||||
user: mergedUser,
|
||||
});
|
||||
}
|
||||
}
|
||||
64
fluxer_app/src/records/SavedMessageEntryRecord.ts
Normal file
64
fluxer_app/src/records/SavedMessageEntryRecord.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 Message, MessageRecord} from '~/records/MessageRecord';
|
||||
|
||||
export interface SavedMessageEntryResponse {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
status: SavedMessageStatus;
|
||||
message: Message | null;
|
||||
}
|
||||
|
||||
export type SavedMessageStatus = 'available' | 'missing_permissions';
|
||||
|
||||
export interface SavedMessageMissingEntry {
|
||||
id: string;
|
||||
channelId: string;
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
export class SavedMessageEntryRecord {
|
||||
readonly id: string;
|
||||
readonly channelId: string;
|
||||
readonly messageId: string;
|
||||
readonly status: SavedMessageStatus;
|
||||
readonly message: MessageRecord | null;
|
||||
|
||||
constructor(data: SavedMessageEntryResponse) {
|
||||
this.id = data.id;
|
||||
this.channelId = data.channel_id;
|
||||
this.messageId = data.message_id;
|
||||
this.status = data.status;
|
||||
this.message = data.message ? new MessageRecord(data.message) : null;
|
||||
}
|
||||
|
||||
static fromResponse(response: SavedMessageEntryResponse): SavedMessageEntryRecord {
|
||||
return new SavedMessageEntryRecord(response);
|
||||
}
|
||||
|
||||
toMissingEntry(): SavedMessageMissingEntry {
|
||||
return {
|
||||
id: this.id,
|
||||
channelId: this.channelId,
|
||||
messageId: this.messageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
98
fluxer_app/src/records/ScheduledMessageRecord.ts
Normal file
98
fluxer_app/src/records/ScheduledMessageRecord.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
export interface ScheduledMessageReference {
|
||||
message_id: string;
|
||||
channel_id?: string;
|
||||
guild_id?: string;
|
||||
type?: number;
|
||||
}
|
||||
|
||||
export interface ScheduledAllowedMentions {
|
||||
parse?: Array<'users' | 'roles' | 'everyone'>;
|
||||
users?: Array<string>;
|
||||
roles?: Array<string>;
|
||||
replied_user?: boolean;
|
||||
}
|
||||
|
||||
export interface ScheduledAttachment {
|
||||
id: string;
|
||||
filename: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
flags?: number;
|
||||
}
|
||||
|
||||
export interface ScheduledMessagePayload {
|
||||
content?: string | null;
|
||||
embeds?: Array<unknown>;
|
||||
attachments?: Array<ScheduledAttachment>;
|
||||
message_reference?: ScheduledMessageReference;
|
||||
allowed_mentions?: ScheduledAllowedMentions;
|
||||
flags?: number;
|
||||
nonce?: string;
|
||||
favorite_meme_id?: string;
|
||||
sticker_ids?: Array<string>;
|
||||
tts?: boolean;
|
||||
}
|
||||
|
||||
export type ScheduledMessageStatus = 'pending' | 'invalid';
|
||||
|
||||
export interface ScheduledMessageResponse {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
scheduled_at: string;
|
||||
scheduled_local_at: string;
|
||||
timezone: string;
|
||||
status: ScheduledMessageStatus;
|
||||
status_reason: string | null;
|
||||
payload: ScheduledMessagePayload;
|
||||
created_at: string;
|
||||
invalidated_at: string | null;
|
||||
}
|
||||
|
||||
export class ScheduledMessageRecord {
|
||||
readonly id: string;
|
||||
readonly channelId: string;
|
||||
readonly scheduledAt: Date;
|
||||
readonly scheduledLocalAt: string;
|
||||
readonly timezone: string;
|
||||
readonly payload: ScheduledMessagePayload;
|
||||
readonly status: ScheduledMessageStatus;
|
||||
readonly statusReason: string | null;
|
||||
readonly createdAt: Date;
|
||||
readonly invalidatedAt: Date | null;
|
||||
|
||||
constructor(response: ScheduledMessageResponse) {
|
||||
this.id = response.id;
|
||||
this.channelId = response.channel_id;
|
||||
this.scheduledAt = new Date(response.scheduled_at);
|
||||
this.scheduledLocalAt = response.scheduled_local_at;
|
||||
this.timezone = response.timezone;
|
||||
this.payload = response.payload;
|
||||
this.status = response.status ?? 'pending';
|
||||
this.statusReason = response.status_reason;
|
||||
this.createdAt = new Date(response.created_at);
|
||||
this.invalidatedAt = response.invalidated_at ? new Date(response.invalidated_at) : null;
|
||||
}
|
||||
|
||||
static fromResponse(response: ScheduledMessageResponse): ScheduledMessageRecord {
|
||||
return new ScheduledMessageRecord(response);
|
||||
}
|
||||
}
|
||||
45
fluxer_app/src/records/UserGuildSettingsRecord.tsx
Normal file
45
fluxer_app/src/records/UserGuildSettingsRecord.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
export type MuteConfig = Readonly<{
|
||||
end_time?: string | null;
|
||||
selected_time_window?: number;
|
||||
}> | null;
|
||||
|
||||
export type ChannelOverride = Readonly<{
|
||||
collapsed: boolean;
|
||||
message_notifications: number;
|
||||
muted: boolean;
|
||||
mute_config?: MuteConfig;
|
||||
}>;
|
||||
|
||||
export type UserGuildSettings = Readonly<{
|
||||
guild_id: string | null;
|
||||
message_notifications: number;
|
||||
muted: boolean;
|
||||
mute_config?: MuteConfig;
|
||||
mobile_push: boolean;
|
||||
suppress_everyone: boolean;
|
||||
suppress_roles: boolean;
|
||||
hide_muted_channels: boolean;
|
||||
channel_overrides?: Record<string, ChannelOverride> | null;
|
||||
version: number;
|
||||
}>;
|
||||
|
||||
export type UserGuildSettingsPartial = Partial<Omit<UserGuildSettings, 'guild_id' | 'version'>>;
|
||||
642
fluxer_app/src/records/UserRecord.tsx
Normal file
642
fluxer_app/src/records/UserRecord.tsx
Normal file
@@ -0,0 +1,642 @@
|
||||
/*
|
||||
* 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 {
|
||||
MAX_BIO_LENGTH_NON_PREMIUM,
|
||||
MAX_BIO_LENGTH_PREMIUM,
|
||||
MAX_BOOKMARKS_NON_PREMIUM,
|
||||
MAX_BOOKMARKS_PREMIUM,
|
||||
MAX_FAVORITE_MEMES_NON_PREMIUM,
|
||||
MAX_FAVORITE_MEMES_PREMIUM,
|
||||
MAX_GUILDS_NON_PREMIUM,
|
||||
MAX_GUILDS_PREMIUM,
|
||||
MAX_MESSAGE_LENGTH_NON_PREMIUM,
|
||||
MAX_MESSAGE_LENGTH_PREMIUM,
|
||||
UserFlags,
|
||||
} from '~/Constants';
|
||||
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
|
||||
import RuntimeConfigStore from '~/stores/RuntimeConfigStore';
|
||||
import {getAttachmentMaxSize} from '~/utils/AttachmentUtils';
|
||||
import * as SnowflakeUtils from '~/utils/SnowflakeUtils';
|
||||
|
||||
export type BackupCode = Readonly<{
|
||||
code: string;
|
||||
consumed: boolean;
|
||||
}>;
|
||||
|
||||
export type UserProfile = Readonly<{
|
||||
bio: string | null;
|
||||
banner: string | null;
|
||||
banner_color?: number | null;
|
||||
pronouns: string | null;
|
||||
accent_color: string | null;
|
||||
}>;
|
||||
|
||||
export type UserPartial = Readonly<{
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
global_name?: string | null;
|
||||
avatar: string | null;
|
||||
avatar_color?: number | null;
|
||||
bot?: boolean;
|
||||
system?: boolean;
|
||||
flags: number;
|
||||
}>;
|
||||
|
||||
export type RequiredAction =
|
||||
| 'REQUIRE_VERIFIED_EMAIL'
|
||||
| 'REQUIRE_REVERIFIED_EMAIL'
|
||||
| 'REQUIRE_VERIFIED_PHONE'
|
||||
| 'REQUIRE_REVERIFIED_PHONE'
|
||||
| 'REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE'
|
||||
| 'REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE'
|
||||
| 'REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE'
|
||||
| 'REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE';
|
||||
|
||||
export type UserPrivate = Readonly<
|
||||
UserPartial &
|
||||
UserProfile & {
|
||||
email: string | null;
|
||||
mfa_enabled: boolean;
|
||||
phone: string | null;
|
||||
authenticator_types: Array<number>;
|
||||
verified: boolean;
|
||||
premium_type: number | null;
|
||||
premium_since: string | null;
|
||||
premium_until: string | null;
|
||||
premium_will_cancel: boolean;
|
||||
premium_billing_cycle: string | null;
|
||||
premium_lifetime_sequence: number | null;
|
||||
premium_badge_hidden: boolean;
|
||||
premium_badge_masked: boolean;
|
||||
premium_badge_timestamp_hidden: boolean;
|
||||
premium_badge_sequence_hidden: boolean;
|
||||
premium_purchase_disabled: boolean;
|
||||
premium_enabled_override: boolean;
|
||||
password_last_changed_at: string | null;
|
||||
required_actions: Array<RequiredAction> | null;
|
||||
nsfw_allowed: boolean;
|
||||
pending_manual_verification: boolean;
|
||||
pending_bulk_message_deletion: {
|
||||
scheduled_at: string;
|
||||
channel_count: number;
|
||||
message_count: number;
|
||||
} | null;
|
||||
has_dismissed_premium_onboarding: boolean;
|
||||
has_ever_purchased: boolean;
|
||||
has_unread_gift_inventory: boolean;
|
||||
unread_gift_inventory_count: number;
|
||||
used_mobile_client: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export type User = Readonly<UserPartial & Partial<UserPrivate>>;
|
||||
|
||||
export type PendingBulkMessageDeletion = Readonly<{
|
||||
scheduledAt: Date;
|
||||
channelCount: number;
|
||||
messageCount: number;
|
||||
}>;
|
||||
|
||||
export class UserRecord {
|
||||
readonly id: string;
|
||||
readonly username: string;
|
||||
readonly discriminator: string;
|
||||
readonly globalName: string | null;
|
||||
readonly avatar: string | null;
|
||||
readonly avatarColor?: number | null;
|
||||
readonly bot: boolean;
|
||||
readonly system: boolean;
|
||||
readonly flags: number;
|
||||
private readonly _email?: string | null;
|
||||
readonly bio?: string | null;
|
||||
readonly banner?: string | null;
|
||||
readonly bannerColor?: number | null;
|
||||
readonly pronouns?: string | null;
|
||||
readonly accentColor?: string | null;
|
||||
readonly mfaEnabled?: boolean;
|
||||
readonly phone?: string | null;
|
||||
readonly authenticatorTypes?: Array<number>;
|
||||
private readonly _verified?: boolean;
|
||||
private readonly _premiumType?: number | null;
|
||||
private readonly _premiumSince?: Date | null;
|
||||
private readonly _premiumUntil?: Date | null;
|
||||
private readonly _premiumWillCancel?: boolean;
|
||||
private readonly _premiumBillingCycle?: string | null;
|
||||
readonly premiumLifetimeSequence?: number | null;
|
||||
readonly premiumBadgeHidden?: boolean;
|
||||
readonly premiumBadgeMasked?: boolean;
|
||||
readonly premiumBadgeTimestampHidden?: boolean;
|
||||
readonly premiumBadgeSequenceHidden?: boolean;
|
||||
readonly premiumPurchaseDisabled?: boolean;
|
||||
readonly premiumEnabledOverride?: boolean;
|
||||
readonly passwordLastChangedAt?: Date | null;
|
||||
readonly requiredActions?: Array<RequiredAction> | null;
|
||||
readonly pendingManualVerification?: boolean;
|
||||
readonly pendingBulkMessageDeletion: PendingBulkMessageDeletion | null;
|
||||
private readonly _nsfwAllowed?: boolean;
|
||||
readonly hasDismissedPremiumOnboarding?: boolean;
|
||||
private readonly _hasEverPurchased?: boolean;
|
||||
private readonly _hasUnreadGiftInventory?: boolean;
|
||||
private readonly _unreadGiftInventoryCount?: number;
|
||||
private readonly _usedMobileClient?: boolean;
|
||||
|
||||
constructor(user: User) {
|
||||
this.id = user.id;
|
||||
this.username = user.username;
|
||||
this.discriminator = user.discriminator;
|
||||
this.globalName = user.global_name ?? null;
|
||||
this.avatar = user.avatar;
|
||||
if ('avatar_color' in user) this.avatarColor = user.avatar_color;
|
||||
this.bot = user.bot ?? false;
|
||||
this.system = user.system ?? false;
|
||||
this.flags = user.flags;
|
||||
|
||||
if ('email' in user) this._email = user.email;
|
||||
if ('bio' in user) this.bio = user.bio;
|
||||
if ('banner' in user) this.banner = user.banner;
|
||||
if ('banner_color' in user) this.bannerColor = user.banner_color;
|
||||
if ('pronouns' in user) this.pronouns = user.pronouns;
|
||||
if ('accent_color' in user) this.accentColor = user.accent_color;
|
||||
if ('mfa_enabled' in user) this.mfaEnabled = user.mfa_enabled;
|
||||
if ('phone' in user) this.phone = user.phone;
|
||||
if ('authenticator_types' in user) this.authenticatorTypes = user.authenticator_types;
|
||||
if ('verified' in user) this._verified = user.verified;
|
||||
if ('premium_type' in user) this._premiumType = user.premium_type;
|
||||
if ('premium_since' in user) this._premiumSince = user.premium_since ? new Date(user.premium_since) : null;
|
||||
if ('premium_until' in user) this._premiumUntil = user.premium_until ? new Date(user.premium_until) : null;
|
||||
if ('premium_will_cancel' in user) this._premiumWillCancel = user.premium_will_cancel;
|
||||
if ('premium_billing_cycle' in user) this._premiumBillingCycle = user.premium_billing_cycle;
|
||||
if ('premium_lifetime_sequence' in user) this.premiumLifetimeSequence = user.premium_lifetime_sequence;
|
||||
if ('premium_badge_hidden' in user) this.premiumBadgeHidden = user.premium_badge_hidden;
|
||||
if ('premium_badge_masked' in user) this.premiumBadgeMasked = user.premium_badge_masked;
|
||||
if ('premium_badge_timestamp_hidden' in user)
|
||||
this.premiumBadgeTimestampHidden = user.premium_badge_timestamp_hidden;
|
||||
if ('premium_badge_sequence_hidden' in user) this.premiumBadgeSequenceHidden = user.premium_badge_sequence_hidden;
|
||||
if ('premium_purchase_disabled' in user) this.premiumPurchaseDisabled = user.premium_purchase_disabled;
|
||||
if ('premium_enabled_override' in user) this.premiumEnabledOverride = user.premium_enabled_override;
|
||||
if ('password_last_changed_at' in user)
|
||||
this.passwordLastChangedAt = user.password_last_changed_at ? new Date(user.password_last_changed_at) : null;
|
||||
if ('required_actions' in user) {
|
||||
this.requiredActions = user.required_actions && user.required_actions.length > 0 ? user.required_actions : null;
|
||||
}
|
||||
if ('pending_manual_verification' in user) this.pendingManualVerification = user.pending_manual_verification;
|
||||
if ('pending_bulk_message_deletion' in user && user.pending_bulk_message_deletion) {
|
||||
this.pendingBulkMessageDeletion = {
|
||||
scheduledAt: new Date(user.pending_bulk_message_deletion.scheduled_at),
|
||||
channelCount: user.pending_bulk_message_deletion.channel_count,
|
||||
messageCount: user.pending_bulk_message_deletion.message_count,
|
||||
};
|
||||
} else {
|
||||
this.pendingBulkMessageDeletion = null;
|
||||
}
|
||||
if ('nsfw_allowed' in user) this._nsfwAllowed = user.nsfw_allowed;
|
||||
if ('has_dismissed_premium_onboarding' in user)
|
||||
this.hasDismissedPremiumOnboarding = user.has_dismissed_premium_onboarding;
|
||||
if ('has_ever_purchased' in user) this._hasEverPurchased = user.has_ever_purchased;
|
||||
if ('has_unread_gift_inventory' in user) this._hasUnreadGiftInventory = user.has_unread_gift_inventory;
|
||||
if ('unread_gift_inventory_count' in user) this._unreadGiftInventoryCount = user.unread_gift_inventory_count;
|
||||
if ('used_mobile_client' in user) this._usedMobileClient = user.used_mobile_client;
|
||||
}
|
||||
|
||||
get email(): string | null | undefined {
|
||||
if (DeveloperOptionsStore.unclaimedAccountOverride === true) {
|
||||
return null;
|
||||
}
|
||||
return this._email;
|
||||
}
|
||||
|
||||
get verified(): boolean | undefined {
|
||||
const verifiedOverride = DeveloperOptionsStore.emailVerifiedOverride;
|
||||
if (verifiedOverride != null) {
|
||||
return verifiedOverride;
|
||||
}
|
||||
return this._verified;
|
||||
}
|
||||
|
||||
get premiumType(): number | null {
|
||||
const override = DeveloperOptionsStore.premiumTypeOverride;
|
||||
return override != null ? override : (this._premiumType ?? null);
|
||||
}
|
||||
|
||||
get premiumSince(): Date | null | undefined {
|
||||
const override = DeveloperOptionsStore.premiumSinceOverride;
|
||||
return override != null ? override : this._premiumSince;
|
||||
}
|
||||
|
||||
get premiumUntil(): Date | null | undefined {
|
||||
const override = DeveloperOptionsStore.premiumUntilOverride;
|
||||
return override != null ? override : this._premiumUntil;
|
||||
}
|
||||
|
||||
get premiumBillingCycle(): string | null | undefined {
|
||||
const override = DeveloperOptionsStore.premiumBillingCycleOverride;
|
||||
return override != null ? override : this._premiumBillingCycle;
|
||||
}
|
||||
|
||||
get premiumWillCancel(): boolean | undefined {
|
||||
const override = DeveloperOptionsStore.premiumWillCancelOverride;
|
||||
return override != null ? override : this._premiumWillCancel;
|
||||
}
|
||||
|
||||
getPendingBulkMessageDeletion(): PendingBulkMessageDeletion | null {
|
||||
return this.pendingBulkMessageDeletion;
|
||||
}
|
||||
|
||||
hasPendingBulkMessageDeletion(): boolean {
|
||||
return this.pendingBulkMessageDeletion != null;
|
||||
}
|
||||
|
||||
get hasEverPurchased(): boolean | undefined {
|
||||
const override = DeveloperOptionsStore.hasEverPurchasedOverride;
|
||||
return override != null ? override : this._hasEverPurchased;
|
||||
}
|
||||
|
||||
get hasUnreadGiftInventory(): boolean | undefined {
|
||||
const override = DeveloperOptionsStore.hasUnreadGiftInventoryOverride;
|
||||
return override != null ? override : this._hasUnreadGiftInventory;
|
||||
}
|
||||
|
||||
get unreadGiftInventoryCount(): number | undefined {
|
||||
const override = DeveloperOptionsStore.unreadGiftInventoryCountOverride;
|
||||
return override != null ? override : this._unreadGiftInventoryCount;
|
||||
}
|
||||
|
||||
get nsfwAllowed(): boolean | undefined {
|
||||
return this._nsfwAllowed;
|
||||
}
|
||||
|
||||
get usedMobileClient(): boolean | undefined {
|
||||
return this._usedMobileClient;
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<User>): UserRecord {
|
||||
const baseFields: UserPartial = {
|
||||
id: updates.id ?? this.id,
|
||||
username: updates.username ?? this.username,
|
||||
discriminator: updates.discriminator ?? this.discriminator,
|
||||
avatar: 'avatar' in updates ? (updates.avatar as string | null) : this.avatar,
|
||||
bot: updates.bot ?? this.bot,
|
||||
system: updates.system ?? this.system,
|
||||
flags: updates.flags ?? this.flags,
|
||||
};
|
||||
|
||||
const pendingBulkMessageDeletionValue =
|
||||
'pending_bulk_message_deletion' in updates
|
||||
? updates.pending_bulk_message_deletion
|
||||
: this.pendingBulkMessageDeletion
|
||||
? {
|
||||
scheduled_at: this.pendingBulkMessageDeletion.scheduledAt.toISOString(),
|
||||
channel_count: this.pendingBulkMessageDeletion.channelCount,
|
||||
message_count: this.pendingBulkMessageDeletion.messageCount,
|
||||
}
|
||||
: null;
|
||||
|
||||
const privateFields: Partial<UserPrivate> = {
|
||||
...(this._email !== undefined || updates.email !== undefined ? {email: updates.email ?? this._email} : {}),
|
||||
...(this.bio !== undefined || 'bio' in updates
|
||||
? {bio: 'bio' in updates && updates.bio !== undefined ? (updates.bio as string | null) : this.bio}
|
||||
: {}),
|
||||
...(this.avatarColor !== undefined || 'avatar_color' in updates
|
||||
? {
|
||||
avatar_color:
|
||||
'avatar_color' in updates && updates.avatar_color !== undefined
|
||||
? (updates.avatar_color as number | null)
|
||||
: this.avatarColor,
|
||||
}
|
||||
: {}),
|
||||
...(this.banner !== undefined || 'banner' in updates
|
||||
? {
|
||||
banner:
|
||||
'banner' in updates && updates.banner !== undefined ? (updates.banner as string | null) : this.banner,
|
||||
}
|
||||
: {}),
|
||||
...(this.bannerColor !== undefined || 'banner_color' in updates
|
||||
? {
|
||||
banner_color:
|
||||
'banner_color' in updates && updates.banner_color !== undefined
|
||||
? (updates.banner_color as number | null)
|
||||
: this.bannerColor,
|
||||
}
|
||||
: {}),
|
||||
...(this.globalName !== undefined || 'global_name' in updates
|
||||
? {
|
||||
global_name:
|
||||
'global_name' in updates && updates.global_name !== undefined
|
||||
? (updates.global_name as string | null)
|
||||
: this.globalName,
|
||||
}
|
||||
: {}),
|
||||
...(this.pronouns !== undefined || 'pronouns' in updates
|
||||
? {
|
||||
pronouns:
|
||||
'pronouns' in updates && updates.pronouns !== undefined
|
||||
? (updates.pronouns as string | null)
|
||||
: this.pronouns,
|
||||
}
|
||||
: {}),
|
||||
...(this.accentColor !== undefined || 'accent_color' in updates
|
||||
? {
|
||||
accent_color:
|
||||
'accent_color' in updates && updates.accent_color !== undefined
|
||||
? (updates.accent_color as string | null)
|
||||
: this.accentColor,
|
||||
}
|
||||
: {}),
|
||||
...(this.mfaEnabled !== undefined || updates.mfa_enabled !== undefined
|
||||
? {mfa_enabled: updates.mfa_enabled ?? this.mfaEnabled}
|
||||
: {}),
|
||||
...(this.phone !== undefined || 'phone' in updates
|
||||
? {phone: 'phone' in updates && updates.phone !== undefined ? (updates.phone as string | null) : this.phone}
|
||||
: {}),
|
||||
...(this.authenticatorTypes !== undefined || updates.authenticator_types !== undefined
|
||||
? {authenticator_types: updates.authenticator_types ?? this.authenticatorTypes}
|
||||
: {}),
|
||||
...(this._verified !== undefined || updates.verified !== undefined
|
||||
? {verified: updates.verified ?? this._verified}
|
||||
: {}),
|
||||
...(this._premiumType !== undefined || updates.premium_type !== undefined
|
||||
? {premium_type: updates.premium_type ?? this._premiumType}
|
||||
: {}),
|
||||
...(this._premiumSince !== undefined || updates.premium_since !== undefined
|
||||
? {premium_since: updates.premium_since ?? (this._premiumSince ? this._premiumSince.toISOString() : null)}
|
||||
: {}),
|
||||
...(this._premiumUntil !== undefined || updates.premium_until !== undefined
|
||||
? {premium_until: updates.premium_until ?? (this._premiumUntil ? this._premiumUntil.toISOString() : null)}
|
||||
: {}),
|
||||
...(this._premiumWillCancel !== undefined || updates.premium_will_cancel !== undefined
|
||||
? {premium_will_cancel: updates.premium_will_cancel ?? this._premiumWillCancel}
|
||||
: {}),
|
||||
...(this._premiumBillingCycle !== undefined || updates.premium_billing_cycle !== undefined
|
||||
? {premium_billing_cycle: updates.premium_billing_cycle ?? this._premiumBillingCycle}
|
||||
: {}),
|
||||
...(this.premiumLifetimeSequence !== undefined || updates.premium_lifetime_sequence !== undefined
|
||||
? {premium_lifetime_sequence: updates.premium_lifetime_sequence ?? this.premiumLifetimeSequence}
|
||||
: {}),
|
||||
...(this.premiumBadgeHidden !== undefined || updates.premium_badge_hidden !== undefined
|
||||
? {premium_badge_hidden: updates.premium_badge_hidden ?? this.premiumBadgeHidden}
|
||||
: {}),
|
||||
...(this.premiumBadgeMasked !== undefined || updates.premium_badge_masked !== undefined
|
||||
? {premium_badge_masked: updates.premium_badge_masked ?? this.premiumBadgeMasked}
|
||||
: {}),
|
||||
...(this.premiumBadgeTimestampHidden !== undefined || updates.premium_badge_timestamp_hidden !== undefined
|
||||
? {premium_badge_timestamp_hidden: updates.premium_badge_timestamp_hidden ?? this.premiumBadgeTimestampHidden}
|
||||
: {}),
|
||||
...(this.premiumBadgeSequenceHidden !== undefined || updates.premium_badge_sequence_hidden !== undefined
|
||||
? {premium_badge_sequence_hidden: updates.premium_badge_sequence_hidden ?? this.premiumBadgeSequenceHidden}
|
||||
: {}),
|
||||
...(this.premiumPurchaseDisabled !== undefined || updates.premium_purchase_disabled !== undefined
|
||||
? {premium_purchase_disabled: updates.premium_purchase_disabled ?? this.premiumPurchaseDisabled}
|
||||
: {}),
|
||||
...(this.premiumEnabledOverride !== undefined || updates.premium_enabled_override !== undefined
|
||||
? {premium_enabled_override: updates.premium_enabled_override ?? this.premiumEnabledOverride}
|
||||
: {}),
|
||||
...(this.passwordLastChangedAt !== undefined || updates.password_last_changed_at !== undefined
|
||||
? {
|
||||
password_last_changed_at:
|
||||
updates.password_last_changed_at ?? this.passwordLastChangedAt?.toISOString() ?? null,
|
||||
}
|
||||
: {}),
|
||||
...(this.pendingManualVerification !== undefined || updates.pending_manual_verification !== undefined
|
||||
? {
|
||||
pending_manual_verification: updates.pending_manual_verification ?? this.pendingManualVerification,
|
||||
}
|
||||
: {}),
|
||||
pending_bulk_message_deletion: pendingBulkMessageDeletionValue,
|
||||
...(this.requiredActions !== undefined || 'required_actions' in updates
|
||||
? {
|
||||
required_actions:
|
||||
'required_actions' in updates
|
||||
? updates.required_actions && (updates.required_actions as Array<RequiredAction>).length > 0
|
||||
? (updates.required_actions as Array<RequiredAction>)
|
||||
: null
|
||||
: this.requiredActions,
|
||||
}
|
||||
: {}),
|
||||
...(this._nsfwAllowed !== undefined || updates.nsfw_allowed !== undefined
|
||||
? {nsfw_allowed: updates.nsfw_allowed ?? this._nsfwAllowed}
|
||||
: {}),
|
||||
...(this.hasDismissedPremiumOnboarding !== undefined || updates.has_dismissed_premium_onboarding !== undefined
|
||||
? {
|
||||
has_dismissed_premium_onboarding:
|
||||
updates.has_dismissed_premium_onboarding ?? this.hasDismissedPremiumOnboarding,
|
||||
}
|
||||
: {}),
|
||||
...(this._hasEverPurchased !== undefined || updates.has_ever_purchased !== undefined
|
||||
? {has_ever_purchased: updates.has_ever_purchased ?? this._hasEverPurchased}
|
||||
: {}),
|
||||
...(this._hasUnreadGiftInventory !== undefined || updates.has_unread_gift_inventory !== undefined
|
||||
? {has_unread_gift_inventory: updates.has_unread_gift_inventory ?? this._hasUnreadGiftInventory}
|
||||
: {}),
|
||||
...(this._unreadGiftInventoryCount !== undefined || updates.unread_gift_inventory_count !== undefined
|
||||
? {unread_gift_inventory_count: updates.unread_gift_inventory_count ?? this._unreadGiftInventoryCount}
|
||||
: {}),
|
||||
...(this._usedMobileClient !== undefined || updates.used_mobile_client !== undefined
|
||||
? {used_mobile_client: updates.used_mobile_client ?? this._usedMobileClient}
|
||||
: {}),
|
||||
};
|
||||
|
||||
return new UserRecord({
|
||||
...baseFields,
|
||||
...privateFields,
|
||||
});
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
return this.globalName || this.username;
|
||||
}
|
||||
|
||||
get tag(): string {
|
||||
return `${this.username}#${this.discriminator}`;
|
||||
}
|
||||
|
||||
get createdAt(): Date {
|
||||
return new Date(SnowflakeUtils.extractTimestamp(this.id));
|
||||
}
|
||||
|
||||
isPremium(): boolean {
|
||||
if (RuntimeConfigStore.isSelfHosted()) {
|
||||
return true;
|
||||
}
|
||||
return this.premiumType != null && this.premiumType > 0;
|
||||
}
|
||||
|
||||
get maxGuilds(): number {
|
||||
return this.isPremium() ? MAX_GUILDS_PREMIUM : MAX_GUILDS_NON_PREMIUM;
|
||||
}
|
||||
|
||||
get maxMessageLength(): number {
|
||||
return this.isPremium() ? MAX_MESSAGE_LENGTH_PREMIUM : MAX_MESSAGE_LENGTH_NON_PREMIUM;
|
||||
}
|
||||
|
||||
get maxAttachmentSize(): number {
|
||||
return getAttachmentMaxSize(this.isPremium());
|
||||
}
|
||||
|
||||
get maxBioLength(): number {
|
||||
return this.isPremium() ? MAX_BIO_LENGTH_PREMIUM : MAX_BIO_LENGTH_NON_PREMIUM;
|
||||
}
|
||||
|
||||
get maxBookmarks(): number {
|
||||
return this.isPremium() ? MAX_BOOKMARKS_PREMIUM : MAX_BOOKMARKS_NON_PREMIUM;
|
||||
}
|
||||
|
||||
get maxFavoriteMemes(): number {
|
||||
return this.isPremium() ? MAX_FAVORITE_MEMES_PREMIUM : MAX_FAVORITE_MEMES_NON_PREMIUM;
|
||||
}
|
||||
|
||||
isStaff(): boolean {
|
||||
return (this.flags & UserFlags.STAFF) !== 0;
|
||||
}
|
||||
|
||||
isClaimed(): boolean {
|
||||
return !!this.email;
|
||||
}
|
||||
|
||||
equals(other: UserRecord): boolean {
|
||||
return (
|
||||
this.id === other.id &&
|
||||
this.username === other.username &&
|
||||
this.discriminator === other.discriminator &&
|
||||
this.avatar === other.avatar &&
|
||||
this.avatarColor === other.avatarColor &&
|
||||
this.bot === other.bot &&
|
||||
this.system === other.system &&
|
||||
this.flags === other.flags &&
|
||||
this._email === other._email &&
|
||||
this.bio === other.bio &&
|
||||
this.banner === other.banner &&
|
||||
this.bannerColor === other.bannerColor &&
|
||||
this.pronouns === other.pronouns &&
|
||||
this.mfaEnabled === other.mfaEnabled &&
|
||||
this.phone === other.phone &&
|
||||
JSON.stringify(this.authenticatorTypes) === JSON.stringify(other.authenticatorTypes) &&
|
||||
this._verified === other._verified &&
|
||||
this._premiumType === other._premiumType &&
|
||||
this.premiumSince?.getTime() === other.premiumSince?.getTime() &&
|
||||
this.premiumUntil?.getTime() === other.premiumUntil?.getTime() &&
|
||||
this.premiumWillCancel === other.premiumWillCancel &&
|
||||
this._premiumBillingCycle === other._premiumBillingCycle &&
|
||||
this.premiumLifetimeSequence === other.premiumLifetimeSequence &&
|
||||
this.premiumBadgeHidden === other.premiumBadgeHidden &&
|
||||
this.premiumBadgeMasked === other.premiumBadgeMasked &&
|
||||
this.premiumBadgeTimestampHidden === other.premiumBadgeTimestampHidden &&
|
||||
this.premiumBadgeSequenceHidden === other.premiumBadgeSequenceHidden &&
|
||||
this.premiumPurchaseDisabled === other.premiumPurchaseDisabled &&
|
||||
this.premiumEnabledOverride === other.premiumEnabledOverride &&
|
||||
this.passwordLastChangedAt?.getTime() === other.passwordLastChangedAt?.getTime() &&
|
||||
JSON.stringify(this.requiredActions) === JSON.stringify(other.requiredActions) &&
|
||||
this.pendingManualVerification === other.pendingManualVerification &&
|
||||
((this.pendingBulkMessageDeletion === null && other.pendingBulkMessageDeletion === null) ||
|
||||
(this.pendingBulkMessageDeletion != null &&
|
||||
other.pendingBulkMessageDeletion != null &&
|
||||
this.pendingBulkMessageDeletion.channelCount === other.pendingBulkMessageDeletion.channelCount &&
|
||||
this.pendingBulkMessageDeletion.messageCount === other.pendingBulkMessageDeletion.messageCount &&
|
||||
this.pendingBulkMessageDeletion.scheduledAt.getTime() ===
|
||||
other.pendingBulkMessageDeletion.scheduledAt.getTime())) &&
|
||||
this._nsfwAllowed === other._nsfwAllowed &&
|
||||
this.hasDismissedPremiumOnboarding === other.hasDismissedPremiumOnboarding &&
|
||||
this._hasUnreadGiftInventory === other._hasUnreadGiftInventory &&
|
||||
this._unreadGiftInventoryCount === other._unreadGiftInventoryCount &&
|
||||
this._usedMobileClient === other._usedMobileClient
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): User {
|
||||
const normalizeDate = (value: Date | string | number | null | undefined): string | null => {
|
||||
if (value === null || value === undefined) return null;
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
||||
};
|
||||
|
||||
const baseFields: UserPartial = {
|
||||
id: this.id,
|
||||
username: this.username,
|
||||
discriminator: this.discriminator,
|
||||
global_name: this.globalName,
|
||||
avatar: this.avatar,
|
||||
bot: this.bot,
|
||||
system: this.system,
|
||||
flags: this.flags,
|
||||
};
|
||||
|
||||
const privateFields: Partial<UserPrivate> = {
|
||||
...(this._email !== undefined ? {email: this._email} : {}),
|
||||
...(this.bio !== undefined ? {bio: this.bio} : {}),
|
||||
...(this.banner !== undefined ? {banner: this.banner} : {}),
|
||||
...(this.avatarColor !== undefined ? {avatar_color: this.avatarColor} : {}),
|
||||
...(this.bannerColor !== undefined ? {banner_color: this.bannerColor} : {}),
|
||||
...(this.pronouns !== undefined ? {pronouns: this.pronouns} : {}),
|
||||
...(this.accentColor !== undefined ? {accent_color: this.accentColor} : {}),
|
||||
...(this.mfaEnabled !== undefined ? {mfa_enabled: this.mfaEnabled} : {}),
|
||||
...(this.phone !== undefined ? {phone: this.phone} : {}),
|
||||
...(this.authenticatorTypes !== undefined ? {authenticator_types: this.authenticatorTypes} : {}),
|
||||
...(this._verified !== undefined ? {verified: this._verified} : {}),
|
||||
...(this._premiumType !== undefined ? {premium_type: this._premiumType} : {}),
|
||||
...(this._premiumSince !== undefined ? {premium_since: normalizeDate(this._premiumSince)} : {}),
|
||||
...(this._premiumUntil !== undefined ? {premium_until: normalizeDate(this._premiumUntil)} : {}),
|
||||
...(this._premiumWillCancel !== undefined ? {premium_will_cancel: this._premiumWillCancel} : {}),
|
||||
...(this._premiumBillingCycle !== undefined ? {premium_billing_cycle: this._premiumBillingCycle} : {}),
|
||||
...(this.premiumLifetimeSequence !== undefined ? {premium_lifetime_sequence: this.premiumLifetimeSequence} : {}),
|
||||
...(this.premiumBadgeHidden !== undefined ? {premium_badge_hidden: this.premiumBadgeHidden} : {}),
|
||||
...(this.premiumBadgeMasked !== undefined ? {premium_badge_masked: this.premiumBadgeMasked} : {}),
|
||||
...(this.premiumBadgeTimestampHidden !== undefined
|
||||
? {premium_badge_timestamp_hidden: this.premiumBadgeTimestampHidden}
|
||||
: {}),
|
||||
...(this.premiumBadgeSequenceHidden !== undefined
|
||||
? {premium_badge_sequence_hidden: this.premiumBadgeSequenceHidden}
|
||||
: {}),
|
||||
...(this.premiumPurchaseDisabled !== undefined ? {premium_purchase_disabled: this.premiumPurchaseDisabled} : {}),
|
||||
...(this.premiumEnabledOverride !== undefined ? {premium_enabled_override: this.premiumEnabledOverride} : {}),
|
||||
...(this.passwordLastChangedAt !== undefined
|
||||
? {password_last_changed_at: normalizeDate(this.passwordLastChangedAt)}
|
||||
: {}),
|
||||
...(this.requiredActions !== undefined ? {required_actions: this.requiredActions} : {}),
|
||||
...(this.pendingManualVerification !== undefined
|
||||
? {pending_manual_verification: this.pendingManualVerification}
|
||||
: {}),
|
||||
...(this.pendingBulkMessageDeletion !== undefined
|
||||
? {
|
||||
pending_bulk_message_deletion: this.pendingBulkMessageDeletion
|
||||
? {
|
||||
scheduled_at: this.pendingBulkMessageDeletion.scheduledAt.toISOString(),
|
||||
channel_count: this.pendingBulkMessageDeletion.channelCount,
|
||||
message_count: this.pendingBulkMessageDeletion.messageCount,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
: {}),
|
||||
...(this._nsfwAllowed !== undefined ? {nsfw_allowed: this._nsfwAllowed} : {}),
|
||||
...(this.hasDismissedPremiumOnboarding !== undefined
|
||||
? {has_dismissed_premium_onboarding: this.hasDismissedPremiumOnboarding}
|
||||
: {}),
|
||||
...(this._hasUnreadGiftInventory !== undefined ? {has_unread_gift_inventory: this._hasUnreadGiftInventory} : {}),
|
||||
...(this._unreadGiftInventoryCount !== undefined
|
||||
? {unread_gift_inventory_count: this._unreadGiftInventoryCount}
|
||||
: {}),
|
||||
...(this._usedMobileClient !== undefined ? {used_mobile_client: this._usedMobileClient} : {}),
|
||||
};
|
||||
|
||||
return {
|
||||
...baseFields,
|
||||
...privateFields,
|
||||
};
|
||||
}
|
||||
}
|
||||
95
fluxer_app/src/records/WebhookRecord.tsx
Normal file
95
fluxer_app/src/records/WebhookRecord.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 {UserPartial, UserRecord} from '~/records/UserRecord';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import * as SnowflakeUtils from '~/utils/SnowflakeUtils';
|
||||
import {webhookUrl} from '~/utils/UrlUtils';
|
||||
|
||||
export type Webhook = Readonly<{
|
||||
id: string;
|
||||
guild_id: string;
|
||||
channel_id: string;
|
||||
user: UserPartial;
|
||||
name: string;
|
||||
avatar: string | null;
|
||||
token: string;
|
||||
}>;
|
||||
|
||||
export class WebhookRecord {
|
||||
readonly id: string;
|
||||
readonly guildId: string;
|
||||
readonly channelId: string;
|
||||
readonly name: string;
|
||||
readonly avatar: string | null;
|
||||
readonly token: string;
|
||||
readonly creatorId: string;
|
||||
readonly createdAt: Date;
|
||||
private readonly creatorSnapshot: UserPartial;
|
||||
|
||||
constructor(webhook: Webhook) {
|
||||
this.id = webhook.id;
|
||||
this.guildId = webhook.guild_id;
|
||||
this.channelId = webhook.channel_id;
|
||||
this.name = webhook.name;
|
||||
this.avatar = webhook.avatar ?? null;
|
||||
this.token = webhook.token;
|
||||
this.creatorId = webhook.user.id;
|
||||
this.createdAt = new Date(SnowflakeUtils.extractTimestamp(webhook.id));
|
||||
this.creatorSnapshot = webhook.user;
|
||||
UserStore.cacheUsers([webhook.user]);
|
||||
}
|
||||
|
||||
get webhookUrl(): string {
|
||||
return webhookUrl(this.id, this.token);
|
||||
}
|
||||
|
||||
get creator(): UserRecord | null {
|
||||
return UserStore.getUser(this.creatorId)!;
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
withUpdates(updates: Partial<Webhook>): WebhookRecord {
|
||||
return new WebhookRecord({
|
||||
id: updates.id ?? this.id,
|
||||
guild_id: updates.guild_id ?? this.guildId,
|
||||
channel_id: updates.channel_id ?? this.channelId,
|
||||
user: updates.user ?? this.creatorSnapshot,
|
||||
name: updates.name ?? this.name,
|
||||
avatar: updates.avatar ?? this.avatar,
|
||||
token: updates.token ?? this.token,
|
||||
});
|
||||
}
|
||||
|
||||
toJSON(): Webhook {
|
||||
const creator = this.creator;
|
||||
return {
|
||||
id: this.id,
|
||||
guild_id: this.guildId,
|
||||
channel_id: this.channelId,
|
||||
user: creator ? creator.toJSON() : this.creatorSnapshot,
|
||||
name: this.name,
|
||||
avatar: this.avatar,
|
||||
token: this.token,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user