Some of breadthread is working, added basic role tracking

This commit is contained in:
Bradley Bickford 2025-07-07 21:46:12 -04:00
parent b474d55612
commit 5999aeeb0c
15 changed files with 255 additions and 22 deletions

Binary file not shown.

View File

@ -1,4 +1,4 @@
import { Client, Events, GatewayIntentBits, Guild, GuildBasedChannel, Interaction } from "discord.js" import { CacheType, ChatInputCommandInteraction, Client, Events, GatewayIntentBits, Guild, GuildBasedChannel, Interaction, Role } from "discord.js"
import { utilities } from "./utilties" import { utilities } from "./utilties"
import { commands } from "./commands" import { commands } from "./commands"
import { config } from "./config" import { config } from "./config"
@ -13,7 +13,7 @@ export const client : Client = new Client({
] ]
}) })
let db: SQLCommon export let db: SQLCommon
if (config.DB_MODE == "sqlite") { if (config.DB_MODE == "sqlite") {
db = new utilities.sqlite.SqliteDB("breadbot_test.db") db = new utilities.sqlite.SqliteDB("breadbot_test.db")
@ -24,6 +24,7 @@ if (config.DB_MODE == "sqlite") {
//TODO I really don't want this to be here. //TODO I really don't want this to be here.
utilities.events.messages.setupMessageCapture(client, db) utilities.events.messages.setupMessageCapture(client, db)
utilities.events.roles.setupRoleCapture(client, db)
} }
client.once(Events.ClientReady, () => { client.once(Events.ClientReady, () => {
@ -39,6 +40,10 @@ client.once(Events.ClientReady, () => {
guild.channels.cache.forEach(async (channel: GuildBasedChannel) => { guild.channels.cache.forEach(async (channel: GuildBasedChannel) => {
await utilities.channels.insertChannel(db, channel) await utilities.channels.insertChannel(db, channel)
}) })
guild.roles.cache.forEach(async (role: Role) => {
await utilities.roles.insertRole(db, role)
})
}) })
}) })
@ -51,16 +56,29 @@ client.on(Events.GuildCreate, async (guild : Guild) => {
}) })
}) })
client.on(Events.ChannelCreate, async (channel) => {
console.log("CHANNEL CREATE CALLED")
await utilities.channels.insertChannel(db, channel)
})
client.on(Events.ThreadCreate, async (channel) => {
console.log("THREAD CREATE CALLED")
console.log(channel.toString())
await utilities.channels.insertChannel(db, channel)
})
client.on(Events.InteractionCreate, async (interaction: Interaction) => { client.on(Events.InteractionCreate, async (interaction: Interaction) => {
if (!interaction.isCommand()) { if (!interaction.isCommand()) {
return return
} }
if (commands[interaction.commandName as keyof typeof commands]) { if (commands[interaction.commandName as keyof typeof commands]) {
commands[interaction.commandName as keyof typeof commands].execute(interaction) commands[interaction.commandName as keyof typeof commands].execute(interaction as ChatInputCommandInteraction)
} }
}) })
setInterval(async () => {
await utilities.breadthread.breadthreadProcessLocks(db, client)
}, 5000)
client.login(config.DISCORD_TOKEN) client.login(config.DISCORD_TOKEN)

View File

