Finalizing the work for moving to TypeORM before testing of previously working functions begins

This commit is contained in:
Bradley Bickford 2025-11-18 16:31:02 -05:00
parent 68d8415a77
commit 7d8e252b79
16 changed files with 82 additions and 261 deletions

View File

@ -20,6 +20,7 @@ import { commands } from "./commands"
import { DBCall } from "./utilties/storage/entities/DBCall"
import { DBCallTranscriptions } from "./utilties/storage/entities/DBCallTranscriptions"
import { DBCallUsers } from "./utilties/storage/entities/DBCallUsers"
import { DBMessageRegex } from "./utilties/storage/entities/DBMessageRegex"
console.log(__dirname + path.sep + "utilities" + path.sep + "storage" + path.sep + "entities" + path.sep + "*.ts")
@ -36,7 +37,8 @@ export const dataSource = new DataSource({
DBMessageAttachments,
DBCall,
DBCallTranscriptions,
DBCallUsers
DBCallUsers,
DBMessageRegex
],
synchronize: true,
logging: true

View File

@ -1,55 +0,0 @@
import { CommandInteraction, SlashCommandBuilder } from "discord.js";
export const enabled: boolean = true
export const data = new SlashCommandBuilder()
.setName("breadalert")
.setDescription("Controls event alerting using the Bread Alert subsystem")
.addSubcommand((subcommand) =>
subcommand
.setName("list")
.setDescription("List the current Bread Alert active alerts")
.addIntegerOption(option =>
option
.setName("count")
.setDescription("The number of future alerts to return, default 5")
.setRequired(false)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("add")
.setDescription("Add a new Bread Alert")
.addStringOption(option =>
option
.setName("name")
.setDescription("The name of the event, must be unique")
.setRequired(true)
)
.addStringOption(option =>
option
.setName("date")
.setDescription("The date and time of the event in YYYY-MM-DD HH:MM:SS format")
.setRequired(true)
)
.addStringOption(option =>
option
.setName("notifications")
.setDescription("A comma separated list of time offsets that determine when to alert prior to the event")
.setRequired(false)
)
)
.addSubcommand(subcommand =>
subcommand
.setName("delete")
.setDescription("Delete a Bread Alert")
.addStringOption(option =>
option
.setName("name")
.setDescription("The name of the event to remove")
)
)
export async function execute(interaction: CommandInteraction) {
return interaction.reply("NOT IMPLEMENTED!")
}

View File

@ -1,7 +1,5 @@
import * as ping from "./ping";
import * as breadalert from "./breadalert"
export const commands = {
ping,
breadalert
ping
}

View File

@ -1,57 +0,0 @@
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

@ -1,16 +1,18 @@
import { Guild } from "discord.js";
import { SQLCommon } from "../storage/interfaces";
import { DBMessageRegex } from "../storage/entities/DBMessageRegex";
import { Repository } from "typeorm";
import { DBServer } from "../storage/entities/DBServer";
export async function getRegexesForGuild(db: SQLCommon, guild: Guild): Promise<any[]> {
return db.getAllParameterized(
"SELECT * FROM message_regexes WHERE server_snowflake = ?",
[guild.id]
)
}
export async function getRoleExclusionSnowflakesForGuild(db: SQLCommon, guild: Guild): Promise<string[]> {
return (await db.getAllParameterized(
"SELECT role_snowflake FROM message_regex_no_role_check WHERE server_snowflake = ?",
[guild.id]
)).map((o) => (o as any).role_snowflake)
export async function getRegexesForGuild(db: Repository<DBServer>, guild: Guild): Promise<DBMessageRegex[] | null | undefined> {
return (await db.findOne({
select: {
regexes: true
},
relations: {
regexes: true
},
where: {
server_snowflake: guild.id
}
}))?.regexes
}

View File

@ -1,20 +1,21 @@
import { Guild, VoiceBasedChannel } from "discord.js";
import { SQLCommon } from "../storage/interfaces";
import { VoiceBasedChannel } from "discord.js";
import { IsNull, Repository } from "typeorm";
import { DBCall } from "../storage/entities/DBCall";
export async function breadbotInCall(db: SQLCommon, channel: VoiceBasedChannel) : Promise<boolean> {
const queryResult: Object[] = await db.getAllParameterized(
"SELECT * FROM calls WHERE channel_snowflake = ? AND call_end_time IS NULL",
[channel.id]
)
return queryResult.length != 0
export async function breadbotInCall(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<boolean> {
return (await db.findOne({
where: {
channel: {channel_snowflake: channel.id},
call_end_time: IsNull()
}
})) != null
}
export async function getExistingCallID(db: SQLCommon, channel: VoiceBasedChannel) : Promise<Number> {
const queryResult: any[] = await db.getAllParameterized(
"SELECT * FROM calls WHERE channel_snowflake = ? AND call_end_time IS NULL",
[channel.id]
)
return queryResult.length != 0 ? queryResult[0].call_id : -1
export async function getExistingCallID(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<Number | undefined> {
return (await db.findOne({
where: {
channel: {channel_snowflake: channel.id},
call_end_time: IsNull()
}
}))?.call_id
}

View File

@ -1,7 +1,8 @@
import { Client, Events, VoiceState } from "discord.js"
import { SQLCommon } from "../storage/interfaces";
import { Repository } from "typeorm"
import { DBCall } from "../storage/entities/DBCall"
export function setupVoice(client: Client, db: SQLCommon) {
export function setupVoice(client: Client, db: Repository<DBCall>) {
client.on(Events.VoiceStateUpdate, (oldState: VoiceState, newState: VoiceState) => {
if(oldState.channel == null && newState.channel != null) {
// TODO Null Type Safety Risk?
@ -14,6 +15,6 @@ export function setupVoice(client: Client, db: SQLCommon) {
})
}
async function processCallJoin(db: SQLCommon, voiceState: VoiceState) {
async function processCallJoin(db: Repository<DBCall>, voiceState: VoiceState) {
}

View File

@ -1,21 +1,15 @@
import * as commands from "./discord/commands"
import * as sqlite from "./storage/sqlite"
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 breadthread from "./breadbot/breadthread"
import * as roles from "./discord/roles"
import { events } from "./events"
export const utilities = {
commands,
sqlite,
tables,
guilds,
channels,
users,
events,
breadthread,
roles
}

View File

@ -1,8 +1,9 @@
import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm";
import { Column, Entity, ManyToOne, OneToMany, OneToOne, PrimaryColumn } from "typeorm";
import { DBChannel } from "./DBChannel";
import { DBUser } from "./DBUser";
import { DBMessageContentChanges } from "./DBMessageContentChanges";
import { DBMessageAttachments } from "./DBMessageAttachment";
import { DBMessageRegexMatches } from "./DBMessageRegexMatches";
@Entity()
export class DBMessage {
@ -29,4 +30,7 @@ export class DBMessage {
@OneToMany(() => DBMessageAttachments, (ma: DBMessageAttachments) => ma.attachment_snowflake, {nullable: true})
attachments: DBMessageAttachments[] | null
@OneToOne(() => DBMessageRegexMatches, (mrm: DBMessageRegexMatches) => mrm.message, {nullable: true})
violation_regex: DBMessageRegexMatches | null
}

View File

@ -0,0 +1,18 @@
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { DBServer } from "./DBServer";
import { DBMessageRegexMatches } from "./DBMessageRegexMatches";
@Entity()
export class DBMessageRegex {
@PrimaryGeneratedColumn()
message_regex_id: number
@ManyToOne(() => DBServer, (server: DBServer) => server.regexes)
server: DBServer
@Column()
regex: string
@OneToMany(() => DBMessageRegexMatches, (mrm: DBMessageRegexMatches) => mrm.regex, {nullable: true})
matches: DBMessageRegexMatches[] | null
}

View File

@ -0,0 +1,15 @@
import { Entity, ManyToOne, OneToOne, PrimaryGeneratedColumn } from "typeorm";
import { DBMessage } from "./DBMessage";
import { DBMessageRegex } from "./DBMessageRegex";
@Entity()
export class DBMessageRegexMatches {
@PrimaryGeneratedColumn()
message_regex_match_id: number
@OneToOne(() => DBMessage, (message: DBMessage) => message.violation_regex)
message: DBMessage
@ManyToOne(() => DBMessageRegex, (regex: DBMessageRegex) => regex.matches)
regex: DBMessageRegex
}

View File

@ -1,6 +1,7 @@
import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
import { DBChannel } from "./DBChannel";
import { DBRole } from "./DBRole";
import { DBMessageRegex } from "./DBMessageRegex";
@Entity()
export class DBServer {
@ -18,4 +19,7 @@ export class DBServer {
@OneToMany(() => DBRole, (role: DBRole) => role.server)
roles: DBRole[]
@OneToMany(() => DBMessageRegex, (regex: DBMessageRegex) => regex.server, {nullable: true})
regexes: DBMessageRegex[] | null
}

View File

@ -1,7 +0,0 @@
export enum SQLResult {
CREATED,
UPDATED,
DELETED,
ALREADYEXISTS,
FAILED
}

View File

@ -1,7 +0,0 @@
export interface SQLCommon {
run(query: string) : Promise<number>
runParameterized(query: string, parameters: any[]): Promise<number>
getAll(query: string) : Promise<Object[]>
getAllParameterized(query: string, parameters: any[]) : Promise<Object[]>
}

View File

@ -1,68 +0,0 @@
import * as sqlite3 from 'sqlite3'
import { SQLCommon } from "./interfaces"
export class SqliteDB implements SQLCommon {
private db : sqlite3.Database;
public constructor(private readonly dbName: string) {
this.db = new sqlite3.Database(this.dbName);
}
async run(query: string): Promise<number> {
return new Promise((resolve, reject) => {
this.db.run(query, function (this : sqlite3.RunResult, err: Error) {
if (err) {
// TODO Winston should handle this
console.log(err)
reject(err)
} else {
resolve(this.changes)
}
})
})
}
async runParameterized(query: string, parameters: any[]): Promise<number> {
return new Promise((resolve, reject) => {
this.db.run(query, parameters, function (this : sqlite3.RunResult, err: Error) {
if (err) {
// TODO Winston should handle this
console.log(err)
reject(err)
} else {
resolve(this.changes)
}
})
})
}
async getAll(query: string): Promise<Object[]> {
return new Promise((resolve, reject) => {
this.db.all(query, (err: Error, rows: Object[]) => {
if (err) {
// TODO Winston should handle this
console.log(err)
reject(err)
} else {
resolve(rows)
}
})
})
}
getAllParameterized(query: string, parameters: any[]): Promise<Object[]> {
return new Promise((resolve, reject) => {
this.db.all(query, parameters, (err: Error, rows: Object[]) => {
if (err) {
// TODO Winston should handle this
console.log(err)
reject(err)
} else {
resolve(rows)
}
})
})
}
}

View File

@ -1,24 +0,0 @@
import { SQLCommon } from "./interfaces";
const tables: string[] = [
"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);",
"CREATE TABLE IF NOT EXISTS message_regex_words (message_regex_words_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,message_regexes_id bigint,word text NOT NULL);",
"CREATE TABLE IF NOT EXISTS calls (call_id bigint NOT NULL PRIMARY KEY AUTOINCREMENT, channel_snowflake bigint NOT NULL, call_start_time datetime NOT NULL, call_end_time datetime DEFAULT NULL, call_consolidated INTEGER DEFAULT 0 CHECK(call_consolidated IN (0, 1)), call_transcribed INTEGER DEFAULT 0 CHECK(call_transcribed IN (0, 1)), call_data_cleaned_up INTEGER DEFAULT 0 CHECK(call_data_cleaned_up IN (0, 1)));",
"CREATE TABLE IF NOT EXISTS call_transcriptions (transcription_id bitint NOT NULL PRIMARY KEY AUTOINCREMENT, call_id bigint NOT NULL, user_snowflake bigint NOT NULL, speaking_start_time datetime NOT NULL, text TEXT NOT NULL);",
"CREATE TABLE IF NOT EXISTS call_users (call_users_id bigint NOT NULL PRIMARY KEY AUTOINCREMENT, call_id bigint NOT NULL, user_snowflake bigint NOT NULL, call_join_time datetime NOT NULL, call_leave_time datetime DEFAULT NULL);"
]
const constraints: string[] = [
"ALTER TABLE channels ADD CONSTRAINT channels_server_snowflake_fk FOREIGN KEY (server_snowflake) REFERENCES servers (server_snowflake);"
]
export async function makeTables(db: SQLCommon): Promise<number[]> {
return Promise.all(tables.map((statement) => db.run(statement)))
}
export async function makeConstraints(db: SQLCommon): Promise<number[]> {
return Promise.all(constraints.map((statement) => db.run(statement)))
}