/* 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 . */ import { User } from "./User"; import { Member } from "./Member"; import { Role } from "./Role"; import { Channel } from "./Channel"; import { InteractionType } from "../interfaces/Interaction"; import { Application } from "./Application"; import { Column, CreateDateColumn, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId } 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 { ActionRowComponent, ApplicationCommandType, Embed, MessageType, PartialMessage, Poll, Reaction } from "@spacebar/schemas"; @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.guild) 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) 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) mention_channels: Channel[]; @JoinTable({ name: "message_stickers" }) @ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" }) sticker_items?: Sticker[]; @OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, { cascade: true, orphanedRowAction: "delete", }) attachments?: Attachment[]; @Column({ type: "simple-json" }) embeds: Embed[]; @Column({ type: "simple-json" }) reactions: Reaction[]; @Column({ type: "text", nullable: true }) 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: "simple-json", nullable: true }) activity?: { type: number; party_id: string; }; @Column({ default: 0 }) flags: number; @Column({ type: "simple-json", nullable: true }) 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: "simple-json", nullable: true }) interaction?: { id: string; type: InteractionType; name: string; }; @Column({ type: "simple-json", nullable: true }) interaction_metadata?: { id: string; type: InteractionType; user_id: string; authorizing_integration_owners: object; name: string; command_type: ApplicationCommandType; }; @Column({ type: "simple-json", nullable: true }) components?: ActionRowComponent[]; @Column({ type: "simple-json", nullable: true }) poll?: Poll; @Column({ nullable: true }) username?: string; @Column({ nullable: true }) avatar?: string; toJSON(): Message { return { ...this, author_id: undefined, member_id: undefined, webhook_id: this.webhook_id ?? undefined, application_id: undefined, 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, author: { ...(this.author?.toPublicUser() ?? undefined), // Webhooks username: this.username ?? this.author?.username, avatar: this.avatar ?? this.author?.avatar, }, activity: this.activity ?? undefined, application: this.application ?? undefined, components: this.components ?? undefined, poll: this.poll ?? undefined, content: this.content ?? "", pinned: this.pinned, }; } 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 }; } withSignedAttachments(data: NewUrlUserSignatureData) { return { ...this, attachments: this.attachments?.map((attachment: Attachment) => Attachment.prototype.signUrls.call(attachment, data)), }; } static async createWithDefaults(opts: Partial): Promise { 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; Object.assign(message, { tts: false, embeds: [], reactions: [], flags: 0, type: 0, timestamp: new Date(), ...opts, }); return message; } }