@ -1,4 +1,7 @@
import { ChannelType, CommandInteraction, SlashCommandBuilder } from "discord.js"; 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 enabled: boolean = true
@ -35,6 +38,23 @@ export const data = new SlashCommandBuilder()
) )
export async function execute(interaction: CommandInteraction) { export async function execute(interaction: ChatInputCommandInteraction) {
return interaction.reply("NOT IMPLEMENTED!") 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

@ -0,0 +1,57 @@
import { timeShorthandToSeconds } from "../time/conversions";
import { SQLCommon } from "../storage/interfaces";
import { Client } from "discord.js";
export async function breadthreadLockExists(db: SQLCommon, channelId: string) : Promise<boolean> {
const queryResult: Object[] = await db.getAllParameterized(
"SELECT * FROM breadthread_autolock WHERE channel_snowflake = ?",
[channelId]
)
return queryResult.length != 0
}
export async function breadthreadEnsureAutoLock(db: SQLCommon, channelId: string, inactiveTimeUntilLocked: string) {
const timeUntilLocked = timeShorthandToSeconds(inactiveTimeUntilLocked)
if(await breadthreadLockExists(db, channelId)) {
await db.runParameterized(
"UPDATE breadthread_autolock SET inactivity_seconds = ? WHERE channel_snowflake = ?",
[timeUntilLocked, channelId]
)
} else {
await db.runParameterized(
"INSERT INTO breadthread_autolock (channel_snowflake, inactivity_seconds, locked) VALUES (?, ?, ?)",
[channelId, timeUntilLocked, 0]
)
}
}
export async function breadthreadRemoveAutoLock(db: SQLCommon, channelId: string) {
await db.runParameterized(
"DELETE FROM breadthread_autolock WHERE channel_snowflake = ?",
[channelId]
)
}
export async function breadthreadProcessLocks(db: SQLCommon, client: Client) {
const currentTimeSeconds = Math.round((new Date()).getTime() / 1000);
(await db.getAll("SELECT * FROM breadthread_autolock WHERE locked = 0")).forEach(async (row : any) => {
const channel = client.channels.cache.find(row.channel_snowflake)
if(channel?.isThread()) {
const lastMessageTime: number = Math.round(
channel.lastMessage?.createdAt.getTime() ?? 0 / 1000
)
if(lastMessageTime != 0 && currentTimeSeconds - lastMessageTime >= row.inactivity_seconds) {
await channel.setLocked(true, "Breadbot is locking this thread because the inactivity timeout was exceeded!")
await db.runParameterized(
"UPDATE breadthread_autolock SET locked = 1 WHERE locked = 0 AND channel_snowflake = ?",
[channel.id]
)
}
}
})
}

View File

@ -44,6 +44,7 @@ export async function insertChannel(db: SQLCommon, channel: GuildBasedChannel |
return SQLResult.CREATED return SQLResult.CREATED
} catch (err) { } catch (err) {
//TODO Winston should handle this //TODO Winston should handle this
console.log("CHANNEL INSERT ERROR")
console.log(err) console.log(err)
return SQLResult.FAILED return SQLResult.FAILED
} }

View File

@ -39,6 +39,7 @@ export async function insertMessage(db: SQLCommon, message: OmitPartialGroupDMCh
return SQLResult.CREATED return SQLResult.CREATED
} catch (err) { } catch (err) {
//TODO Winston should handle this //TODO Winston should handle this
console.log("MESSAGE INSERTION ERROR")
console.log(err) console.log(err)
return SQLResult.FAILED return SQLResult.FAILED
} }
@ -52,16 +53,22 @@ export async function updateMessageContentHistory(db: SQLCommon, message: OmitPa
} }
try { try {
console.log([message.id, message.editedTimestamp ?? message.createdTimestamp, message.content, message.id])
await db.runParameterized( await db.runParameterized(
"INSERT INTO message_content_changes (message_snowflake, message_change_old_timestamp, message_change_old_content) " + "INSERT INTO message_content_changes (message_snowflake, message_change_old_timestamp, message_change_old_content) " +
"SELECT message_snowflake, message_timestamp, message_content FROM messages WHERE message_snowflake = ?;" + "SELECT messages.message_snowflake, message_timestamp, message_content FROM messages WHERE message_snowflake = ?;",
[message.id]
)
await db.runParameterized(
"UPDATE messages SET message_timestamp = ?, message_content = ? WHERE message_snowflake = ?;", "UPDATE messages SET message_timestamp = ?, message_content = ? WHERE message_snowflake = ?;",
[message.id, message.editedTimestamp, message.content, message.id] [message.editedTimestamp ?? message.createdTimestamp, message.content, message.id]
) )
return SQLResult.UPDATED return SQLResult.UPDATED
} catch (err) { } catch (err) {
//TODO Winston should handle this //TODO Winston should handle this
console.log("MESSAGE MODIFY FAILED")
console.log(err) console.log(err)
return SQLResult.FAILED return SQLResult.FAILED
} }

