Message component rework

This commit is contained in:
Bradley Bickford 2025-11-17 14:22:59 -05:00
parent 34f57b96dc
commit 69f2206a4b
9 changed files with 157 additions and 203 deletions

View File

@ -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")
})

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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<Message<boolean>> | PartialMessage) : Promise<boolean> {
const queryResult: Object[] = await db.getAllParameterized(
"SELECT * FROM messages WHERE message_snowflake = ?",
[message.id]
)
return queryResult.length != 0
export async function doesMessageExist(db: Repository<DBMessage>, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) : Promise<boolean> {
return (await db.findOne({"where": {message_snowflake: message.id}})) != null
}
export async function doesAttachmentExist(db: SQLCommon, attachment: Attachment) : Promise<boolean> {
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<DBMessageAttachments>, attachment: Attachment) : Promise<boolean> {
return (await db.findOne({"where": {attachment_snowflake: attachment.id}})) != null
}
export async function insertMessage(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>>) : Promise<SQLResult> {
const alreadyExists: boolean = await doesMessageExist(db, message)
export async function insertMessage(messageDB: Repository<DBMessage>, maDB: Repository<DBMessageAttachments>, message: OmitPartialGroupDMChannel<Message<boolean>>) : Promise<DBMessage | null> {
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<Message<boolean>>) : Promise<SQLResult> {
const messageIDExists: boolean = await doesMessageExist(db, message)
export async function updateMessageContentHistory(messageDB: Repository<DBMessage>, mccDB: Repository<DBMessageContentChanges>,
ma: Repository<DBMessageAttachments>, message: OmitPartialGroupDMChannel<Message<boolean>>) : Promise<DBMessage | null> {
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<Message<boolean>> | PartialMessage) : Promise<SQLResult> {
const messageIDExists: boolean = await doesMessageExist(db, message)
export async function markMessageDeleted(db: Repository<DBMessage>, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) : Promise<DBMessage | null> {
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<Message<boolean>> | PartialMessage) : Promise<SQLResult> {
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
}
}

View File

@ -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<boolean> {
const queryResult: Object[] = await db.getAllParameterized(
"SELECT * FROM users WHERE user_snowflake = ?",
[user.id]
)
return queryResult.length != 0
export async function doesUserExist(db: Repository<DBUser>, user: User): Promise<boolean> {
return (await db.findOne({"where": {user_snowflake: user.id}})) != null
}
export async function insertUser(db: SQLCommon, user: User): Promise<SQLResult> {
export async function insertUser(db: Repository<DBUser>, user: User): Promise<DBUser | null> {
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
}
}

View File

@ -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<DBChannel>,
userDB: Repository<DBUser>,
messageDB: Repository<DBMessage>,
mccDB: Repository<DBMessageContentChanges>,
maDB: Repository<DBMessageAttachments>
) {
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<Message<boolean>>) {
const channelOk: SQLResult = await insertChannel(db, message.channel)
const userOk: SQLResult = await insertUser(db, message.author)
async function processMessageCreate(
channelDB: Repository<DBChannel>,
userDB: Repository<DBUser>,
messageDB: Repository<DBMessage>,
maDB: Repository<DBMessageAttachments>,
message: OmitPartialGroupDMChannel<Message<boolean>>) {
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)
})
}
}
}
async function processMessageModify(db: SQLCommon, newMessage: OmitPartialGroupDMChannel<Message<boolean>>) {
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)
})
if (channelOk != null && userOk != null) {
await insertMessage(messageDB, maDB, message)
}
}
async function processMessageModify(
messageDB: Repository<DBMessage>,
mccDB: Repository<DBMessageContentChanges>,
maDB: Repository<DBMessageAttachments>,
newMessage: OmitPartialGroupDMChannel<Message<boolean>>) {
await updateMessageContentHistory(messageDB, mccDB, maDB, newMessage)
}
async function processMessageDeleted(db: SQLCommon, deletedMessage: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) {
async function processMessageDeleted(db: Repository<DBMessage>, deletedMessage: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) {
await markMessageDeleted(db, deletedMessage)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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);",