diff --git a/breadbot_test.db b/breadbot_test.db deleted file mode 100644 index 101cdfa..0000000 Binary files a/breadbot_test.db and /dev/null differ diff --git a/src/breadbot.ts b/src/breadbot.ts index 3d4addc..064332a 100644 --- a/src/breadbot.ts +++ b/src/breadbot.ts @@ -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 { commands } from "./commands" 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") { 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. utilities.events.messages.setupMessageCapture(client, db) + utilities.events.roles.setupRoleCapture(client, db) } client.once(Events.ClientReady, () => { @@ -39,6 +40,10 @@ client.once(Events.ClientReady, () => { guild.channels.cache.forEach(async (channel: GuildBasedChannel) => { 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) => { if (!interaction.isCommand()) { return } 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) \ No newline at end of file diff --git a/src/commands/breadthread.ts b/src/commands/breadthread.ts index 516fc7b..6a3645d 100644 --- a/src/commands/breadthread.ts +++ b/src/commands/breadthread.ts @@ -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 @@ -35,6 +38,23 @@ export const data = new SlashCommandBuilder() ) -export async function execute(interaction: CommandInteraction) { - return interaction.reply("NOT IMPLEMENTED!") +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/utilties/breadbot/breadthread.ts b/src/utilties/breadbot/breadthread.ts new file mode 100644 index 0000000..77e95d3 --- /dev/null +++ b/src/utilties/breadbot/breadthread.ts @@ -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 { + 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] + ) + } + } + }) +} \ No newline at end of file diff --git a/src/utilties/discord/channels.ts b/src/utilties/discord/channels.ts index af6b482..d543cc1 100644 --- a/src/utilties/discord/channels.ts +++ b/src/utilties/discord/channels.ts @@ -44,6 +44,7 @@ export async function insertChannel(db: SQLCommon, channel: GuildBasedChannel | return SQLResult.CREATED } catch (err) { //TODO Winston should handle this + console.log("CHANNEL INSERT ERROR") console.log(err) return SQLResult.FAILED } diff --git a/src/utilties/discord/messages.ts b/src/utilties/discord/messages.ts index ddcf827..c52f683 100644 --- a/src/utilties/discord/messages.ts +++ b/src/utilties/discord/messages.ts @@ -39,6 +39,7 @@ export async function insertMessage(db: SQLCommon, message: OmitPartialGroupDMCh return SQLResult.CREATED } catch (err) { //TODO Winston should handle this + console.log("MESSAGE INSERTION ERROR") console.log(err) return SQLResult.FAILED } @@ -52,16 +53,22 @@ export async function updateMessageContentHistory(db: SQLCommon, message: OmitPa } 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 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 = ?;", - [message.id, message.editedTimestamp, message.content, message.id] + [message.editedTimestamp ?? message.createdTimestamp, message.content, message.id] ) return SQLResult.UPDATED } catch (err) { //TODO Winston should handle this + console.log("MESSAGE MODIFY FAILED") console.log(err) return SQLResult.FAILED } diff --git a/src/utilties/discord/roles.ts b/src/utilties/discord/roles.ts new file mode 100644 index 0000000..4b1eaf0 --- /dev/null +++ b/src/utilties/discord/roles.ts @@ -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 { + 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 { + 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 { + 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 { + 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 + } +} \ No newline at end of file diff --git a/src/utilties/discord/users.ts b/src/utilties/discord/users.ts index c8a0694..b05f7e5 100644 --- a/src/utilties/discord/users.ts +++ b/src/utilties/discord/users.ts @@ -27,6 +27,7 @@ export async function insertUser(db: SQLCommon, user: User): Promise return SQLResult.CREATED } catch (err) { //TODO Winston should handle this + console.log("USER INSERT ERROR") console.log(err) return SQLResult.FAILED } diff --git a/src/utilties/events/index.ts b/src/utilties/events/index.ts new file mode 100644 index 0000000..cae1d7d --- /dev/null +++ b/src/utilties/events/index.ts @@ -0,0 +1,7 @@ +import * as roles from "./roles" +import * as messages from "./messages" + +export const events = { + roles, + messages +} \ No newline at end of file diff --git a/src/utilties/events/messages.ts b/src/utilties/events/messages.ts index c190519..bfbb3e0 100644 --- a/src/utilties/events/messages.ts +++ b/src/utilties/events/messages.ts @@ -25,7 +25,7 @@ async function processMessageCreate(db: SQLCommon, message: OmitPartialGroupDMCh 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) { @@ -35,6 +35,7 @@ async function processMessageCreate(db: SQLCommon, message: OmitPartialGroupDMCh await Promise.all(allAttachments).catch((error) => { // TODO Winston should handle this + console.log("MESSAGE ATTACHMENT INSERT ERROR") console.log(error) }) } diff --git a/src/utilties/events/roles.ts b/src/utilties/events/roles.ts new file mode 100644 index 0000000..283bb24 --- /dev/null +++ b/src/utilties/events/roles.ts @@ -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) + }) +} \ No newline at end of file diff --git a/src/utilties/index.ts b/src/utilties/index.ts index 3c15413..67a9f5b 100644 --- a/src/utilties/index.ts +++ b/src/utilties/index.ts @@ -4,11 +4,9 @@ import * as tables from "./storage/tables" import * as guilds from "./discord/guilds" import * as channels from "./discord/channels" import * as users from "./discord/users" -import * as messages from "./events/messages" - -const events = { - messages -} +import * as breadthread from "./breadbot/breadthread" +import * as roles from "./discord/roles" +import { events } from "./events" export const utilities = { commands, @@ -17,5 +15,7 @@ export const utilities = { guilds, channels, users, - events + events, + breadthread, + roles } \ No newline at end of file diff --git a/src/utilties/storage/sqlite.ts b/src/utilties/storage/sqlite.ts index 3f7a3e6..fec7541 100644 --- a/src/utilties/storage/sqlite.ts +++ b/src/utilties/storage/sqlite.ts @@ -15,7 +15,7 @@ export class SqliteDB implements SQLCommon { if (err) { // TODO Winston should handle this console.log(err) - throw err + reject(err) } else { resolve(this.changes) } @@ -29,7 +29,7 @@ export class SqliteDB implements SQLCommon { if (err) { // TODO Winston should handle this console.log(err) - throw err + reject(err) } else { resolve(this.changes) } @@ -43,9 +43,8 @@ export class SqliteDB implements SQLCommon { if (err) { // TODO Winston should handle this console.log(err) - throw err + reject(err) } else { - console.log("Got rows") resolve(rows) } }) @@ -58,7 +57,7 @@ export class SqliteDB implements SQLCommon { if (err) { // TODO Winston should handle this console.log(err) - throw err + reject(err) } else { resolve(rows) } diff --git a/src/utilties/storage/tables.ts b/src/utilties/storage/tables.ts index 28d53eb..fd7fa77 100644 --- a/src/utilties/storage/tables.ts +++ b/src/utilties/storage/tables.ts @@ -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 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 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[] = [ diff --git a/src/utilties/time/conversions.ts b/src/utilties/time/conversions.ts new file mode 100644 index 0000000..95a6edf --- /dev/null +++ b/src/utilties/time/conversions.ts @@ -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 +} \ No newline at end of file