View File

@ -0,0 +1,75 @@
import { Role } from "discord.js";
import { SQLCommon } from "../storage/interfaces";
import { SQLResult } from "../storage/enumerations";
export async function doesRoleExist(db: SQLCommon, role : Role) : Promise<boolean> {
const queryResult : Object[] = await db.getAllParameterized(
"SELECT * FROM roles WHERE role_snowflake = ?",
[role.id]
)
return queryResult.length != 0
}
export async function insertRole(db: SQLCommon, role: Role) : Promise<SQLResult> {
const alreadyExists: boolean = await doesRoleExist(db, role)
if(alreadyExists) {
return SQLResult.ALREADYEXISTS
}
try {
await db.runParameterized(
"INSERT INTO roles VALUES (?, ?, ?, 0)",
[role.id, role.guild.id, role.name]
)
return SQLResult.CREATED
} catch (err) {
console.log("ROLE INSERT ERROR")
console.log(err)
return SQLResult.FAILED
}
}
export async function updateRole(db: SQLCommon, role: Role): Promise<SQLResult> {
const roleExists: boolean = await doesRoleExist(db, role)
if(!roleExists) {
return SQLResult.FAILED
}
try {
await db.runParameterized(
"UPDATE roles SET role_name = ? WHERE role_snowflake = ?",
[role.name, role.id]
)
return SQLResult.UPDATED
} catch (err) {
console.log("ROLE UPDATE FAILED")
console.log(err)
return SQLResult.FAILED
}
}
export async function markRoleDeleted(db: SQLCommon, role: Role) : Promise<SQLResult> {
const roleExists: boolean = await doesRoleExist(db, role)
if(!roleExists) {
return SQLResult.FAILED
}
try {
await db.runParameterized(
"UPDATE roles SET is_deleted = 1 WHERE role_snowflake = ?",
[role.id]
)
return SQLResult.UPDATED
} catch (err) {
console.log("ROLE DELETE FAILED")
console.log(err)
return SQLResult.FAILED
}
}

View File

@ -27,6 +27,7 @@ export async function insertUser(db: SQLCommon, user: User): Promise<SQLResult>
return SQLResult.CREATED return SQLResult.CREATED
} catch (err) { } catch (err) {
//TODO Winston should handle this //TODO Winston should handle this
console.log("USER INSERT ERROR")
console.log(err) console.log(err)
return SQLResult.FAILED return SQLResult.FAILED
} }

View File

@ -0,0 +1,7 @@
import * as roles from "./roles"
import * as messages from "./messages"
export const events = {
roles,
messages
}

View File

@ -35,6 +35,7 @@ async function processMessageCreate(db: SQLCommon, message: OmitPartialGroupDMCh
await Promise.all(allAttachments).catch((error) => { await Promise.all(allAttachments).catch((error) => {
// TODO Winston should handle this // TODO Winston should handle this
console.log("MESSAGE ATTACHMENT INSERT ERROR")
console.log(error) console.log(error)
}) })
} }

View File

