diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts
index 56b2b899..e0ec56ba 100644
--- a/src/util/connections/Connection.ts
+++ b/src/util/connections/Connection.ts
@@ -17,7 +17,7 @@
*/
import crypto from "node:crypto";
-import { ConnectedAccount } from "../entities";
+import { ConnectedAccount } from "../../database/entities";
import { ConnectedAccountSchema, ConnectionCallbackSchema } from "@spacebar/schemas";
import { Config, DiscordApiErrors } from "../util";
diff --git a/src/util/connections/ConnectionConfig.ts b/src/util/connections/ConnectionConfig.ts
index c5da3878..338b97be 100644
--- a/src/util/connections/ConnectionConfig.ts
+++ b/src/util/connections/ConnectionConfig.ts
@@ -17,7 +17,7 @@
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { ConnectionConfigEntity } from "../entities";
+import { ConnectionConfigEntity } from "../../database/entities";
import { OrmUtils } from "../imports";
let config: any;
diff --git a/src/util/connections/RefreshableConnection.ts b/src/util/connections/RefreshableConnection.ts
index 3d7761f7..8418b085 100644
--- a/src/util/connections/RefreshableConnection.ts
+++ b/src/util/connections/RefreshableConnection.ts
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { ConnectedAccount } from "../entities";
+import { ConnectedAccount } from "../../database/entities";
import { Connection } from "./Connection";
import { ConnectedAccountCommonOAuthTokenResponse } from "@spacebar/schemas";
diff --git a/src/util/dtos/ConnectedAccountDTO.ts b/src/util/dtos/ConnectedAccountDTO.ts
index a137d732..dfa3db01 100644
--- a/src/util/dtos/ConnectedAccountDTO.ts
+++ b/src/util/dtos/ConnectedAccountDTO.ts
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { ConnectedAccount } from "../entities";
+import { ConnectedAccount } from "../../database/entities";
export class ConnectedAccountDTO {
id: string;
diff --git a/src/util/dtos/DmChannelDTO.ts b/src/util/dtos/DmChannelDTO.ts
index f7c42d96..9e6f05da 100644
--- a/src/util/dtos/DmChannelDTO.ts
+++ b/src/util/dtos/DmChannelDTO.ts
@@ -17,7 +17,7 @@
*/
import { MinimalPublicUserDTO } from "./UserDTO";
-import { Channel, User } from "../entities";
+import { Channel, User } from "../../database/entities";
import { PublicUserProjection } from "@spacebar/schemas";
export class DmChannelDTO {
diff --git a/src/util/dtos/ReadyGuildDTO.ts b/src/util/dtos/ReadyGuildDTO.ts
index 1b9677e0..b2475847 100644
--- a/src/util/dtos/ReadyGuildDTO.ts
+++ b/src/util/dtos/ReadyGuildDTO.ts
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { Channel, Emoji, Guild, Role, Sticker } from "../entities";
+import { Channel, Emoji, Guild, Role, Sticker } from "../../database/entities";
import { ChannelOverride, ChannelType, PublicMember, PublicUser, UserGuildSettings } from "@spacebar/schemas";
// TODO: this is not the best place for this type
diff --git a/src/util/dtos/UserDTO.ts b/src/util/dtos/UserDTO.ts
index 2a79f8f1..63f1c3ad 100644
--- a/src/util/dtos/UserDTO.ts
+++ b/src/util/dtos/UserDTO.ts
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { User } from "../entities";
+import { User } from "../../database/entities";
export class MinimalPublicUserDTO {
avatar?: string | null;
diff --git a/src/util/entities/Application.ts b/src/util/entities/Application.ts
deleted file mode 100644
index c4f40343..00000000
--- a/src/util/entities/Application.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Team } from "./Team";
-import { User } from "./User";
-import { Guild } from "./Guild";
-
-@Entity({
- name: "applications",
-})
-export class Application extends BaseClass {
- @Column()
- name: string;
-
- @Column({ nullable: true })
- icon?: string;
-
- @Column({ nullable: true })
- description: string;
-
- @Column({ nullable: true })
- summary: string = "";
-
- @Column({ type: "jsonb", nullable: true })
- type?: object; // TODO: this type is bad
-
- @Column()
- hook: boolean = true;
-
- @Column()
- bot_public?: boolean = true;
-
- @Column()
- bot_require_code_grant?: boolean = false;
-
- @Column()
- verify_key: string;
-
- @JoinColumn({ name: "owner_id" })
- @ManyToOne(() => User, { onDelete: "CASCADE" })
- owner: User;
-
- // TODO: enum this? https://discord.com/developers/docs/resources/application#application-object-application-flags
- @Column()
- flags: number = 0;
-
- @Column({ type: "varchar", nullable: true })
- redirect_uris: string[] = [];
-
- @Column({ nullable: true })
- rpc_application_state: number = 0;
-
- @Column({ nullable: true })
- store_application_state: number = 1;
-
- @Column({ nullable: true })
- verification_state: number = 1;
-
- @Column({ nullable: true })
- interactions_endpoint_url?: string;
-
- @Column({ nullable: true })
- integration_public: boolean = true;
-
- @Column({ nullable: true })
- integration_require_code_grant: boolean = false;
-
- @Column({ nullable: true })
- discoverability_state: number = 1;
-
- @Column({ nullable: true })
- discovery_eligibility_flags: number = 2240;
-
- @JoinColumn({ name: "bot_user_id" })
- @OneToOne(() => User, { onDelete: "CASCADE" })
- bot?: User;
-
- @Column({ type: "varchar", array: true, nullable: true })
- tags?: string[];
-
- @Column({ nullable: true })
- cover_image?: string; // the application's default rich presence invite cover image hash
-
- @Column({ type: "jsonb", nullable: true })
- install_params?: { scopes: string[]; permissions: string };
-
- @Column({ nullable: true })
- terms_of_service_url?: string;
-
- @Column({ nullable: true })
- privacy_policy_url?: string;
-
- @Column({ nullable: true })
- @RelationId((application: Application) => application.guild)
- guild_id?: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild)
- guild?: Guild; // guild to which the app is linked, e.g. a developer support server
-
- @Column({ nullable: true })
- custom_install_url?: string;
-
- //just for us
-
- //@Column({ type: "varchar", array: true, nullable: true })
- //rpc_origins?: string[];
-
- //@Column({ nullable: true })
- //primary_sku_id?: string; // if this application is a game sold, this field will be the id of the "Game SKU" that is created,
-
- //@Column({ nullable: true })
- //slug?: string; // if this application is a game sold, this field will be the URL slug that links to the store page
-
- @JoinColumn({ name: "team_id" })
- @ManyToOne(() => Team, {
- onDelete: "CASCADE",
- nullable: true,
- })
- team?: Team;
-}
diff --git a/src/util/entities/ApplicationCommand.ts b/src/util/entities/ApplicationCommand.ts
deleted file mode 100644
index 892bb833..00000000
--- a/src/util/entities/ApplicationCommand.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import {
- ApplicationCommandHandlerType,
- ApplicationCommandOption,
- ApplicationCommandIndexPermissions,
- ApplicationCommandType,
- Snowflake,
- ApplicationIntegrationType,
- InteractionContextType,
-} from "@spacebar/schemas";
-
-@Entity({
- name: "application_commands",
-})
-export class ApplicationCommand extends BaseClass {
- @Column({ default: ApplicationCommandType.CHAT_INPUT })
- type?: ApplicationCommandType;
-
- @Column()
- application_id: Snowflake;
-
- @Column({ nullable: true })
- guild_id?: Snowflake;
-
- @Column()
- name: string;
-
- @Column({ nullable: true, type: "jsonb" })
- name_localizations?: Record<string, string>;
-
- @Column()
- description: string;
-
- @Column({ nullable: true, type: "jsonb" })
- description_localizations?: Record<string, string>;
-
- @Column({ type: "jsonb", default: [] })
- options: ApplicationCommandOption[];
-
- @Column({ nullable: true, type: String })
- default_member_permissions: string | null;
-
- /*
- * @deprecated
- */
- @Column({ default: true })
- dm_permission?: boolean;
-
- @Column({ nullable: true, type: "jsonb" })
- permissions?: ApplicationCommandIndexPermissions;
-
- @Column({ default: false })
- nsfw?: boolean;
-
- @Column({ nullable: true, type: "jsonb" })
- integration_types?: ApplicationIntegrationType[];
-
- @Column({ default: 0 })
- global_popularity_rank?: number;
-
- @Column({ nullable: true, type: "jsonb" })
- contexts?: InteractionContextType[];
-
- @Column({ default: 0 })
- version: Snowflake;
-
- @Column({ default: 0 })
- handler?: ApplicationCommandHandlerType;
-}
diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts
deleted file mode 100644
index ff9643fc..00000000
--- a/src/util/entities/Attachment.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { Config, deleteFile } from "../util";
-import { BaseClass } from "./BaseClass";
-import { getUrlSignature, NewUrlUserSignatureData, NewUrlSignatureData } from "../Signing";
-import { PublicAttachment } from "../../schemas/api/messages/Attachments";
-
-@Entity({
- name: "attachments",
-})
-export class Attachment extends BaseClass {
- @Column()
- filename: string; // name of file attached
-
- @Column()
- size: number; // size of file in bytes
-
- @Column({ nullable: true })
- height?: number; // height of file (if image)
-
- @Column({ nullable: true })
- width?: number; // width of file (if image)
-
- @Column({ nullable: true })
- content_type?: string;
-
- @Column({ nullable: true })
- @RelationId((attachment: Attachment) => attachment.message)
- message_id: string;
-
- @Column({ nullable: true })
- @RelationId((attachment: Attachment) => attachment.channel)
- channel_id: string;
-
- @JoinColumn({ name: "message_id" })
- @ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, {
- onDelete: "CASCADE",
- })
- message: import("./Message").Message;
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => require("./Channel").Channel, {
- onDelete: "CASCADE",
- })
- channel: import("./Channel").Channel;
-
- @BeforeRemove()
- onDelete() {
- return deleteFile(new URL(this.toJSON().url).pathname);
- }
-
- toJSON() {
- const channelId = this.channel_id ?? this.channel?.id ?? this.message?.channel_id;
- const messageId = this.message_id ?? this.message?.id;
- return {
- ...this,
- url: `${Config.get().cdn.endpointPublic}/attachments/${channelId}/${messageId}/${this.filename}`,
- proxy_url: `${Config.get().cdn.endpointPublic}/attachments/${channelId}/${messageId}/${this.filename}`,
- };
- }
- signUrls(data: NewUrlUserSignatureData): PublicAttachment {
- const att = Attachment.prototype.toJSON.apply(this);
- return {
- ...att,
- url: getUrlSignature(new NewUrlSignatureData({ ...data, url: att.url }))
- .applyToUrl(att.url)
- .toString(),
- proxy_url: att.proxy_url
- ? getUrlSignature(new NewUrlSignatureData({ ...data, url: att.proxy_url }))
- .applyToUrl(att.proxy_url)
- .toString()
- : att.proxy_url,
- };
- }
-}
diff --git a/src/util/entities/AuditLog.ts b/src/util/entities/AuditLog.ts
deleted file mode 100644
index f47f6dc7..00000000
--- a/src/util/entities/AuditLog.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-import { AuditLogChange, AuditLogEvents } from "@spacebar/schemas";
-
-@Entity({
- name: "audit_logs",
-})
-export class AuditLog extends BaseClass {
- @JoinColumn({ name: "target_id" })
- @ManyToOne(() => User)
- target?: User;
-
- @Column({ nullable: true })
- @RelationId((auditlog: AuditLog) => auditlog.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, (user: User) => user.id)
- user: User;
-
- @Column({ type: "int" })
- action_type: AuditLogEvents;
-
- @Column({ type: "jsonb", nullable: true })
- options?: {
- delete_member_days?: string;
- members_removed?: string;
- channel_id?: string;
- messaged_id?: string;
- count?: string;
- id?: string;
- type?: string;
- role_name?: string;
- };
-
- @Column()
- @Column({ type: "jsonb" })
- changes: AuditLogChange[];
-
- @Column({ nullable: true })
- reason?: string;
-}
diff --git a/src/util/entities/AutomodRule.ts b/src/util/entities/AutomodRule.ts
deleted file mode 100644
index 97312984..00000000
--- a/src/util/entities/AutomodRule.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2024 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { BaseClass } from "./BaseClass";
-import { Entity, JoinColumn, ManyToOne, Column } from "typeorm";
-import { User } from "./User";
-import { AutomodAction, AutomodRuleEventType, AutomodRuleTriggerMetadata, AutomodRuleTriggerType } from "@spacebar/schemas";
-
-@Entity({
- name: "automod_rules",
-})
-export class AutomodRule extends BaseClass {
- @JoinColumn({ name: "creator_id" })
- @ManyToOne(() => User, { onDelete: "CASCADE" })
- creator: User;
-
- @Column()
- enabled: boolean;
-
- @Column()
- event_type: AutomodRuleEventType;
-
- @Column({ type: "int8", array: true })
- exempt_channels: string[];
-
- @Column({ type: "int8", array: true })
- exempt_roles: string[];
-
- @Column()
- guild_id: string;
-
- @Column()
- name: string;
-
- @Column()
- position: number;
-
- @Column()
- trigger_type: AutomodRuleTriggerType;
-
- @Column({
- type: "jsonb",
- nullable: true,
- })
- trigger_metadata?: // this is null for "Block suspected spam content"
- AutomodRuleTriggerMetadata;
-
- @Column({
- type: "jsonb",
- })
- actions: AutomodAction[];
-}
diff --git a/src/util/entities/BackupCodes.ts b/src/util/entities/BackupCodes.ts
deleted file mode 100644
index 6759a9a3..00000000
--- a/src/util/entities/BackupCodes.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-import crypto from "node:crypto";
-import { Config } from "../util";
-
-@Entity({
- name: "backup_codes",
-})
-export class BackupCode extends BaseClass {
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, { onDelete: "CASCADE" })
- user: User;
-
- @Column()
- code: string;
-
- @Column()
- consumed: boolean;
-
- @Column()
- expired: boolean;
-}
-
-export function generateMfaBackupCodes(user_id: string) {
- const backup_codes: BackupCode[] = [];
- for (let i = 0; i < Config.get().security.mfaBackupCodeCount; i++) {
- const code = BackupCode.create({
- user: { id: user_id },
- code: crypto.randomBytes(4).toString("hex"), // 8 characters
- consumed: false,
- expired: false,
- });
- backup_codes.push(code);
- }
-
- return backup_codes;
-}
diff --git a/src/util/entities/Badge.ts b/src/util/entities/Badge.ts
deleted file mode 100644
index 924ef79e..00000000
--- a/src/util/entities/Badge.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity } from "typeorm";
-import { BaseClassWithoutId } from "./BaseClass";
-
-@Entity({
- name: "badges",
-})
-export class Badge extends BaseClassWithoutId {
- @Column({ primary: true })
- id: string;
-
- @Column()
- description: string;
-
- @Column()
- icon: string;
-
- @Column({ nullable: true })
- link?: string;
-}
diff --git a/src/util/entities/Ban.ts b/src/util/entities/Ban.ts
deleted file mode 100644
index 17121ace..00000000
--- a/src/util/entities/Ban.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-import { User } from "./User";
-
-@Entity({
- name: "bans",
-})
-export class Ban extends BaseClass {
- @Column({ nullable: true })
- @RelationId((ban: Ban) => ban.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-
- @Column({ nullable: true })
- @RelationId((ban: Ban) => ban.guild)
- guild_id: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, {
- onDelete: "CASCADE",
- })
- guild: Guild;
-
- @Column({ nullable: true })
- @RelationId((ban: Ban) => ban.executor)
- executor_id: string;
-
- @JoinColumn({ name: "executor_id" })
- @ManyToOne(() => User)
- executor: User;
-
- @Column({ nullable: true })
- ip?: string;
-
- @Column({ nullable: true })
- reason?: string;
-}
diff --git a/src/util/entities/BaseClass.ts b/src/util/entities/BaseClass.ts
deleted file mode 100644
index a64138ad..00000000
--- a/src/util/entities/BaseClass.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { BaseEntity, BeforeInsert, BeforeUpdate, FindOptionsWhere, PrimaryColumn } from "typeorm";
-import { getDatabase } from "@spacebar/database";
-import { Snowflake } from "../util";
-import { OrmUtils } from "../imports";
-import { annotationsKey } from "../util/Decorators";
-
-export class BaseClassWithoutId extends BaseEntity {
- private get construct() {
- return this.constructor;
- }
-
- // stores custom annotations we may stick on the properties
- [annotationsKey]: { [p: string]: string[] };
-
- // retrieves the custom annotations as its not super straight forward
- get_annotations() {
- return Object.getPrototypeOf(this)[annotationsKey];
- }
-
- // Loops through all the keys and compares it to annotations. If the RemoveEmpty is there it sets the value to undefined if null
- clean_data() {
- const annotations = this.get_annotations();
- if (annotations == undefined || annotations.length > 0)
- //prevent errors if there are no annotations on an object
- return;
- for (const key in this) {
- if (
- key in this && // This object has this property, should never fail but better to be safe
- key in annotations && // If this property has an annotation
- annotations[key].indexOf("JsonRemoveEmpty") > -1 && // if one of the annotations is JsonRemoveEmpty
- (this[key] == null || // If this property is null
- (typeof this[key] == "object" && Object.keys(this[key]).length == 0))
- ) {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-expect-error
- this[key] = undefined; // set to undefined to remove
- }
- if (
- key in this && // This object has this property, should never fail but better to be safe
- key in annotations && // If this property has an annotation
- annotations[key].indexOf("JsonNumber") > -1 && // if one of the annotations is JsonRemoveEmpty
- typeof this[key] == "string" // and its a String
- ) {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-expect-error
- this[key] = Number(this[key]); // convert string back to number
- }
- }
- return this;
- }
-
- private get metadata() {
- return getDatabase()?.getMetadata(this.construct);
- }
-
- assign(props: object) {
- OrmUtils.mergeDeep(this, props);
- return this;
- }
-
- // TODO: fix eslint
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- toJSON(): any {
- this.clean_data();
- return Object.fromEntries(
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- this.metadata!.columns // @ts-ignore
- .map((x) => [x.propertyName, this[x.propertyName]])
- .concat(
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- this.metadata.relations.map((x) => [
- x.propertyName,
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- this[x.propertyName],
- ]),
- ),
- );
- }
-
- 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) {
- const repository = this.getRepository();
- return repository.decrement(conditions, propertyPath, value);
- }
-
- public async insert(): Promise<this> {
- await getDatabase()!.getRepository(this.construct).insert(this);
- return this;
- }
-}
-
-export class BaseClass extends BaseClassWithoutId {
- @PrimaryColumn({ type: "int8" })
- id: string = Snowflake.generate();
-
- @BeforeUpdate()
- @BeforeInsert()
- _do_validate() {
- if (!this.id) this.id = Snowflake.generate();
- }
-}
diff --git a/src/util/entities/Categories.ts b/src/util/entities/Categories.ts
deleted file mode 100644
index 8c7ca78c..00000000
--- a/src/util/entities/Categories.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, PrimaryColumn } from "typeorm";
-import { BaseClassWithoutId } from "./BaseClass";
-
-// TODO: categories:
-// [{
-// "id": 16,
-// "default": "Anime & Manga",
-// "localizations": {
-// "de": "Anime & Manga",
-// "fr": "Anim\u00e9s et mangas",
-// "ru": "\u0410\u043d\u0438\u043c\u0435 \u0438 \u043c\u0430\u043d\u0433\u0430"
-// }
-// },
-// "is_primary": false/true
-// }]
-// Also populate discord default categories
-
-@Entity({
- name: "categories",
-})
-export class Categories extends BaseClassWithoutId {
- // Not using snowflake
-
- @PrimaryColumn()
- id: number;
-
- @Column({ nullable: true })
- name: string;
-
- @Column({ type: "jsonb" })
- localizations: string;
-
- // Whether to show the category prominently (e.g. in a sidebar) instead of only secondary (e.g. in search results)
- @Column({ nullable: true })
- is_primary: boolean;
-
- @Column({ nullable: true })
- icon?: string;
-}
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
deleted file mode 100644
index e3c1c979..00000000
--- a/src/util/entities/Channel.ts
+++ /dev/null
@@ -1,736 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { HTTPError } from "lambert-server/HTTPError";
-import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
-import { DmChannelDTO } from "../dtos";
-import { ChannelCreateEvent, ChannelRecipientRemoveEvent, ThreadCreateEvent, ThreadMembersUpdateEvent } from "../interfaces";
-import { InvisibleCharacters, Snowflake, emitEvent, getPermission, trimSpecial, Permissions, Config, DiscordApiErrors } from "../util";
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-import { Invite } from "./Invite";
-import { Message } from "./Message";
-import { Tag } from "./Tag";
-import { ReadState } from "./ReadState";
-import { Recipient } from "./Recipient";
-import { User } from "./User";
-import { VoiceState } from "./VoiceState";
-import { Webhook } from "./Webhook";
-import { Member } from "./Member";
-import { ChannelPermissionOverwrite, ChannelType, PublicChannel, PublicUserProjection, ThreadMetadata } from "@spacebar/schemas";
-import { OrmUtils } from "../imports";
-import { ThreadMember } from "./ThreadMember";
-
-@Entity({
- name: "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[];
-
- @OneToMany(() => ThreadMember, (member: ThreadMember) => member.channel, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- thread_members?: ThreadMember[];
-
- @Column({ nullable: true })
- last_message_id?: string;
-
- @Column({ nullable: true })
- @RelationId((channel: Channel) => channel.guild)
- guild_id?: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, (guild) => guild.channels, {
- onDelete: "CASCADE",
- nullable: true,
- })
- guild?: Guild;
-
- @Column({ nullable: true })
- @RelationId((channel: Channel) => channel.parent)
- parent_id: string | null;
-
- @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, type: "timestamp with time zone" })
- last_pin_timestamp?: Date | null; // ISO8601
-
- @Column({ nullable: true })
- default_auto_archive_duration?: number;
-
- @Column({ type: "jsonb", 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[];
-
- @Column()
- flags: number = 0;
-
- @Column({ nullable: true })
- default_thread_rate_limit_per_user?: number = 0;
-
- @Column({ type: "jsonb", nullable: true })
- thread_metadata?: ThreadMetadata;
-
- @Column({ nullable: true })
- member_count?: number;
-
- @Column({ nullable: true })
- message_count?: number;
-
- @Column({ nullable: true })
- total_message_sent?: number;
-
- @JoinColumn({ name: "available_tags_ids" })
- @OneToMany(() => Tag, (tag: Tag) => tag.channel, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- available_tags?: Tag[];
-
- @Column("text", { array: true, nullable: true })
- applied_tags?: string[];
-
- @Column("text", { nullable: true })
- status?: string | null;
-
- /** Must be calculated Channel.calculatePosition */
- position: number;
-
- // TODO: DM channel
- static async createChannel(
- channel: Partial<Channel>,
- user_id: string = "0",
- opts?: {
- keepId?: boolean;
- skipExistsCheck?: boolean;
- skipPermissionCheck?: boolean;
- skipEventEmit?: boolean;
- skipNameChecks?: boolean;
- },
- ): Promise<Channel> {
- if (!opts?.skipPermissionCheck) {
- // Always check if user has permission first
- const permissions = await getPermission(user_id, channel.guild_id);
- permissions.hasThrow("MANAGE_CHANNELS");
- }
-
- const guild = await Guild.findOneOrFail({
- where: { id: channel.guild_id },
- select: {
- features: !opts?.skipNameChecks,
- channel_ordering: true,
- id: true,
- },
- });
-
- if (!opts?.skipNameChecks) {
- if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) {
- for (const 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 && channel.type !== ChannelType.GUILD_STAGE_VOICE && channel.type !== ChannelType.GUILD_VOICE) ||
- guild.features.includes("IRC_LIKE_CATEGORY_NAMES")
- ) {
- 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) {
- // TODO: should threads even be routed through this function instead of createThreadChannel?
- case ChannelType.GUILD_PUBLIC_THREAD:
- case ChannelType.GUILD_PRIVATE_THREAD:
- case ChannelType.GUILD_NEWS_THREAD:
- case ChannelType.GUILD_TEXT:
- case ChannelType.GUILD_FORUM:
- case ChannelType.GUILD_MEDIA:
- 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
-
- const position = (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0;
-
- channel = {
- ...channel,
- ...(!opts?.keepId && { id: Snowflake.generate() }),
- created_at: new Date(),
- position,
- // from #876 (threads): shouldnt these be undefined?
- // message_count: 0,
- // member_count: 0,
- // total_message_sent: 0,
- };
-
- // TODO: figure out why the generic is required here
- const ret = Channel.create<Channel>(channel);
-
- await Promise.all([
- ret.save(),
- !opts?.skipEventEmit
- ? emitEvent({
- event: "CHANNEL_CREATE",
- data: ret.toJSON(),
- guild_id: channel.guild_id,
- } satisfies ChannelCreateEvent)
- : Promise.resolve(),
- Guild.insertChannelInOrder(guild.id, ret.id, position, guild),
- ]);
-
- return ret;
- }
- threadOnly() {
- return this.type === ChannelType.GUILD_FORUM || this.type === ChannelType.GUILD_MEDIA;
- }
-
- static async createThreadChannel(
- channel: Partial<Channel>,
- metadata: Partial<ThreadMetadata>,
- user_id: string = "0",
- opts?: {
- keepId?: boolean;
- skipExistsCheck?: boolean;
- skipParentExistsCheck?: boolean;
- skipPermissionCheck?: boolean;
- skipEventEmit?: boolean;
- skipNameChecks?: boolean;
- },
- ): Promise<Channel> {
- channel = {
- // set the default type to private
- type: ChannelType.GUILD_PRIVATE_THREAD,
- ...channel,
- ...(!opts?.keepId && { id: Snowflake.generate() }),
- created_at: new Date(),
- position: 0, // TODO:
- message_count: 0,
- member_count: 1,
- total_message_sent: 0,
- };
-
- const exists = await Channel.findOne({
- where: {
- id: channel.id,
- },
- });
-
- const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } });
-
- if (!opts?.skipExistsCheck && !guild.features.includes("ALLOW_EXISTING_THREAD_FOR_MESSAGE") && exists) throw DiscordApiErrors.THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE;
-
- if (!channel.parent_id) throw new HTTPError("Parent id not set", 400);
- const parent = await Channel.findOneOrFail({ where: { id: channel.parent_id } });
-
- if (!opts?.skipPermissionCheck) {
- // Always check if user has permission first
- const permissions = await getPermission(user_id, parent.guild_id);
- permissions.hasThrow(channel.type === ChannelType.GUILD_PRIVATE_THREAD ? "CREATE_PRIVATE_THREADS" : "CREATE_PUBLIC_THREADS");
- }
-
- channel = {
- ...channel,
- permission_overwrites: parent.permission_overwrites,
- nsfw: parent.nsfw,
- owner_id: user_id,
- guild_id: parent.guild_id,
- thread_metadata: {
- create_timestamp: new Date().toISOString(),
- archive_timestamp: new Date().toISOString(),
- archived: false,
- auto_archive_duration: 0,
- invitable: channel.type === ChannelType.GUILD_NEWS_THREAD || channel.type === ChannelType.GUILD_PUBLIC_THREAD ? Config.get().guild.publicThreadsInvitable : false,
- locked: false,
- ...metadata,
- },
- };
-
- if (!opts?.skipParentExistsCheck) {
- if (!parent) throw new HTTPError("Parent channel doesn't exist", 400);
- if (parent.guild_id !== channel.guild_id) throw new HTTPError("The category channel needs to be in the guild");
- }
-
- if (!opts?.skipNameChecks) {
- const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } });
- if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) {
- for (const character of InvisibleCharacters) if (channel.name.includes(character)) throw new HTTPError("Channel name cannot include invalid characters", 403);
-
- 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);
- }
- }
-
- // TODO: eagerly auto generate position of all guild channels
-
- const thread = await OrmUtils.mergeDeep(new Channel(), channel).save();
-
- const member = {
- id: thread.id,
- user_id,
- join_timestamp: new Date(),
- muted: false,
- mute_config: null,
- flags: 0,
- };
- if (channel.member_count) channel.member_count++;
-
- const threadMember = await OrmUtils.mergeDeep(new ThreadMember(), member).save();
-
- if (!opts?.skipEventEmit) {
- await Promise.all([
- emitEvent({
- event: "THREAD_CREATE",
- data: {
- ...thread,
- newly_created: true,
- },
- guild_id: channel.guild_id,
- } satisfies ThreadCreateEvent),
- emitEvent({
- event: "THREAD_MEMBERS_UPDATE",
- data: {
- guild_id: channel.guild_id!, // TODO: is this the right fix?
- id: thread.id,
- member_count: channel.member_count ?? 0, //TODO: is this the right fix?
- added_members: [threadMember],
- removed_member_ids: [],
- },
- guild_id: channel.guild_id,
- } satisfies ThreadMembersUpdateEvent),
- ]);
- }
-
- return thread;
- }
-
- static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
- recipients = [...new Set(recipients)].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;
- let needsTx = true;
-
- const channelRecipients = [...recipients, creator_user_id];
-
- const userRecipients = await Recipient.find({
- where: { user_id: creator_user_id },
- relations: { channel: { recipients: true } },
- });
-
- for (const ur of userRecipients) {
- if (!ur.channel.recipients) continue;
- const re = ur.channel.recipients.map((r) => r.user_id);
- if (re.length === channelRecipients.length) {
- if (channelRecipients.every((_) => re.includes(_))) {
- if (channel == null) {
- channel = ur.channel;
- if (!ur.closed) needsTx = false;
- await ur.assign({ closed: false }).save();
- }
- }
- }
- }
-
- if (channel == null) {
- name = trimSpecial(name);
-
- channel = await Channel.create({
- name,
- type,
- owner_id: type === ChannelType.GROUP_DM ? creator_user_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 (!needsTx) {
- /*ignored*/
- } else if (type === ChannelType.GROUP_DM && channel.recipients) {
- for (const 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,
- } satisfies ChannelRecipientRemoveEvent);
- }
-
- static async deleteChannel(channel: Channel) {
- // TODO Delete attachments from the CDN for messages in the channel
- await Channel.delete({ id: channel.id });
-
- if (channel.guild_id) {
- const guild = await Guild.findOneOrFail({
- where: { id: channel.guild_id },
- select: { channel_ordering: true },
- });
-
- const updatedOrdering = guild.channel_ordering.filter((id) => id != channel.id);
- await Guild.update({ id: channel.guild_id }, { channel_ordering: updatedOrdering });
- }
- }
-
- static async calculatePosition(channel_id: string, guild_id: string, guild?: Guild) {
- if (!guild)
- guild = await Guild.findOneOrFail({
- where: { id: guild_id },
- select: { channel_ordering: true },
- });
-
- return guild.channel_ordering.findIndex((id) => channel_id == id);
- }
-
- static async getOrderedChannels(guild_id: string, guild?: Guild) {
- if (!guild)
- guild = await Guild.findOneOrFail({
- where: { id: guild_id },
- select: { channel_ordering: true },
- });
-
- const channels = await Promise.all(guild.channel_ordering.map((id) => Channel.findOne({ where: { id } })));
-
- return channels
- .filter((channel) => channel !== null)
- .reduce((r, v) => {
- v = v as Channel;
-
- v.position = (guild as Guild).channel_ordering.indexOf(v.id);
- r[v.position] = v;
- return r;
- }, [] as Array<Channel>);
- }
-
- isDm() {
- return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM;
- }
-
- isThread() {
- return this.type === ChannelType.GUILD_NEWS_THREAD || this.type === ChannelType.GUILD_PUBLIC_THREAD || this.type === ChannelType.GUILD_PRIVATE_THREAD;
- }
- isForum() {
- return this.type === ChannelType.GUILD_FORUM || this.type === ChannelType.GUILD_MEDIA;
- }
-
- isPrivateThread() {
- return this.type === ChannelType.GUILD_PRIVATE_THREAD;
- }
-
- isPublicThread() {
- return this.type === ChannelType.GUILD_NEWS_THREAD || this.type === ChannelType.GUILD_PUBLIC_THREAD;
- }
-
- // Does the channel support sending messages ( eg categories do not )
- isWritable() {
- const disallowedChannelTypes = [ChannelType.GUILD_CATEGORY, ChannelType.GUILD_STAGE_VOICE];
- return disallowedChannelTypes.indexOf(this.type) == -1;
- }
-
- async getUserPermissions(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise<Permissions> {
- if (this.isDm()) return this.owner_id == (opts.user_id ?? opts.user?.id) ? Permissions.ALL : Permissions.DEFAULT_DM_PERMISSIONS;
- let guild = opts.guild;
- if (!guild) {
- if (this.guild) guild = this.guild;
- else if (this.guild_id) guild = await Guild.findOneOrFail({ where: { id: this.guild_id } });
- else {
- console.error("Channel.getUserPermissions: called without guild for non-DM channel.");
- return Permissions.NONE;
- }
- }
-
- // check if we can resolve here to short-circuit possibly calling the database unnecessarily
- // TODO: do we want to have an instance-wide opt out of this behavior? It would just be an extra if statement here
- const ownerId = guild?.owner?.id ?? guild?.owner_id;
- if (!!opts.user_id && ownerId === opts.user_id) return Permissions.ALL;
- if (!!opts.user?.id && ownerId === opts.user?.id) return Permissions.ALL;
- if (!!opts.member?.id && ownerId === opts.member?.id) return Permissions.ALL;
-
- let member = opts.member;
- if (!member) {
- if (opts.user) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user.id }, relations: { roles: true } });
- else if (opts.user_id) member = await Member.findOneOrFail({ where: { guild_id: guild.id, id: opts.user_id }, relations: { roles: true } });
- else {
- console.error("Channel.getUserPermissions: called without user or member for non-DM channel.");
- return Permissions.NONE;
- }
- }
-
- const roles = (
- member.roles ||
- (
- await Member.findOneOrFail({
- where: { guild_id: guild.id, index: member.index },
- relations: { roles: true },
- select: {
- roles: {
- id: true,
- permissions: true,
- position: true,
- },
- },
- loadEagerRelations: false,
- })
- ).roles
- ).sort((a, b) => a.position - b.position); // ascending by position
-
- return Permissions.finalPermission({
- user: {
- ...member,
- roles: roles.map((r) => r.id),
- flags: member.user?.flags ?? (await User.findOneOrFail({ where: { id: member.id }, select: { flags: true } })).flags,
- },
- guild: { id: guild.id, owner_id: guild.owner_id!, roles }, // We don't care about including *all* guild roles, as not all of them are relevant...
- channel: this,
- });
- }
-
- // TODO: should we throw for missing args?
- async canViewChannel(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise<boolean> {
- if (this.isDm()) return await this.canViewDmChannel(opts.user_id, opts.user);
-
- const userPerms = await this.getUserPermissions(opts);
- return userPerms.has("VIEW_CHANNEL");
- }
-
- private async canViewDmChannel(user_id?: string, user?: User): Promise<boolean> {
- const userId = user_id ?? user?.id;
- if (!userId) {
- console.error("Channel.canViewChannel: called without user for DM channel.");
- return false;
- }
- if (!user) return false;
- if (this.recipients) return this.recipients.some((r) => r.user_id === user.id && !r.closed);
- else {
- // we dont have recipients on hand
- const recipient = await Recipient.findOne({ where: { channel_id: this.id, user_id: user.id } });
- return recipient == null ? false : !recipient.closed;
- }
- }
-
- toJSON(): PublicChannel {
- return {
- ...this,
- last_pin_timestamp: this.last_pin_timestamp?.toISOString(),
- guild_id: this.guild_id ?? undefined,
- recipients: undefined, //this.recipients?.map(x=>x.user.toPublicUser()), // TODO: fix me
- owner: undefined, // TODO: fix me - this is thread owner
-
- // these fields are not returned depending on the type of channel
- bitrate: this.bitrate || undefined,
- user_limit: this.user_limit || undefined,
- rate_limit_per_user: this.rate_limit_per_user || undefined,
- owner_id: this.owner_id || undefined,
- ...(this.isThread() && this.thread_members ? { member_ids_preview: this.thread_members.map((_) => _.member.id) } : {}),
- default_auto_archive_duration: this.default_auto_archive_duration ?? undefined,
- retention_policy_id: undefined,
- thread_metadata: this.thread_metadata
- ? {
- ...this.thread_metadata,
- archive_timestamp: new Date(this.thread_metadata.archive_timestamp).toISOString().replace("Z", "+00:00"),
- create_timestamp: new Date(this.thread_metadata.create_timestamp).toISOString().replace("Z", "+00:00"),
- }
- : undefined,
- member_count: this.member_count ?? undefined,
- message_count: this.message_count ?? undefined,
- total_message_sent: this.total_message_sent ?? undefined,
- applied_tags: this.applied_tags ?? undefined,
- permission_overwrites: this.isThread() ? undefined : this.permission_overwrites,
- };
- }
-}
diff --git a/src/util/entities/ClientRelease.ts b/src/util/entities/ClientRelease.ts
deleted file mode 100644
index fc8fe86c..00000000
--- a/src/util/entities/ClientRelease.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity } from "typeorm";
-import { BaseClass } from "./BaseClass";
-
-@Entity({
- name: "client_release",
-})
-export class ClientRelease extends BaseClass {
- @Column()
- name: string;
-
- @Column()
- pub_date: Date;
-
- @Column()
- url: string;
-
- @Column()
- platform: string;
-
- @Column()
- enabled: boolean;
-
- @Column({ nullable: true })
- notes?: string;
-}
diff --git a/src/util/entities/CloudAttachment.ts b/src/util/entities/CloudAttachment.ts
deleted file mode 100644
index 408df024..00000000
--- a/src/util/entities/CloudAttachment.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-import { Channel } from "./Channel";
-
-@Entity({
- name: "cloud_attachments",
-})
-export class CloudAttachment extends BaseClass {
- // Internal tracking metadata
- @Column({ name: "user_id", nullable: true })
- @RelationId((att: CloudAttachment) => att.user)
- userId: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, { nullable: true, onDelete: "SET NULL" })
- user?: User;
-
- @Column({ name: "channel_id", nullable: true })
- @RelationId((att: CloudAttachment) => att.channel)
- channelId?: string; // channel the file is uploaded to
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => Channel, { nullable: true, onDelete: "SET NULL" })
- channel?: Channel; // channel the file is uploaded to
-
- @Column({ name: "upload_filename" })
- uploadFilename: string;
-
- // User-provided info
- @Column({ name: "user_attachment_id", nullable: true })
- userAttachmentId?: string;
-
- @Column({ name: "user_filename" })
- userFilename: string; // name of file attached
-
- @Column({ name: "user_file_size", nullable: true })
- userFileSize?: number; // size of file in bytes
-
- @Column({ name: "user_original_content_type", nullable: true })
- userOriginalContentType?: string;
-
- @Column({ name: "user_is_clip", nullable: true })
- userIsClip?: boolean; // whether the file is a clip
-
- // Actual file info, initialised after upload
- @Column({ nullable: true })
- size?: number; // size of file in bytes
-
- @Column({ nullable: true })
- height?: number; // height of file (if image)
-
- @Column({ nullable: true })
- width?: number; // width of file (if image)
-
- @Column({ name: "content_type", nullable: true })
- contentType?: string;
-
- // @BeforeRemove()
- // onDelete() {
- // return deleteFile(new URL(this.url).pathname);
- // }
-}
diff --git a/src/util/entities/Config.ts b/src/util/entities/Config.ts
deleted file mode 100644
index 7d4e05af..00000000
--- a/src/util/entities/Config.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, PrimaryColumn } from "typeorm";
-import { BaseClassWithoutId } from "./BaseClass";
-
-@Entity({
- name: "config",
-})
-export class ConfigEntity extends BaseClassWithoutId {
- @PrimaryColumn()
- key: string;
-
- @Column({ type: "simple-json", nullable: true })
- value: number | boolean | null | string | undefined;
-}
diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts
deleted file mode 100644
index d0ed8097..00000000
--- a/src/util/entities/ConnectedAccount.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-import { ConnectedAccountTokenData } from "@spacebar/schemas";
-
-@Entity({
- name: "connected_accounts",
-})
-export class ConnectedAccount extends BaseClass {
- @Column()
- external_id: string;
-
- @Column({ nullable: true })
- @RelationId((account: ConnectedAccount) => account.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-
- @Column({ select: false })
- friend_sync?: boolean = false;
-
- @Column()
- name: string;
-
- @Column({ select: false })
- revoked?: boolean = false;
-
- @Column({ select: false })
- show_activity?: number = 0;
-
- @Column()
- type: string;
-
- @Column()
- verified?: boolean = true;
-
- @Column({ select: false })
- visibility?: number = 0;
-
- @Column({ type: "varchar", array: true })
- integrations?: string[] = [];
-
- @Column({ type: "jsonb", name: "metadata", nullable: true })
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- metadata_?: any;
-
- @Column()
- metadata_visibility?: number = 0;
-
- @Column()
- two_way_link?: boolean = false;
-
- @Column({ select: false, nullable: true, type: "jsonb" })
- token_data?: ConnectedAccountTokenData | null;
-
- async revoke() {
- this.revoked = true;
- this.token_data = null;
- await this.save();
- }
-}
diff --git a/src/util/entities/ConnectionConfigEntity.ts b/src/util/entities/ConnectionConfigEntity.ts
deleted file mode 100644
index 9ad2af8b..00000000
--- a/src/util/entities/ConnectionConfigEntity.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, PrimaryColumn } from "typeorm";
-import { BaseClassWithoutId } from "./BaseClass";
-
-@Entity({
- name: "connection_config",
-})
-export class ConnectionConfigEntity extends BaseClassWithoutId {
- @PrimaryColumn()
- key: string;
-
- @Column({ type: "simple-json", nullable: true })
- value: number | boolean | null | string | Date | undefined;
-}
diff --git a/src/util/entities/EmbedCache.ts b/src/util/entities/EmbedCache.ts
deleted file mode 100644
index b34be18c..00000000
--- a/src/util/entities/EmbedCache.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { BaseClass } from "./BaseClass";
-import { Entity, Column } from "typeorm";
-import { Embed } from "@spacebar/schemas";
-
-@Entity({
- name: "embed_cache",
-})
-export class EmbedCache extends BaseClass {
- @Column()
- url: string;
-
- @Column({ type: "jsonb", nullable: true })
- embed?: Embed;
-
- @Column({ type: "jsonb", nullable: true })
- embeds?: Embed[];
-
- @Column({ name: "created_at", type: "timestamp with time zone" })
- createdAt: Date;
-}
diff --git a/src/util/entities/Emoji.ts b/src/util/entities/Emoji.ts
deleted file mode 100644
index 586550f9..00000000
--- a/src/util/entities/Emoji.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { User } from ".";
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-
-@Entity({
- name: "emojis",
-})
-export class Emoji extends BaseClass {
- @Column()
- animated: boolean;
-
- @Column()
- available: boolean; // whether this emoji can be used, may be false due to various reasons
-
- @Column()
- guild_id: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, (guild) => guild.emojis, {
- onDelete: "CASCADE",
- })
- guild: Guild;
-
- @Column({ nullable: true })
- @RelationId((emoji: Emoji) => emoji.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User)
- user: User;
-
- @Column()
- managed: boolean;
-
- @Column()
- name: string;
-
- @Column()
- require_colons: boolean;
-
- @Column({ type: "int8", array: true })
- roles: string[]; // roles this emoji is whitelisted to (new discord feature?)
-
- @Column({ type: "int8", array: true, nullable: true })
- groups: string[]; // user groups this emoji is whitelisted to (Spacebar extension)
-}
diff --git a/src/util/entities/Encryption.ts b/src/util/entities/Encryption.ts
deleted file mode 100644
index 9c76701a..00000000
--- a/src/util/entities/Encryption.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity } from "typeorm";
-import { BaseClass } from "./BaseClass";
-
-@Entity({
- name: "security_settings",
-})
-export class SecuritySettings extends BaseClass {
- @Column({ nullable: true })
- guild_id: string;
-
- @Column({ nullable: true })
- channel_id: string;
-
- @Column()
- encryption_permission_mask: number;
-
- @Column({ type: "varchar", array: true })
- allowed_algorithms: string[];
-
- @Column()
- current_algorithm: string;
-
- @Column({ nullable: true })
- used_since_message: string;
-}
diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
deleted file mode 100644
index 0cea637e..00000000
--- a/src/util/entities/Guild.ts
+++ /dev/null
@@ -1,492 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
-import { arrayRemove } from "@spacebar/extensions";
-import { Config, GuildWelcomeScreen, Snowflake, handleFile } from "..";
-import { Ban } from "./Ban";
-import { BaseClass } from "./BaseClass";
-import { Channel } from "./Channel";
-import { Emoji } from "./Emoji";
-import { Invite } from "./Invite";
-import { Member } from "./Member";
-import { Role } from "./Role";
-import { Sticker } from "./Sticker";
-import { Template } from "./Template";
-import { User } from "./User";
-import { VoiceState } from "./VoiceState";
-import { Webhook } from "./Webhook";
-// TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0}
-// TODO: guild_scheduled_events
-// TODO: stage_instances
-// TODO: threads
-// TODO:
-// "keywords": [
-// "Genshin Impact",
-// "Paimon",
-// "Honkai Impact",
-// "ARPG",
-// "Open-World",
-// "Waifu",
-// "Anime",
-// "Genshin",
-// "miHoYo",
-// "Gacha"
-// ],
-
-export const PublicGuildRelations = [
- "channels",
- "emojis",
- "roles",
- "stickers",
- "voice_states",
- // "members", // TODO: These are public, but all members should not be fetched.
- // "members.user",
-];
-
-@Entity({
- name: "guilds",
-})
-export class Guild extends BaseClass {
- @Column({ type: String, nullable: true })
- @RelationId((guild: Guild) => guild.afk_channel)
- afk_channel_id?: string | null;
-
- @JoinColumn({ name: "afk_channel_id" })
- @ManyToOne(() => Channel)
- afk_channel?: Channel;
-
- @Column({ nullable: true })
- afk_timeout?: number;
-
- // * commented out -> use owner instead
- // application id of the guild creator if it is bot-created
- // @Column({ nullable: true })
- // application?: string;
-
- @JoinColumn({ name: "ban_ids" })
- @OneToMany(() => Ban, (ban: Ban) => ban.guild, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- bans: Ban[];
-
- @Column({ nullable: true })
- banner?: string;
-
- @Column({ nullable: true })
- default_message_notifications?: number;
-
- @Column({ nullable: true })
- description?: string;
-
- @Column({ nullable: true })
- discovery_splash?: string;
-
- @Column({ nullable: true })
- explicit_content_filter?: number;
-
- @Column({ type: "varchar", array: true })
- features: string[] = []; //TODO use enum
- //TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features
-
- @Column({ nullable: true })
- primary_category_id?: string; // TODO: this was number?
-
- @Column({ nullable: true })
- icon?: string;
-
- @Column()
- large?: boolean = false;
-
- @Column({ nullable: true })
- max_members?: number;
-
- @Column({ nullable: true })
- max_presences?: number;
-
- @Column({ nullable: true })
- max_video_channel_users?: number;
-
- @Column({ nullable: true })
- member_count?: number;
-
- @Column({ nullable: true })
- presence_count?: number; // users online
-
- @OneToMany(() => Member, (member: Member) => member.guild, {
- cascade: true,
- orphanedRowAction: "delete",
- onDelete: "CASCADE",
- })
- members: Member[];
-
- @JoinColumn({ name: "role_ids" })
- @OneToMany(() => Role, (role: Role) => role.guild, {
- cascade: true,
- orphanedRowAction: "delete",
- onDelete: "CASCADE",
- })
- roles: Role[];
-
- @JoinColumn({ name: "channel_ids" })
- @OneToMany(() => Channel, (channel: Channel) => channel.guild, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- channels: Channel[];
-
- @Column({ nullable: true })
- @RelationId((guild: Guild) => guild.template)
- template_id?: string;
-
- @JoinColumn({ name: "template_id", referencedColumnName: "id" })
- @ManyToOne(() => Template)
- template: Template;
-
- @JoinColumn({ name: "emoji_ids" })
- @OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild, {
- cascade: true,
- orphanedRowAction: "delete",
- onDelete: "CASCADE",
- })
- emojis: Emoji[];
-
- @JoinColumn({ name: "sticker_ids" })
- @OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild, {
- cascade: true,
- orphanedRowAction: "delete",
- onDelete: "CASCADE",
- })
- stickers: Sticker[];
-
- @JoinColumn({ name: "invite_ids" })
- @OneToMany(() => Invite, (invite: Invite) => invite.guild, {
- cascade: true,
- orphanedRowAction: "delete",
- onDelete: "CASCADE",
- })
- invites: Invite[];
-
- @JoinColumn({ name: "voice_state_ids" })
- @OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild, {
- cascade: true,
- orphanedRowAction: "delete",
- onDelete: "CASCADE",
- })
- voice_states: VoiceState[];
-
- @JoinColumn({ name: "webhook_ids" })
- @OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild, {
- cascade: true,
- orphanedRowAction: "delete",
- onDelete: "CASCADE",
- })
- webhooks: Webhook[];
-
- @Column({ nullable: true })
- mfa_level?: number;
-
- @Column()
- name: string;
-
- @Column({ nullable: true })
- @RelationId((guild: Guild) => guild.owner)
- owner_id?: string; // optional to allow for ownerless guilds
-
- @JoinColumn({ name: "owner_id", referencedColumnName: "id" })
- @ManyToOne(() => User)
- owner?: User; // optional to allow for ownerless guilds
-
- @Column({ nullable: true })
- preferred_locale?: string;
-
- @Column({ nullable: true })
- premium_subscription_count?: number;
-
- @Column()
- premium_tier?: number; // crowd premium level
-
- @Column({ type: String, nullable: true })
- @RelationId((guild: Guild) => guild.public_updates_channel)
- public_updates_channel_id: string | null;
-
- @JoinColumn({ name: "public_updates_channel_id" })
- @ManyToOne(() => Channel)
- public_updates_channel?: Channel;
-
- @Column({ type: String, nullable: true })
- @RelationId((guild: Guild) => guild.rules_channel)
- rules_channel_id?: string | null;
-
- @JoinColumn({ name: "rules_channel_id" })
- @ManyToOne(() => Channel)
- rules_channel?: string;
-
- @Column({ nullable: true })
- region?: string;
-
- @Column({ nullable: true })
- splash?: string;
-
- @Column({ type: String, nullable: true })
- @RelationId((guild: Guild) => guild.system_channel)
- system_channel_id?: string | null;
-
- @JoinColumn({ name: "system_channel_id" })
- @ManyToOne(() => Channel)
- system_channel?: Channel;
-
- @Column({ nullable: true })
- system_channel_flags?: number;
-
- @Column()
- unavailable: boolean = false;
-
- @Column({ nullable: true })
- verification_level?: number;
-
- /**
- * DEPRECATED: Look at the new Guild onboarding screens.
- */
- @Column({ type: "jsonb" })
- welcome_screen: GuildWelcomeScreen;
-
- @Column({ nullable: true, type: "int8" })
- @RelationId((guild: Guild) => guild.widget_channel)
- widget_channel_id?: string;
-
- @JoinColumn({ name: "widget_channel_id" })
- @ManyToOne(() => Channel)
- widget_channel?: Channel;
-
- @Column()
- widget_enabled: boolean = true;
-
- @Column({ nullable: true })
- nsfw_level?: number;
-
- @Column()
- nsfw: boolean = false;
-
- // TODO: nested guilds
- @Column({ nullable: true })
- parent?: string;
-
- // only for developer portal
- permissions?: number;
-
- //new guild settings, 11/08/2022:
- @Column({ nullable: true })
- premium_progress_bar_enabled: boolean = false;
-
- @Column({ select: false, type: "int8", array: true })
- channel_ordering: string[];
-
- @Column()
- discovery_weight: number = 0;
-
- @Column()
- discovery_excluded: boolean = false;
-
- async ToGuildSource() {
- if (!this.features.includes("DISCOVERABLE")) {
- return null;
- }
- return {
- id: this.id,
- name: this.name,
- icon: this.icon,
- description: this.description,
- banner: this.banner,
- splash: this.splash,
- discovery_splash: this.discovery_splash,
- features: this.features,
- vanity_url_code: null,
- preferred_locale: this.preferred_locale || "en",
- premium_subscription_count: this.premium_subscription_count,
- approximate_member_count: await Member.countBy({
- guild_id: this.id,
- }),
- approximate_presence_count: await Member.countBy({
- guild_id: this.id,
- user: {
- sessions: {
- status: "online",
- },
- },
- }),
- emojis: this.emojis ?? undefined,
- emoji_count: this.emojis ? this.emojis.length : undefined,
- stickers: this.stickers ?? undefined,
- sticker_count: this.stickers ? this.stickers.length : undefined,
- auto_removed: false,
- primary_category_id: this.primary_category_id,
- keywords: [],
- is_published: false,
- reasons_to_join: [],
- };
- }
-
- static async createGuild(body: {
- name?: string;
- icon?: string | null;
- owner_id?: string;
- roles?: Partial<Role>[];
- channels?: Partial<Channel>[];
- source_guild_id: string | null;
- }) {
- const guild_id = Snowflake.generate();
-
- const guild = await Guild.create({
- id: guild_id,
- name: body.name || "Spacebar",
- icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
- owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds
- 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: 4, // defaults effect: suppress the setup tips to save performance
- nsfw_level: 0,
- verification_level: 0,
- welcome_screen: {
- enabled: false,
- description: "",
- welcome_channels: [],
- },
- channel_ordering: [],
- afk_timeout: Config.get().defaults.guild.afkTimeout,
- default_message_notifications: Config.get().defaults.guild.defaultMessageNotifications,
- explicit_content_filter: Config.get().defaults.guild.explicitContentFilter,
- features: Config.get().guild.defaultFeatures,
- max_members: Config.get().limits.guild.maxMembers,
- max_presences: Config.get().defaults.guild.maxPresences,
- max_video_channel_users: Config.get().defaults.guild.maxVideoChannelUsers,
- region: Config.get().regions.default,
- }).save();
-
- // we have to create the role _after_ the guild because else we would get a foreign key error
- // TODO: make the @everyone a pseudorole that is dynamically generated at runtime so we can save storage
- await Role.create({
- id: guild_id,
- guild_id: guild_id,
- color: 0,
- colors: { primary_color: 0 },
- hoist: false,
- managed: false,
- mentionable: false,
- name: "@everyone",
- permissions: "2251804225",
- position: 0,
- icon: undefined,
- unicode_emoji: undefined,
- flags: 0, // TODO?
- }).save();
-
- // create custom roles if provided
- if (body.roles && body.roles.length) {
- await Promise.all(
- body.roles?.map(
- (role) =>
- new Promise((resolve) => {
- Role.create({
- ...role,
- guild_id,
- id:
- // role.id === body.template_guild_id indicates that this is the @everyone role
- role.id === body.source_guild_id || role.id == "0" ? guild_id : Snowflake.generate(),
- })
- .save()
- .then(resolve);
- }),
- ),
- );
- }
-
- if (!body.channels || !body.channels.length) {
- body.channels = [{ id: "01", type: 0, name: "general", nsfw: false }];
- }
-
- const ids = new Map();
-
- body.channels.forEach((x) => {
- if (x.id) {
- ids.set(x.id, Snowflake.generate());
- }
- });
-
- for (const channel of body.channels.sort((a) => (a.parent_id ? 1 : -1))) {
- const id = ids.get(channel.id) || Snowflake.generate();
-
- const parent_id = ids.get(channel.parent_id);
-
- const saved = await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, {
- keepId: true,
- skipExistsCheck: true,
- skipPermissionCheck: true,
- skipEventEmit: true,
- });
-
- await Guild.insertChannelInOrder(guild.id, saved.id, parent_id ?? channel.position ?? 0, guild);
- }
-
- return guild;
- }
-
- /** Insert a channel into the guild ordering by parent channel id or position */
- static async insertChannelInOrder(guild_id: string, channel_id: string, position: number, guild?: Guild): Promise<number>;
- static async insertChannelInOrder(guild_id: string, channel_id: string, parent_id: string, guild?: Guild): Promise<number>;
- static async insertChannelInOrder(guild_id: string, channel_id: string, insertPoint: string | number, guild?: Guild): Promise<number>;
- static async insertChannelInOrder(guild_id: string, channel_id: string, insertPoint: string | number, guild?: Guild): Promise<number> {
- if (!guild)
- guild = await Guild.findOneOrFail({
- where: { id: guild_id },
- select: { channel_ordering: true },
- });
-
- guild.channel_ordering ??= [];
-
- let position;
- if (typeof insertPoint == "string") position = guild.channel_ordering.indexOf(insertPoint) + 1;
- else position = insertPoint;
-
- arrayRemove(guild.channel_ordering, channel_id);
-
- guild.channel_ordering.splice(position, 0, channel_id);
- await Guild.update({ id: guild_id }, { channel_ordering: guild.channel_ordering });
- return position;
- }
-
- toJSON(): Guild {
- return {
- ...this,
- unavailable: this.unavailable == false ? undefined : true,
- channel_ordering: undefined,
- discovery_weight: undefined,
- discovery_excluded: undefined,
- parent: undefined,
- primary_category_id: undefined,
- nsfw: undefined,
- template_id: undefined,
- presence_count: undefined,
- };
- }
-}
diff --git a/src/util/entities/InstanceBan.ts b/src/util/entities/InstanceBan.ts
deleted file mode 100644
index 03424e5a..00000000
--- a/src/util/entities/InstanceBan.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, CreateDateColumn, Entity, FindOptionsWhere, Index, JoinColumn, OneToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-
-@Entity({
- name: "instance_bans",
-})
-export class InstanceBan extends BaseClass {
- @Column({ type: "bigint" })
- @CreateDateColumn()
- created_at: Date = new Date();
-
- @Column()
- reason: string;
-
- @Index()
- @Column({ nullable: true })
- user_id?: string;
-
- @Index()
- @Column({ nullable: true })
- fingerprint?: string;
-
- @Index()
- @Column({ nullable: true })
- ip_address?: string;
-
- // chain of trust type tracking
-
- @Column({ default: false })
- is_allowlisted: boolean = false;
-
- @Column({ default: false })
- is_from_other_instance_ban: boolean = false;
-
- @Column({ nullable: true })
- @RelationId((instance_ban: InstanceBan) => instance_ban.origin_instance_ban)
- origin_instance_ban_id?: string;
-
- @JoinColumn({ name: "origin_instance_ban_id" })
- @OneToOne(() => InstanceBan, { nullable: true, onDelete: "SET NULL" })
- origin_instance_ban?: InstanceBan;
-
- static async findInstanceBans(opts: { userId?: string; ipAddress?: string; fingerprint?: string; propagateBan?: boolean }) {
- const optionalChecks: FindOptionsWhere<InstanceBan>[] = [{ user_id: opts.userId }];
- if (opts?.ipAddress) optionalChecks.push({ ip_address: opts.ipAddress });
- if (opts?.fingerprint) optionalChecks.push({ fingerprint: opts.fingerprint });
- const instanceBans = await InstanceBan.find({ where: optionalChecks });
-
- const banReasons = [];
- for (const ban of instanceBans) {
- if (ban.is_allowlisted) continue;
- if (opts?.fingerprint && ban.fingerprint === opts.fingerprint) banReasons.push("fingerprint");
- if (opts?.ipAddress && ban.ip_address === opts.ipAddress) banReasons.push("ipAddress");
- if (opts?.userId && ban.user_id === opts?.userId) banReasons.push("userId");
- }
-
- const banViralityPromises: Promise<InstanceBan>[] = [];
- if (opts.propagateBan && banReasons.length > 0) {
- if (opts?.ipAddress && !instanceBans.find((b) => b.ip_address === opts.ipAddress))
- banViralityPromises.push(
- InstanceBan.create({
- user_id: opts.userId,
- ip_address: opts.ipAddress,
- reason: "Propagated from other instance ban",
- is_from_other_instance_ban: true,
- origin_instance_ban: instanceBans[0],
- }).save(),
- );
- if (opts?.fingerprint && !instanceBans.find((b) => b.fingerprint === opts.fingerprint))
- banViralityPromises.push(
- InstanceBan.create({
- user_id: opts.userId,
- fingerprint: opts.fingerprint,
- reason: "Propagated from other instance ban",
- is_from_other_instance_ban: true,
- origin_instance_ban: instanceBans[0],
- }).save(),
- );
- if (opts?.userId && !instanceBans.find((b) => b.user_id === opts.userId))
- banViralityPromises.push(
- InstanceBan.create({
- user_id: opts.userId,
- reason: "Propagated from other instance ban",
- is_from_other_instance_ban: true,
- origin_instance_ban: instanceBans[0],
- }).save(),
- );
- }
-
- await Promise.all(banViralityPromises);
- return banReasons;
- }
-}
diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts
deleted file mode 100644
index cc5374eb..00000000
--- a/src/util/entities/Invite.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm";
-import { BaseClassWithoutId } from "./BaseClass";
-import { Channel } from "./Channel";
-import { Guild } from "./Guild";
-import { Member } from "./Member";
-import { User } from "./User";
-
-export const PublicInviteRelation = ["inviter", "guild", "channel"];
-
-@Entity({
- name: "invites",
-})
-export class Invite extends BaseClassWithoutId {
- @PrimaryColumn()
- code: string;
-
- @Column()
- temporary: boolean;
-
- @Column()
- uses: number;
-
- @Column()
- max_uses: number;
-
- @Column()
- max_age: number;
-
- @Column()
- created_at: Date;
-
- @Column({ nullable: true })
- expires_at?: Date;
-
- @Column({ nullable: true })
- @RelationId((invite: Invite) => invite.guild)
- guild_id: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, (guild) => guild.invites, {
- onDelete: "CASCADE",
- })
- guild: Guild;
-
- @Column({ nullable: true })
- @RelationId((invite: Invite) => invite.channel)
- channel_id: string;
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => Channel, {
- onDelete: "CASCADE",
- })
- channel: Channel;
-
- @Column({ nullable: true })
- @RelationId((invite: Invite) => invite.inviter)
- inviter_id?: string;
-
- @JoinColumn({ name: "inviter_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- inviter: User;
-
- @Column({ nullable: true })
- @RelationId((invite: Invite) => invite.target_user)
- target_user_id: string;
-
- @JoinColumn({ name: "target_user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- target_user?: string; // could be used for "User specific invites" https://github.com/spacebarchat/server/issues/326
-
- @Column({ nullable: true })
- target_user_type?: number;
-
- @Column({ nullable: true })
- vanity_url?: boolean;
-
- @Column()
- flags: number;
-
- isExpired() {
- if (this.max_age !== 0 && this.expires_at && this.expires_at < new Date()) return true;
- if (this.max_uses !== 0 && this.uses >= this.max_uses) return true;
- return false;
- }
- toPublicJSON() {
- return {
- ...this,
- inviter: this.inviter.toPublicUser(),
- };
- }
-
- static async joinGuild(user_id: string, code: string) {
- const invite = await Invite.findOneOrFail({ where: { code } });
- if (invite.isExpired()) {
- await Invite.delete({ code });
- throw new Error("Invite is expired");
- }
- 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);
- return invite;
- }
-}
diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts
deleted file mode 100644
index b6b1ca11..00000000
--- a/src/util/entities/Member.ts
+++ /dev/null
@@ -1,488 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { HTTPError } from "lambert-server/HTTPError";
-import { BeforeInsert, BeforeUpdate, Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, Not, PrimaryGeneratedColumn, RelationId } from "typeorm";
-import { Ban, Channel, PublicGuildRelations } from ".";
-import { ReadyGuildDTO } from "../dtos";
-import { GuildCreateEvent, GuildDeleteEvent, GuildMemberAddEvent, GuildMemberRemoveEvent, GuildMemberUpdateEvent, MessageCreateEvent } from "../interfaces";
-import { Config, emitEvent, DiscordApiErrors, Stopwatch } from "../util";
-import { BaseClassWithoutId } from "./BaseClass";
-import { Guild } from "./Guild";
-import { Message } from "./Message";
-import { Role } from "./Role";
-import { User } from "./User";
-import { AvatarDecorationData, Collectibles, DisplayNameStyle, PublicMember, PublicMemberProjection, UserGuildSettings } from "@spacebar/schemas";
-
-export const MemberPrivateProjection: (keyof Member)[] = [
- "id",
- "guild",
- "guild_id",
- "deaf",
- "joined_at",
- "last_message_id",
- "mute",
- "nick",
- "pending",
- "premium_since",
- "roles",
- "settings",
- "user",
- "avatar",
- "banner",
- "bio",
- "theme_colors",
- "pronouns",
- "communication_disabled_until",
- "flags",
-];
-
-@Entity({
- name: "members",
-})
-@Index(["id", "guild_id"], { unique: true })
-export class Member extends BaseClassWithoutId {
- @PrimaryGeneratedColumn()
- index: string;
-
- @Column()
- @RelationId((member: Member) => member.user)
- id: string;
-
- @JoinColumn({ name: "id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-
- @Column()
- @RelationId((member: Member) => member.guild)
- guild_id: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, {
- onDelete: "CASCADE",
- })
- guild: Guild;
-
- @Column({ nullable: true })
- nick?: string;
-
- @JoinTable({
- name: "member_roles",
- joinColumn: { name: "index", referencedColumnName: "index" },
- inverseJoinColumn: {
- name: "role_id",
- referencedColumnName: "id",
- },
- })
- @ManyToMany(() => Role, { cascade: true })
- roles: Role[];
-
- @Column()
- joined_at: Date;
-
- @Column({ type: "bigint", nullable: true })
- premium_since?: number;
-
- @Column()
- deaf: boolean;
-
- @Column()
- mute: boolean;
-
- @Column()
- pending: boolean;
-
- @Column({ type: "jsonb", select: false })
- settings: UserGuildSettings;
-
- @Column({ nullable: true })
- last_message_id?: string;
-
- /**
- @JoinColumn({ name: "id" })
- @ManyToOne(() => User, {
- onDelete: "DO NOTHING",
- // do not auto-kick force-joined members just because their joiners left the server
- }) **/
- @Column({ nullable: true })
- joined_by: string;
-
- @Column({ nullable: true })
- avatar?: string;
-
- @Column({ nullable: true })
- banner: string;
-
- @Column()
- bio: string;
-
- @Column({ nullable: true, type: "int4", array: true })
- theme_colors?: number[]; // TODO: Separate `User` and `UserProfile` models
-
- @Column({ nullable: true })
- pronouns?: string;
-
- @Column({ nullable: true, type: Date })
- communication_disabled_until: Date | null;
-
- // TODO: add this when we have proper read receipts
- // @Column({ type: "jsonb" })
- // read_state: ReadState;
-
- @Column({ type: "jsonb", nullable: true })
- avatar_decoration_data?: AvatarDecorationData;
-
- @Column({ type: "jsonb", nullable: true })
- display_name_styles?: DisplayNameStyle;
-
- @Column({ type: "jsonb", nullable: true })
- collectibles?: Collectibles;
-
- @Column({ type: "int", default: 0 })
- flags: number = 0;
-
- @BeforeUpdate()
- @BeforeInsert()
- validate() {
- if (this.nick) {
- this.nick = this.nick.split("\n").join("");
- this.nick = this.nick.split("\t").join("");
- }
- if (this.nick === "") this.nick = undefined;
- if (this.pronouns === "") this.pronouns = undefined;
- }
-
- static async IsInGuildOrFail(user_id: string, guild_id: string) {
- if (
- await Member.count({
- where: { id: user_id, guild_id },
- })
- )
- return;
- 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: true },
- where: { id: guild_id },
- });
- if (guild.owner_id === user_id) throw new Error("The owner cannot be removed from the guild");
- const member = await Member.findOneOrFail({
- where: { id: user_id, guild_id },
- relations: { user: true },
- });
-
- // use promise all to execute all promises at the same time -> save time
- return Promise.all([
- Member.delete({
- id: user_id,
- guild_id,
- }),
- Guild.decrement({ id: guild_id }, "member_count", 1),
-
- emitEvent({
- event: "GUILD_DELETE",
- data: {
- id: guild_id,
- },
- user_id: user_id,
- } satisfies GuildDeleteEvent),
- emitEvent({
- event: "GUILD_MEMBER_REMOVE",
- data: { guild_id, user: member.user.toPublicUser() },
- guild_id,
- } satisfies GuildMemberRemoveEvent),
- ]);
- }
-
- static async addRole(user_id: string, guild_id: string, role_id: string) {
- const [member] = await Promise.all([
- Member.findOneOrFail({
- where: { id: user_id, guild_id },
- relations: { user: true, roles: true }, // we don't want to load the role objects just the ids
- select: {
- index: true,
- roles: {
- id: true,
- },
- },
- }),
- Role.findOneOrFail({
- where: { id: role_id, guild_id },
- select: { id: true },
- }),
- ]);
- member.roles.push(Role.create({ id: role_id }));
-
- await Promise.all([
- member.save(),
- emitEvent({
- event: "GUILD_MEMBER_UPDATE",
- data: {
- guild_id,
- user: member.user,
- roles: member.roles.map((x) => x.id),
- },
- guild_id,
- } satisfies GuildMemberUpdateEvent),
- ]);
- }
-
- 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 },
- relations: { user: true, roles: true }, // we don't want to load the role objects just the ids
- select: {
- index: true,
- roles: {
- id: true,
- },
- },
- }),
- Role.findOneOrFail({ where: { id: role_id, guild_id } }),
- ]);
- member.roles = member.roles.filter((x) => x.id !== role_id);
-
- await Promise.all([
- member.save(),
- emitEvent({
- event: "GUILD_MEMBER_UPDATE",
- data: {
- guild_id,
- user: member.user,
- roles: member.roles.map((x) => x.id),
- },
- guild_id,
- } satisfies GuildMemberUpdateEvent),
- ]);
- }
-
- static async changeNickname(user_id: string, guild_id: string, nickname: string) {
- const member = await Member.findOneOrFail({
- where: {
- id: user_id,
- guild_id,
- },
- relations: { user: true, roles: true },
- });
-
- // @ts-expect-error Member nickname is nullable
- member.nick = nickname || null;
-
- await Promise.all([
- member.save(),
-
- emitEvent({
- event: "GUILD_MEMBER_UPDATE",
- data: {
- guild_id,
- user: member.user,
- nick: nickname || undefined,
- roles: member.roles.map((x) => x.id),
- },
- guild_id,
- } satisfies GuildMemberUpdateEvent),
- ]);
- }
-
- static async addToGuild(user_id: string, guild_id: string, isRegistration: boolean = false) {
- const totalSw = Stopwatch.startNew();
- const incSw = Stopwatch.startNew();
- const logTrace = (...data: unknown[]) => {
- if (process.env.LOG_VERBOSE_TRACES !== "true") return;
- console.log("[Member.addToGuild]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`);
- };
-
- if (!isRegistration) {
- const isBanned = Ban.exists({ where: { guild_id, user_id } });
- const isMember = Member.exists({ where: { id: user_id, guild_id } });
-
- if (await isBanned) throw DiscordApiErrors.USER_BANNED;
- logTrace("Check bans");
-
- if (await isMember) throw new HTTPError("You are already a member of this guild", 400);
- logTrace("Check existing membership");
-
- 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} guild limit.`, 403);
- }
- logTrace("Enforce max guilds");
- }
-
- const guild = await Guild.findOneOrFail({
- where: {
- id: guild_id,
- },
- relations: PublicGuildRelations,
- relationLoadStrategy: "query",
- });
- const channelPositionsGuild = await Guild.findOneOrFail({
- where: { id: guild_id },
- select: { channel_ordering: true },
- });
- logTrace("Find guild");
-
- const newMember = Member.create({
- id: user_id,
- guild_id,
- nick: undefined,
- joined_at: new Date(),
- deaf: false,
- mute: false,
- pending: false,
- bio: "",
- roles: [Role.create({ id: guild_id })], // @everyone role
- // read_state: {},
- settings: {
- guild_id: null,
- mute_config: null,
- mute_scheduled_events: false,
- flags: 0,
- hide_muted_channels: false,
- notify_highlights: 0,
- channel_overrides: {},
- message_notifications: guild.default_message_notifications,
- mobile_push: true,
- muted: false,
- suppress_everyone: false,
- suppress_roles: false,
- version: 0,
- },
- // Member.save is needed because else the roles relations wouldn't be updated
- });
-
- let memberCount = 0;
- let memberPreview: PublicMember[] = [];
- if (!isRegistration) {
- for await (const channel of guild.channels) {
- channel.position = await Channel.calculatePosition(channel.id, guild_id, channelPositionsGuild);
- }
-
- logTrace("Reorder channels");
-
- memberCount = isRegistration ? 0 : await Member.count({ where: { guild_id } });
- logTrace("Get member count");
-
- memberPreview = (
- await Member.find({
- where: {
- guild_id,
- user: {
- id: Not(user_id),
- sessions: {
- status: Not("invisible" as const), // lol typescript?
- },
- },
- },
- relations: { user: true, roles: true },
- take: 10,
- })
- ).map((member) => member.toPublicMember());
- logTrace("Calculate member preview");
- }
-
- const user = await User.getPublicUser(user_id);
- logTrace("Get user");
-
- await Promise.all([
- newMember.save(), // TODO: can we somehow insert the roles manually? We have no entity for this... Would skip a few select's
- Guild.increment({ id: guild_id }, "member_count", 1),
- emitEvent({
- event: "GUILD_MEMBER_ADD",
- data: {
- ...newMember.toPublicMember(),
- user: user,
- guild_id,
- },
- guild_id,
- origin: "util/entities/Member.ts:377/addToGuild(user_id, guild_id)",
- } satisfies GuildMemberAddEvent),
- isRegistration
- ? null
- : emitEvent({
- event: "GUILD_CREATE",
- data: {
- ...new ReadyGuildDTO(guild).toJSON(),
- members: [...memberPreview, { ...newMember.toPublicMember(), user }],
- member_count: memberCount + 1,
- guild_hashes: {},
- guild_scheduled_events: [],
- joined_at: newMember.joined_at,
- presences: [],
- stage_instances: [],
- threads: [],
- embedded_activities: [],
- voice_states: guild.voice_states.map((x) => x.toPublicVoiceState()),
- },
- user_id,
- } satisfies GuildCreateEvent),
- ]);
- logTrace("Save member info");
-
- const welcomeChannelId = guild.system_channel_id;
- if (welcomeChannelId && (await Channel.exists({ where: { id: welcomeChannelId } }))) {
- // Send a welcome message
- const message = Message.create({
- type: 7,
- guild_id: guild.id,
- channel_id: welcomeChannelId,
- author: user,
- timestamp: new Date(),
- reactions: [],
- attachments: [],
- embeds: [],
- sticker_items: [],
- edited_timestamp: undefined,
- mentions: [],
- mention_channels: [],
- mention_roles: [],
- mention_everyone: false,
- });
-
- await Promise.all([
- message.insert(),
- emitEvent({
- event: "MESSAGE_CREATE",
- channel_id: message.channel_id,
- data: message.toJSON(),
- } satisfies MessageCreateEvent),
- Channel.update({ id: welcomeChannelId }, { last_message_id: message.id }),
- ]);
- logTrace("Send welcome message");
- }
- }
-
- toPublicMember() {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const member: any = {};
- PublicMemberProjection.forEach((x) => {
- member[x] = this[x];
- });
-
- if (this.roles) member.roles = this.roles.map((x: Role) => x.id);
- if (this.user) member.user = this.user.toPublicUser();
-
- return member as PublicMember;
- }
-}
diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
deleted file mode 100644
index 68e7cbbb..00000000
--- a/src/util/entities/Message.ts
+++ /dev/null
@@ -1,486 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { User } from "./User";
-import { Member } from "./Member";
-import { Role } from "./Role";
-import { Channel } from "./Channel";
-import { Application } from "./Application";
-import { Column, CreateDateColumn, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId, FindOneOptions, Raw, Not, BaseEntity, In } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-import { Webhook } from "./Webhook";
-import { Sticker } from "./Sticker";
-import { Attachment } from "./Attachment";
-import { NewUrlUserSignatureData } from "../Signing";
-import {
- ApplicationCommandType,
- BaseMessageComponents,
- Embed,
- MessageComponentType,
- MessageSnapshot,
- MessageType,
- PartialMessage,
- Poll,
- PublicMessage,
- Reaction,
- UnfurledMediaItem,
- PartialUser,
- InteractionType,
-} from "@spacebar/schemas";
-import { MessageFlags } from "@spacebar/util";
-import { JsonRemoveEmpty } from "../util/Decorators";
-
-@Entity({
- name: "messages",
-})
-@Index(["channel_id", "id"], { unique: true })
-export class Message extends BaseClass {
- @Column({ nullable: true })
- @RelationId((message: Message) => message.channel)
- @Index()
- channel_id?: string;
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => Channel, {
- onDelete: "CASCADE",
- })
- channel: Channel;
-
- @Column({ nullable: true })
- @RelationId((message: Message) => message.thread)
- @JsonRemoveEmpty
- thread_id?: string;
-
- @JoinColumn({ name: "thread_id" })
- @ManyToOne(() => Channel, {
- onDelete: "CASCADE",
- })
- @JsonRemoveEmpty
- thread?: Channel;
-
- @Column({ nullable: true })
- @RelationId((message: Message) => message.guild)
- @JsonRemoveEmpty
- guild_id?: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, {
- onDelete: "CASCADE",
- })
- guild?: Guild;
-
- @Column({ nullable: true })
- @RelationId((message: Message) => message.author)
- @Index()
- author_id?: string;
-
- @JoinColumn({ name: "author_id", referencedColumnName: "id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- author?: User;
-
- @Column({ nullable: true })
- @RelationId((message: Message) => message.member)
- member_id?: string;
-
- @JoinColumn({ name: "member_id", referencedColumnName: "id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- member?: Member;
-
- @Column({ nullable: true })
- @RelationId((message: Message) => message.webhook)
- @JsonRemoveEmpty
- webhook_id?: string;
-
- @JoinColumn({ name: "webhook_id" })
- @ManyToOne(() => Webhook)
- webhook?: Webhook;
-
- @Column({ nullable: true })
- @RelationId((message: Message) => message.application)
- application_id?: string;
-
- @JoinColumn({ name: "application_id" })
- @ManyToOne(() => Application)
- application?: Application;
-
- @Column({ nullable: true })
- content?: string;
-
- @Column()
- @CreateDateColumn()
- timestamp: Date;
-
- @Column({ nullable: true })
- edited_timestamp?: Date;
-
- @Column({ nullable: true })
- tts?: boolean;
-
- @Column({ nullable: true })
- mention_everyone?: boolean;
-
- @JoinTable({ name: "message_user_mentions" })
- @ManyToMany(() => User)
- mentions: User[];
-
- @JoinTable({ name: "message_role_mentions" })
- @ManyToMany(() => Role)
- mention_roles: Role[];
-
- @JoinTable({ name: "message_channel_mentions" })
- @ManyToMany(() => Channel)
- @JsonRemoveEmpty
- mention_channels: Channel[];
-
- @JoinTable({ name: "message_stickers" })
- @ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" })
- @JsonRemoveEmpty
- sticker_items?: Sticker[];
-
- @OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- attachments?: Attachment[];
-
- @Column({ type: "jsonb" })
- embeds: Embed[];
-
- @Column({ type: "jsonb" })
- @JsonRemoveEmpty
- reactions: Reaction[];
-
- @Column({ type: "text", nullable: true })
- @JsonRemoveEmpty
- nonce?: string;
-
- @Column({ nullable: true, type: Date })
- pinned_at?: Date | null;
-
- get pinned(): boolean {
- return this.pinned_at != null;
- }
-
- @Column({ type: "int" })
- type: MessageType;
-
- @Column({ type: "jsonb", nullable: true })
- @JsonRemoveEmpty
- activity?: {
- type: number;
- party_id: string;
- };
-
- @Column({ default: 0 })
- flags: number;
-
- @Column({ type: "jsonb", nullable: true })
- @JsonRemoveEmpty
- message_reference?: {
- message_id?: string;
- channel_id?: string;
- guild_id?: string;
- type?: number; // 0 = DEFAULT, 1 = FORWARD
- };
-
- @JoinColumn({ name: "message_reference_id" })
- @ManyToOne(() => Message, { onDelete: "SET NULL" })
- referenced_message?: Message | null;
-
- @Column({ type: "jsonb", nullable: true })
- @JsonRemoveEmpty
- interaction?: {
- id: string;
- type: InteractionType;
- name: string;
- };
-
- @Column({ type: "jsonb", nullable: true })
- @JsonRemoveEmpty
- interaction_metadata?: {
- id: string;
- type: InteractionType;
- user_id: string;
- authorizing_integration_owners: object;
- name: string;
- command_type: ApplicationCommandType;
- };
-
- @Column({ type: "jsonb", nullable: true })
- components?: BaseMessageComponents[];
-
- @Column({ type: "jsonb", nullable: true })
- @JsonRemoveEmpty
- poll?: Poll;
-
- @Column({ nullable: true })
- username?: string;
-
- @Column({ nullable: true })
- avatar?: string;
-
- @Column({ default: "[]", type: "jsonb" })
- message_snapshots: MessageSnapshot[];
-
- get isWebhook() {
- return this.webhook_id != null && this.webhook != null;
- }
-
- static async fillReplies(messages: Message[]) {
- const ms = messages
- .filter((msg) => msg.message_reference && !msg.referenced_message?.id && msg.message_reference.message_id)
- .filter((msg) => [MessageType.REPLY, MessageType.THREAD_STARTER_MESSAGE, MessageType.CONTEXT_MENU_COMMAND].includes(msg.type));
- if (!ms.length) return;
- const curMs = new Map(messages.map((m) => [m.id, m] as const));
- const neededIds = new Set(ms.map((m) => m.message_reference!.message_id as string)).difference(curMs);
- if (neededIds.size) {
- const newMessages = await Message.find({
- where: {
- id: In([...neededIds]),
- },
- relations: { author: true, mentions: true, mention_roles: true, mention_channels: true },
- });
- newMessages.forEach((msg) => curMs.set(msg.id, msg));
- }
- for (const message of ms) {
- message.referenced_message = curMs.get(message.message_reference!.message_id as string) || null;
- }
- }
-
- toJSON(shallow = false): PublicMessage {
- // this.clean_data();
- return {
- ...this,
- channel_id: this.channel_id ?? this.channel.id,
- channel: undefined,
-
- timestamp: this.timestamp.toISOString(),
- edited_timestamp: this.edited_timestamp ? this.edited_timestamp.toISOString() : null,
-
- author_id: undefined,
- member_id: undefined,
- webhook_id: this.webhook_id ?? undefined,
- application_id: undefined,
- mentions: this.mentions?.map((user) => {
- if (user && !user.toPublicUser) console.trace("toPublic user missing!!!");
- return (user?.toPublicUser?.() ?? user ?? undefined) as unknown as PartialUser;
- }),
-
- mention_roles: this.mention_roles?.map((role) => role.id) ?? [],
- mention_channels: this.mention_channels?.map((ch) => ch.toJSON()) ?? [],
- attachments: this.attachments?.map((att) => att.toJSON()) ?? [],
-
- nonce: this.nonce ?? undefined,
- tts: this.tts ?? false,
- guild: this.guild ?? undefined,
- webhook: this.webhook ?? undefined,
- interaction: this.interaction ?? undefined,
- interaction_metadata: this.interaction_metadata ?? undefined,
- reactions: this.reactions ?? undefined,
- sticker_items: this.sticker_items ?? undefined,
- message_reference: this.message_reference ?? undefined,
- mention_everyone: this.mention_everyone ?? false,
- author: {
- ...(this.author?.toPublicUser() ?? undefined),
- // Webhooks
- username: this.username ?? this.author?.username ?? null,
- avatar: this.avatar ?? this.author?.avatar ?? null,
- },
- activity: this.activity ?? undefined,
- application: this.application ?? undefined,
- components: this.components ?? [],
- poll: this.poll ?? undefined,
- content: this.content ?? "",
- pinned: this.pinned,
- thread: this.thread ? this.thread.toJSON() : this.thread,
- referenced_message: this.referenced_message && !shallow ? this.referenced_message.toJSON(true) : undefined,
- };
- }
-
- toPartialMessage(): PartialMessage {
- return {
- id: this.id,
- // lobby_id: this.lobby_id,
- channel_id: this.channel_id!,
- type: this.type,
- content: this.content!,
- author: { ...this.author!, avatar: this.author?.avatar ?? null },
- flags: this.flags,
- application_id: this.application_id,
- //channel: this.channel, // TODO: ephemeral DM channels
- // recipient_id: this.recipient_id, // TODO: ephemeral DM channels
- };
- }
-
- toSnapshot(): MessageSnapshot {
- return {
- message: {
- attachments: this.attachments?.map((x) => x.toJSON()),
- components: this.components,
- content: this.content!,
- edited_timestamp: this.edited_timestamp,
- embeds: this.embeds,
- flags: this.flags,
- mention_roles: this.mention_roles?.map((x) => x.id),
- mentions: this.mentions.map((x) => x.toPublicUser() as unknown as PartialUser), // TODO: write a proper method for this
- timestamp: this.timestamp,
- type: this.type,
- },
- };
- }
-
- withSignedAttachments(data: NewUrlUserSignatureData) {
- function signMedia(media: UnfurledMediaItem) {
- Object.assign(media, Attachment.prototype.signUrls.call(media, data));
- }
- return {
- ...this,
- attachments: this.attachments?.map((attachment: Attachment) => Attachment.prototype.signUrls.call(attachment, data)),
- components: this.components
- ? this.components.map((comp) => {
- comp = structuredClone(comp);
- if (comp.type === MessageComponentType.Section) {
- const accessory = comp.accessory;
- if (accessory.type === MessageComponentType.Thumbnail) {
- signMedia(accessory.media);
- }
- } else if (comp.type === MessageComponentType.MediaGallery) {
- comp.items.forEach(({ media }) => signMedia(media));
- } else if (comp.type === MessageComponentType.File) {
- signMedia(comp.file);
- } else if (comp.type === MessageComponentType.Container) {
- for (const elm of comp.components) {
- switch (elm.type) {
- case MessageComponentType.Separator:
- case MessageComponentType.TextDisplay:
- case MessageComponentType.ActionRow:
- break;
- case MessageComponentType.Section: {
- const accessory = elm.accessory;
- if (accessory.type === MessageComponentType.Thumbnail) {
- signMedia(accessory.media);
- }
- break;
- }
- case MessageComponentType.MediaGallery:
- elm.items.forEach(({ media }) => signMedia(media));
- break;
- case MessageComponentType.File: {
- signMedia(elm.file);
- break;
- }
-
- default:
- elm satisfies never;
- }
- }
- }
- return comp;
- })
- : this.components,
- };
- }
-
- static async createWithDefaults(opts: Partial<Message>): Promise<Message> {
- const message = Message.create();
-
- if (!opts.author) {
- if (!opts.author_id) throw new Error("Either author or author_id must be provided to create a Message");
- opts.author = await User.findOneOrFail({ where: { id: opts.author_id! } });
- }
-
- if (!opts.channel) {
- if (!opts.channel_id) throw new Error("Either channel or channel_id must be provided to create a Message");
- opts.channel = await Channel.findOneOrFail({ where: { id: opts.channel_id! } });
- opts.guild_id ??= opts.channel.guild_id;
- }
-
- if (!opts.member_id) opts.member_id = message.author_id;
- if (!opts.member) opts.member = await Member.findOneOrFail({ where: { id: opts.member_id! } });
-
- if (!opts.guild) {
- if (opts.guild_id) opts.guild = await Guild.findOneOrFail({ where: { id: opts.guild_id! } });
- else if (opts.channel?.guild?.id) opts.guild = opts.channel.guild;
- else if (opts.channel?.guild_id) opts.guild = await Guild.findOneOrFail({ where: { id: opts.channel.guild_id! } });
- else if (opts.member?.guild?.id) opts.guild = opts.member.guild;
- else if (opts.member?.guild_id) opts.guild = await Guild.findOneOrFail({ where: { id: opts.member.guild_id! } });
- else throw new Error("Either guild, guild_id, channel.guild, channel.guild_id, member.guild or member.guild_id must be provided to create a Message");
- }
-
- // try 2 now that we have a guild
- if (!opts.member) opts.member = await Member.findOneOrFail({ where: { id: opts.author!.id, guild_id: opts.guild!.id } });
-
- // set reply type if a message if referenced
- if (opts.message_reference && !opts.type) message.type = MessageType.REPLY;
-
- // backpropagate ids
- opts.channel_id = opts.channel.id;
- opts.guild_id = opts.guild.id;
- opts.author_id = opts.author.id;
- opts.member_id = opts.member.id;
- opts.webhook_id = opts.webhook?.id;
- opts.application_id = opts.application?.id;
-
- delete opts.member;
-
- Object.assign(message, {
- tts: false,
- embeds: [],
- reactions: [],
- flags: 0,
- type: 0,
- timestamp: new Date(),
- ...opts,
- });
- return message;
- }
- static addDefault(options: FindOneOptions<Message>) {
- if (options.where) {
- const arr = options.where instanceof Array ? options.where : [options.where];
- for (const thing of arr) {
- if (!("flags" in thing)) {
- thing.flags = Not(Raw((alias) => `${alias} & ${MessageFlags.FLAGS.EPHEMERAL} = ${MessageFlags.FLAGS.EPHEMERAL}`));
- }
- }
- }
- }
-}
-
-//@ts-expect-error It works but TS types hate it
-Message.findOneOrFail = function (this: Message, options: FindOneOptions<Message>): Promise<Message> {
- Message.addDefault(options as FindOneOptions<Message>);
- //@ts-expect-error how to use generics on call, who knows!
- return BaseEntity.findOneOrFail.call(Message, options);
-};
-//@ts-expect-error It works but TS types hate it
-Message.findOne = function (this: Message, options: FindOneOptions<Message>): Promise<Message> {
- Message.addDefault(options as FindOneOptions<Message>);
- //@ts-expect-error how to use generics on call, who knows!
- return BaseEntity.findOne.call(Message, options);
-};
-//@ts-expect-error It works but TS types hate it
-Message.find = function (this: Message, options: FindOneOptions<Message>): Promise<Message[]> {
- Message.addDefault(options as FindOneOptions<Message>);
- //@ts-expect-error how to use generics on call, who knows!
- return BaseEntity.find.call(Message, options);
-};
diff --git a/src/util/entities/Migration.ts b/src/util/entities/Migration.ts
deleted file mode 100644
index b4a5ff78..00000000
--- a/src/util/entities/Migration.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, PrimaryGeneratedColumn, BaseEntity } from "typeorm";
-
-@Entity({
- name: "migrations",
-})
-export class Migration extends BaseEntity {
- @PrimaryGeneratedColumn()
- id: number;
-
- @Column({ type: "bigint" })
- timestamp: number;
-
- @Column()
- name: string;
-}
diff --git a/src/util/entities/Note.ts b/src/util/entities/Note.ts
deleted file mode 100644
index e9f173ff..00000000
--- a/src/util/entities/Note.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-
-@Entity({
- name: "notes",
-})
-@Unique(["owner", "target"])
-export class Note extends BaseClass {
- @JoinColumn({ name: "owner_id" })
- @ManyToOne(() => User, { onDelete: "CASCADE" })
- owner: User;
-
- @JoinColumn({ name: "target_id" })
- @ManyToOne(() => User, { onDelete: "CASCADE" })
- target: User;
-
- @Column()
- content: string;
-}
diff --git a/src/util/entities/RateLimit.ts b/src/util/entities/RateLimit.ts
deleted file mode 100644
index ab85285c..00000000
--- a/src/util/entities/RateLimit.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity } from "typeorm";
-import { BaseClass } from "./BaseClass";
-
-@Entity({
- name: "rate_limits",
-})
-export class RateLimit extends BaseClass {
- @Column() // no relation as it also
- executor_id: string;
-
- @Column()
- hits: number;
-
- @Column()
- blocked: boolean;
-
- @Column()
- expires_at: Date;
-}
diff --git a/src/util/entities/ReadState.ts b/src/util/entities/ReadState.ts
deleted file mode 100644
index 4d15c6cc..00000000
--- a/src/util/entities/ReadState.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Channel } from "./Channel";
-import { User } from "./User";
-import { ReadStateFlags, ReadStateType } from "@spacebar/schemas";
-
-// for read receipts
-// notification cursor and public read receipt need to be forwards-only (the former to prevent re-pinging when marked as unread, and the latter to be acceptable as a legal acknowledgement in criminal proceedings), and private read marker needs to be advance-rewind capable
-// public read receipt ≥ notification cursor ≥ private fully read marker
-
-@Entity({
- name: "read_states",
-})
-@Index(["channel_id", "user_id"], { unique: true })
-export class ReadState extends BaseClass {
- @Column()
- @RelationId((read_state: ReadState) => read_state.channel)
- channel_id: string;
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => Channel, {
- onDelete: "CASCADE",
- })
- channel: Channel;
-
- @Column()
- @RelationId((read_state: ReadState) => read_state.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-
- @Column({ nullable: true })
- last_message_id?: string;
-
- @Column({ nullable: true })
- last_acked_id?: string;
-
- @Column({ nullable: true })
- notifications_cursor: string;
-
- @Column({ default: 0 })
- mention_count: number;
-
- @Column({ default: 0 })
- badge_count: number;
-
- @Column({ nullable: true })
- last_pin_timestamp?: Date;
-
- @Column({ default: ReadStateType.CHANNEL })
- read_state_type: ReadStateType;
-
- @Column({ default: 0 })
- flags: ReadStateFlags;
-
- // toJSON() {
- // const res = { ...this } as Partial<ReadState>;
- // if (this.read_state_type === ReadStateType.CHANNEL) {
- // delete res.badge_count;
- // delete res.last_acked_id;
- // } else {
- // delete res.mention_count; // mutually exclusive with badge_count
- // delete res.last_message_id; // mutually exclusive with last_acked_id
- // // these only apply to channels:
- // delete res.last_pin_timestamp;
- // delete res.flags;
- // // delete res.last_viewed; // TODO
- // }
- // return res;
- // }
-}
diff --git a/src/util/entities/Recipient.ts b/src/util/entities/Recipient.ts
deleted file mode 100644
index 2dcff244..00000000
--- a/src/util/entities/Recipient.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-
-@Entity({
- name: "recipients",
-})
-export class Recipient extends BaseClass {
- @Column()
- @RelationId((recipient: Recipient) => recipient.channel)
- channel_id: string;
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => require("./Channel").Channel, {
- onDelete: "CASCADE",
- })
- channel: import("./Channel").Channel;
-
- @Column()
- @RelationId((recipient: Recipient) => recipient.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => require("./User").User, {
- onDelete: "CASCADE",
- })
- user: import("./User").User;
-
- @Column({ default: false })
- closed: boolean;
-
- // TODO: settings/mute/nick/added at/encryption keys/read_state
-}
diff --git a/src/util/entities/Relationship.ts b/src/util/entities/Relationship.ts
deleted file mode 100644
index c2256ed3..00000000
--- a/src/util/entities/Relationship.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-import { RelationshipType } from "@spacebar/schemas";
-
-@Entity({
- name: "relationships",
-})
-@Index(["from_id", "to_id"], { unique: true })
-export class Relationship extends BaseClass {
- @Column({})
- @RelationId((relationship: Relationship) => relationship.from)
- from_id: string;
-
- @JoinColumn({ name: "from_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- from: User;
-
- @Column({})
- @RelationId((relationship: Relationship) => relationship.to)
- to_id: string;
-
- @JoinColumn({ name: "to_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- to: User;
-
- @Column({ nullable: true })
- nickname?: string;
-
- @Column({ type: "int" })
- type: RelationshipType;
-
- toPublicRelationship() {
- return {
- id: this.to?.id || this.to_id,
- type: this.type,
- nickname: this.nickname,
- user: this.to?.toPublicUser(),
- };
- }
-}
diff --git a/src/util/entities/ReportMenu.ts b/src/util/entities/ReportMenu.ts
deleted file mode 100644
index a37ae159..00000000
--- a/src/util/entities/ReportMenu.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2024 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { BaseClass } from "./BaseClass";
-import { Entity, Column } from "typeorm";
-import { ReportMenuType } from "../../schemas/api/reports/ReportMenu";
-
-@Entity({
- name: "report_menus",
-})
-export class ReportMenu extends BaseClass {
- @Column()
- type: ReportMenuType;
-
- @Column()
- variant: string;
-
- @Column()
- isCurrent: boolean;
-
- @Column({ nullable: true })
- inherits?: string;
-
- @Column({ type: "jsonb" })
- content: unknown;
-}
diff --git a/src/util/entities/Role.ts b/src/util/entities/Role.ts
deleted file mode 100644
index e6b2e459..00000000
--- a/src/util/entities/Role.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-import { RoleColors } from "@spacebar/schemas";
-
-@Entity({
- name: "roles",
-})
-export class Role extends BaseClass {
- @Column()
- @RelationId((role: Role) => role.guild)
- guild_id: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, (guild) => guild.roles, {
- onDelete: "CASCADE",
- })
- guild: Guild;
-
- @Column()
- color: number;
-
- @Column()
- hoist: boolean;
-
- @Column({ default: false })
- managed: boolean;
-
- @Column()
- mentionable: boolean;
-
- @Column()
- name: string;
-
- @Column()
- permissions: string;
-
- @Column()
- position: number;
-
- @Column({ nullable: true })
- icon?: string;
-
- @Column({ nullable: true })
- unicode_emoji?: string;
-
- @Column({ type: "jsonb", nullable: true })
- tags?: {
- bot_id?: string;
- integration_id?: string;
- premium_subscriber?: boolean;
- };
-
- @Column({ default: 0 })
- flags: number;
-
- @Column({ nullable: false, type: "jsonb" })
- colors: RoleColors;
-
- toJSON(): Role {
- return {
- ...this,
- tags: this.tags ?? undefined,
- };
- }
-}
diff --git a/src/util/entities/SecurityKey.ts b/src/util/entities/SecurityKey.ts
deleted file mode 100644
index cbf80d3d..00000000
--- a/src/util/entities/SecurityKey.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-
-@Entity({
- name: "security_keys",
-})
-export class SecurityKey extends BaseClass {
- @Column({ nullable: true })
- @RelationId((key: SecurityKey) => key.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-
- @Column()
- key_id: string;
-
- @Column()
- public_key: string;
-
- @Column()
- counter: number;
-
- @Column()
- name: string;
-}
diff --git a/src/util/entities/Session.ts b/src/util/entities/Session.ts
deleted file mode 100644
index 1cb77ebe..00000000
--- a/src/util/entities/Session.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2025 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import crypto from "node:crypto";
-import { User } from "./User";
-import { BaseClassWithoutId } from "./BaseClass";
-import { Column, CreateDateColumn, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm";
-import { Activity, ClientStatus, GatewaySession, GatewaySessionClientInfo, PrivateStatus } from "../interfaces";
-import { randomUpperString } from "@spacebar/api";
-import { DateBuilder, IpDataClient, TimeSpan } from "../util";
-
-@Entity({
- name: "sessions",
-})
-export class Session extends BaseClassWithoutId {
- @PrimaryColumn({ nullable: false })
- session_id: string = randomUpperString();
-
- @Column()
- @RelationId((session: Session) => session.user)
- @Index({})
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-
- @Column({ type: "jsonb", default: "[]" })
- activities: Activity[];
-
- @Column({ type: "jsonb" })
- client_info: {
- platform?: string;
- os?: string;
- version?: number;
- location?: string;
- };
-
- @Column({ type: "jsonb" })
- client_status: ClientStatus;
-
- @Column({ nullable: false, type: String })
- status: PrivateStatus;
-
- @Column({ default: false })
- is_admin_session: boolean;
-
- @CreateDateColumn({ type: Date })
- created_at: Date;
-
- @Column({ nullable: true, type: Date })
- last_seen?: Date;
-
- @Column({ nullable: true, type: String })
- last_seen_ip?: string;
-
- @Column({ nullable: true, type: String })
- last_seen_location?: string;
-
- @Column({ nullable: true, type: "jsonb" })
- last_seen_location_info?: ExtendedLocationInfo;
-
- @Column({ nullable: true, type: String })
- session_nickname?: string;
-
- getPublicStatus() {
- return this.status === "invisible" ? "offline" : this.status;
- }
-
- getDiscordDeviceInfo() {
- return {
- id_hash: crypto.createHash("sha256").update(this.session_id).digest("hex"),
- approx_last_used_time: (this.last_seen ?? new Date(0)).toISOString(),
- client_info: {
- os: this.client_info?.os,
- platform:
- this.client_info?.platform + (this.client_info?.version ? ` ${this.client_info?.version}` : "") + (this.session_nickname ? ` (${this.session_nickname})` : ""),
- location: this.last_seen_location,
- },
- };
- }
-
- getExtendedDeviceInfo() {
- return {
- id: this.session_id,
- id_hash: crypto.createHash("sha256").update(this.session_id).digest("hex"),
- status: this.status,
- activities: this.activities,
- client_status: this.client_status,
- approx_last_used_time: (this.last_seen ?? new Date(0)).toISOString(),
- client_info: {
- ...(this.client_info ?? {}),
- location: this.last_seen_location,
- },
- last_seen: this.last_seen,
- last_seen_ip: this.last_seen_ip,
- last_seen_location: this.last_seen_location,
- last_seen_location_info: this.last_seen_location_info,
- };
- }
-
- toPrivateGatewayDeviceInfo(): GatewaySession {
- // TODO: ... or has `show_current_game` privacy setting enabled - except spotify (always visible)
- const hasPrivateActivities = this.status == "offline" || this.status == "invisible";
- const inactiveTreshold = new DateBuilder(new Date(0)).addMinutes(5).buildTimestamp();
-
- return {
- session_id: this.session_id,
- client_info: {
- client: this.client_info?.platform ?? "",
- os: this.client_info?.os ?? "",
- version: this.client_info?.version ?? 0,
- } as GatewaySessionClientInfo,
- status: this.status,
- activities: hasPrivateActivities ? [] : this.activities,
- hidden_activities: hasPrivateActivities ? this.activities : [],
- active: TimeSpan.fromDates(this.last_seen?.getTime() ?? 0, new Date().getTime()).totalMillis < inactiveTreshold,
- };
- }
-
- async updateIpInfo() {
- const ipInfo = await IpDataClient.getIpInfo(this.last_seen_ip!);
- if (ipInfo?.ip) {
- this.last_seen_location = `${ipInfo.emoji_flag} ${ipInfo.postal} ${ipInfo.city}, ${ipInfo.region}, ${ipInfo.country_name}`;
- this.last_seen_location_info = {
- is_eu: ipInfo.is_eu,
- city: ipInfo.city,
- region: ipInfo.region,
- region_code: ipInfo.region_code,
- country_name: ipInfo.country_name,
- country_code: ipInfo.country_code,
- continent_name: ipInfo.continent_name,
- continent_code: ipInfo.continent_code,
- latitude: ipInfo.latitude,
- longitude: ipInfo.longitude,
- postal: ipInfo.postal,
- calling_code: ipInfo.calling_code,
- flag: ipInfo.flag,
- emoji_flag: ipInfo.emoji_flag,
- emoji_unicode: ipInfo.emoji_unicode,
- };
- }
- }
-}
-
-export interface ExtendedLocationInfo {
- is_eu: boolean;
- city: string;
- region: string;
- region_code: string;
- country_name: string;
- country_code: string;
- continent_name: string;
- continent_code: string;
- latitude: number;
- longitude: number;
- postal: string;
- calling_code: string;
- flag: string;
- emoji_flag: string;
- emoji_unicode: string;
-}
-
-export const PrivateSessionProjection: (keyof Session)[] = ["user_id", "session_id", "activities", "client_info", "status"];
diff --git a/src/util/entities/Sticker.ts b/src/util/entities/Sticker.ts
deleted file mode 100644
index c88e0c29..00000000
--- a/src/util/entities/Sticker.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-import { User } from "./User";
-import { StickerFormatType, StickerType } from "@spacebar/schemas";
-
-@Entity({
- name: "stickers",
-})
-export class Sticker extends BaseClass {
- @Column()
- name: string;
-
- @Column({ nullable: true })
- description?: string;
-
- @Column({ nullable: true })
- available?: boolean;
-
- @Column({ nullable: true })
- tags?: string;
-
- @Column({ nullable: true })
- @RelationId((sticker: Sticker) => sticker.pack)
- pack_id?: string;
-
- @JoinColumn({ name: "pack_id" })
- @ManyToOne(() => require("./StickerPack").StickerPack, {
- onDelete: "CASCADE",
- nullable: true,
- })
- pack: import("./StickerPack").StickerPack;
-
- @Column({ nullable: true })
- guild_id?: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, (guild) => guild.stickers, {
- onDelete: "CASCADE",
- })
- guild?: Guild;
-
- @Column({ nullable: true })
- user_id?: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user?: User;
-
- @Column({ type: "int" })
- type: StickerType;
-
- @Column({ type: "int" })
- format_type: StickerFormatType;
-}
diff --git a/src/util/entities/StickerPack.ts b/src/util/entities/StickerPack.ts
deleted file mode 100644
index d4a598f4..00000000
--- a/src/util/entities/StickerPack.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
-import { Sticker } from ".";
-import { BaseClass } from "./BaseClass";
-
-@Entity({
- name: "sticker_packs",
-})
-export class StickerPack extends BaseClass {
- @Column()
- name: string;
-
- @Column({ nullable: true })
- description?: string;
-
- @Column({ nullable: true })
- banner_asset_id?: string;
-
- @OneToMany(() => Sticker, (sticker: Sticker) => sticker.pack, {
- cascade: true,
- orphanedRowAction: "delete",
- })
- stickers: Sticker[];
-
- // sku_id: string
-
- @Column({ nullable: true })
- @RelationId((pack: StickerPack) => pack.cover_sticker)
- cover_sticker_id?: string;
-
- @ManyToOne(() => Sticker, { nullable: true })
- @JoinColumn()
- cover_sticker?: Sticker;
-}
diff --git a/src/util/entities/Stream.ts b/src/util/entities/Stream.ts
deleted file mode 100644
index bf2f3a62..00000000
--- a/src/util/entities/Stream.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-import { Channel } from "./Channel";
-
-@Entity({
- name: "streams",
-})
-export class Stream extends BaseClass {
- @Column()
- @RelationId((stream: Stream) => stream.owner)
- owner_id: string;
-
- @JoinColumn({ name: "owner_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- owner: User;
-
- @Column()
- @RelationId((stream: Stream) => stream.channel)
- channel_id: string;
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => Channel, {
- onDelete: "CASCADE",
- })
- channel: Channel;
-
- @Column()
- endpoint: string;
-}
diff --git a/src/util/entities/StreamSession.ts b/src/util/entities/StreamSession.ts
deleted file mode 100644
index 1dca2424..00000000
--- a/src/util/entities/StreamSession.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-import { Stream } from "./Stream";
-
-@Entity({
- name: "stream_sessions",
-})
-export class StreamSession extends BaseClass {
- @Column()
- @RelationId((session: StreamSession) => session.stream)
- stream_id: string;
-
- @JoinColumn({ name: "stream_id" })
- @ManyToOne(() => Stream, {
- onDelete: "CASCADE",
- })
- stream: Stream;
-
- @Column()
- @RelationId((session: StreamSession) => session.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-
- @Column({ nullable: true })
- token: string;
-
- // this is for gateway session
- @Column()
- session_id: string;
-
- @Column({ default: false })
- used: boolean;
-}
diff --git a/src/util/entities/Tag.ts b/src/util/entities/Tag.ts
deleted file mode 100644
index 63ad4b86..00000000
--- a/src/util/entities/Tag.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Channel } from "./Channel";
-
-@Entity({
- name: "tags",
-})
-export class Tag extends BaseClass {
- @Column()
- channel_id: string;
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => Channel, (channel) => channel.available_tags, {
- onDelete: "CASCADE",
- })
- channel: Channel;
-
- @Column()
- name: string;
-
- @Column()
- moderated: boolean = false;
-
- @Column({ nullable: true })
- emoji_id?: string;
-
- @Column({ nullable: true })
- emoji_name?: string;
-
- toJSON() {
- return {
- name: this.name,
- id: this.id,
- moderated: this.moderated,
- emoji_id: this.emoji_id,
- emoji_name: this.emoji_name,
- };
- }
-}
diff --git a/src/util/entities/Team.ts b/src/util/entities/Team.ts
deleted file mode 100644
index e440e965..00000000
--- a/src/util/entities/Team.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { TeamMember } from "./TeamMember";
-import { User } from "./User";
-
-@Entity({
- name: "teams",
-})
-export class Team extends BaseClass {
- @Column({ nullable: true })
- icon?: string;
-
- @JoinColumn({ name: "member_ids" })
- @OneToMany(() => TeamMember, (member: TeamMember) => member.team, {
- orphanedRowAction: "delete",
- })
- members: TeamMember[];
-
- @Column()
- name: string;
-
- @Column({ nullable: true })
- @RelationId((team: Team) => team.owner_user)
- owner_user_id: string;
-
- @JoinColumn({ name: "owner_user_id" })
- @ManyToOne(() => User)
- owner_user: User;
-}
diff --git a/src/util/entities/TeamMember.ts b/src/util/entities/TeamMember.ts
deleted file mode 100644
index 72e23b76..00000000
--- a/src/util/entities/TeamMember.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { User } from "./User";
-import { TeamMemberRole, TeamMemberState } from "@spacebar/schemas";
-
-@Entity({
- name: "team_members",
-})
-export class TeamMember extends BaseClass {
- @Column({ type: "int" })
- membership_state: TeamMemberState;
-
- @Column({ type: "varchar", array: true })
- permissions: string[];
-
- @Column()
- role: TeamMemberRole;
-
- @Column({ nullable: true })
- @RelationId((member: TeamMember) => member.team)
- team_id: string;
-
- @JoinColumn({ name: "team_id" })
- @ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, {
- onDelete: "CASCADE",
- })
- team: import("./Team").Team;
-
- @Column({ nullable: true })
- @RelationId((member: TeamMember) => member.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-}
diff --git a/src/util/entities/Template.ts b/src/util/entities/Template.ts
deleted file mode 100644
index 79804eed..00000000
--- a/src/util/entities/Template.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-import { User } from "./User";
-
-@Entity({
- name: "templates",
-})
-export class Template extends BaseClass {
- @Column({ unique: true })
- code: string;
-
- @Column()
- name: string;
-
- @Column({ nullable: true })
- description?: string;
-
- @Column({ nullable: true })
- usage_count?: number;
-
- @Column({ nullable: true })
- @RelationId((template: Template) => template.creator)
- creator_id: string;
-
- @JoinColumn({ name: "creator_id" })
- @ManyToOne(() => User)
- creator: User;
-
- @Column()
- created_at: Date;
-
- @Column()
- updated_at: Date;
-
- @Column({ nullable: true })
- @RelationId((template: Template) => template.source_guild)
- source_guild_id: string;
-
- @JoinColumn({ name: "source_guild_id" })
- @ManyToOne(() => Guild, { onDelete: "CASCADE" })
- source_guild: Guild;
-
- @Column({ type: "jsonb" })
- serialized_source_guild: Guild;
-}
diff --git a/src/util/entities/ThreadMember.ts b/src/util/entities/ThreadMember.ts
deleted file mode 100644
index cea61f51..00000000
--- a/src/util/entities/ThreadMember.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2026 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId } from "typeorm";
-import { ThreadMembersUpdateEvent } from "../interfaces";
-import { emitEvent } from "../util";
-import { BaseClassWithoutId } from "./BaseClass";
-import { Channel } from "./Channel";
-import { HTTPError } from "lambert-server/HTTPError";
-import { Member } from "./Member";
-
-// TODO: move
-interface ThreadMemberMuteConfig {
- end_time?: Date;
- selected_time_window?: number;
-}
-
-// TODO: move
-export enum ThreadMemberFlags {
- NONE = 0,
- HAS_INTERACTED = 1 << 0,
- ALL_MESSAGES = 1 << 1,
- ONLY_MENTIONS = 1 << 2,
- NO_MESSAGES = 1 << 3,
-}
-
-@Entity("thread_members")
-@Index(["id", "member_idx"], { unique: true })
-export class ThreadMember extends BaseClassWithoutId {
- @PrimaryGeneratedColumn()
- index: string;
-
- @Column()
- @RelationId((member: ThreadMember) => member.channel)
- id: string;
-
- @JoinColumn({ name: "id" })
- @ManyToOne(() => Channel, {
- onDelete: "CASCADE",
- })
- channel: Channel;
-
- @Column()
- @RelationId((member: ThreadMember) => member.member)
- member_idx: string;
-
- @JoinColumn({ name: "member_idx" })
- @ManyToOne(() => Member, {
- onDelete: "CASCADE",
- })
- member: Member;
-
- @Column()
- join_timestamp: Date;
-
- @Column()
- muted: boolean;
-
- @Column({ nullable: true, type: "jsonb" })
- mute_config?: ThreadMemberMuteConfig;
-
- @Column()
- flags: ThreadMemberFlags;
-
- static async IsInThreadOrFail(member_id: string, thread_id: string) {
- if (await ThreadMember.count({ where: { id: thread_id, member_idx: member_id } })) return true;
- throw new HTTPError("You are not member of this thread", 403);
- }
-
- static async removeFromThread(member_id: string, thread_id: string) {
- const channel = await Channel.findOneOrFail({ where: { id: thread_id } });
- if (
- !(await ThreadMember.count({
- where: {
- id: thread_id,
- member_idx: member_id,
- },
- }))
- )
- throw new HTTPError("You are not member of this thread", 403);
- // // use promise all to execute all promises at the same time -> save time
- // TODO: check for bugs
- if (channel.member_count) channel.member_count--;
- return Promise.all([
- ThreadMember.delete({
- id: thread_id,
- member_idx: member_id,
- }),
- // //Guild.decrement({ id: guild_id }, "member_count", -1),
-
- emitEvent({
- event: "THREAD_MEMBERS_UPDATE",
- data: {
- guild_id: channel.guild_id!, // TODO: is this the right fix?
- id: channel.id,
- member_count: channel.member_count ?? 0,
- removed_member_ids: [member_id],
- },
- channel_id: thread_id,
- } satisfies ThreadMembersUpdateEvent),
- ]);
- }
-
- // static async addRole(user_id: string, guild_id: string, role_id: string) {
- // const [member, role] = await Promise.all([
- // // @ts-ignore
- // Member.findOneOrFail({
- // where: { id: user_id, guild_id },
- // relations: ["user", "roles"], // we don't want to load the role objects just the ids
- // select: ["index"]
- // }),
- // Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] })
- // ]);
- // member.roles.push(OrmUtils.mergeDeep(new Role(), { id: role_id }));
-
- // await Promise.all([
- // member.save(),
- // emitEvent({
- // event: "GUILD_MEMBER_UPDATE",
- // data: {
- // guild_id,
- // user: member.user,
- // roles: member.roles.map((x) => x.id)
- // },
- // guild_id
- // } satisfies GuildMemberUpdateEvent)
- // ]);
- // }
-
- // static async removeRole(user_id: string, guild_id: string, role_id: string) {
- // const [member] = await Promise.all([
- // // @ts-ignore
- // Member.findOneOrFail({
- // where: { id: user_id, guild_id },
- // relations: ["user", "roles"], // we don't want to load the role objects just the ids
- // select: ["index"]
- // }),
- // await Role.findOneOrFail({ where: { id: role_id, guild_id } })
- // ]);
- // member.roles = member.roles.filter((x) => x.id == role_id);
-
- // await Promise.all([
- // member.save(),
- // emitEvent({
- // event: "GUILD_MEMBER_UPDATE",
- // data: {
- // guild_id,
- // user: member.user,
- // roles: member.roles.map((x) => x.id)
- // },
- // guild_id
- // } satisfies GuildMemberUpdateEvent)
- // ]);
- // }
-
- // static async changeNickname(user_id: string, guild_id: string, nickname: string) {
- // const member = await Member.findOneOrFail({
- // where: {
- // id: user_id,
- // guild_id
- // },
- // relations: ["user"]
- // });
- // member.nick = nickname;
-
- // await Promise.all([
- // member.save(),
-
- // emitEvent({
- // event: "GUILD_MEMBER_UPDATE",
- // data: {
- // guild_id,
- // user: member.user,
- // nick: nickname
- // },
- // guild_id
- // } satisfies GuildMemberUpdateEvent)
- // ]);
- // }
-}
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
deleted file mode 100644
index afde3e9b..00000000
--- a/src/util/entities/User.ts
+++ /dev/null
@@ -1,408 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Request } from "express";
-import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm";
-import { Channel, Config, Email, FieldErrors, Snowflake, Stopwatch, trimSpecial } from "..";
-import { Random } from "../util";
-import { BaseClass } from "./BaseClass";
-import { ConnectedAccount } from "./ConnectedAccount";
-import { Member } from "./Member";
-import { Relationship } from "./Relationship";
-import { SecurityKey } from "./SecurityKey";
-import { Session } from "./Session";
-import { UserSettings } from "./UserSettings";
-import {
- AvatarDecorationData,
- ChannelType,
- Collectibles,
- DisplayNameStyle,
- PrimaryGuild,
- PrivateUserProjection,
- PublicUser,
- PublicUserProjection,
- UserPrivate,
-} from "@spacebar/schemas";
-import { JsonNumber } from "../util/Decorators";
-
-@Entity({
- name: "users",
-})
-export class User extends BaseClass {
- @Column()
- username: string; // username max length 32, min 2 (should be configurable)
-
- @Column()
- discriminator: string; // opaque string: 4 digits on discord.com
-
- @Column({ nullable: true })
- avatar?: string; // hash of the user avatar
-
- @Column({ nullable: true })
- accent_color?: number; // banner color of user
-
- @Column({ nullable: true })
- banner?: string; // hash of the user banner
-
- // TODO: Separate `User` and `UserProfile` models
- // puyo: changed from [number, number] because it breaks openapi
- @Column({ nullable: true, type: "int4", array: true })
- theme_colors?: number[];
-
- @Column({ nullable: true })
- pronouns?: string;
-
- @Column({ nullable: true, select: false })
- phone?: string; // phone number of the user
-
- @Column({ select: false })
- desktop: boolean = false; // if the user has desktop app installed
-
- @Column({ select: false })
- mobile: boolean = false; // if the user has mobile app installed
-
- @Column()
- premium: boolean; // if user bought individual premium
-
- @Column()
- premium_type: number; // individual premium level
-
- @Column()
- bot: boolean = false; // if user is bot
-
- @Column()
- bio: string = ""; // short description of the user
-
- @Column()
- system: boolean = false; // shouldn't be used, the api sends this field type true, if the generated message comes from a system generated author
-
- @Column({ select: false })
- nsfw_allowed: boolean = true; // if the user can do age-restricted actions (NSFW channels/guilds/commands) // TODO: depending on age
-
- @Column({ select: false })
- mfa_enabled: boolean = false; // if multi factor authentication is enabled
-
- @Column({ select: false, default: false })
- webauthn_enabled: boolean = false; // if webauthn multi factor authentication is enabled
-
- @Column({ select: false, nullable: true })
- totp_secret?: string = "";
-
- @Column({ nullable: true, select: false })
- totp_last_ticket?: string = "";
-
- @Column()
- created_at: Date; // registration date
-
- @Column({ nullable: true })
- premium_since: Date; // premium date
-
- @Column({ select: false })
- verified: boolean; // email is verified
-
- @Column()
- disabled: boolean = false; // if the account is disabled
-
- @Column()
- deleted: boolean = false; // if the user was deleted
-
- @Column({ nullable: true, select: false })
- email?: string; // email of the user
-
- @Column({ type: "bigint" })
- @JsonNumber
- flags: number = 0; // UserFlags // TODO: generate
-
- @Column({ type: "bigint" })
- @JsonNumber
- public_flags: number = 0;
-
- @Column({ type: "bigint" })
- @JsonNumber
- purchased_flags: number = 0;
-
- @Column()
- premium_usage_flags: number = 0;
-
- @Column({ type: "bigint" })
- @JsonNumber
- rights: string;
-
- @OneToMany(() => Session, (session: Session) => session.user)
- sessions: Session[];
-
- @JoinColumn({ name: "relationship_ids" })
- @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",
- })
- connected_accounts: ConnectedAccount[];
-
- @Column({ type: "jsonb", select: false })
- data: {
- valid_tokens_since: Date; // all tokens with a previous issue date are invalid
- hash?: string; // hash of the password, salt is saved in password (bcrypt)
- };
-
- @Column({ type: "varchar", array: true, select: false })
- fingerprints: string[] = []; // array of fingerprints -> used to prevent multiple accounts
-
- @OneToOne(() => UserSettings, {
- cascade: true,
- orphanedRowAction: "delete",
- nullable: true,
- })
- @JoinColumn()
- settings?: UserSettings;
-
- @OneToMany(() => SecurityKey, (key: SecurityKey) => key.user)
- security_keys: SecurityKey[];
-
- @Column({ type: "int8", array: true, nullable: true })
- badge_ids?: string[];
-
- @Column({ type: "jsonb", nullable: true })
- avatar_decoration_data?: AvatarDecorationData;
-
- @Column({ type: "jsonb", nullable: true })
- display_name_styles?: DisplayNameStyle;
-
- @Column({ type: "jsonb", nullable: true })
- collectibles?: Collectibles;
-
- @Column({ type: "jsonb", nullable: true })
- primary_guild?: PrimaryGuild;
-
- // TODO: I don't like this method?
- validate() {
- if (this.discriminator) {
- const discrim = Number(this.discriminator);
- if (isNaN(discrim) || !Number.isInteger(discrim) || discrim <= 0 || discrim >= 10000)
- throw FieldErrors({
- discriminator: {
- message: "Discriminator must be a number.",
- code: "DISCRIMINATOR_INVALID",
- },
- });
-
- this.discriminator = discrim.toString().padStart(4, "0");
- }
- }
-
- toPublicUser() {
- this.clean_data();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const user: any = {};
- PublicUserProjection.forEach((x) => {
- user[x] = this[x];
- });
- return user as PublicUser;
- }
-
- toPrivateUser(extraFields: (keyof User)[] = []) {
- this.clean_data();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const user: any = {};
- [...PrivateUserProjection, ...extraFields].forEach((x) => {
- user[x] = this[x];
- });
- return user as UserPrivate;
- }
-
- static async getPublicUser(user_id: string): Promise<PublicUser> {
- const user = await User.findOneOrFail({
- where: { id: user_id },
- select: PublicUserProjection,
- });
- return user.toPublicUser();
- }
-
- public 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: true },
- });
- 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?
- const takenDiscriminators = (await User.find({ where: { username }, select: { discriminator: true } })).map((x) => x.discriminator);
- if (takenDiscriminators.length >= 9999) return undefined;
-
- for (let tries = 0; tries < 15; tries++) {
- const discriminator = Random.nextInt(1, 9999).toString().padStart(4, "0");
- if (!takenDiscriminators.includes(discriminator)) return discriminator;
- }
-
- return undefined;
- }
- }
-
- public get tag(): string {
- //const { uniqueUsernames } = Config.get().general;
- const uniqueUsernames = false;
-
- return uniqueUsernames ? this.username : `${this.username}#${this.discriminator}`;
- }
-
- static async register({
- email,
- username,
- password,
- id,
- req,
- bot,
- }: {
- username: string;
- password?: string;
- email?: string;
- date_of_birth?: Date; // "2000-04-03"
- id?: string;
- req?: Request;
- bot?: boolean;
- }) {
- const totalSw = Stopwatch.startNew();
- const incSw = Stopwatch.startNew();
- const logTrace = (...data: unknown[]) => {
- if (process.env.LOG_VERBOSE_TRACES !== "true") return;
- console.log("[User.register]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`);
- };
-
- // trim special utf8 control characters -> Backspace, Newline, ...
- username = trimSpecial(username);
-
- 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",
- message: req?.t("auth:register.USERNAME_TOO_MANY_USERS") || "",
- },
- });
- }
- logTrace("Generate discriminator");
-
- // TODO: save date_of_birth
- // apparently 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 settings = UserSettings.create({
- locale: language,
- });
-
- const user = User.create({
- username: username,
- discriminator,
- id: id || Snowflake.generate(),
- email: email,
- data: {
- hash: password,
- valid_tokens_since: new Date(),
- },
- settings: settings,
-
- premium_since: Config.get().defaults.user.premium ? new Date() : undefined,
- rights: Config.get().register.defaultRights,
- premium: Config.get().defaults.user.premium ?? false,
- premium_type: Config.get().defaults.user.premiumType ?? 0,
- verified: Config.get().defaults.user.verified ?? true,
- created_at: new Date(),
- bot: !!bot,
- });
-
- user.validate();
- logTrace("Generate/validate user");
-
- await Promise.all([user.save(), settings.save()]);
- logTrace("Save user");
-
- // send verification email if users aren't verified by default and we have an email
- if (!Config.get().defaults.user.verified && email) {
- await Email.sendVerifyEmail(user, email).catch((e) => {
- console.error(`Failed to send verification email to ${user.tag}: ${e}`);
- });
- logTrace("Send verify email");
- }
-
- const { autoJoin } = Config.get().guild;
- if (autoJoin.enabled && autoJoin.guilds.length > 0 && !(bot && !autoJoin.bots)) {
- await Promise.all(autoJoin.guilds.map((guild) => Member.addToGuild(user.id, guild, true).catch((e) => console.error("[Autojoin]", e))));
- logTrace("Autojoin", autoJoin.guilds.length, "guilds");
- }
-
- return user;
- }
-
- async getDmChannelWith(user_id: string) {
- const qry = await Channel.getRepository()
- .createQueryBuilder()
- .leftJoinAndSelect("Channel.recipients", "rcp")
- .where("Channel.type = :type", { type: ChannelType.DM })
- .andWhere("rcp.user_id IN (:...user_ids)", { user_ids: [this.id, user_id] })
- .groupBy("Channel.id")
- .having("COUNT(rcp.user_id) = 2")
- .getMany();
-
- // Emma [it/its]@Rory&: is this technically a bug, or am I being too over-cautious?
- if (qry.length > 1) {
- console.warn(`[WARN] User(${this.id})#getDmChannel(${user_id}) returned multiple channels:`);
- for (const channel of qry) {
- console.warn(JSON.stringify(channel));
- }
- throw new Error("Array contains more than one matching element");
- }
-
- return qry[0];
- }
-
- async getDmChannels() {
- const qry = await Channel.getRepository()
- .createQueryBuilder("channel")
- .leftJoinAndSelect("channel.recipients", "rcp")
- .where("channel.type = :type", { type: ChannelType.DM })
- .andWhere("rcp.user_id = :user_id", { user_id: this.id })
- .groupBy("channel.id")
- .addGroupBy("rcp.id")
- .having("COUNT(rcp.id) = 2")
- .getMany();
-
- return qry;
- }
-}
diff --git a/src/util/entities/UserSettings.ts b/src/util/entities/UserSettings.ts
deleted file mode 100644
index 7ccb8e1e..00000000
--- a/src/util/entities/UserSettings.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
-import { BaseClassWithoutId } from "./BaseClass";
-import { CustomStatus, FriendSourceFlags, GuildFolder } from "@spacebar/schemas";
-
-@Entity({
- name: "user_settings",
-})
-export class UserSettings extends BaseClassWithoutId {
- @PrimaryGeneratedColumn()
- index: string;
-
- @Column({ nullable: true })
- afk_timeout: number = 3600;
-
- @Column({ nullable: true })
- allow_accessibility_detection: boolean = true;
-
- @Column({ nullable: true })
- animate_emoji: boolean = true;
-
- @Column({ nullable: true })
- animate_stickers: number = 0;
-
- @Column({ nullable: true })
- contact_sync_enabled: boolean = false;
-
- @Column({ nullable: true })
- convert_emoticons: boolean = false;
-
- @Column({ nullable: true, type: "jsonb" })
- custom_status: CustomStatus | null = null;
-
- @Column({ nullable: true })
- default_guilds_restricted: boolean = false;
-
- @Column({ nullable: true })
- detect_platform_accounts: boolean = false;
-
- @Column({ nullable: true })
- developer_mode: boolean = true;
-
- @Column({ nullable: true })
- disable_games_tab: boolean = true;
-
- @Column({ nullable: true })
- enable_tts_command: boolean = false;
-
- @Column({ nullable: true })
- explicit_content_filter: number = 0;
-
- @Column({ nullable: true })
- friend_discovery_flags: number = 0;
-
- @Column({ nullable: true, type: "jsonb" })
- friend_source_flags: FriendSourceFlags = { all: true };
-
- @Column({ nullable: true })
- gateway_connected: boolean = false;
-
- @Column({ nullable: true })
- gif_auto_play: boolean = false;
-
- @Column({ nullable: true, type: "jsonb" })
- guild_folders: GuildFolder[] = []; // every top guild is displayed as a "folder"
-
- @Column({ nullable: true, type: "jsonb" })
- guild_positions: string[] = []; // guild ids ordered by position
-
- @Column({ nullable: true })
- inline_attachment_media: boolean = true;
-
- @Column({ nullable: true })
- inline_embed_media: boolean = true;
-
- @Column({ nullable: true })
- locale: string = "en-US"; // en_US
-
- @Column({ nullable: true })
- message_display_compact: boolean = false;
-
- @Column({ nullable: true })
- native_phone_integration_enabled: boolean = true;
-
- @Column({ nullable: true })
- render_embeds: boolean = true;
-
- @Column({ nullable: true })
- render_reactions: boolean = true;
-
- @Column({ nullable: true, type: "jsonb" })
- restricted_guilds: string[] = [];
-
- @Column({ nullable: true })
- show_current_game: boolean = true;
-
- @Column({ nullable: true })
- status: "online" | "offline" | "dnd" | "idle" | "invisible" = "online";
-
- @Column({ nullable: true })
- stream_notifications_enabled: boolean = false;
-
- @Column({ nullable: true })
- theme: "dark" | "light" = "dark"; // dark
-
- @Column({ nullable: true })
- timezone_offset: number = 0; // e.g -60
-
- @Column({ nullable: true })
- view_nsfw_guilds: boolean = true;
-
- public static async getOrDefault(userId: string) {
- // raw sql query
- const userSettingsIndex = (await this.getRepository().query('SELECT "settingsIndex" FROM users WHERE id = $1', [userId]))[0]?.settingsIndex as string | null;
-
- console.log(`[INFO/UserSettings] Fetched settings index for user ${userId}:`, userSettingsIndex);
-
- if (!userSettingsIndex) return new UserSettings();
-
- const settings = await UserSettings.findOne({ where: { index: userSettingsIndex } });
- if (!settings) return new UserSettings();
-
- return settings;
- }
-}
diff --git a/src/util/entities/UserSettingsProtos.ts b/src/util/entities/UserSettingsProtos.ts
deleted file mode 100644
index c8ee9d12..00000000
--- a/src/util/entities/UserSettingsProtos.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2025 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from "typeorm";
-import { BaseClassWithoutId } from "./BaseClass";
-import { User } from "./User";
-import { FrecencyUserSettings, PreloadedUserSettings } from "discord-protos";
-
-@Entity({
- name: "user_settings_protos",
-})
-export class UserSettingsProtos extends BaseClassWithoutId {
- @OneToOne(() => User, {
- cascade: true,
- orphanedRowAction: "delete",
- eager: false,
- })
- @JoinColumn({ name: "user_id" })
- user: User;
-
- @PrimaryColumn({ type: "text" })
- user_id: string;
-
- @Column({ nullable: true, type: String, name: "userSettings" })
- _userSettings: string | undefined;
-
- @Column({ nullable: true, type: String, name: "frecencySettings" })
- _frecencySettings: string | undefined;
-
- // @Column({nullable: true, type: "simple-json"})
- // testSettings: {};
-
- bigintReplacer(_key: string, value: unknown): unknown {
- if (typeof value === "bigint") {
- return (value as bigint).toString();
- } else if (value instanceof Uint8Array) {
- return {
- __type: "Uint8Array",
- data: Array.from(value as Uint8Array)
- .map((b) => b.toString(16).padStart(2, "0"))
- .join(""),
- };
- } else {
- return value;
- }
- }
-
- bigintReviver(_key: string, value: unknown): unknown {
- if (typeof value === "string" && /^\d+n$/.test(value)) {
- return BigInt((value as string).slice(0, -1));
- } else if (typeof value === "object" && value !== null && "__type" in value) {
- if (value.__type === "Uint8Array" && "data" in value) {
- return new Uint8Array((value.data as string).match(/.{1,2}/g)!.map((byte: string) => parseInt(byte, 16)));
- }
- }
- return value;
- }
-
- get userSettings(): PreloadedUserSettings | undefined {
- if (!this._userSettings) return undefined;
- return PreloadedUserSettings.fromJson(JSON.parse(this._userSettings, this.bigintReviver));
- }
-
- set userSettings(value: PreloadedUserSettings | undefined) {
- if (value) {
- // this._userSettings = JSON.stringify(value, this.bigintReplacer);
- this._userSettings = PreloadedUserSettings.toJsonString(value);
- } else {
- this._userSettings = undefined;
- }
- }
-
- get frecencySettings(): FrecencyUserSettings | undefined {
- if (!this._frecencySettings) return undefined;
- return FrecencyUserSettings.fromJson(JSON.parse(this._frecencySettings, this.bigintReviver));
- }
-
- set frecencySettings(value: FrecencyUserSettings | undefined) {
- if (value) {
- this._frecencySettings = JSON.stringify(value, this.bigintReplacer);
- } else {
- this._frecencySettings = undefined;
- }
- }
-
- static async getOrDefault(user_id: string, save: boolean = false): Promise<UserSettingsProtos> {
- await User.findOneOrFail({
- where: { id: user_id },
- select: { settings: true },
- });
-
- let userSettings = await UserSettingsProtos.findOne({
- where: { user_id },
- });
-
- let modified = false;
- if (!userSettings) {
- userSettings = UserSettingsProtos.create({
- user_id,
- });
- modified = true;
- }
-
- if (!userSettings.userSettings) {
- userSettings.userSettings = PreloadedUserSettings.create({
- versions: {
- dataVersion: 0,
- clientVersion: 0,
- serverVersion: 0,
- },
- });
- modified = true;
- }
-
- if (!userSettings.frecencySettings) {
- userSettings.frecencySettings = FrecencyUserSettings.create({
- versions: {
- dataVersion: 0,
- clientVersion: 0,
- serverVersion: 0,
- },
- });
- modified = true;
- }
-
- if (modified && save) userSettings = await userSettings.save();
-
- return userSettings;
- }
-}
diff --git a/src/util/entities/ValidRegistrationTokens.ts b/src/util/entities/ValidRegistrationTokens.ts
deleted file mode 100644
index f8cb35a4..00000000
--- a/src/util/entities/ValidRegistrationTokens.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { BaseEntity, Column, Entity, PrimaryColumn } from "typeorm";
-
-@Entity({
- name: "valid_registration_tokens",
-})
-export class ValidRegistrationToken extends BaseEntity {
- @PrimaryColumn()
- token: string;
-
- @Column()
- created_at: Date = new Date();
-
- @Column()
- expires_at: Date;
-}
diff --git a/src/util/entities/VoiceState.ts b/src/util/entities/VoiceState.ts
deleted file mode 100644
index 5abdb7da..00000000
--- a/src/util/entities/VoiceState.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Channel } from "./Channel";
-import { Guild } from "./Guild";
-import { Member } from "./Member";
-import { User } from "./User";
-import { PublicVoiceState, PublicVoiceStateProjection } from "@spacebar/schemas";
-
-//https://gist.github.com/vassjozsef/e482c65df6ee1facaace8b3c9ff66145#file-voice_state-ex
-@Entity({
- name: "voice_states",
-})
-export class VoiceState extends BaseClass {
- @Column({ nullable: true })
- @RelationId((voice_state: VoiceState) => voice_state.guild)
- guild_id: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, (guild) => guild.voice_states, {
- onDelete: "CASCADE",
- })
- guild?: Guild;
-
- @Column({ nullable: true })
- @RelationId((voice_state: VoiceState) => voice_state.channel)
- channel_id: string;
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => Channel, {
- onDelete: "CASCADE",
- })
- channel: Channel;
-
- @Column({ nullable: true })
- @RelationId((voice_state: VoiceState) => voice_state.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-
- // @JoinColumn([{ name: "user_id", referencedColumnName: "id" },{ name: "guild_id", referencedColumnName: "guild_id" }])
- // @ManyToOne(() => Member, {
- // onDelete: "CASCADE",
- // })
- //TODO find a way to make it work without breaking Guild.voice_states
- member: Member;
-
- @Column()
- session_id: string;
-
- @Column({ nullable: true })
- token: string;
-
- @Column()
- deaf: boolean;
-
- @Column()
- mute: boolean;
-
- @Column()
- self_deaf: boolean;
-
- @Column()
- self_mute: boolean;
-
- @Column({ nullable: true })
- self_stream?: boolean;
-
- @Column()
- self_video: boolean;
-
- @Column()
- suppress: boolean; // whether this user is muted by the current user
-
- @Column({ nullable: true, default: null })
- request_to_speak_timestamp?: Date;
-
- toPublicVoiceState() {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const voiceState: any = {};
- PublicVoiceStateProjection.forEach((x) => {
- voiceState[x] = this[x];
- });
- return voiceState as PublicVoiceState;
- }
-}
diff --git a/src/util/entities/Webhook.ts b/src/util/entities/Webhook.ts
deleted file mode 100644
index f631c393..00000000
--- a/src/util/entities/Webhook.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
-import { Application } from "./Application";
-import { BaseClass } from "./BaseClass";
-import { Channel } from "./Channel";
-import { Guild } from "./Guild";
-import { User } from "./User";
-import { WebhookType } from "@spacebar/schemas";
-
-@Entity({
- name: "webhooks",
-})
-export class Webhook extends BaseClass {
- @Column({ type: "int" })
- type: WebhookType;
-
- @Column({ nullable: true })
- name: string;
-
- @Column({ nullable: true })
- avatar: string;
-
- @Column({ nullable: true })
- token?: string;
-
- @Column({ nullable: true })
- @RelationId((webhook: Webhook) => webhook.guild)
- guild_id?: string;
-
- @JoinColumn({ name: "guild_id" })
- @ManyToOne(() => Guild, {
- onDelete: "CASCADE",
- })
- guild?: Guild;
-
- @Column({ nullable: true })
- @RelationId((webhook: Webhook) => webhook.channel)
- channel_id: string;
-
- @JoinColumn({ name: "channel_id" })
- @ManyToOne(() => Channel, {
- onDelete: "CASCADE",
- })
- channel: Channel;
-
- @Column({ nullable: true })
- @RelationId((webhook: Webhook) => webhook.application)
- application_id: string;
-
- @JoinColumn({ name: "application_id" })
- @ManyToOne(() => Application, {
- onDelete: "CASCADE",
- })
- application: Application;
-
- @Column({ nullable: true })
- @RelationId((webhook: Webhook) => webhook.user)
- user_id: string;
-
- @JoinColumn({ name: "user_id" })
- @ManyToOne(() => User, {
- onDelete: "CASCADE",
- })
- user: User;
-
- @Column({ nullable: true })
- @RelationId((webhook: Webhook) => webhook.guild)
- source_guild_id?: string;
-
- @JoinColumn({ name: "source_guild_id" })
- @ManyToOne(() => Guild, {
- onDelete: "CASCADE",
- })
- source_guild?: Guild;
-
- @Column({ nullable: true })
- @RelationId((webhook: Webhook) => webhook.channel)
- source_channel_id: string;
-
- @JoinColumn({ name: "source_channel_id" })
- @ManyToOne(() => Channel, {
- onDelete: "CASCADE",
- })
- source_channel: Channel;
-
- url: string;
-}
diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts
deleted file mode 100644
index 79fceecc..00000000
--- a/src/util/entities/index.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2024 Spacebar and Spacebar Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published
- by the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-*/
-
-export * from "./Application";
-export * from "./ApplicationCommand";
-export * from "./Attachment";
-export * from "./AuditLog";
-export * from "./AutomodRule";
-export * from "./BackupCodes";
-export * from "./Badge";
-export * from "./Ban";
-export * from "./BaseClass";
-export * from "./Categories";
-export * from "./Channel";
-export * from "./ClientRelease";
-export * from "./CloudAttachment";
-export * from "./Config";
-export * from "./ConnectedAccount";
-export * from "./ConnectionConfigEntity";
-export * from "./EmbedCache";
-export * from "./Emoji";
-export * from "./Encryption";
-export * from "./Guild";
-export * from "./InstanceBan";
-export * from "./Invite";
-export * from "./Member";
-export * from "./Message";
-export * from "./Migration";
-export * from "./Note";
-export * from "./RateLimit";
-export * from "./ReadState";
-export * from "./Recipient";
-export * from "./Relationship";
-export * from "./Role";
-export * from "./SecurityKey";
-export * from "./Session";
-export * from "./Sticker";
-export * from "./StickerPack";
-export * from "./Stream";
-export * from "./StreamSession";
-export * from "./Team";
-export * from "./TeamMember";
-export * from "./Template";
-export * from "./ThreadMember";
-export * from "./User";
-export * from "./UserSettings";
-export * from "./UserSettingsProtos";
-export * from "./ValidRegistrationTokens";
-export * from "./VoiceState";
-export * from "./Webhook";
-export * from "./Tag";
diff --git a/src/util/index.ts b/src/util/index.ts
index dd2f7f32..2b739838 100644
--- a/src/util/index.ts
+++ b/src/util/index.ts
@@ -22,7 +22,7 @@ import "reflect-metadata";
export * from "./util/index";
export * from "./interfaces/index";
-export * from "./entities/index";
+
export * from "./dtos/index";
export * from "./imports";
export * from "./config";
diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts
index 6152f70a..22eec64f 100644
--- a/src/util/interfaces/Event.ts
+++ b/src/util/interfaces/Event.ts
@@ -16,27 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {
- ConnectedAccount,
- Invite,
- Role,
- Emoji,
- Channel,
- User,
- Sticker,
- Activity,
- Status,
- Presence,
- UserSettings,
- IReadyGuildDTO,
- ReadState,
- ReadyUserGuildSettingsEntries,
- ReadyPrivateChannel,
- GuildOrUnavailable,
- Snowflake,
- ThreadMember,
- PrivateStatus,
-} from "@spacebar/util";
+import { ConnectedAccount, Invite, Role, Emoji, Channel, User, Sticker, UserSettings, ReadState, ThreadMember } from "@spacebar/database";
+import { Activity, Presence, IReadyGuildDTO, ReadyUserGuildSettingsEntries, ReadyPrivateChannel, GuildOrUnavailable, Snowflake, PrivateStatus } from "@spacebar/util";
import { JsonValue } from "@protobuf-ts/runtime";
import {
ApplicationCommand,
diff --git a/src/util/util/Application.ts b/src/util/util/Application.ts
index 64c87775..f4e08cde 100644
--- a/src/util/util/Application.ts
+++ b/src/util/util/Application.ts
@@ -1,5 +1,23 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2026 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
import { Request } from "express";
-import { Application, User } from "../entities";
+import { Application, User } from "../../database/entities";
export async function createAppBotUser(app: Application, req: Request) {
const user = await User.register({
diff --git a/src/util/util/Config.ts b/src/util/util/Config.ts
index 3c1c45f1..46c5a168 100644
--- a/src/util/util/Config.ts
+++ b/src/util/util/Config.ts
@@ -20,7 +20,7 @@ import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import { OrmUtils } from "..";
import { ConfigValue } from "../config";
-import { ConfigEntity } from "../entities";
+import { ConfigEntity } from "../../database/entities";
import { JsonValue } from "@protobuf-ts/runtime";
import { bold, red, redBright } from "picocolors";
diff --git a/src/util/util/Decorators.ts b/src/util/util/Decorators.ts
index 18910b9d..3211cb27 100644
--- a/src/util/util/Decorators.ts
+++ b/src/util/util/Decorators.ts
@@ -1,4 +1,22 @@
-import { BaseClassWithoutId } from "@spacebar/util";
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2026 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { BaseClassWithoutId } from "@spacebar/database";
export const annotationsKey = Symbol("Annotations");
diff --git a/src/util/util/Permissions.ts b/src/util/util/Permissions.ts
index 66dd1943..6585f692 100644
--- a/src/util/util/Permissions.ts
+++ b/src/util/util/Permissions.ts
@@ -2,7 +2,7 @@
// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
// @fc-license-skip
-import { Channel, Guild, Member, Role, User } from "../entities";
+import { Channel, Guild, Member, Role, User } from "../../database/entities";
import { BitField, BitFieldResolvable, BitFlag } from "./BitField";
import { HTTPError } from "lambert-server/HTTPError";
import { ChannelPermissionOverwrite, ChannelPermissionOverwriteType, ChannelType, UserFlags } from "@spacebar/schemas";
diff --git a/src/util/util/Presence.ts b/src/util/util/Presence.ts
index 5a475adc..8180a026 100644
--- a/src/util/util/Presence.ts
+++ b/src/util/util/Presence.ts
@@ -16,7 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { emitEvent, Member, PresenceUpdateEvent, Recipient, Relationship, Session } from "@spacebar/util";
+import { Member, Recipient, Relationship, Session } from "@spacebar/database";
+import { emitEvent, PresenceUpdateEvent } from "@spacebar/util";
import { RelationshipType } from "@spacebar/schemas";
import { Not } from "typeorm";
diff --git a/src/util/util/Rights.ts b/src/util/util/Rights.ts
index f0425749..0eee0a3f 100644
--- a/src/util/util/Rights.ts
+++ b/src/util/util/Rights.ts
@@ -17,7 +17,7 @@
*/
import { BitField, BitFieldResolvable, BitFlag } from "./BitField";
-import { User } from "../entities";
+import { User } from "../../database/entities";
import { HTTPError } from "lambert-server/HTTPError";
export type RightResolvable = bigint | number | Rights | RightResolvable[] | RightString;
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index a1c4536e..10bd5e2c 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -18,7 +18,7 @@
import jwt from "jsonwebtoken";
import { Config } from "./Config";
-import { InstanceBan, Session, User } from "../entities";
+import { InstanceBan, Session, User } from "../../database/entities";
import crypto from "node:crypto";
import fs from "node:fs/promises";
import { existsSync } from "node:fs";
diff --git a/src/util/util/cdn.ts b/src/util/util/cdn.ts
index 46fa0ebd..ef934f78 100644
--- a/src/util/util/cdn.ts
+++ b/src/util/util/cdn.ts
@@ -18,7 +18,7 @@
import FormData from "form-data";
import { HTTPError } from "lambert-server/HTTPError";
-import { Attachment } from "../entities";
+import { Attachment } from "../../database/entities";
import { Config } from "./Config";
export async function uploadFile(
diff --git a/src/util/util/email/index.ts b/src/util/util/email/index.ts
index cceb99f4..9b37b1b0 100644
--- a/src/util/util/email/index.ts
+++ b/src/util/util/email/index.ts
@@ -18,7 +18,7 @@
import fs from "node:fs/promises";
import path from "node:path";
-import { User } from "../../entities";
+import { User } from "../../../database/entities";
import { Config } from "../Config";
import { generateToken } from "../Token";
import { IEmail, IEmailClient } from "./clients/IEmailClient";
|