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