@ -0,0 +1,23 @@
import { Client, Events } from "discord.js";
import { SQLCommon } from "../storage/interfaces";
import { SQLResult } from "../storage/enumerations";
import { insertRole, markRoleDeleted, updateRole } from "../discord/roles";
import { insertGuild } from "../discord/guilds";
export function setupRoleCapture(client: Client, db: SQLCommon) {
client.on(Events.GuildRoleCreate, async (role) => {
const serverOk: SQLResult = await insertGuild(db, role.guild)
if (serverOk == SQLResult.ALREADYEXISTS || serverOk == SQLResult.CREATED) {
await insertRole(db, role)
}
})
client.on(Events.GuildRoleUpdate, async (role) => {
await updateRole(db, role)
})
client.on(Events.GuildRoleDelete, async (role) => {
await markRoleDeleted(db, role)
})
}

View File

@ -4,11 +4,9 @@ import * as tables from "./storage/tables"
import * as guilds from "./discord/guilds" import * as guilds from "./discord/guilds"
import * as channels from "./discord/channels" import * as channels from "./discord/channels"
import * as users from "./discord/users" import * as users from "./discord/users"
import * as messages from "./events/messages" import * as breadthread from "./breadbot/breadthread"
import * as roles from "./discord/roles"
const events = { import { events } from "./events"
messages
}
export const utilities = { export const utilities = {
commands, commands,
@ -17,5 +15,7 @@ export const utilities = {
guilds, guilds,
channels, channels,
users, users,
events events,
breadthread,
roles
} }

View File

@ -15,7 +15,7 @@ export class SqliteDB implements SQLCommon {
if (err) { if (err) {
// TODO Winston should handle this // TODO Winston should handle this
console.log(err) console.log(err)
throw err reject(err)
} else { } else {
resolve(this.changes) resolve(this.changes)
} }
@ -29,7 +29,7 @@ export class SqliteDB implements SQLCommon {
if (err) { if (err) {
// TODO Winston should handle this // TODO Winston should handle this
console.log(err) console.log(err)
throw err reject(err)
} else { } else {
resolve(this.changes) resolve(this.changes)
} }
@ -43,9 +43,8 @@ export class SqliteDB implements SQLCommon {
if (err) { if (err) {
// TODO Winston should handle this // TODO Winston should handle this
console.log(err) console.log(err)
throw err reject(err)
} else { } else {
console.log("Got rows")
resolve(rows) resolve(rows)
} }
}) })
@ -58,7 +57,7 @@ export class SqliteDB implements SQLCommon {
if (err) { if (err) {
// TODO Winston should handle this // TODO Winston should handle this
console.log(err) console.log(err)
throw err reject(err)
} else { } else {
resolve(rows) resolve(rows)
} }

View File

@ -6,7 +6,9 @@ const tables: string[] = [
"CREATE TABLE IF NOT EXISTS users (user_snowflake bigint NOT NULL PRIMARY KEY,user_name text NOT NULL,user_displayname text);", "CREATE TABLE IF NOT EXISTS users (user_snowflake bigint NOT NULL PRIMARY KEY,user_name text NOT NULL,user_displayname text);",
"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 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_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 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 roles (role_snowflake bigint NOT NULL PRIMARY KEY,server_snowflake bigint NOT NULL,role_name text NOT NULL,is_deleted bit NOT NULL);"
] ]
const constraints: string[] = [ const constraints: string[] = [

View File

@ -0,0 +1,22 @@
export function timeShorthandToSeconds(shorthand: string) : number {
let totalSeconds: number = 0
shorthand.split(" ").forEach((value) => {
console.log(value)
const unit: string = value.substring(value.length - 1, value.length)
if (unit == "d" || unit == "D") {
totalSeconds += Number.parseInt(value.substring(0, value.length - 1)) * 86400
} else if (unit == "h" || unit == "H") {
totalSeconds += Number.parseInt(value.substring(0, value.length - 1)) * 3600
} else if (unit == "m" || unit == "M") {
totalSeconds += Number.parseInt(value.substring(0, value.length - 1)) * 60
} else if (unit == "s" || unit == "S") {
totalSeconds += Number.parseInt(value.substring(0, value.length - 1))
} else {
console.log(`An invalid shorthand directive was sent ${value}`)
}
})
return totalSeconds
}