diff options
Diffstat (limited to 'src/util/entities')
24 files changed, 819 insertions, 522 deletions
diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts index 7b4b17eb..055b6f4b 100644 --- a/src/util/entities/Attachment.ts +++ b/src/util/entities/Attachment.ts @@ -1,4 +1,11 @@ -import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { + BeforeRemove, + Column, + Entity, + JoinColumn, + ManyToOne, + RelationId, +} from "typeorm"; import { URL } from "url"; import { deleteFile } from "../util/cdn"; import { BaseClass } from "./BaseClass"; @@ -31,9 +38,13 @@ export class Attachment extends BaseClass { message_id: string; @JoinColumn({ name: "message_id" }) - @ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, { - onDelete: "CASCADE", - }) + @ManyToOne( + () => require("./Message").Message, + (message: import("./Message").Message) => message.attachments, + { + onDelete: "CASCADE", + }, + ) message: import("./Message").Message; @BeforeRemove() diff --git a/src/util/entities/AuditLog.ts b/src/util/entities/AuditLog.ts index b003e7ba..9cc97742 100644 --- a/src/util/entities/AuditLog.ts +++ b/src/util/entities/AuditLog.ts @@ -5,24 +5,24 @@ import { User } from "./User"; export enum AuditLogEvents { // guild level - GUILD_UPDATE = 1, + GUILD_UPDATE = 1, GUILD_IMPORT = 2, GUILD_EXPORTED = 3, GUILD_ARCHIVE = 4, GUILD_UNARCHIVE = 5, // join-leave - USER_JOIN = 6, + USER_JOIN = 6, USER_LEAVE = 7, // channels - CHANNEL_CREATE = 10, + CHANNEL_CREATE = 10, CHANNEL_UPDATE = 11, CHANNEL_DELETE = 12, // permission overrides - CHANNEL_OVERWRITE_CREATE = 13, + CHANNEL_OVERWRITE_CREATE = 13, CHANNEL_OVERWRITE_UPDATE = 14, CHANNEL_OVERWRITE_DELETE = 15, // kick and ban - MEMBER_KICK = 20, + MEMBER_KICK = 20, MEMBER_PRUNE = 21, MEMBER_BAN_ADD = 22, MEMBER_BAN_REMOVE = 23, @@ -79,17 +79,17 @@ export enum AuditLogEvents { // application commands APPLICATION_COMMAND_PERMISSION_UPDATE = 121, // automod - POLICY_CREATE = 140, + POLICY_CREATE = 140, POLICY_UPDATE = 141, POLICY_DELETE = 142, - MESSAGE_BLOCKED_BY_POLICIES = 143, // in fosscord, blocked messages are stealth-dropped + MESSAGE_BLOCKED_BY_POLICIES = 143, // in fosscord, blocked messages are stealth-dropped // instance policies affecting the guild GUILD_AFFECTED_BY_POLICIES = 216, // message moves IN_GUILD_MESSAGE_MOVE = 223, CROSS_GUILD_MESSAGE_MOVE = 224, // message routing - ROUTE_CREATE = 225, + ROUTE_CREATE = 225, ROUTE_UPDATE = 226, } diff --git a/src/util/entities/BackupCodes.ts b/src/util/entities/BackupCodes.ts index d532a39a..81cdbb6d 100644 --- a/src/util/entities/BackupCodes.ts +++ b/src/util/entities/BackupCodes.ts @@ -24,7 +24,7 @@ export function generateMfaBackupCodes(user_id: string) { for (let i = 0; i < 10; i++) { const code = BackupCode.create({ user: { id: user_id }, - code: crypto.randomBytes(4).toString("hex"), // 8 characters + code: crypto.randomBytes(4).toString("hex"), // 8 characters consumed: false, expired: false, }); @@ -32,4 +32,4 @@ export function generateMfaBackupCodes(user_id: string) { } return backup_codes; -} \ No newline at end of file +} diff --git a/src/util/entities/BaseClass.ts b/src/util/entities/BaseClass.ts index d5a7c2bf..9942b60e 100644 --- a/src/util/entities/BaseClass.ts +++ b/src/util/entities/BaseClass.ts @@ -1,5 +1,12 @@ import "reflect-metadata"; -import { BaseEntity, BeforeInsert, BeforeUpdate, FindOptionsWhere, ObjectIdColumn, PrimaryColumn } from "typeorm"; +import { + BaseEntity, + BeforeInsert, + BeforeUpdate, + FindOptionsWhere, + ObjectIdColumn, + PrimaryColumn, +} from "typeorm"; import { Snowflake } from "../util/Snowflake"; import "missing-native-js-functions"; import { getDatabase } from ".."; @@ -22,23 +29,40 @@ export class BaseClassWithoutId extends BaseEntity { toJSON(): any { return Object.fromEntries( this.metadata!.columns // @ts-ignore - .map((x) => [x.propertyName, this[x.propertyName]]) // @ts-ignore - .concat(this.metadata.relations.map((x) => [x.propertyName, this[x.propertyName]])) + .map((x) => [x.propertyName, this[x.propertyName]]) + .concat( + // @ts-ignore + this.metadata.relations.map((x) => [ + x.propertyName, + // @ts-ignore + this[x.propertyName], + ]), + ), ); } - static increment<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) { + static increment<T extends BaseClass>( + conditions: FindOptionsWhere<T>, + propertyPath: string, + value: number | string, + ) { const repository = this.getRepository(); return repository.increment(conditions, propertyPath, value); } - static decrement<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) { + static decrement<T extends BaseClass>( + conditions: FindOptionsWhere<T>, + propertyPath: string, + value: number | string, + ) { const repository = this.getRepository(); return repository.decrement(conditions, propertyPath, value); } } -export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") ? ObjectIdColumn : PrimaryColumn; +export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") + ? ObjectIdColumn + : PrimaryColumn; export class BaseClass extends BaseClassWithoutId { @PrimaryIdColumn() diff --git a/src/util/entities/Categories.ts b/src/util/entities/Categories.ts index 81fbc303..f12b237d 100644 --- a/src/util/entities/Categories.ts +++ b/src/util/entities/Categories.ts @@ -1,4 +1,4 @@ -import { PrimaryColumn, Column, Entity} from "typeorm"; +import { PrimaryColumn, Column, Entity } from "typeorm"; import { BaseClassWithoutId } from "./BaseClass"; // TODO: categories: @@ -16,18 +16,18 @@ import { BaseClassWithoutId } from "./BaseClass"; // Also populate discord default categories @Entity("categories") -export class Categories extends BaseClassWithoutId { // Not using snowflake - - @PrimaryColumn() - id: number; +export class Categories extends BaseClassWithoutId { + // Not using snowflake - @Column({ nullable: true }) - name: string; + @PrimaryColumn() + id: number; - @Column({ type: "simple-json" }) - localizations: string; + @Column({ nullable: true }) + name: string; - @Column({ nullable: true }) - is_primary: boolean; + @Column({ type: "simple-json" }) + localizations: string; -} \ No newline at end of file + @Column({ nullable: true }) + is_primary: boolean; +} diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index 2200bfa3..14f36857 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -1,389 +1,457 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; -import { BaseClass } from "./BaseClass"; -import { Guild } from "./Guild"; -import { PublicUserProjection, User } from "./User"; -import { HTTPError } from "lambert-server"; -import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters, ChannelTypes } from "../util"; -import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; -import { Recipient } from "./Recipient"; -import { Message } from "./Message"; -import { ReadState } from "./ReadState"; -import { Invite } from "./Invite"; -import { VoiceState } from "./VoiceState"; -import { Webhook } from "./Webhook"; -import { DmChannelDTO } from "../dtos"; - -export enum ChannelType { - GUILD_TEXT = 0, // a text channel within a guild - DM = 1, // a direct message between users - GUILD_VOICE = 2, // a voice channel within a guild - GROUP_DM = 3, // a direct message between multiple users - GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels - GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route - GUILD_STORE = 6, // a channel in which game developers can sell their things - ENCRYPTED = 7, // end-to-end encrypted channel - ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel - TRANSACTIONAL = 9, // event chain style transactional channel - GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel - GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel - GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission - GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience - DIRECTORY = 14, // guild directory listing channel - GUILD_FORUM = 15, // forum composed of IM threads - TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12 - KANBAN = 34, // confluence like kanban board - VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage) - CUSTOM_START = 64, // start custom channel types from here - UNHANDLED = 255 // unhandled unowned pass-through channel type -} - -@Entity("channels") -export class Channel extends BaseClass { - @Column() - created_at: Date; - - @Column({ nullable: true }) - name?: string; - - @Column({ type: "text", nullable: true }) - icon?: string | null; - - @Column({ type: "int" }) - type: ChannelType; - - @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - recipients?: Recipient[]; - - @Column({ nullable: true }) - last_message_id?: string; - - @Column({ nullable: true }) - @RelationId((channel: Channel) => channel.guild) - guild_id?: string; - - @JoinColumn({ name: "guild_id" }) - @ManyToOne(() => Guild, { - onDelete: "CASCADE", - }) - guild: Guild; - - @Column({ nullable: true }) - @RelationId((channel: Channel) => channel.parent) - parent_id: string; - - @JoinColumn({ name: "parent_id" }) - @ManyToOne(() => Channel) - parent?: Channel; - - // for group DMs and owned custom channel types - @Column({ nullable: true }) - @RelationId((channel: Channel) => channel.owner) - owner_id?: string; - - @JoinColumn({ name: "owner_id" }) - @ManyToOne(() => User) - owner: User; - - @Column({ nullable: true }) - last_pin_timestamp?: number; - - @Column({ nullable: true }) - default_auto_archive_duration?: number; - - @Column({ nullable: true }) - position?: number; - - @Column({ type: "simple-json", nullable: true }) - permission_overwrites?: ChannelPermissionOverwrite[]; - - @Column({ nullable: true }) - video_quality_mode?: number; - - @Column({ nullable: true }) - bitrate?: number; - - @Column({ nullable: true }) - user_limit?: number; - - @Column() - nsfw: boolean = false; - - @Column({ nullable: true }) - rate_limit_per_user?: number; - - @Column({ nullable: true }) - topic?: string; - - @OneToMany(() => Invite, (invite: Invite) => invite.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - invites?: Invite[]; - - @Column({ nullable: true }) - retention_policy_id?: string; - - @OneToMany(() => Message, (message: Message) => message.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - messages?: Message[]; - - @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - voice_states?: VoiceState[]; - - @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - read_states?: ReadState[]; - - @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { - cascade: true, - orphanedRowAction: "delete", - }) - webhooks?: Webhook[]; - - // TODO: DM channel - static async createChannel( - channel: Partial<Channel>, - user_id: string = "0", - opts?: { - keepId?: boolean; - skipExistsCheck?: boolean; - skipPermissionCheck?: boolean; - skipEventEmit?: boolean; - skipNameChecks?: boolean; - } - ) { - if (!opts?.skipPermissionCheck) { - // Always check if user has permission first - const permissions = await getPermission(user_id, channel.guild_id); - permissions.hasThrow("MANAGE_CHANNELS"); - } - - if (!opts?.skipNameChecks) { - const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } }); - if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) { - for (var character of InvisibleCharacters) - if (channel.name.includes(character)) - throw new HTTPError("Channel name cannot include invalid characters", 403); - - // Categories skip these checks on discord.com - if (channel.type !== ChannelType.GUILD_CATEGORY) { - if (channel.name.includes(" ")) - throw new HTTPError("Channel name cannot include invalid characters", 403); - - if (channel.name.match(/\-\-+/g)) - throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403); - - if (channel.name.charAt(0) === "-" || - channel.name.charAt(channel.name.length - 1) === "-") - throw new HTTPError("Channel name cannot start/end with dash.", 403); - } - else - channel.name = channel.name.trim(); //category names are trimmed client side on discord.com - } - - if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { - if (!channel.name) - throw new HTTPError("Channel name cannot be empty.", 403); - } - } - - switch (channel.type) { - case ChannelType.GUILD_TEXT: - case ChannelType.GUILD_NEWS: - case ChannelType.GUILD_VOICE: - if (channel.parent_id && !opts?.skipExistsCheck) { - const exists = await Channel.findOneOrFail({ where: { id: channel.parent_id } }); - if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400); - if (exists.guild_id !== channel.guild_id) - throw new HTTPError("The category channel needs to be in the guild"); - } - break; - case ChannelType.GUILD_CATEGORY: - case ChannelType.UNHANDLED: - break; - case ChannelType.DM: - case ChannelType.GROUP_DM: - throw new HTTPError("You can't create a dm channel in a guild"); - case ChannelType.GUILD_STORE: - default: - throw new HTTPError("Not yet supported"); - } - - if (!channel.permission_overwrites) channel.permission_overwrites = []; - // TODO: eagerly auto generate position of all guild channels - - channel = { - ...channel, - ...(!opts?.keepId && { id: Snowflake.generate() }), - created_at: new Date(), - position: (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0, - }; - - await Promise.all([ - Channel.create(channel).save(), - !opts?.skipEventEmit - ? emitEvent({ - event: "CHANNEL_CREATE", - data: channel, - guild_id: channel.guild_id, - } as ChannelCreateEvent) - : Promise.resolve(), - ]); - - return channel; - } - - static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) { - recipients = recipients.unique().filter((x) => x !== creator_user_id); - // TODO: check config for max number of recipients - /** if you want to disallow note to self channels, uncomment the conditional below - - const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); - if (otherRecipientsUsers.length !== recipients.length) { - throw new HTTPError("Recipient/s not found"); - } - **/ - - const type = recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM; - - let channel = null; - - const channelRecipients = [...recipients, creator_user_id]; - - const userRecipients = await Recipient.find({ - where: { user_id: creator_user_id }, - relations: ["channel", "channel.recipients"], - }); - - for (let ur of userRecipients) { - let re = ur.channel.recipients!.map((r) => r.user_id); - if (re.length === channelRecipients.length) { - if (containsAll(re, channelRecipients)) { - if (channel == null) { - channel = ur.channel; - await ur.assign({ closed: false }).save(); - } - } - } - } - - if (channel == null) { - name = trimSpecial(name); - - channel = await Channel.create({ - name, - type, - owner_id: undefined, - created_at: new Date(), - last_message_id: undefined, - recipients: channelRecipients.map( - (x) => - Recipient.create({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) }) - ), - nsfw: false, - }).save(); - } - - const channel_dto = await DmChannelDTO.from(channel); - - if (type === ChannelType.GROUP_DM) { - for (let recipient of channel.recipients!) { - await emitEvent({ - event: "CHANNEL_CREATE", - data: channel_dto.excludedRecipients([recipient.user_id]), - user_id: recipient.user_id, - }); - } - } else { - await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id }); - } - - if (recipients.length === 1) return channel_dto; - else return channel_dto.excludedRecipients([creator_user_id]); - } - - static async removeRecipientFromChannel(channel: Channel, user_id: string) { - await Recipient.delete({ channel_id: channel.id, user_id: user_id }); - channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id); - - if (channel.recipients?.length === 0) { - await Channel.deleteChannel(channel); - await emitEvent({ - event: "CHANNEL_DELETE", - data: await DmChannelDTO.from(channel, [user_id]), - user_id: user_id, - }); - return; - } - - await emitEvent({ - event: "CHANNEL_DELETE", - data: await DmChannelDTO.from(channel, [user_id]), - user_id: user_id, - }); - - //If the owner leave the server user is the new owner - if (channel.owner_id === user_id) { - channel.owner_id = "1"; // The channel is now owned by the server user - await emitEvent({ - event: "CHANNEL_UPDATE", - data: await DmChannelDTO.from(channel, [user_id]), - channel_id: channel.id, - }); - } - - await channel.save(); - - await emitEvent({ - event: "CHANNEL_RECIPIENT_REMOVE", - data: { - channel_id: channel.id, - user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }), - }, - channel_id: channel.id, - } as ChannelRecipientRemoveEvent); - } - - static async deleteChannel(channel: Channel) { - await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util - //TODO before deleting the channel we should check and delete other relations - await Channel.delete({ id: channel.id }); - } - - isDm() { - return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM; - } - - // Does the channel support sending messages ( eg categories do not ) - isWritable() { - const disallowedChannelTypes = [ - ChannelType.GUILD_CATEGORY, - ChannelType.GUILD_STAGE_VOICE, - ChannelType.VOICELESS_WHITEBOARD, - ]; - return disallowedChannelTypes.indexOf(this.type) == -1; - } -} - -export interface ChannelPermissionOverwrite { - allow: string; - deny: string; - id: string; - type: ChannelPermissionOverwriteType; -} - -export enum ChannelPermissionOverwriteType { - role = 0, - member = 1, - group = 2, -} +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + RelationId, +} from "typeorm"; +import { BaseClass } from "./BaseClass"; +import { Guild } from "./Guild"; +import { PublicUserProjection, User } from "./User"; +import { HTTPError } from "lambert-server"; +import { + containsAll, + emitEvent, + getPermission, + Snowflake, + trimSpecial, + InvisibleCharacters, + ChannelTypes, +} from "../util"; +import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; +import { Recipient } from "./Recipient"; +import { Message } from "./Message"; +import { ReadState } from "./ReadState"; +import { Invite } from "./Invite"; +import { VoiceState } from "./VoiceState"; +import { Webhook } from "./Webhook"; +import { DmChannelDTO } from "../dtos"; + +export enum ChannelType { + GUILD_TEXT = 0, // a text channel within a guild + DM = 1, // a direct message between users + GUILD_VOICE = 2, // a voice channel within a guild + GROUP_DM = 3, // a direct message between multiple users + GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels + GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route + GUILD_STORE = 6, // a channel in which game developers can sell their things + ENCRYPTED = 7, // end-to-end encrypted channel + ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel + TRANSACTIONAL = 9, // event chain style transactional channel + GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel + GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel + GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission + GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience + DIRECTORY = 14, // guild directory listing channel + GUILD_FORUM = 15, // forum composed of IM threads + TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12 + KANBAN = 34, // confluence like kanban board + VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage) + CUSTOM_START = 64, // start custom channel types from here + UNHANDLED = 255, // unhandled unowned pass-through channel type +} + +@Entity("channels") +export class Channel extends BaseClass { + @Column() + created_at: Date; + + @Column({ nullable: true }) + name?: string; + + @Column({ type: "text", nullable: true }) + icon?: string | null; + + @Column({ type: "int" }) + type: ChannelType; + + @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + recipients?: Recipient[]; + + @Column({ nullable: true }) + last_message_id?: string; + + @Column({ nullable: true }) + @RelationId((channel: Channel) => channel.guild) + guild_id?: string; + + @JoinColumn({ name: "guild_id" }) + @ManyToOne(() => Guild, { + onDelete: "CASCADE", + }) + guild: Guild; + + @Column({ nullable: true }) + @RelationId((channel: Channel) => channel.parent) + parent_id: string; + + @JoinColumn({ name: "parent_id" }) + @ManyToOne(() => Channel) + parent?: Channel; + + // for group DMs and owned custom channel types + @Column({ nullable: true }) + @RelationId((channel: Channel) => channel.owner) + owner_id?: string; + + @JoinColumn({ name: "owner_id" }) + @ManyToOne(() => User) + owner: User; + + @Column({ nullable: true }) + last_pin_timestamp?: number; + + @Column({ nullable: true }) + default_auto_archive_duration?: number; + + @Column({ nullable: true }) + position?: number; + + @Column({ type: "simple-json", nullable: true }) + permission_overwrites?: ChannelPermissionOverwrite[]; + + @Column({ nullable: true }) + video_quality_mode?: number; + + @Column({ nullable: true }) + bitrate?: number; + + @Column({ nullable: true }) + user_limit?: number; + + @Column() + nsfw: boolean = false; + + @Column({ nullable: true }) + rate_limit_per_user?: number; + + @Column({ nullable: true }) + topic?: string; + + @OneToMany(() => Invite, (invite: Invite) => invite.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + invites?: Invite[]; + + @Column({ nullable: true }) + retention_policy_id?: string; + + @OneToMany(() => Message, (message: Message) => message.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + messages?: Message[]; + + @OneToMany( + () => VoiceState, + (voice_state: VoiceState) => voice_state.channel, + { + cascade: true, + orphanedRowAction: "delete", + }, + ) + voice_states?: VoiceState[]; + + @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + read_states?: ReadState[]; + + @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { + cascade: true, + orphanedRowAction: "delete", + }) + webhooks?: Webhook[]; + + // TODO: DM channel + static async createChannel( + channel: Partial<Channel>, + user_id: string = "0", + opts?: { + keepId?: boolean; + skipExistsCheck?: boolean; + skipPermissionCheck?: boolean; + skipEventEmit?: boolean; + skipNameChecks?: boolean; + }, + ) { + if (!opts?.skipPermissionCheck) { + // Always check if user has permission first + const permissions = await getPermission(user_id, channel.guild_id); + permissions.hasThrow("MANAGE_CHANNELS"); + } + + if (!opts?.skipNameChecks) { + const guild = await Guild.findOneOrFail({ + where: { id: channel.guild_id }, + }); + if ( + !guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && + channel.name + ) { + for (var character of InvisibleCharacters) + if (channel.name.includes(character)) + throw new HTTPError( + "Channel name cannot include invalid characters", + 403, + ); + + // Categories skip these checks on discord.com + if (channel.type !== ChannelType.GUILD_CATEGORY) { + if (channel.name.includes(" ")) + throw new HTTPError( + "Channel name cannot include invalid characters", + 403, + ); + + if (channel.name.match(/\-\-+/g)) + throw new HTTPError( + "Channel name cannot include multiple adjacent dashes.", + 403, + ); + + if ( + channel.name.charAt(0) === "-" || + channel.name.charAt(channel.name.length - 1) === "-" + ) + throw new HTTPError( + "Channel name cannot start/end with dash.", + 403, + ); + } else channel.name = channel.name.trim(); //category names are trimmed client side on discord.com + } + + if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { + if (!channel.name) + throw new HTTPError("Channel name cannot be empty.", 403); + } + } + + switch (channel.type) { + case ChannelType.GUILD_TEXT: + case ChannelType.GUILD_NEWS: + case ChannelType.GUILD_VOICE: + if (channel.parent_id && !opts?.skipExistsCheck) { + const exists = await Channel.findOneOrFail({ + where: { id: channel.parent_id }, + }); + if (!exists) + throw new HTTPError( + "Parent id channel doesn't exist", + 400, + ); + if (exists.guild_id !== channel.guild_id) + throw new HTTPError( + "The category channel needs to be in the guild", + ); + } + break; + case ChannelType.GUILD_CATEGORY: + case ChannelType.UNHANDLED: + break; + case ChannelType.DM: + case ChannelType.GROUP_DM: + throw new HTTPError("You can't create a dm channel in a guild"); + case ChannelType.GUILD_STORE: + default: + throw new HTTPError("Not yet supported"); + } + + if (!channel.permission_overwrites) channel.permission_overwrites = []; + // TODO: eagerly auto generate position of all guild channels + + channel = { + ...channel, + ...(!opts?.keepId && { id: Snowflake.generate() }), + created_at: new Date(), + position: + (channel.type === ChannelType.UNHANDLED + ? 0 + : channel.position) || 0, + }; + + await Promise.all([ + Channel.create(channel).save(), + !opts?.skipEventEmit + ? emitEvent({ + event: "CHANNEL_CREATE", + data: channel, + guild_id: channel.guild_id, + } as ChannelCreateEvent) + : Promise.resolve(), + ]); + + return channel; + } + + static async createDMChannel( + recipients: string[], + creator_user_id: string, + name?: string, + ) { + recipients = recipients.unique().filter((x) => x !== creator_user_id); + // TODO: check config for max number of recipients + /** if you want to disallow note to self channels, uncomment the conditional below + + const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); + if (otherRecipientsUsers.length !== recipients.length) { + throw new HTTPError("Recipient/s not found"); + } + **/ + + const type = + recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM; + + let channel = null; + + const channelRecipients = [...recipients, creator_user_id]; + + const userRecipients = await Recipient.find({ + where: { user_id: creator_user_id }, + relations: ["channel", "channel.recipients"], + }); + + for (let ur of userRecipients) { + let re = ur.channel.recipients!.map((r) => r.user_id); + if (re.length === channelRecipients.length) { + if (containsAll(re, channelRecipients)) { + if (channel == null) { + channel = ur.channel; + await ur.assign({ closed: false }).save(); + } + } + } + } + + if (channel == null) { + name = trimSpecial(name); + + channel = await Channel.create({ + name, + type, + owner_id: undefined, + created_at: new Date(), + last_message_id: undefined, + recipients: channelRecipients.map((x) => + Recipient.create({ + user_id: x, + closed: !( + type === ChannelType.GROUP_DM || + x === creator_user_id + ), + }), + ), + nsfw: false, + }).save(); + } + + const channel_dto = await DmChannelDTO.from(channel); + + if (type === ChannelType.GROUP_DM) { + for (let recipient of channel.recipients!) { + await emitEvent({ + event: "CHANNEL_CREATE", + data: channel_dto.excludedRecipients([recipient.user_id]), + user_id: recipient.user_id, + }); + } + } else { + await emitEvent({ + event: "CHANNEL_CREATE", + data: channel_dto, + user_id: creator_user_id, + }); + } + + if (recipients.length === 1) return channel_dto; + else return channel_dto.excludedRecipients([creator_user_id]); + } + + static async removeRecipientFromChannel(channel: Channel, user_id: string) { + await Recipient.delete({ channel_id: channel.id, user_id: user_id }); + channel.recipients = channel.recipients?.filter( + (r) => r.user_id !== user_id, + ); + + if (channel.recipients?.length === 0) { + await Channel.deleteChannel(channel); + await emitEvent({ + event: "CHANNEL_DELETE", + data: await DmChannelDTO.from(channel, [user_id]), + user_id: user_id, + }); + return; + } + + await emitEvent({ + event: "CHANNEL_DELETE", + data: await DmChannelDTO.from(channel, [user_id]), + user_id: user_id, + }); + + //If the owner leave the server user is the new owner + if (channel.owner_id === user_id) { + channel.owner_id = "1"; // The channel is now owned by the server user + await emitEvent({ + event: "CHANNEL_UPDATE", + data: await DmChannelDTO.from(channel, [user_id]), + channel_id: channel.id, + }); + } + + await channel.save(); + + await emitEvent({ + event: "CHANNEL_RECIPIENT_REMOVE", + data: { + channel_id: channel.id, + user: await User.findOneOrFail({ + where: { id: user_id }, + select: PublicUserProjection, + }), + }, + channel_id: channel.id, + } as ChannelRecipientRemoveEvent); + } + + static async deleteChannel(channel: Channel) { + await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util + //TODO before deleting the channel we should check and delete other relations + await Channel.delete({ id: channel.id }); + } + + isDm() { + return ( + this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM + ); + } + + // Does the channel support sending messages ( eg categories do not ) + isWritable() { + const disallowedChannelTypes = [ + ChannelType.GUILD_CATEGORY, + ChannelType.GUILD_STAGE_VOICE, + ChannelType.VOICELESS_WHITEBOARD, + ]; + return disallowedChannelTypes.indexOf(this.type) == -1; + } +} + +export interface ChannelPermissionOverwrite { + allow: string; + deny: string; + id: string; + type: ChannelPermissionOverwriteType; +} + +export enum ChannelPermissionOverwriteType { + role = 0, + member = 1, + group = 2, +} diff --git a/src/util/entities/ClientRelease.ts b/src/util/entities/ClientRelease.ts index c5afd307..2723ab67 100644 --- a/src/util/entities/ClientRelease.ts +++ b/src/util/entities/ClientRelease.ts @@ -1,4 +1,4 @@ -import { Column, Entity} from "typeorm"; +import { Column, Entity } from "typeorm"; import { BaseClass } from "./BaseClass"; @Entity("client_release") diff --git a/src/util/entities/Config.ts b/src/util/entities/Config.ts index 9aabc1a8..cd7a6923 100644 --- a/src/util/entities/Config.ts +++ b/src/util/entities/Config.ts @@ -191,17 +191,17 @@ export interface ConfigValue { allowTemplateCreation: Boolean; allowDiscordTemplates: Boolean; allowRaws: Boolean; - }, + }; client: { useTestClient: Boolean; releases: { useLocalRelease: Boolean; //TODO upstreamVersion: string; }; - }, + }; metrics: { timeout: number; - }, + }; sentry: { enabled: boolean; endpoint: string; @@ -230,7 +230,8 @@ export const DefaultConfigOptions: ConfigValue = { }, general: { instanceName: "Fosscord Instance", - instanceDescription: "This is a Fosscord instance made in pre-release days", + instanceDescription: + "This is a Fosscord instance made in pre-release days", frontPage: null, tosPage: null, correspondenceEmail: "noreply@localhost.local", @@ -318,8 +319,9 @@ export const DefaultConfigOptions: ConfigValue = { sitekey: null, secret: null, }, - ipdataApiKey: "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9", - defaultRights: "30644591655936", // See util/scripts/rights.js + ipdataApiKey: + "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9", + defaultRights: "30644591655936", // See util/scripts/rights.js }, login: { requireCaptcha: false, @@ -395,22 +397,23 @@ export const DefaultConfigOptions: ConfigValue = { enabled: true, allowTemplateCreation: true, allowDiscordTemplates: true, - allowRaws: false + allowRaws: false, }, client: { useTestClient: true, releases: { useLocalRelease: true, - upstreamVersion: "0.0.264" - } + upstreamVersion: "0.0.264", + }, }, metrics: { - timeout: 30000 + timeout: 30000, }, sentry: { enabled: false, - endpoint: "https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6", + endpoint: + "https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6", traceSampleRate: 1.0, - environment: hostname() - } -}; \ No newline at end of file + environment: hostname(), + }, +}; diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts index 09ae30ab..a893ff34 100644 --- a/src/util/entities/ConnectedAccount.ts +++ b/src/util/entities/ConnectedAccount.ts @@ -2,7 +2,8 @@ import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; -export interface PublicConnectedAccount extends Pick<ConnectedAccount, "name" | "type" | "verified"> {} +export interface PublicConnectedAccount + extends Pick<ConnectedAccount, "name" | "type" | "verified"> {} @Entity("connected_accounts") export class ConnectedAccount extends BaseClass { diff --git a/src/util/entities/Emoji.ts b/src/util/entities/Emoji.ts index a3615b7d..0aa640b5 100644 --- a/src/util/entities/Emoji.ts +++ b/src/util/entities/Emoji.ts @@ -40,7 +40,7 @@ export class Emoji extends BaseClass { @Column({ type: "simple-array" }) roles: string[]; // roles this emoji is whitelisted to (new discord feature?) - + @Column({ type: "simple-array", nullable: true }) groups: string[]; // user groups this emoji is whitelisted to (Fosscord extension) } diff --git a/src/util/entities/Encryption.ts b/src/util/entities/Encryption.ts index b597b90a..4c427b32 100644 --- a/src/util/entities/Encryption.ts +++ b/src/util/entities/Encryption.ts @@ -1,9 +1,23 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + RelationId, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { Guild } from "./Guild"; import { PublicUserProjection, User } from "./User"; import { HTTPError } from "lambert-server"; -import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util"; +import { + containsAll, + emitEvent, + getPermission, + Snowflake, + trimSpecial, + InvisibleCharacters, +} from "../util"; import { BitField, BitFieldResolvable, BitFlag } from "../util/BitField"; import { Recipient } from "./Recipient"; import { Message } from "./Message"; @@ -13,7 +27,6 @@ import { DmChannelDTO } from "../dtos"; @Entity("security_settings") export class SecuritySettings extends BaseClass { - @Column({ nullable: true }) guild_id: string; @@ -31,5 +44,4 @@ export class SecuritySettings extends BaseClass { @Column({ nullable: true }) used_since_message: string; - } diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts index 2ce7c213..8854fec0 100644 --- a/src/util/entities/Guild.ts +++ b/src/util/entities/Guild.ts @@ -1,4 +1,13 @@ -import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToMany, + ManyToOne, + OneToMany, + OneToOne, + RelationId, +} from "typeorm"; import { Config, handleFile, Snowflake } from ".."; import { Ban } from "./Ban"; import { BaseClass } from "./BaseClass"; @@ -86,7 +95,7 @@ export class Guild extends BaseClass { //TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features @Column({ nullable: true }) - primary_category_id?: string; // TODO: this was number? + primary_category_id?: string; // TODO: this was number? @Column({ nullable: true }) icon?: string; @@ -269,7 +278,7 @@ export class Guild extends BaseClass { @Column() nsfw: boolean; - + // TODO: nested guilds @Column({ nullable: true }) parent?: string; @@ -332,10 +341,13 @@ export class Guild extends BaseClass { permissions: String("2251804225"), position: 0, icon: undefined, - unicode_emoji: undefined + unicode_emoji: undefined, }).save(); - if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general", nsfw: false }]; + if (!body.channels || !body.channels.length) + body.channels = [ + { id: "01", type: 0, name: "general", nsfw: false }, + ]; const ids = new Map(); @@ -345,17 +357,23 @@ export class Guild extends BaseClass { } }); - for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) { + for (const channel of body.channels?.sort((a, b) => + a.parent_id ? 1 : -1, + )) { var id = ids.get(channel.id) || Snowflake.generate(); var parent_id = ids.get(channel.parent_id); - await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, { - keepId: true, - skipExistsCheck: true, - skipPermissionCheck: true, - skipEventEmit: true, - }); + await Channel.createChannel( + { ...channel, guild_id, id, parent_id }, + body.owner_id, + { + keepId: true, + skipExistsCheck: true, + skipPermissionCheck: true, + skipEventEmit: true, + }, + ); } return guild; diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts index 4f36f247..90dec92a 100644 --- a/src/util/entities/Invite.ts +++ b/src/util/entities/Invite.ts @@ -1,4 +1,11 @@ -import { Column, Entity, JoinColumn, ManyToOne, RelationId, PrimaryColumn } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + RelationId, + PrimaryColumn, +} from "typeorm"; import { Member } from "./Member"; import { BaseClassWithoutId } from "./BaseClass"; import { Channel } from "./Channel"; @@ -76,7 +83,8 @@ export class Invite extends BaseClassWithoutId { static async joinGuild(user_id: string, code: string) { const invite = await Invite.findOneOrFail({ where: { code } }); - if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0) await Invite.delete({ code }); + if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0) + await Invite.delete({ code }); else await invite.save(); await Member.addToGuild(user_id, invite.guild_id); diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts index 7d1346ba..f2762adc 100644 --- a/src/util/entities/Member.ts +++ b/src/util/entities/Member.ts @@ -22,7 +22,6 @@ import { GuildMemberRemoveEvent, GuildMemberUpdateEvent, MessageCreateEvent, - } from "../interfaces"; import { HTTPError } from "lambert-server"; import { Role } from "./Role"; @@ -126,19 +125,34 @@ export class Member extends BaseClassWithoutId { if (this.nick) { this.nick = this.nick.split("\n").join(""); this.nick = this.nick.split("\t").join(""); - if (BannedWords.find(this.nick)) throw FieldErrors({ nick: { message: "Bad nickname", code: "INVALID_NICKNAME" } }); + if (BannedWords.find(this.nick)) + throw FieldErrors({ + nick: { message: "Bad nickname", code: "INVALID_NICKNAME" }, + }); } } static async IsInGuildOrFail(user_id: string, guild_id: string) { - if (await Member.count({ where: { id: user_id, guild: { id: guild_id } } })) return true; + if ( + await Member.count({ + where: { id: user_id, guild: { id: guild_id } }, + }) + ) + return true; throw new HTTPError("You are not member of this guild", 403); } static async removeFromGuild(user_id: string, guild_id: string) { - const guild = await Guild.findOneOrFail({ select: ["owner_id"], where: { id: guild_id } }); - if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild"); - const member = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["user"] }); + const guild = await Guild.findOneOrFail({ + select: ["owner_id"], + where: { id: guild_id }, + }); + if (guild.owner_id === user_id) + throw new Error("The owner cannot be removed of the guild"); + const member = await Member.findOneOrFail({ + where: { id: user_id, guild_id }, + relations: ["user"], + }); // use promise all to execute all promises at the same time -> save time return Promise.all([ @@ -169,9 +183,12 @@ export class Member extends BaseClassWithoutId { where: { id: user_id, guild_id }, relations: ["user", "roles"], // we don't want to load the role objects just the ids //@ts-ignore - select: ["index", "roles.id"], // TODO fix type + select: ["index", "roles.id"], // TODO fix type + }), + Role.findOneOrFail({ + where: { id: role_id, guild_id }, + select: ["id"], }), - Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }), ]); member.roles.push(Role.create({ id: role_id })); @@ -189,7 +206,11 @@ export class Member extends BaseClassWithoutId { ]); } - static async removeRole(user_id: string, guild_id: string, role_id: string) { + static async removeRole( + user_id: string, + guild_id: string, + role_id: string, + ) { const [member] = await Promise.all([ Member.findOneOrFail({ where: { id: user_id, guild_id }, @@ -215,7 +236,11 @@ export class Member extends BaseClassWithoutId { ]); } - static async changeNickname(user_id: string, guild_id: string, nickname: string) { + static async changeNickname( + user_id: string, + guild_id: string, + nickname: string, + ) { const member = await Member.findOneOrFail({ where: { id: user_id, @@ -249,7 +274,10 @@ export class Member extends BaseClassWithoutId { const { maxGuilds } = Config.get().limits.user; const guild_count = await Member.count({ where: { id: user_id } }); if (guild_count >= maxGuilds) { - throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403); + throw new HTTPError( + `You are at the ${maxGuilds} server limit.`, + 403, + ); } const guild = await Guild.findOneOrFail({ @@ -259,7 +287,11 @@ export class Member extends BaseClassWithoutId { relations: [...PublicGuildRelations, "system_channel"], }); - if (await Member.count({ where: { id: user.id, guild: { id: guild_id } } })) + if ( + await Member.count({ + where: { id: user.id, guild: { id: guild_id } }, + }) + ) throw new HTTPError("You are already a member of this guild", 400); const member = { @@ -268,7 +300,7 @@ export class Member extends BaseClassWithoutId { nick: undefined, roles: [guild_id], // @everyone role joined_at: new Date(), - premium_since: (new Date()).getTime(), + premium_since: new Date().getTime(), deaf: false, mute: false, pending: false, @@ -339,7 +371,11 @@ export class Member extends BaseClassWithoutId { }); await Promise.all([ message.save(), - emitEvent({ event: "MESSAGE_CREATE", channel_id: message.channel_id, data: message } as MessageCreateEvent) + emitEvent({ + event: "MESSAGE_CREATE", + channel_id: message.channel_id, + data: message, + } as MessageCreateEvent), ]); } } @@ -362,7 +398,7 @@ export interface UserGuildSettings { channel_overrides: { [channel_id: string]: ChannelOverride; - } | null, + } | null; message_notifications: number; mobile_push: boolean; mute_config: MuteConfig | null; @@ -389,7 +425,7 @@ export const DefaultUserGuildSettings: UserGuildSettings = { notify_highlights: 0, suppress_everyone: false, suppress_roles: false, - version: 453, // ? + version: 453, // ? guild_id: null, }; diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index be790502..a52b4785 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -51,7 +51,7 @@ export enum MessageType { SELF_COMMAND_SCRIPT = 43, // self command scripts ENCRYPTION = 50, CUSTOM_START = 63, - UNHANDLED = 255 + UNHANDLED = 255, } @Entity("messages") @@ -115,7 +115,10 @@ export class Message extends BaseClass { @ManyToOne(() => Application) application?: Application; - @Column({ nullable: true, type: process.env.PRODUCTION ? "longtext" : undefined }) + @Column({ + nullable: true, + type: process.env.PRODUCTION ? "longtext" : undefined, + }) content?: string; @Column() @@ -147,10 +150,14 @@ export class Message extends BaseClass { @ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" }) sticker_items?: Sticker[]; - @OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, { - cascade: true, - orphanedRowAction: "delete", - }) + @OneToMany( + () => Attachment, + (attachment: Attachment) => attachment.message, + { + cascade: true, + orphanedRowAction: "delete", + }, + ) attachments?: Attachment[]; @Column({ type: "simple-json" }) @@ -176,7 +183,7 @@ export class Message extends BaseClass { @Column({ nullable: true }) flags?: string; - + @Column({ type: "simple-json", nullable: true }) message_reference?: { message_id: string; @@ -204,7 +211,11 @@ export class Message extends BaseClass { @BeforeInsert() validate() { if (this.content) { - if (BannedWords.find(this.content)) throw new HTTPError("Message was blocked by automatic moderation", 200000); + if (BannedWords.find(this.content)) + throw new HTTPError( + "Message was blocked by automatic moderation", + 200000, + ); } } } diff --git a/src/util/entities/Migration.ts b/src/util/entities/Migration.ts index 3f39ae72..f4e54eae 100644 --- a/src/util/entities/Migration.ts +++ b/src/util/entities/Migration.ts @@ -1,7 +1,14 @@ -import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm"; +import { + Column, + Entity, + ObjectIdColumn, + PrimaryGeneratedColumn, +} from "typeorm"; import { BaseClassWithoutId } from "."; -export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith("mongodb") +export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith( + "mongodb", +) ? ObjectIdColumn : PrimaryGeneratedColumn; diff --git a/src/util/entities/Note.ts b/src/util/entities/Note.ts index 36017c5e..b3ac45ee 100644 --- a/src/util/entities/Note.ts +++ b/src/util/entities/Note.ts @@ -15,4 +15,4 @@ export class Note extends BaseClass { @Column() content: string; -} \ No newline at end of file +} diff --git a/src/util/entities/ReadState.ts b/src/util/entities/ReadState.ts index b915573b..53ed5589 100644 --- a/src/util/entities/ReadState.ts +++ b/src/util/entities/ReadState.ts @@ -1,4 +1,11 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + RelationId, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; import { Message } from "./Message"; @@ -33,8 +40,8 @@ export class ReadState extends BaseClass { // fully read marker @Column({ nullable: true }) - last_message_id: string; - + last_message_id: string; + // public read receipt @Column({ nullable: true }) public_ack: string; diff --git a/src/util/entities/Relationship.ts b/src/util/entities/Relationship.ts index c3592c76..25b52757 100644 --- a/src/util/entities/Relationship.ts +++ b/src/util/entities/Relationship.ts @@ -1,4 +1,11 @@ -import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm"; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + RelationId, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { User } from "./User"; diff --git a/src/util/entities/StickerPack.ts b/src/util/entities/StickerPack.ts index ec8c69a2..04d74bac 100644 --- a/src/util/entities/StickerPack.ts +++ b/src/util/entities/StickerPack.ts @@ -1,4 +1,12 @@ -import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, + RelationId, +} from "typeorm"; import { Sticker } from "."; import { BaseClass } from "./BaseClass"; diff --git a/src/util/entities/Team.ts b/src/util/entities/Team.ts index 22140b7f..8f410bb4 100644 --- a/src/util/entities/Team.ts +++ b/src/util/entities/Team.ts @@ -1,4 +1,12 @@ -import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm"; +import { + Column, + Entity, + JoinColumn, + ManyToMany, + ManyToOne, + OneToMany, + RelationId, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { TeamMember } from "./TeamMember"; import { User } from "./User"; diff --git a/src/util/entities/TeamMember.ts b/src/util/entities/TeamMember.ts index b726e1e8..3f4a0422 100644 --- a/src/util/entities/TeamMember.ts +++ b/src/util/entities/TeamMember.ts @@ -20,9 +20,13 @@ export class TeamMember extends BaseClass { team_id: string; @JoinColumn({ name: "team_id" }) - @ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, { - onDelete: "CASCADE", - }) + @ManyToOne( + () => require("./Team").Team, + (team: import("./Team").Team) => team.members, + { + onDelete: "CASCADE", + }, + ) team: import("./Team").Team; @Column({ nullable: true }) diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 84a8a674..1389a424 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -1,9 +1,24 @@ -import { BeforeInsert, BeforeUpdate, Column, Entity, FindOneOptions, JoinColumn, OneToMany } from "typeorm"; +import { + BeforeInsert, + BeforeUpdate, + Column, + Entity, + FindOneOptions, + JoinColumn, + OneToMany, +} from "typeorm"; import { BaseClass } from "./BaseClass"; import { BitField } from "../util/BitField"; import { Relationship } from "./Relationship"; import { ConnectedAccount } from "./ConnectedAccount"; -import { Config, FieldErrors, Snowflake, trimSpecial, BannedWords, adjustEmail } from ".."; +import { + Config, + FieldErrors, + Snowflake, + trimSpecial, + BannedWords, + adjustEmail, +} from ".."; import { Member, Session } from "."; export enum PublicUserEnum { @@ -38,7 +53,7 @@ export enum PrivateUserEnum { export type PrivateUserKeys = keyof typeof PrivateUserEnum | PublicUserKeys; export const PublicUserProjection = Object.values(PublicUserEnum).filter( - (x) => typeof x === "string" + (x) => typeof x === "string", ) as PublicUserKeys[]; export const PrivateUserProjection = [ ...PublicUserProjection, @@ -48,7 +63,7 @@ export const PrivateUserProjection = [ // Private user data that should never get sent to the client export type PublicUser = Pick<User, PublicUserKeys>; -export interface UserPublic extends Pick<User, PublicUserKeys> { } +export interface UserPublic extends Pick<User, PublicUserKeys> {} export interface UserPrivate extends Pick<User, PrivateUserKeys> { locale: string; @@ -144,17 +159,25 @@ export class User extends BaseClass { sessions: Session[]; @JoinColumn({ name: "relationship_ids" }) - @OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, { - cascade: true, - orphanedRowAction: "delete", - }) + @OneToMany( + () => Relationship, + (relationship: Relationship) => relationship.from, + { + cascade: true, + orphanedRowAction: "delete", + }, + ) relationships: Relationship[]; @JoinColumn({ name: "connected_account_ids" }) - @OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, { - cascade: true, - orphanedRowAction: "delete", - }) + @OneToMany( + () => ConnectedAccount, + (account: ConnectedAccount) => account.user, + { + cascade: true, + orphanedRowAction: "delete", + }, + ) connected_accounts: ConnectedAccount[]; @Column({ type: "simple-json", select: false }) @@ -177,16 +200,43 @@ export class User extends BaseClass { @BeforeInsert() validate() { this.email = adjustEmail(this.email); - if (!this.email) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } }); - if (!this.email.match(/([a-z\d.-]{3,})@([a-z\d.-]+).([a-z]{2,})/g)) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } }); + if (!this.email) + throw FieldErrors({ + email: { message: "Invalid email", code: "EMAIL_INVALID" }, + }); + if (!this.email.match(/([a-z\d.-]{3,})@([a-z\d.-]+).([a-z]{2,})/g)) + throw FieldErrors({ + email: { message: "Invalid email", code: "EMAIL_INVALID" }, + }); const discrim = Number(this.discriminator); - if (this.discriminator.length > 4) throw FieldErrors({ email: { message: "Discriminator cannot be more than 4 digits.", code: "DISCRIMINATOR_INVALID" } }); - if (isNaN(discrim)) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } }); - if (discrim <= 0 || discrim >= 10000) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } }); + if (this.discriminator.length > 4) + throw FieldErrors({ + email: { + message: "Discriminator cannot be more than 4 digits.", + code: "DISCRIMINATOR_INVALID", + }, + }); + if (isNaN(discrim)) + throw FieldErrors({ + email: { + message: "Discriminator must be a number.", + code: "DISCRIMINATOR_INVALID", + }, + }); + if (discrim <= 0 || discrim >= 10000) + throw FieldErrors({ + email: { + message: "Discriminator must be a number.", + code: "DISCRIMINATOR_INVALID", + }, + }); this.discriminator = discrim.toString().padStart(4, "0"); - if (BannedWords.find(this.username)) throw FieldErrors({ username: { message: "Bad username", code: "INVALID_USERNAME" } }); + if (BannedWords.find(this.username)) + throw FieldErrors({ + username: { message: "Bad username", code: "INVALID_USERNAME" }, + }); } toPublicUser() { @@ -202,17 +252,25 @@ export class User extends BaseClass { where: { id: user_id }, ...opts, //@ts-ignore - select: [...PublicUserProjection, ...(opts?.select || [])], // TODO: fix + select: [...PublicUserProjection, ...(opts?.select || [])], // TODO: fix }); } - private static async generateDiscriminator(username: string): Promise<string | undefined> { + private static async generateDiscriminator( + username: string, + ): Promise<string | undefined> { if (Config.get().register.incrementingDiscriminators) { // discriminator will be incrementally generated // First we need to figure out the currently highest discrimnator for the given username and then increment it - const users = await User.find({ where: { username }, select: ["discriminator"] }); - const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator))); + const users = await User.find({ + where: { username }, + select: ["discriminator"], + }); + const highestDiscriminator = Math.max( + 0, + ...users.map((u) => Number(u.discriminator)), + ); const discriminator = highestDiscriminator + 1; if (discriminator >= 10000) { @@ -226,8 +284,13 @@ export class User extends BaseClass { // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database? for (let tries = 0; tries < 5; tries++) { - const discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); - const exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] }); + const discriminator = Math.randomIntBetween(1, 9999) + .toString() + .padStart(4, "0"); + const exists = await User.findOne({ + where: { discriminator, username: username }, + select: ["id"], + }); if (!exists) return discriminator; } @@ -265,7 +328,8 @@ export class User extends BaseClass { // TODO: save date_of_birth // appearently discord doesn't save the date of birth and just calculate if nsfw is allowed // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false - const language = req.language === "en" ? "en-US" : req.language || "en-US"; + const language = + req.language === "en" ? "en-US" : req.language || "en-US"; const user = User.create({ created_at: new Date(), @@ -295,8 +359,8 @@ export class User extends BaseClass { }, settings: { ...defaultSettings, locale: language }, purchased_flags: 5, // TODO: idk what the values for this are - premium_usage_flags: 2, // TODO: idk what the values for this are - extended_settings: "", // TODO: was {} + premium_usage_flags: 2, // TODO: idk what the values for this are + extended_settings: "", // TODO: was {} fingerprints: [], }); @@ -305,7 +369,7 @@ export class User extends BaseClass { setImmediate(async () => { if (Config.get().guild.autoJoin.enabled) { for (const guild of Config.get().guild.autoJoin.guilds || []) { - await Member.addToGuild(user.id, guild).catch((e) => { }); + await Member.addToGuild(user.id, guild).catch((e) => {}); } } }); @@ -372,7 +436,7 @@ export interface UserSettings { disable_games_tab: boolean; enable_tts_command: boolean; explicit_content_filter: number; - friend_source_flags: { all: boolean; }; + friend_source_flags: { all: boolean }; gateway_connected: boolean; gif_auto_play: boolean; // every top guild is displayed as a "folder" diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts index 49793810..c439a4b7 100644 --- a/src/util/entities/index.ts +++ b/src/util/entities/index.ts @@ -29,4 +29,4 @@ export * from "./VoiceState"; export * from "./Webhook"; export * from "./ClientRelease"; export * from "./BackupCodes"; -export * from "./Note"; \ No newline at end of file +export * from "./Note"; |