diff --git a/util/src/entities/BaseClass.ts b/util/src/entities/BaseClass.ts
index d20078e5..aabca016 100644
--- a/util/src/entities/BaseClass.ts
+++ b/util/src/entities/BaseClass.ts
@@ -52,12 +52,12 @@ export class BaseClassWithoutId extends BaseEntity {
static increment<T extends BaseClass>(conditions: FindConditions<T>, propertyPath: string, value: number | string) {
const repository = this.getRepository();
- return repository.increment(conditions, propertyPath, value);
+ return repository.increment(conditions as T, propertyPath, value);
}
static decrement<T extends BaseClass>(conditions: FindConditions<T>, propertyPath: string, value: number | string) {
const repository = this.getRepository();
- return repository.decrement(conditions, propertyPath, value);
+ return repository.decrement(conditions as T, propertyPath, value);
}
}
diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
index bd2e5a58..1cc4a538 100644
--- a/util/src/entities/Channel.ts
+++ b/util/src/entities/Channel.ts
@@ -1,332 +1,357 @@
-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 } 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 server
- DM = 1, // a direct message between users
- GUILD_VOICE = 2, // a voice channel within a server
- GROUP_DM = 3, // a direct message between multiple users
- GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
- GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
- GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
- // TODO: what are channel types between 7-9?
- 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
-}
-
-@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;
-
- // only for group dms
- @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({ nullable: true })
- nsfw?: boolean;
-
- @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[];
-
- @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;
- }
- ) {
- if (!opts?.skipPermissionCheck) {
- // Always check if user has permission first
- const permissions = await getPermission(user_id, channel.guild_id);
- permissions.hasThrow("MANAGE_CHANNELS");
- }
-
- switch (channel.type) {
- case ChannelType.GUILD_TEXT:
- case ChannelType.GUILD_VOICE:
- if (channel.parent_id && !opts?.skipExistsCheck) {
- const exists = await Channel.findOneOrFail({ 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:
- break;
- case ChannelType.DM:
- case ChannelType.GROUP_DM:
- throw new HTTPError("You can't create a dm channel in a guild");
- // TODO: check if guild is community server
- case ChannelType.GUILD_STORE:
- case ChannelType.GUILD_NEWS:
- default:
- throw new HTTPError("Not yet supported");
- }
-
- if (!channel.permission_overwrites) channel.permission_overwrites = [];
- // TODO: auto generate position
-
- channel = {
- ...channel,
- ...(!opts?.keepId && { id: Snowflake.generate() }),
- created_at: new Date(),
- position: channel.position || 0,
- };
-
- await Promise.all([
- new Channel(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);
- const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })), select: ["id"] });
-
- // TODO: check config for max number of recipients
- if (otherRecipientsUsers.length !== recipients.length) {
- throw new HTTPError("Recipient/s not found");
- }
-
- const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_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 new Channel({
- name,
- type,
- owner_id: type === ChannelType.DM ? undefined : creator_user_id,
- created_at: new Date(),
- last_message_id: null,
- recipients: channelRecipients.map(
- (x) =>
- new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })
- ),
- }).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 });
- }
-
- 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 we make the first recipient in the list the new owner
- if (channel.owner_id === user_id) {
- channel.owner_id = channel.recipients!.find((r) => r.user_id !== user_id)!.user_id; //Is there a criteria to choose the new owner?
- 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;
- }
-}
-
-export interface ChannelPermissionOverwrite {
- allow: string;
- deny: string;
- id: string;
- type: ChannelPermissionOverwriteType;
-}
-
-export enum ChannelPermissionOverwriteType {
- role = 0,
- member = 1,
-}
+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 { 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 server
+ DM = 1, // a direct message between users
+ GUILD_VOICE = 2, // a voice channel within a server
+ GROUP_DM = 3, // a direct message between multiple users
+ GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
+ GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
+ GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
+ ENCRYPTED = 7, // end-to-end encrypted channel
+ ENCRYPTED_THREAD = 8, // end-to-end encrypted thread 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
+ 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;
+
+ // only for group dms
+ @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({ nullable: true })
+ nsfw?: boolean;
+
+ @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[];
+
+ @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({ 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);
+
+ 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)
+ }
+
+ 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_VOICE:
+ if (channel.parent_id && !opts?.skipExistsCheck) {
+ const exists = await Channel.findOneOrFail({ 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:
+ break;
+ case ChannelType.DM:
+ case ChannelType.GROUP_DM:
+ throw new HTTPError("You can't create a dm channel in a guild");
+ // TODO: check if guild is community server
+ case ChannelType.GUILD_STORE:
+ case ChannelType.GUILD_NEWS:
+ default:
+ throw new HTTPError("Not yet supported");
+ }
+
+ if (!channel.permission_overwrites) channel.permission_overwrites = [];
+ // TODO: auto generate position
+
+ channel = {
+ ...channel,
+ ...(!opts?.keepId && { id: Snowflake.generate() }),
+ created_at: new Date(),
+ position: channel.position || 0,
+ };
+
+ await Promise.all([
+ new Channel(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);
+ const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
+
+ // TODO: check config for max number of recipients
+ if (otherRecipientsUsers.length !== recipients.length) {
+ throw new HTTPError("Recipient/s not found");
+ }
+
+ const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_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 new Channel({
+ name,
+ type,
+ owner_id: type === ChannelType.DM ? undefined : null, // 1:1 DMs are ownerless in fosscord-server
+ created_at: new Date(),
+ last_message_id: null,
+ recipients: channelRecipients.map(
+ (x) =>
+ new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })
+ ),
+ }).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 });
+ }
+
+ 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;
+ }
+}
+
+export interface ChannelPermissionOverwrite {
+ allow: string;
+ deny: string;
+ id: string;
+ type: ChannelPermissionOverwriteType;
+}
+
+export enum ChannelPermissionOverwriteType {
+ role = 0,
+ member = 1,
+}
diff --git a/util/src/entities/clientRelase.ts b/util/src/entities/ClientRelase.ts
index e021b82b..e021b82b 100644
--- a/util/src/entities/clientRelase.ts
+++ b/util/src/entities/ClientRelase.ts
diff --git a/util/src/entities/Config.ts b/util/src/entities/Config.ts
index 2d003c99..6993cc09 100644
--- a/util/src/entities/Config.ts
+++ b/util/src/entities/Config.ts
@@ -3,6 +3,7 @@ import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
import crypto from "crypto";
import { Snowflake } from "../util/Snowflake";
import { SessionsReplace } from "..";
+import { hostname } from "os";
@Entity("config")
export class ConfigEntity extends BaseClassWithoutId {
@@ -148,6 +149,7 @@ export interface ConfigValue {
minUpperCase: number;
minSymbols: number;
};
+ incrementingDiscriminators: boolean; // random otherwise
};
regions: {
default: string;
@@ -188,6 +190,12 @@ export interface ConfigValue {
},
metrics: {
timeout: number;
+ },
+ sentry: {
+ enabled: boolean;
+ endpoint: string;
+ traceSampleRate: number;
+ environment: string;
}
}
@@ -328,6 +336,7 @@ export const DefaultConfigOptions: ConfigValue = {
minUpperCase: 2,
minSymbols: 0,
},
+ incrementingDiscriminators: false,
},
regions: {
default: "fosscord",
@@ -377,5 +386,11 @@ export const DefaultConfigOptions: ConfigValue = {
},
metrics: {
timeout: 30000
+ },
+ sentry: {
+ enabled: false,
+ endpoint: "https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6",
+ traceSampleRate: 1.0,
+ environment: hostname()
}
};
diff --git a/util/src/entities/Emoji.ts b/util/src/entities/Emoji.ts
index 03218375..32d39234 100644
--- a/util/src/entities/Emoji.ts
+++ b/util/src/entities/Emoji.ts
@@ -10,7 +10,7 @@ export class Emoji extends BaseClass {
animated: boolean;
@Column()
- available: boolean; // whether this emoji can be used, may be false due to loss of Server Boosts
+ available: boolean; // whether this emoji can be used, may be false due to various reasons
@Column()
guild_id: string;
@@ -40,4 +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" })
+ groups: string[]; // user groups this emoji is whitelisted to (Fosscord extension)
}
diff --git a/util/src/entities/Guild.ts b/util/src/entities/Guild.ts
index 157f0921..6a1df4d6 100644
--- a/util/src/entities/Guild.ts
+++ b/util/src/entities/Guild.ts
@@ -213,7 +213,7 @@ export class Guild extends BaseClass {
owner: User;
@Column({ nullable: true })
- preferred_locale?: string; // only community guilds can choose this
+ preferred_locale?: string;
@Column({ nullable: true })
premium_subscription_count?: number;
@@ -301,22 +301,22 @@ export class Guild extends BaseClass {
name: body.name || "Fosscord",
icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
region: Config.get().regions.default,
- owner_id: body.owner_id,
+ owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds
afk_timeout: 300,
- default_message_notifications: 0,
+ default_message_notifications: 1, // defaults effect: setting the push default at mentions-only will save a lot
explicit_content_filter: 0,
features: [],
id: guild_id,
max_members: 250000,
max_presences: 250000,
- max_video_channel_users: 25,
+ max_video_channel_users: 200,
presence_count: 0,
member_count: 0, // will automatically be increased by addMember()
mfa_level: 0,
preferred_locale: "en-US",
premium_subscription_count: 0,
premium_tier: 0,
- system_channel_flags: 0,
+ system_channel_flags: 4, // defaults effect: suppress the setup tips to save performance
unavailable: false,
nsfw: false,
nsfw_level: 0,
@@ -326,20 +326,24 @@ export class Guild extends BaseClass {
description: "No description",
welcome_channels: [],
},
- widget_enabled: false,
+ widget_enabled: true, // NB: don't set it as false to prevent artificial restrictions
}).save();
// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error
+ // TODO: make the @everyone a pseudorole that is dynamically generated at runtime so we can save storage
await new Role({
id: guild_id,
guild_id: guild_id,
color: 0,
hoist: false,
managed: false,
+ // NB: in Fosscord, every role will be non-managed, as we use user-groups instead of roles for managed groups
mentionable: false,
name: "@everyone",
permissions: String("2251804225"),
position: 0,
+ icon: null,
+ unicode_emoji: null
}).save();
if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general" }];
@@ -355,7 +359,6 @@ export class Guild extends BaseClass {
for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) {
var id = ids.get(channel.id) || Snowflake.generate();
- // TODO: should we abort if parent_id is a category? (to disallow sub category channels)
var parent_id = ids.get(channel.parent_id);
await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, {
diff --git a/util/src/entities/Role.ts b/util/src/entities/Role.ts
index 9fca99a5..4b721b5b 100644
--- a/util/src/entities/Role.ts
+++ b/util/src/entities/Role.ts
@@ -36,6 +36,12 @@ export class Role extends BaseClass {
@Column()
position: number;
+ @Column({ nullable: true })
+ icon: string;
+
+ @Column({ nullable: true })
+ unicode_emoji: string;
+
@Column({ type: "simple-json", nullable: true })
tags?: {
bot_id?: string;
diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts
index bc852616..5f2618e0 100644
--- a/util/src/entities/User.ts
+++ b/util/src/entities/User.ts
@@ -64,7 +64,7 @@ export class User extends BaseClass {
setDiscriminator(val: string) {
const number = Number(val);
if (isNaN(number)) throw new Error("invalid discriminator");
- if (number <= 0 || number > 10000) throw new Error("discriminator must be between 1 and 9999");
+ if (number <= 0 || number >= 10000) throw new Error("discriminator must be between 1 and 9999");
this.discriminator = val.toString().padStart(4, "0");
}
@@ -178,6 +178,35 @@ export class User extends BaseClass {
);
}
+ 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 discriminator = highestDiscriminator + 1;
+ if (discriminator >= 10000) {
+ return undefined;
+ }
+
+ return discriminator.toString().padStart(4, "0");
+ } else {
+ // discriminator will be randomly generated
+
+ // 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"] });
+ if (!exists) return discriminator;
+ }
+
+ return undefined;
+ }
+ }
+
static async register({
email,
username,
@@ -194,21 +223,9 @@ export class User extends BaseClass {
// trim special uf8 control characters -> Backspace, Newline, ...
username = trimSpecial(username);
- // discriminator will be randomly generated
- let discriminator = "";
-
- let exists;
- // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
- // if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error
- // else just continue
- // 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++) {
- discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
- exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] });
- if (!exists) break;
- }
-
- if (exists) {
+ const discriminator = await User.generateDiscriminator(username);
+ if (!discriminator) {
+ // We've failed to generate a valid and unused discriminator
throw FieldErrors({
username: {
code: "USERNAME_TOO_MANY_USERS",
diff --git a/util/src/entities/index.ts b/util/src/entities/index.ts
index fdf18f23..c1f979d4 100644
--- a/util/src/entities/index.ts
+++ b/util/src/entities/index.ts
@@ -26,4 +26,4 @@ export * from "./Template";
export * from "./User";
export * from "./VoiceState";
export * from "./Webhook";
-export * from "./clientRelase";
\ No newline at end of file
+export * from "./ClientRelase";
\ No newline at end of file
diff --git a/util/src/util/Database.ts b/util/src/util/Database.ts
index 6124ffab..e8177093 100644
--- a/util/src/util/Database.ts
+++ b/util/src/util/Database.ts
@@ -3,7 +3,7 @@ import "reflect-metadata";
import { Connection, createConnection } from "typeorm";
import * as Models from "../entities";
import { Migration } from "../entities/Migration";
-import { yellow, green } from "nanocolors";
+import { yellow, green, red } from "picocolors";
// UUID extension option is only supported with postgres
// We want to generate all id's with Snowflakes that's why we have our own BaseEntity class
@@ -19,6 +19,9 @@ export function initDatabase(): Promise<Connection> {
const isSqlite = type.includes("sqlite");
console.log(`[Database] ${yellow(`connecting to ${type} db`)}`);
+ if(isSqlite) {
+ console.log(`[Database] ${red(`You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`)}`);
+ }
// @ts-ignore
promise = createConnection({
type,
diff --git a/util/src/util/InvisibleCharacters.ts b/util/src/util/InvisibleCharacters.ts
new file mode 100644
index 00000000..2b014e14
--- /dev/null
+++ b/util/src/util/InvisibleCharacters.ts
@@ -0,0 +1,56 @@
+// List from https://invisible-characters.com/
+export const InvisibleCharacters = [
+ '\u{9}', //Tab
+ '\u{20}', //Space
+ '\u{ad}', //Soft hyphen
+ '\u{34f}', //Combining grapheme joiner
+ '\u{61c}', //Arabic letter mark
+ '\u{115f}', //Hangul choseong filler
+ '\u{1160}', //Hangul jungseong filler
+ '\u{17b4}', //Khmer vowel inherent AQ
+ '\u{17b5}', //Khmer vowel inherent AA
+ '\u{180e}', //Mongolian vowel separator
+ '\u{2000}', //En quad
+ '\u{2001}', //Em quad
+ '\u{2002}', //En space
+ '\u{2003}', //Em space
+ '\u{2004}', //Three-per-em space
+ '\u{2005}', //Four-per-em space
+ '\u{2006}', //Six-per-em space
+ '\u{2007}', //Figure space
+ '\u{2008}', //Punctuation space
+ '\u{2009}', //Thin space
+ '\u{200a}', //Hair space
+ '\u{200b}', //Zero width space
+ '\u{200c}', //Zero width non-joiner
+ '\u{200d}', //Zero width joiner
+ '\u{200e}', //Left-to-right mark
+ '\u{200f}', //Right-to-left mark
+ '\u{202f}', //Narrow no-break space
+ '\u{205f}', //Medium mathematical space
+ '\u{2060}', //Word joiner
+ '\u{2061}', //Function application
+ '\u{2062}', //Invisible times
+ '\u{2063}', //Invisible separator
+ '\u{2064}', //Invisible plus
+ '\u{206a}', //Inhibit symmetric swapping
+ '\u{206b}', //Activate symmetric swapping
+ '\u{206c}', //Inhibit arabic form shaping
+ '\u{206d}', //Activate arabic form shaping
+ '\u{206e}', //National digit shapes
+ '\u{206f}', //Nominal digit shapes
+ '\u{3000}', //Ideographic space
+ '\u{2800}', //Braille pattern blank
+ '\u{3164}', //Hangul filler
+ '\u{feff}', //Zero width no-break space
+ '\u{ffa0}', //Haldwidth hangul filler
+ '\u{1d159}', //Musical symbol null notehead
+ '\u{1d173}', //Musical symbol begin beam
+ '\u{1d174}', //Musical symbol end beam
+ '\u{1d175}', //Musical symbol begin tie
+ '\u{1d176}', //Musical symbol end tie
+ '\u{1d177}', //Musical symbol begin slur
+ '\u{1d178}', //Musical symbol end slur
+ '\u{1d179}', //Musical symbol begin phrase
+ '\u{1d17a}' //Musical symbol end phrase
+];
\ No newline at end of file
diff --git a/util/src/util/index.ts b/util/src/util/index.ts
index c5703468..98e1146c 100644
--- a/util/src/util/index.ts
+++ b/util/src/util/index.ts
@@ -18,3 +18,4 @@ export * from "./Snowflake";
export * from "./String";
export * from "./Array";
export * from "./TraverseDirectory";
+export * from "./InvisibleCharacters";
\ No newline at end of file
|