From 69f2206a4b54d240ff4c855a5d48a75c3e315608 Mon Sep 17 00:00:00 2001 From: Bradley Bickford Date: Mon, 17 Nov 2025 14:22:59 -0500 Subject: [PATCH] Message component rework --- src/breadbot.ts | 19 ++- src/commands/breadthread.ts | 61 -------- src/commands/index.ts | 4 +- src/utilties/discord/messages.ts | 130 ++++++++---------- src/utilties/discord/users.ts | 30 ++-- src/utilties/events/messages.ts | 75 +++++----- src/utilties/storage/entities/DBMessage.ts | 8 +- .../storage/entities/DBMessageAttachment.ts | 29 ++++ src/utilties/storage/tables.ts | 4 - 9 files changed, 157 insertions(+), 203 deletions(-) delete mode 100644 src/commands/breadthread.ts create mode 100644 src/utilties/storage/entities/DBMessageAttachment.ts diff --git a/src/breadbot.ts b/src/breadbot.ts index 25a71a3..f80ead5 100644 --- a/src/breadbot.ts +++ b/src/breadbot.ts @@ -11,13 +11,25 @@ import { insertChannel } from "./utilties/discord/channels" import { insertRole } from "./utilties/discord/roles" import { setupRoleCapture } from "./utilties/events/roles" import { DBUser } from "./utilties/storage/entities/DBUser" +import { DBMessage } from "./utilties/storage/entities/DBMessage" +import { DBMessageContentChanges } from "./utilties/storage/entities/DBMessageContentChanges" +import { DBMessageAttachments } from "./utilties/storage/entities/DBMessageAttachment" +import { setupMessageCapture } from "./utilties/events/messages" console.log(__dirname + path.sep + "utilities" + path.sep + "storage" + path.sep + "entities" + path.sep + "*.ts") export const dataSource = new DataSource({ type: "sqlite", database: "breadbot.db", - entities: [DBServer, DBChannel, DBRole, DBUser], + entities: [ + DBServer, + DBChannel, + DBRole, + DBUser, + DBMessage, + DBMessageContentChanges, + DBMessageAttachments + ], synchronize: true, logging: true }) @@ -38,6 +50,10 @@ client.once(Events.ClientReady, async () => { const serverRepo = dataSource.getRepository(DBServer) const channelRepo = dataSource.getRepository(DBChannel) const roleRepo = dataSource.getRepository(DBRole) + const userRepo = dataSource.getRepository(DBUser) + const messageRepo = dataSource.getRepository(DBMessage) + const mccRepo = dataSource.getRepository(DBMessageContentChanges) + const maRepo = dataSource.getRepository(DBMessageAttachments) client.guilds.cache.forEach(async (guild: Guild) => { const server: DBServer | null = await insertGuild(serverRepo, guild) @@ -54,6 +70,7 @@ client.once(Events.ClientReady, async () => { }) setupRoleCapture(client, serverRepo, roleRepo) + setupMessageCapture(client, channelRepo, userRepo, messageRepo, mccRepo, maRepo) console.log("Breadbot is Ready") }) diff --git a/src/commands/breadthread.ts b/src/commands/breadthread.ts deleted file mode 100644 index cd84bf5..0000000 --- a/src/commands/breadthread.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-nocheck -import { ChannelType, ChatInputCommandInteraction, CommandInteraction, MessageFlags, SlashCommandBuilder } from "discord.js"; -import { breadthreadEnsureAutoLock, breadthreadRemoveAutoLock } from "../utilties/breadbot/breadthread"; -import { db } from "../breadbot"; -import { timeShorthandToSeconds } from "../utilties/time/conversions"; - -export const enabled: boolean = true - -export const data = new SlashCommandBuilder() - .setName("breadthread") - .setDescription("Manages Breadbot's extended thread features") - .addSubcommand(subcommand => - subcommand - .setName("autolock") - .setDescription("Enables auto locking of a thread after a period of thread inactivity") - .addChannelOption(option => - option - .setName("channel") - .setDescription("The name of the thread you want to autolock") - .addChannelTypes( - ChannelType.PublicThread, - ChannelType.PrivateThread, - ChannelType.AnnouncementThread - ) - .setRequired(true) - ) - .addBooleanOption(option => - option - .setName("enable") - .setDescription("Enable or disable the auto locking") - .setRequired(true) - ) - .addStringOption(option => - option - .setName("timeinactive") - .setDescription("How long the thread needs to be inactive before locking, default is 3 days") - .setRequired(false) - ) - - ) - -export async function execute(interaction: ChatInputCommandInteraction) { - await interaction.deferReply({flags: MessageFlags.Ephemeral}) - - if(interaction.options.getSubcommand() === "autolock") { - if(interaction.options.getBoolean("enable")) { - await breadthreadEnsureAutoLock( - db, - interaction.options.getChannel("channel", true).id, - interaction.options.getString("timeinactive") ?? "3d" - ) - } else { - await breadthreadRemoveAutoLock( - db, - interaction.options.getChannel("channel", true).id - ) - } - - await interaction.editReply("Autolock Action OK") - } -} \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts index 609f6e6..03ea096 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,9 +1,7 @@ import * as ping from "./ping"; import * as breadalert from "./breadalert" -import * as breadthread from "./breadthread" export const commands = { ping, - breadalert, - breadthread + breadalert } \ No newline at end of file diff --git a/src/utilties/discord/messages.ts b/src/utilties/discord/messages.ts index c52f683..629dcd0 100644 --- a/src/utilties/discord/messages.ts +++ b/src/utilties/discord/messages.ts @@ -1,118 +1,100 @@ import { Attachment, Message, OmitPartialGroupDMChannel, PartialMessage } from "discord.js"; -import { SQLCommon } from "../storage/interfaces"; -import { SQLResult } from "../storage/enumerations"; +import { Repository } from "typeorm"; +import { DBMessage } from "../storage/entities/DBMessage"; +import { DBMessageAttachments } from "../storage/entities/DBMessageAttachment"; +import { DBMessageContentChanges } from "../storage/entities/DBMessageContentChanges"; // TODO Do partial messages affect other functionality elsewhere? -export async function doesMessageExist(db: SQLCommon, message: OmitPartialGroupDMChannel> | PartialMessage) : Promise { - const queryResult: Object[] = await db.getAllParameterized( - "SELECT * FROM messages WHERE message_snowflake = ?", - [message.id] - ) - - return queryResult.length != 0 +export async function doesMessageExist(db: Repository, message: OmitPartialGroupDMChannel> | PartialMessage) : Promise { + return (await db.findOne({"where": {message_snowflake: message.id}})) != null } -export async function doesAttachmentExist(db: SQLCommon, attachment: Attachment) : Promise { - const queryResult: Object[] = await db.getAllParameterized( - "SELECT * FROM message_attachments WHERE attachment_snowflake = ?", - [attachment.id] - ) - - return queryResult.length != 0 +export async function doesAttachmentExist(db: Repository, attachment: Attachment) : Promise { + return (await db.findOne({"where": {attachment_snowflake: attachment.id}})) != null } -export async function insertMessage(db: SQLCommon, message: OmitPartialGroupDMChannel>) : Promise { - const alreadyExists: boolean = await doesMessageExist(db, message) +export async function insertMessage(messageDB: Repository, maDB: Repository, message: OmitPartialGroupDMChannel>) : Promise { + const alreadyExists: boolean = await doesMessageExist(messageDB, message) if(alreadyExists) { - return SQLResult.ALREADYEXISTS + return await messageDB.findOne({"where": {message_snowflake: message.id}}) } try { - await db.runParameterized( - "INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?)", - [message.id, message.channel.id, message.author.id, - message.content, message.createdTimestamp, 0] - ) + const newMessage: DBMessage = await messageDB.create({ + message_snowflake: message.id, + channel: {channel_snowflake: message.channel.id}, + user: {user_snowflake: message.author.id}, + message_content: message.content, + message_timestamp: message.createdAt, + attachments: message.attachments.size == 0 ? null : message.attachments.map((attachment: Attachment) => { + return maDB.create({ + attachment_snowflake: attachment.id, + message: {message_snowflake: message.id}, + attachment_name: attachment.name, + attachment_description: attachment.description, + attachment_timestamp: message.createdAt, + attachment_mime_type: attachment.contentType, + attachment_url: attachment.url + }) + }) + }) - return SQLResult.CREATED + return await messageDB.save(newMessage) } catch (err) { //TODO Winston should handle this console.log("MESSAGE INSERTION ERROR") console.log(err) - return SQLResult.FAILED + return null } } -export async function updateMessageContentHistory(db: SQLCommon, message: OmitPartialGroupDMChannel>) : Promise { - const messageIDExists: boolean = await doesMessageExist(db, message) +export async function updateMessageContentHistory(messageDB: Repository, mccDB: Repository, + ma: Repository, message: OmitPartialGroupDMChannel>) : Promise { + let dbMessage: DBMessage | null = await messageDB.findOne({"where": {message_snowflake: message.id}}) - if(!messageIDExists) { - return SQLResult.FAILED + if(dbMessage == null) { + return null } try { - console.log([message.id, message.editedTimestamp ?? message.createdTimestamp, message.content, message.id]) - await db.runParameterized( - "INSERT INTO message_content_changes (message_snowflake, message_change_old_timestamp, message_change_old_content) " + - "SELECT messages.message_snowflake, message_timestamp, message_content FROM messages WHERE message_snowflake = ?;", - [message.id] - ) + const contentChange: DBMessageContentChanges = mccDB.create({ + message: {message_snowflake: message.id}, + message_change_old_content: dbMessage.message_content, + message_change_old_timestamp: dbMessage.message_timestamp + }) - await db.runParameterized( - "UPDATE messages SET message_timestamp = ?, message_content = ? WHERE message_snowflake = ?;", - [message.editedTimestamp ?? message.createdTimestamp, message.content, message.id] - ) + dbMessage.message_content = message.content + dbMessage.message_timestamp = message.editedAt ?? message.createdAt - return SQLResult.UPDATED + // TODO This should really be a transaction + // TODO Changes to attachments aren't captured + return await mccDB.save(contentChange).then(async (dbmcc: DBMessageContentChanges) => { + return await messageDB.save(dbMessage) + }) } catch (err) { //TODO Winston should handle this console.log("MESSAGE MODIFY FAILED") console.log(err) - return SQLResult.FAILED + return null } } -export async function markMessageDeleted(db: SQLCommon, message: OmitPartialGroupDMChannel> | PartialMessage) : Promise { - const messageIDExists: boolean = await doesMessageExist(db, message) +export async function markMessageDeleted(db: Repository, message: OmitPartialGroupDMChannel> | PartialMessage) : Promise { + let dbMessage: DBMessage | null = await db.findOne({"where": {message_snowflake: message.id}}) - if(!messageIDExists) { - return SQLResult.FAILED + if(dbMessage == null) { + return null } try { - await db.runParameterized( - "UPDATE messages SET message_deleted = 1 WHERE message_snowflake = ?", - [message.id] - ) + dbMessage.message_deleted = true - return SQLResult.UPDATED + return await db.save(dbMessage) } catch (err) { // TODO Winston should handle this console.log(err) - return SQLResult.FAILED - } -} - -export async function insertAttachment(db: SQLCommon, attachment: Attachment, message: OmitPartialGroupDMChannel> | PartialMessage) : Promise { - const alreadyExists: boolean = await doesAttachmentExist(db, attachment) - - if(alreadyExists) { - return SQLResult.ALREADYEXISTS - } - - try { - await db.runParameterized( - "INSERT INTO message_attachments VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - [attachment.id, message.id, attachment.name, attachment.description, message.createdTimestamp, - attachment.contentType, attachment.url, 0] - ) - - return SQLResult.CREATED - } catch (err) { - // TODO Winston should handle this - console.log(err) - return SQLResult.FAILED + return null } } \ No newline at end of file diff --git a/src/utilties/discord/users.ts b/src/utilties/discord/users.ts index b05f7e5..9ba9235 100644 --- a/src/utilties/discord/users.ts +++ b/src/utilties/discord/users.ts @@ -1,34 +1,30 @@ import { User } from "discord.js"; -import { SQLCommon } from "../storage/interfaces"; -import { SQLResult } from "../storage/enumerations"; +import { Repository } from "typeorm"; +import { DBUser } from "../storage/entities/DBUser"; -export async function doesUserExist(db: SQLCommon, user: User): Promise { - const queryResult: Object[] = await db.getAllParameterized( - "SELECT * FROM users WHERE user_snowflake = ?", - [user.id] - ) - - return queryResult.length != 0 +export async function doesUserExist(db: Repository, user: User): Promise { + return (await db.findOne({"where": {user_snowflake: user.id}})) != null } -export async function insertUser(db: SQLCommon, user: User): Promise { +export async function insertUser(db: Repository, user: User): Promise { const alreadyExists: boolean = await doesUserExist(db, user) if(alreadyExists) { - return SQLResult.ALREADYEXISTS + return null } try { - await db.runParameterized( - "INSERT INTO users VALUES (?, ?, ?)", - [user.id, user.username, user.displayName] - ) + const newUser: DBUser = db.create({ + user_snowflake: user.id, + user_name: user.username, + user_displayname: user.displayName + }) - return SQLResult.CREATED + return await db.save(newUser) } catch (err) { //TODO Winston should handle this console.log("USER INSERT ERROR") console.log(err) - return SQLResult.FAILED + return null } } \ No newline at end of file diff --git a/src/utilties/events/messages.ts b/src/utilties/events/messages.ts index 0e91d74..f0fc219 100644 --- a/src/utilties/events/messages.ts +++ b/src/utilties/events/messages.ts @@ -1,64 +1,57 @@ -// @ts-nocheck import { Client, Events, Message, OmitPartialGroupDMChannel, PartialMessage } from "discord.js"; -import { SQLResult } from "../storage/enumerations"; -import { SQLCommon } from "../storage/interfaces"; import { insertChannel } from "../discord/channels"; import { insertUser } from "../discord/users"; -import { insertAttachment, insertMessage, markMessageDeleted, updateMessageContentHistory } from "../discord/messages"; +import { insertMessage, markMessageDeleted, updateMessageContentHistory } from "../discord/messages"; +import { Repository } from "typeorm"; +import { DBMessage } from "../storage/entities/DBMessage"; +import { DBMessageContentChanges } from "../storage/entities/DBMessageContentChanges"; +import { DBMessageAttachments } from "../storage/entities/DBMessageAttachment"; +import { DBChannel } from "../storage/entities/DBChannel"; +import { DBUser } from "../storage/entities/DBUser"; -export function setupMessageCapture(client: Client, db: SQLCommon) { +export function setupMessageCapture(client: Client, + channelDB: Repository, + userDB: Repository, + messageDB: Repository, + mccDB: Repository, + maDB: Repository +) { client.on(Events.MessageCreate, async (message) => { - await processMessageCreate(db, message) + await processMessageCreate(channelDB, userDB, messageDB, maDB, message) }) client.on(Events.MessageUpdate, async (oldMessage, newMessage) => { - await processMessageModify(db, newMessage) + await processMessageModify(messageDB, mccDB, maDB, newMessage) }) client.on(Events.MessageDelete, async (deletedMessage) => { - await processMessageDeleted(db, deletedMessage) + await processMessageDeleted(messageDB, deletedMessage) }) } -async function processMessageCreate(db: SQLCommon, message: OmitPartialGroupDMChannel>) { - const channelOk: SQLResult = await insertChannel(db, message.channel) - const userOk: SQLResult = await insertUser(db, message.author) +async function processMessageCreate( + channelDB: Repository, + userDB: Repository, + messageDB: Repository, + maDB: Repository, + message: OmitPartialGroupDMChannel>) { + const channelOk: DBChannel | null = await insertChannel(channelDB, message.channel) + const userOk: DBUser | null = await insertUser(userDB, message.author) - if (channelOk == SQLResult.ALREADYEXISTS || channelOk == SQLResult.CREATED || - userOk == SQLResult.ALREADYEXISTS || userOk == SQLResult.CREATED) { - - await insertMessage(db, message) - // TODO observe success of message insertion - if(message.attachments.size != 0) { - const allAttachments: void[] = message.attachments.map((attachment) => { - insertAttachment(db, attachment, message) - }) - - await Promise.all(allAttachments).catch((error) => { - // TODO Winston should handle this - console.log("MESSAGE ATTACHMENT INSERT ERROR") - console.log(error) - }) - } + if (channelOk != null && userOk != null) { + await insertMessage(messageDB, maDB, message) } } -async function processMessageModify(db: SQLCommon, newMessage: OmitPartialGroupDMChannel>) { - await updateMessageContentHistory(db, newMessage) - - if(newMessage.attachments.size != 0) { - const allAttachments: void[] = newMessage.attachments.map((attachment) => { - insertAttachment(db, attachment, newMessage) - }) - - await Promise.all(allAttachments).catch((error) => { - // TODO Winston should handle this - console.log(error) - }) - } +async function processMessageModify( + messageDB: Repository, + mccDB: Repository, + maDB: Repository, + newMessage: OmitPartialGroupDMChannel>) { + await updateMessageContentHistory(messageDB, mccDB, maDB, newMessage) } -async function processMessageDeleted(db: SQLCommon, deletedMessage: OmitPartialGroupDMChannel> | PartialMessage) { +async function processMessageDeleted(db: Repository, deletedMessage: OmitPartialGroupDMChannel> | PartialMessage) { await markMessageDeleted(db, deletedMessage) } \ No newline at end of file diff --git a/src/utilties/storage/entities/DBMessage.ts b/src/utilties/storage/entities/DBMessage.ts index 99df0c0..c186e21 100644 --- a/src/utilties/storage/entities/DBMessage.ts +++ b/src/utilties/storage/entities/DBMessage.ts @@ -2,6 +2,7 @@ import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm"; import { DBChannel } from "./DBChannel"; import { DBUser } from "./DBUser"; import { DBMessageContentChanges } from "./DBMessageContentChanges"; +import { DBMessageAttachments } from "./DBMessageAttachment"; @Entity() export class DBMessage { @@ -20,9 +21,12 @@ export class DBMessage { @Column({type: "datetime"}) message_timestamp: Date - @Column() + @Column({default: false}) message_deleted: boolean @OneToMany(() => DBMessageContentChanges, (mcc: DBMessageContentChanges) => mcc.message, {nullable: true}) - changes: DBMessageContentChanges | null + changes: DBMessageContentChanges[] | null + + @OneToMany(() => DBMessageAttachments, (ma: DBMessageAttachments) => ma.attachment_snowflake, {nullable: true}) + attachments: DBMessageAttachments[] | null } \ No newline at end of file diff --git a/src/utilties/storage/entities/DBMessageAttachment.ts b/src/utilties/storage/entities/DBMessageAttachment.ts new file mode 100644 index 0000000..6f3a9b7 --- /dev/null +++ b/src/utilties/storage/entities/DBMessageAttachment.ts @@ -0,0 +1,29 @@ +import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm"; +import { DBMessage } from "./DBMessage"; + +@Entity() +export class DBMessageAttachments { + @PrimaryColumn({"type": "bigint"}) + attachment_snowflake: string + + @ManyToOne(() => DBMessage, (message: DBMessage) => message.attachments) + message: DBMessage + + @Column() + attachment_name: string + + @Column({nullable: true}) + attachment_description: string | null + + @Column({type: "datetime"}) + attachment_timestamp: Date + + @Column({nullable: true}) + attachment_mime_type: string | null + + @Column() + attachment_url: string + + @Column({default: false}) + attachment_download: boolean +} \ No newline at end of file diff --git a/src/utilties/storage/tables.ts b/src/utilties/storage/tables.ts index 187be1a..97426af 100644 --- a/src/utilties/storage/tables.ts +++ b/src/utilties/storage/tables.ts @@ -1,10 +1,6 @@ import { SQLCommon } from "./interfaces"; const tables: string[] = [ - "CREATE TABLE IF NOT EXISTS messages (message_snowflake bigint NOT NULL PRIMARY KEY,channel_snowflake bigint NOT NULL,user_snowflake bigint NOT NULL,message_content longtext NOT NULL,message_timestamp datetime NOT NULL,message_deleted bit NOT NULL);", - "CREATE TABLE IF NOT EXISTS message_content_changes (message_change_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,message_snowflake bigint NOT NULL,message_change_old_timestamp datetime NOT NULL,message_change_old_content longtext NOT NULL);", - "CREATE TABLE IF NOT EXISTS message_attachments (attachment_snowflake bigint NOT NULL PRIMARY KEY,message_snowflake bigint NOT NULL,attachment_name text NOT NULL,attachment_description text,attachment_timestamp datetime NOT NULL,attachment_mime_type text,attachment_url text NOT NULL,attachment_downloaded bit NOT NULL);", - "CREATE TABLE IF NOT EXISTS breadthread_autolock (breadthread_autolock_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,channel_snowflake bigint NOT NULL,inactivity_seconds bigint NOT NULL,locked bit NOT NULL);", "CREATE TABLE IF NOT EXISTS message_scan_regex_matches (message_scan_regex_matches_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,message_snowflake bigint NOT NULL,message_regexes_id bigint NOT NULL);", "CREATE TABLE IF NOT EXISTS message_regex_no_role_check (message_regex_no_role_check_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,server_snowflake bigint NOT NULL,role_snowflake bigint NOT NULL);", "CREATE TABLE IF NOT EXISTS message_regexes (message_regexes_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,server_snowflake bigint NOT NULL,regex text NOT NULL,priority int NOT NULL,severity int NOT NULL);",