DB Storage Re-engineering #1

Open
Bradley wants to merge 4 commits from typescript_db_rework into typescript_refactor
36 changed files with 1886 additions and 684 deletions

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
node_modules
dist
breadbot.db
.env
tools/profanity_filter/bin/Words.json
tools/profanity_filter/src/Words.json

1339
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,25 +3,27 @@
"version": "1.0.0",
"main": "breadbot.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch src/breadbot.ts",
"start": "node dist/breadbot.js",
"build": "tsup src/breadbot.ts --minify"
"watch": "tsc -w",
"start": "tsc && node dist/breadbot.js",
"typeorm": "./node_modules/.bin/typeorm"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@discordjs/opus": "^0.9.0",
"@discordjs/voice": "^0.18.0",
"discord.js": "^14.20.0",
"dotenv": "^16.5.0",
"sqlite3": "^5.1.7",
"@discordjs/opus": "^0.9.0",
"@discordjs/voice": "^0.16.0",
"libsodium-wrappers": "^0.7.13",
"node-crc": "1.3.2",
"prism-media": "^2.0.0-alpha.0"
"prism-media": "^2.0.0-alpha.0",
"reflect-metadata": "^0.2.2",
"sqlite3": "^5.1.7",
"typeorm": "^0.3.27"
},
"devDependencies": {
"@types/node": "^24.10.0",
"tsup": "^8.5.0",
"tsx": "^4.20.3",
"typescript": "^5.8.3"

View File

@ -1,8 +1,48 @@
import { CacheType, ChatInputCommandInteraction, Client, Events, GatewayIntentBits, Guild, GuildBasedChannel, Interaction, Role } from "discord.js"
import "reflect-metadata"
import { ChatInputCommandInteraction, Client, Events, GatewayIntentBits, Guild, GuildBasedChannel, Interaction, Role } from "discord.js"
import { config } from "./config"
import { DataSource } from "typeorm"
import { DBServer } from "./utilties/storage/entities/DBServer"
import path from "path"
import { DBChannel } from "./utilties/storage/entities/DBChannel"
import { DBRole } from "./utilties/storage/entities/DBRole"
import { insertGuild } from "./utilties/discord/guilds"
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"
import { utilities } from "./utilties"
import { commands } from "./commands"
import { config } from "./config"
import { SQLCommon } from "./utilties/storage/interfaces"
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")
export const dataSource = new DataSource({
type: "sqlite",
database: "breadbot.db",
entities: [
DBServer,
DBChannel,
DBRole,
DBUser,
DBMessage,
DBMessageContentChanges,
DBMessageAttachments,
DBCall,
DBCallTranscriptions,
DBCallUsers,
DBMessageRegex
],
synchronize: true,
logging: true
})
export const client : Client = new Client({
intents: [
@ -14,58 +54,50 @@ export const client : Client = new Client({
]
})
export let db: SQLCommon
client.once(Events.ClientReady, async () => {
await dataSource.initialize()
if (config.DB_MODE == "sqlite") {
db = new utilities.sqlite.SqliteDB("breadbot_test.db")
db.run("PRAGMA foreign_keys = ON")
utilities.tables.makeTables(db)
//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, () => {
// TODO Winston should handle this
console.log("Breadbot is ready")
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) => {
await utilities.commands.deployCommands(guild.id)
// TODO handle failures?
await utilities.guilds.insertGuild(db, guild)
const server: DBServer | null = await insertGuild(serverRepo, guild)
if (server != null) {
guild.channels.cache.forEach(async (channel: GuildBasedChannel) => {
await utilities.channels.insertChannel(db, channel)
await insertChannel(channelRepo, channel)
})
guild.roles.cache.forEach(async (role: Role) => {
await utilities.roles.insertRole(db, role)
})
await insertRole(roleRepo, role)
})
}
})
client.on(Events.GuildCreate, async (guild : Guild) => {
await utilities.commands.deployCommands(guild.id)
await utilities.guilds.insertGuild(db, guild)
await utilities.guilds.insertGuild(serverRepo, guild)
guild.channels.cache.forEach(async (channel: GuildBasedChannel) => {
await utilities.channels.insertChannel(db, channel)
await utilities.channels.insertChannel(channelRepo, channel)
})
guild.roles.cache.forEach(async (role: Role) => {
await insertRole(roleRepo, role)
})
})
client.on(Events.ChannelCreate, async (channel) => {
console.log("CHANNEL CREATE CALLED")
await utilities.channels.insertChannel(db, channel)
await utilities.channels.insertChannel(channelRepo, channel)
})
client.on(Events.ThreadCreate, async (channel) => {
console.log("THREAD CREATE CALLED")
console.log(channel.toString())
await utilities.channels.insertChannel(db, channel)
await utilities.channels.insertChannel(channelRepo, channel)
})
client.on(Events.InteractionCreate, async (interaction: Interaction) => {
@ -78,8 +110,10 @@ client.on(Events.InteractionCreate, async (interaction: Interaction) => {
}
})
setInterval(async () => {
await utilities.breadthread.breadthreadProcessLocks(db, client)
}, 5000)
setupRoleCapture(client, serverRepo, roleRepo)
setupMessageCapture(client, channelRepo, userRepo, messageRepo, mccRepo, maRepo)
console.log("Breadbot is Ready")
})
client.login(config.DISCORD_TOKEN)

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,60 +0,0 @@
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,5 @@
import * as ping from "./ping";
import * as breadalert from "./breadalert"
import * as breadthread from "./breadthread"
export const commands = {
ping,
breadalert,
breadthread
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,50 +1,37 @@
import { SQLCommon } from "../storage/interfaces";
import { DMChannel, GuildBasedChannel, PartialDMChannel } from "discord.js";
import { SQLResult } from "../storage/enumerations";
import { Repository } from "typeorm";
import { DBChannel } from "../storage/entities/DBChannel";
export async function doesChannelExistByID(db: SQLCommon, channelID: string) : Promise<boolean> {
const queryResult : Object[] = await db.getAllParameterized(
"SELECT * FROM channels WHERE channel_snowflake = ?",
[channelID]
)
return queryResult.length != 0
export async function doesChannelExistByID(db: Repository<DBChannel>, channelID: string) : Promise<boolean> {
return (await db.findOne({"where": {channel_snowflake: channelID}})) != null
}
export async function doesChannelExist(db: SQLCommon, channel : GuildBasedChannel | DMChannel | PartialDMChannel) : Promise<boolean> {
const queryResult : Object[] = await db.getAllParameterized(
"SELECT * FROM channels WHERE channel_snowflake = ?",
[channel.id]
)
return queryResult.length != 0
export async function doesChannelExist(db: Repository<DBChannel>, channel : GuildBasedChannel | DMChannel | PartialDMChannel) : Promise<boolean> {
return await doesChannelExistByID(db, channel.id)
}
export async function insertChannel(db: SQLCommon, channel: GuildBasedChannel | DMChannel | PartialDMChannel) : Promise<SQLResult> {
export async function insertChannel(db: Repository<DBChannel>, channel: GuildBasedChannel | DMChannel | PartialDMChannel) : Promise<DBChannel | null> {
const alreadyExists: boolean = await doesChannelExist(db, channel)
if(alreadyExists) {
return SQLResult.ALREADYEXISTS
return await db.findOne({"where": {channel_snowflake: channel.id}})
}
try {
if (channel.isDMBased()) {
await db.runParameterized(
"INSERT INTO channels VALUES (?, ?, ?, ?, ?, ?)",
[channel.id, null, channel.recipient?.username, channel.isThread(), channel.isDMBased(), channel.isVoiceBased()]
)
} else {
await db.runParameterized(
"INSERT INTO channels VALUES (?, ?, ?, ?, ?, ?)",
[channel.id, channel.guild.id, channel.name, channel.isThread(), channel.isDMBased(), channel.isVoiceBased()]
)
}
const newChannel : DBChannel = await db.create({
channel_snowflake: channel.id,
channel_name: channel.isDMBased() ? channel.recipient?.username : channel.name,
is_dm: channel.isDMBased(),
is_thread: channel.isThread(),
is_voice: channel.isVoiceBased(),
server: channel.isDMBased() ? null : {server_snowflake: channel.guild.id}
})
return SQLResult.CREATED
return await db.save(newChannel)
} catch (err) {
//TODO Winston should handle this
console.log("CHANNEL INSERT ERROR")
console.log(err)
return SQLResult.FAILED
return null
}
}

View File

@ -1,33 +1,29 @@
import { Guild } from "discord.js";
import { SQLCommon } from "../storage/interfaces";
import { SQLResult } from "../storage/enumerations";
import { Repository } from "typeorm";
import { DBServer } from "../storage/entities/DBServer";
export async function doesGuildExist(db: SQLCommon, guild : Guild) : Promise<boolean> {
const queryResult : Object[] = await db.getAllParameterized(
"SELECT * FROM servers WHERE server_snowflake = ?",
[guild.id]
)
return queryResult.length != 0
export async function doesGuildExist(db: Repository<DBServer>, guild : Guild) : Promise<boolean> {
return (await db.findOne({"where": {server_snowflake: guild.id}})) != null
}
export async function insertGuild(db: SQLCommon, guild: Guild) : Promise<SQLResult> {
export async function insertGuild(db: Repository<DBServer>, guild: Guild) : Promise<DBServer | null> {
const alreadyExists: boolean = await doesGuildExist(db, guild)
if (alreadyExists) {
return SQLResult.ALREADYEXISTS
return await db.findOne({"where": {server_snowflake: guild.id}})
}
try {
await db.runParameterized(
"INSERT INTO servers VALUES (?, ?, ?)",
[guild.id, guild.name, guild.description]
)
const server: DBServer = await db.create({
server_snowflake: guild.id,
server_name: guild.name,
server_description: guild.description ?? ""
})
return SQLResult.CREATED
return await db.save(server)
} catch (err) {
console.log("Insert Failed")
//TODO Winston should handle this
console.log(err)
return SQLResult.FAILED
return null
}
}

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,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 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
}
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)
}))?.regexes
}

View File

@ -1,75 +1,79 @@
import { Role } from "discord.js";
import { SQLCommon } from "../storage/interfaces";
import { SQLResult } from "../storage/enumerations";
import { Repository } from "typeorm";
import { DBRole } from "../storage/entities/DBRole";
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 doesRoleExist(db: Repository<DBRole>, role : Role) : Promise<boolean> {
return (await db.findOne({"where": {role_snowflake: role.id}})) != null
}
export async function insertRole(db: SQLCommon, role: Role) : Promise<SQLResult> {
export async function insertRole(db: Repository<DBRole>, role: Role) : Promise<DBRole | null> {
const alreadyExists: boolean = await doesRoleExist(db, role)
if(alreadyExists) {
return SQLResult.ALREADYEXISTS
return await db.findOne({"where": {role_snowflake: role.id}})
}
try {
await db.runParameterized(
"INSERT INTO roles VALUES (?, ?, ?, 0)",
[role.id, role.guild.id, role.name]
)
const newRole : DBRole = await db.create({
role_snowflake: role.id,
server: {server_snowflake: role.guild.id},
role_name: role.name,
is_deleted: false
})
return SQLResult.CREATED
return await db.save(newRole)
} catch (err) {
console.log("ROLE INSERT ERROR")
console.log(err)
return SQLResult.FAILED
return null
}
}
export async function updateRole(db: SQLCommon, role: Role): Promise<SQLResult> {
export async function updateRole(db: Repository<DBRole>, role: Role): Promise<DBRole | null> {
const roleExists: boolean = await doesRoleExist(db, role)
if(!roleExists) {
return SQLResult.FAILED
return null
}
try {
await db.runParameterized(
"UPDATE roles SET role_name = ? WHERE role_snowflake = ?",
[role.name, role.id]
)
const toUpdate: DBRole | null = await db.findOne({"where": {role_snowflake: role.id}})
if(toUpdate != null) {
toUpdate.role_name = role.name
return await db.save(toUpdate)
} else {
return null
}
return SQLResult.UPDATED
} catch (err) {
console.log("ROLE UPDATE FAILED")
console.log(err)
return SQLResult.FAILED
return null
}
}
export async function markRoleDeleted(db: SQLCommon, role: Role) : Promise<SQLResult> {
export async function markRoleDeleted(db: Repository<DBRole>, role: Role) : Promise<DBRole | null> {
const roleExists: boolean = await doesRoleExist(db, role)
if(!roleExists) {
return SQLResult.FAILED
return null
}
try {
await db.runParameterized(
"UPDATE roles SET is_deleted = 1 WHERE role_snowflake = ?",
[role.id]
)
const toUpdate: DBRole | null = await db.findOne({"where": {role_snowflake: role.id}})
return SQLResult.UPDATED
if(toUpdate != null) {
toUpdate.is_deleted = true
return await db.save(toUpdate)
} else {
return null
}
} catch (err) {
console.log("ROLE DELETE FAILED")
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

@ -0,0 +1,21 @@
import { VoiceBasedChannel } from "discord.js";
import { IsNull, Repository } from "typeorm";
import { DBCall } from "../storage/entities/DBCall";
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: 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,63 +1,57 @@
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

@ -1,23 +1,24 @@
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";
import { Repository } from "typeorm";
import { DBServer } from "../storage/entities/DBServer";
import { DBRole } from "../storage/entities/DBRole";
export function setupRoleCapture(client: Client, db: SQLCommon) {
export function setupRoleCapture(client: Client, guildDB: Repository<DBServer>, roleDB: Repository<DBRole>) {
client.on(Events.GuildRoleCreate, async (role) => {
const serverOk: SQLResult = await insertGuild(db, role.guild)
const serverOk: DBServer | null = await insertGuild(guildDB, role.guild)
if (serverOk == SQLResult.ALREADYEXISTS || serverOk == SQLResult.CREATED) {
await insertRole(db, role)
if (serverOk != null) {
await insertRole(roleDB, role)
}
})
client.on(Events.GuildRoleUpdate, async (role) => {
await updateRole(db, role)
await updateRole(roleDB, role)
})
client.on(Events.GuildRoleDelete, async (role) => {
await markRoleDeleted(db, role)
await markRoleDeleted(roleDB, role)
})
}

View File

@ -0,0 +1,20 @@
import { Client, Events, VoiceState } from "discord.js"
import { Repository } from "typeorm"
import { DBCall } from "../storage/entities/DBCall"
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?
if (newState.member?.id == client.user?.id) {
return
}
}
})
}
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

@ -0,0 +1,34 @@
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { DBChannel } from "./DBChannel";
import { DBCallTranscriptions } from "./DBCallTranscriptions";
import { DBCallUsers } from "./DBCallUsers";
@Entity()
export class DBCall {
@PrimaryGeneratedColumn()
call_id: number
@ManyToOne(() => DBChannel, (channel: DBChannel) => channel.calls)
channel: DBChannel
@Column({type: "datetime"})
call_start_time: Date
@Column({type: "datetime", nullable: true, default: null})
call_end_time: Date | null
@Column({default: false})
call_consolidated: boolean
@Column({default: false})
call_transcribed: boolean
@Column({default: false})
call_data_cleaned_up: boolean
@OneToMany(() => DBCallTranscriptions, (transcription: DBCallTranscriptions) => transcription.call, {nullable: true})
transcriptions: DBCallTranscriptions[] | null
@OneToMany(() => DBCallUsers, (callUser: DBCallUsers) => callUser.call, {nullable: true})
participants: DBCallUsers | null
}

View File

@ -0,0 +1,21 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { DBCall } from "./DBCall";
import { DBUser } from "./DBUser";
@Entity()
export class DBCallTranscriptions {
@PrimaryGeneratedColumn()
transcription_id: number
@ManyToOne(() => DBCall, (call: DBCall) => call.transcriptions)
call: DBCall
@ManyToOne(() => DBUser, (user: DBUser) => user.transcriptions)
user: DBUser
@Column({type: "datetime"})
speaking_start_time: Date
@Column()
text: string
}

View File

@ -0,0 +1,21 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { DBCall } from "./DBCall";
import { DBUser } from "./DBUser";
@Entity()
export class DBCallUsers {
@PrimaryGeneratedColumn()
call_users_id: number
@ManyToOne(() => DBCall, (call: DBCall) => call.participants)
call: DBCall
@ManyToOne(() => DBUser, (user: DBUser) => user.call_history)
user: DBUser
@Column({type: "datetime"})
call_join_time: Date
@Column({type: "datetime", nullable: true, default: null})
call_leave_time: Date | null
}

View File

@ -0,0 +1,31 @@
import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm";
import { DBServer } from "./DBServer";
import { DBMessage } from "./DBMessage";
import { DBCall } from "./DBCall";
@Entity()
export class DBChannel {
@PrimaryColumn({type: "bigint"})
channel_snowflake: string
@ManyToOne(() => DBServer, (server: DBServer) => server.channels, {nullable: true})
server: DBServer | null
@Column()
channel_name: string
@Column()
is_thread: boolean
@Column()
is_dm: boolean
@Column()
is_voice: boolean
@OneToMany(() => DBMessage, (message: DBMessage) => message.channel)
messages: DBMessage[] | null
@OneToMany(() => DBCall, (call: DBCall) => call.channel)
calls: DBCall[] | null
}

View File

@ -0,0 +1,36 @@
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 {
@PrimaryColumn({type: "bigint"})
message_snowflake: string
@ManyToOne(() => DBChannel, (channel: DBChannel) => channel.messages)
channel: DBChannel
@ManyToOne(() => DBUser, (user: DBUser) => user.messages)
user: DBUser
@Column({type: "longtext"})
message_content: string
@Column({type: "datetime"})
message_timestamp: Date
@Column({default: false})
message_deleted: boolean
@OneToMany(() => DBMessageContentChanges, (mcc: DBMessageContentChanges) => mcc.message, {nullable: true})
changes: DBMessageContentChanges[] | null
@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,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

@ -0,0 +1,17 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { DBMessage } from "./DBMessage";
@Entity()
export class DBMessageContentChanges {
@PrimaryGeneratedColumn()
message_change_id: number
@ManyToOne(() => DBMessage, (message: DBMessage) => message.changes)
message: DBMessage
@Column({type: "datetime"})
message_change_old_timestamp: Date
@Column({type: "longtext"})
message_change_old_content: string
}

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

@ -0,0 +1,17 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { DBServer } from "./DBServer";
@Entity()
export class DBRole {
@PrimaryColumn({type: "bigint"})
role_snowflake: string
@ManyToOne(() => DBServer, (server: DBServer) => server.roles)
server: DBServer
@Column()
role_name: string
@Column()
is_deleted: boolean
}

View File

@ -0,0 +1,25 @@
import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
import { DBChannel } from "./DBChannel";
import { DBRole } from "./DBRole";
import { DBMessageRegex } from "./DBMessageRegex";
@Entity()
export class DBServer {
@PrimaryColumn({type: "bigint"})
server_snowflake: string
@Column()
server_name: string
@Column()
server_description: string
@OneToMany(() => DBChannel, (channel: DBChannel) => channel.server)
channels: DBChannel[]
@OneToMany(() => DBRole, (role: DBRole) => role.server)
roles: DBRole[]
@OneToMany(() => DBMessageRegex, (regex: DBMessageRegex) => regex.server, {nullable: true})
regexes: DBMessageRegex[] | null
}

View File

@ -0,0 +1,25 @@
import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
import { DBMessage } from "./DBMessage";
import { DBCallTranscriptions } from "./DBCallTranscriptions";
import { DBCallUsers } from "./DBCallUsers";
@Entity()
export class DBUser {
@PrimaryColumn({type: "bigint"})
user_snowflake: string
@Column()
user_name: string
@Column()
user_displayname: string
@OneToMany(() => DBMessage, (message: DBMessage) => message.user)
messages: DBMessage[]
@OneToMany(() => DBCallTranscriptions, (transcription: DBCallTranscriptions) => transcription.user, {nullable: true})
transcriptions: DBCallTranscriptions[] | null
@OneToMany(() => DBCallUsers, (call_user: DBCallUsers) => call_user.user)
call_history: DBCallUsers[] | 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,32 +0,0 @@
import { SQLCommon } from "./interfaces";
const tables: string[] = [
"CREATE TABLE IF NOT EXISTS servers (server_snowflake bigint NOT NULL PRIMARY KEY,server_name text NOT NULL,server_description mediumtext);",
"CREATE TABLE IF NOT EXISTS channels (channel_snowflake bigint NOT NULL PRIMARY KEY,server_snowflake bigint,channel_name text,is_thread bit NOT NULL,is_dm bit NOT NULL,is_voice bit NOT NULL);",
"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 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);",
"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)))
}

View File

@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["es5", "es6"],
"target": "es6",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
@ -10,6 +11,10 @@
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictNullChecks": true,
"skipLibCheck": true
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false,
"sourceMap": true
}
}