15 Commits

Author SHA1 Message Date
47e1f44e8f Beginning work on BreadAsleep, need to find an intelligent way to get database access to command callbacks 2026-01-01 21:21:54 -05:00
7df2bb6dfc Fixes to get voice working properly, not fully confident it'll work under pressure but it's done well in small tests 2026-01-01 17:00:39 -05:00
81b8085b51 Profanity stuff is working-ish 2025-12-28 18:36:36 -05:00
c4187c1590 Some profanity prevention stuff, breadmixer rewrite 2025-12-27 20:47:34 -05:00
6e93022cb3 A various number of fixes related to ensuring message content is captured, and adding downloading of attachments 2025-12-21 19:12:16 -05:00
4329fe30d7 A more complete re-engineering of the voice stuff 2025-12-13 22:04:41 -05:00
7d8e252b79 Finalizing the work for moving to TypeORM before testing of previously working functions begins 2025-11-18 16:31:02 -05:00
68d8415a77 Working on Voice stuff 2025-11-17 16:16:03 -05:00
69f2206a4b Message component rework 2025-11-17 14:22:59 -05:00
34f57b96dc Large swathes of re-engineering 2025-11-12 20:41:13 -05:00
6f868dff1e Pausing voice efforts to take a look at the database understructure problem... 2025-11-11 11:03:45 -05:00
ba5b14c05a Fixing Noah's project package and removing Words.json from the cache 2025-07-12 21:16:13 -04:00
bce2bf234e broke it all, all destroyed 2025-07-12 20:57:33 -04:00
9041f3a804 Adding regex tool 2025-07-12 19:16:12 -04:00
719ab8a02e Adding regex tool 2025-07-12 14:44:38 -04:00
49 changed files with 2824 additions and 664 deletions

8
.gitignore vendored
View File

@@ -1,2 +1,10 @@
node_modules
dist
media
breadbot.db
.env
tools/profanity_filter/bin/Words.json
tools/profanity_filter/src/Words.json
bin/config.json
bin/Words.json
bin/__pycache__

198
bin/breadbot_common.py Normal file
View File

@@ -0,0 +1,198 @@
import sqlite3
import mysql.connector
import subprocess
import string
import random
from pathlib import Path
from datetime import datetime
class Database():
def __init__(self):
pass
def close(self):
pass
def query(self, query: str, parameters: list=None) -> tuple[int, list[tuple]]:
pass
def select(self, table: str, columns: list[str]=None, where: list[dict]=None, values: list=None) -> list[tuple]:
query_string = "SELECT {columns} FROM {table}{where}".format(
columns = "*" if columns is None else ",".join(columns),
table = table,
where = self.__generate_basic_where_clause(where) if not where is None else ""
)
return self.query(query_string, values)[1]
def insert(self, table: str, columns: list[str], values: list) -> int:
query_string = "INSERT INTO {table} ({columns}) VALUES ({values})".format(
table = table,
columns = ",".join(columns),
values = ("?," * len(values))[:-1]
)
return self.query(query_string, values)[0]
def update(self, table: str, columns: list[str], values: list, where: list[dict]=None) -> int:
query_string = "UPDATE {table} SET {set_rules}{where}".format(
table = table,
set_rules = ",".join([element + "=?" for element in columns]),
where = self.__generate_basic_where_clause(where) if not where is None else ""
)
return self.query(query_string, values)[0]
def delete(self, table: str, values: list, where: list[dict]=None) -> int:
query_string = "DELETE FROM {table}{where}".format(
table = table,
where = self.__generate_basic_where_clause(where) if not where is None else ""
)
return self.query(query_string, values)[0]
# TODO This probably breaks with MySQL, because question mark bad, maybe just have MySQL class override this
def __generate_basic_where_clause(self, where: list[dict]):
return " WHERE {clauses}".format(
clauses = "".join([
element["name"] + " " + element["compare"] + " ?" + (" " + element["boolean_op"] + " " if "boolean_op" in element else "")
for element in where
])
)
class SQLite(Database):
def __init__(self, db_name: str):
super(Database, self).__init__()
self.db = sqlite3.connect(db_name)
self.db.autocommit = True
def close(self):
self.db.close()
def query(self, query: str, parameters: list=None) -> tuple[int, list[tuple]]:
if parameters is None:
cursor = self.db.execute(query)
else:
cursor = self.db.execute(query, parameters)
if query.casefold().startswith("SELECT".casefold()):
rows = list(cursor)
return (len(rows), rows)
elif query.casefold().startswith("INSERT".casefold()):
return (cursor.lastrowid, None)
else:
return (cursor.rowcount, None)
class MySQL(Database):
def __init__(self, host: str, user: str, password: str, db_name: str):
super(Database, self).__init__()
self.db = mysql.connector.connect(
host = host,
user = user,
password = password,
database = db_name
)
self.db.autocommit = True
def close(self):
self.db.close()
def query(self, query: str, parameters: list=None) -> tuple[int, list[tuple]]:
with self.db.cursor() as cursor:
if parameters is None:
cursor.execute(query)
else:
cursor.execute(query, parameters)
if query.casefold().startswith("SELECT".casefold()):
rows = cursor.fetchall()
return (len(rows), rows)
elif query.casefold().startswith("INSERT".casefold()):
return (cursor.lastrowid, None)
else:
return (cursor.rowcount, None)
# Data class (effective struct) because complex dictionary access is uggo.
class TranscriptableFile():
def __init__(self, file_path: str, real_date: datetime, milliseconds_from_start: int, user_snowflake: str=None):
self.file_path = file_path
self.real_date = real_date
self.milliseconds_from_start = milliseconds_from_start
self.user_snowflake = user_snowflake
def mix_audio_with_ffmpeg(files: list[TranscriptableFile], media_folder_path: str, call_id: int, is_final_pass: bool) -> TranscriptableFile:
filter_list = [
"[{input_id}]adelay={delay}|{delay}[a{input_id}]".format(
input_id = index,
delay = files[index].milliseconds_from_start
)
for index in range(len(files))
]
command_list = ["ffmpeg"]
for file in files:
command_list.append("-i")
command_list.append(file.file_path)
command_list.append("-filter_complex")
filter_string = "\"" + ";".join(filter_list) + ";"
filter_string = filter_string + "".join([
"[a{input_id}]".format(
input_id = index
)
for index in range(len(files))
])
filter_string = filter_string + "amix=inputs={input_count}:normalize=0[a]".format(
input_count = len(files)
)
if is_final_pass:
filter_string = filter_string + ";[a]volume=3[boosted]\""
else:
filter_string = filter_string + "\""
command_list.append(filter_string)
command_list.append("-map")
if is_final_pass:
command_list.append("\"[boosted]\"")
else:
command_list.append("\"[a]\"")
output_file_name = Path(
media_folder_path,
str(call_id),
"output.mp3" if is_final_pass else "intermediate-" + "".join(random.choices(string.ascii_uppercase + string.digits, k=10)) + ".mp3"
)
command_list.append(str(output_file_name))
# TODO shell = True isn't great, I don't remember the reason why it has to be this way
# I *think* it had something to do with me not using ffmpeg's absolute path
ffmpeg_process = subprocess.Popen(
' '.join(command_list),
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
shell = True
)
stdout, stderr = ffmpeg_process.communicate()
if ffmpeg_process.returncode != 0:
print("An FFMPEG process failed")
print(stdout)
print(stderr)
raise Exception("An FFMPEG process broke spectacularly")
return TranscriptableFile(output_file_name, files[0].real_date, files[0].milliseconds_from_start)

123
bin/breadmixer.py Normal file
View File

@@ -0,0 +1,123 @@
import json
import os
import copy
from pathlib import Path
from datetime import datetime, timezone
from breadbot_common import SQLite, MySQL, TranscriptableFile, mix_audio_with_ffmpeg
from txtai.pipeline import Transcription
MAX_FILES_PER_CYCLE=50
script_path = Path(__file__).resolve()
config_path = Path(script_path.parent, "config.json")
with open(config_path, 'r') as config_file:
config_json = json.loads(config_file.read())
if config_json["db"]["type"].casefold() == "SQLITE".casefold():
db = SQLite(Path(script_path.parent.parent, config_json["db"]["db_path"]))
else:
db = MySQL(
config_json["db"]["host"],
config_json["db"]["user"],
config_json["db"]["password"],
config_json["db"]["db_name"]
)
calls_needing_work = db.query(
"SELECT * FROM db_call WHERE NOT call_end_time IS NULL AND call_consolidated = 0 AND call_transcribed = 0"
)
if calls_needing_work[0] == 0:
print("No work to do, exiting")
transcriber = Transcription("openai/whisper-base")
for call in calls_needing_work[1]:
all_files = os.listdir(Path(
config_json["media_voice_folder"],
str(call[0])
))
transcriptable_files = []
for file in all_files:
print(file)
file_name_no_extension = file.split('.')[0]
timestamp = int(file_name_no_extension.split('-')[0])
user_snowflake = file_name_no_extension.split('-')[1]
file_stamp_as_datetime = datetime.fromtimestamp(timestamp / 1000, timezone.utc)
print(file_stamp_as_datetime)
print(type(call[1]))
print(call[1])
time_diff = file_stamp_as_datetime - datetime.fromisoformat(call[1] + 'Z')
print(time_diff)
transcriptable_files.append(TranscriptableFile(
file_path = str(Path(config_json["media_voice_folder"], str(call[0]), file)),
real_date = file_stamp_as_datetime,
milliseconds_from_start = int((time_diff.seconds * 1000) + (time_diff.microseconds / 1000)),
user_snowflake = user_snowflake
))
transcriptable_files.sort(key=lambda a: a.milliseconds_from_start)
# TODO Possibly RAM abusive solution to wanting to keep the original list around
ffmpeg_files = copy.deepcopy(transcriptable_files)
for file in ffmpeg_files:
print(file.file_path)
print(file.real_date)
print(file.milliseconds_from_start)
# TODO Error handling for all ffmpeg operations
while len(ffmpeg_files) > MAX_FILES_PER_CYCLE:
ffmpeg_files = [
mix_audio_with_ffmpeg(
ffmpeg_files[index:min(index + MAX_FILES_PER_CYCLE, len(ffmpeg_files))],
config_json["media_voice_folder"],
call[0],
False
)
for index in range(0, len(ffmpeg_files), MAX_FILES_PER_CYCLE)
]
final_pass_file = mix_audio_with_ffmpeg(
ffmpeg_files,
config_json["media_voice_folder"],
call[0],
True
)
db.update("db_call", ["call_consolidated"], [1, call[0]], [{
"name": "call_id",
"compare": "="
}])
for file in os.listdir(Path(config_json["media_voice_folder"], str(call[0]))):
if file.startswith("intermediate"):
os.remove(Path(config_json["media_voice_folder"], str(call[0]), file))
for file in transcriptable_files:
text = transcriber(file.file_path)
db.insert(
"db_call_transcriptions",
["speaking_start_time", "text", "callCallId", "userUserSnowflake"],
[file.real_date, text, call[0], file.user_snowflake]
)
db.update("db_call", ["call_transcribed"], [1, call[0]], [{
"name": "call_id",
"compare": "="
}])

View File

@@ -0,0 +1,44 @@
# The hidden file filed with profanity and first version of this program brought to you
# by Noah Lacorazza, rewritten from Java to Python by Brad
import json
from pathlib import Path
from breadbot_common import SQLite, MySQL
script_path = Path(__file__).resolve()
config_path = Path(script_path.parent, "config.json")
words_path = Path(script_path.parent, "Words.json")
with open(config_path, 'r') as config_file:
config_json = json.loads(config_file.read())
with open(words_path, 'r') as words_file:
words_list = json.loads(words_file.read())
if config_json["db"]["type"].casefold() == "SQLITE".casefold():
db = SQLite(Path(script_path.parent.parent, config_json["db"]["db_path"]))
else:
db = MySQL(
config_json["db"]["host"],
config_json["db"]["user"],
config_json["db"]["password"],
config_json["db"]["db_name"]
)
print(db.select("db_server", ["server_snowflake"]))
for element in db.select("db_server", ["server_snowflake"]):
for word in words_list:
regex_string = "(^|\\W|\\b)"
for i in range(len(word)):
if word[i] in config_json["profanity"]["replacers"].keys():
regex_string = regex_string + config_json["profanity"]["replacers"][word[i]] + "{1,}"
else:
regex_string = regex_string + word[i] + "{1,}"
regex_string = regex_string + "($|\\W|\\b)"
db.insert("db_message_regex", ["regex", "word", "serverServerSnowflake"], [regex_string, word, element[0]])
db.close()

1339
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,20 +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"
"libsodium-wrappers": "^0.7.13",
"node-crc": "1.3.2",
"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,84 +1,131 @@
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"
import { DBMessageRegexMatches } from "./utilties/storage/entities/DBMessageRegexMatches"
import { setupVoice } from "./utilties/events/voice"
import { DBBreadAsleep } from "./utilties/storage/entities/DBBreadAsleep"
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,
DBMessageRegexMatches,
DBBreadAsleep
],
synchronize: true,
logging: false
})
export const client : Client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.MessageContent
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates
]
})
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)
const regexesRepo = dataSource.getRepository(DBMessageRegex)
const matchesRepo = dataSource.getRepository(DBMessageRegexMatches)
const callRepo = dataSource.getRepository(DBCall)
const callUserRepo = dataSource.getRepository(DBCallUsers)
const breadAsleepRepo = dataSource.getRepository(DBBreadAsleep)
client.guilds.cache.forEach(async (guild: Guild) => {
await utilities.commands.deployCommands(guild.id)
const server: DBServer | null = await insertGuild(serverRepo, guild)
// TODO handle failures?
await utilities.guilds.insertGuild(db, guild)
if (server != null) {
guild.channels.cache.forEach(async (channel: GuildBasedChannel) => {
await insertChannel(channelRepo, channel)
})
guild.roles.cache.forEach(async (role: Role) => {
await insertRole(roleRepo, role)
})
}
})
client.on(Events.GuildCreate, async (guild : Guild) => {
await utilities.commands.deployCommands(guild.id)
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 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)
guild.channels.cache.forEach(async (channel: GuildBasedChannel) => {
await utilities.channels.insertChannel(db, channel)
client.on(Events.ChannelCreate, async (channel) => {
await utilities.channels.insertChannel(channelRepo, channel)
})
client.on(Events.ThreadCreate, async (channel) => {
await utilities.channels.insertChannel(channelRepo, 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 as ChatInputCommandInteraction)
}
})
setupRoleCapture(client, serverRepo, roleRepo)
setupMessageCapture(client, serverRepo, channelRepo, userRepo, messageRepo, mccRepo, maRepo, regexesRepo, matchesRepo)
setupVoice(client, callRepo, channelRepo, userRepo, callUserRepo)
console.log("Breadbot is Ready")
})
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 as ChatInputCommandInteraction)
}
})
setInterval(async () => {
await utilities.breadthread.breadthreadProcessLocks(db, client)
}, 5000)
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

@@ -0,0 +1,55 @@
import { CommandInteraction, SlashCommandBuilder, SlashCommandSubcommandBuilder } from "discord.js";
module.exports = {
enabled: true,
data: new SlashCommandBuilder()
.setName('breadasleep')
.setDescription("Set, list, or remove Bread Asleep notification configurations")
.addSubcommand((subcommand) =>
subcommand
.setName("list")
.setDescription("Lists any existing Bread Asleep configurations")
)
.addSubcommand((subcommand) =>
subcommand
.setName("set")
.setDescription("Sets the Bread Asleep configuration for a given role")
.addRoleOption((option) =>
option
.setName("role")
.setDescription("The role to apply the configuration to")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("starttime")
.setDescription("The time when Bread Asleep warnings will start in 24 hour HH:MM:SS format")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("endtime")
.setDescription("The time when Bread Asleep warnings will end in 24 hour HH:MM:SS format")
.setRequired(true)
)
.addIntegerOption((option) =>
option
.setName("timeoutduration")
.setDescription("The amount of time Bread Asleep will wait between sending warnings in minutes, default 5 minutes")
)
)
.addSubcommand((subcommand) =>
subcommand
.setName("remove")
.setDescription("Removes the Bread Asleep configuration for a given role")
.addRoleOption((option) =>
option
.setName("role")
.setDescription("The role to remove the Bread Asleep configuration from")
)
),
async execute(interaction: CommandInteraction) {
await 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

@@ -2,14 +2,16 @@ import dotenv from "dotenv"
dotenv.config()
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DB_MODE } = process.env
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DB_MODE, MEDIA_VOICE_FOLDER, MEDIA_ATTACHMENT_FOLDER } = process.env
if (!DISCORD_TOKEN || !DISCORD_CLIENT_ID || !DB_MODE) {
if (!DISCORD_TOKEN || !DISCORD_CLIENT_ID || !DB_MODE || !MEDIA_VOICE_FOLDER || !MEDIA_ATTACHMENT_FOLDER) {
throw new Error("Missing environment variables")
}
export const config = {
DISCORD_TOKEN,
DISCORD_CLIENT_ID,
DB_MODE
DB_MODE,
MEDIA_VOICE_FOLDER,
MEDIA_ATTACHMENT_FOLDER
}

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,51 +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()]
)
} else {
await db.runParameterized(
"INSERT INTO channels VALUES (?, ?, ?, ?, ?)",
[channel.id, channel.guild.id, channel.name, channel.isThread(), channel.isDMBased()]
)
}
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,103 @@
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";
import { DBMessageRegex } from "../storage/entities/DBMessageRegex";
// 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
console.log(dbMessage)
// 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

@@ -0,0 +1,80 @@
import { Client, Guild, Message, OmitPartialGroupDMChannel, PartialMessage, TextChannel } from "discord.js";
import { DBMessageRegex } from "../storage/entities/DBMessageRegex";
import { Repository } from "typeorm";
import { DBServer } from "../storage/entities/DBServer";
import { DBMessageRegexMatches } from "../storage/entities/DBMessageRegexMatches";
import { DBMessage } from "../storage/entities/DBMessage";
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
}
export async function checkMatchingRegexes(regexes: DBMessageRegex[], testString: string) : Promise<DBMessageRegex[] | null> {
let matchedRegexes: DBMessageRegex[] = []
regexes.forEach((regex) => {
console.log(regex.regex)
const regexObj = new RegExp(regex.regex, 'gmi')
if(regexObj.test(testString)) {
matchedRegexes.push(regex)
}
})
if(matchedRegexes.length != 0) {
return matchedRegexes
} else {
return null
}
}
export async function insertAnyRegexMatches(regexes: DBMessageRegex[], db: Repository<DBMessageRegexMatches>, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) {
let matches : DBMessageRegexMatches[] = []
regexes.forEach(async (regex) => {
matches.push(db.create({
message: {
message_snowflake: message.id
},
regex: regex
}))
})
await db.save(matches)
}
export async function checkYourProfanity(client: Client, serverDB: Repository<DBServer>, matchDB: Repository<DBMessageRegexMatches>, regexDB: Repository<DBMessageRegex>, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) {
if(message.guild != null && message.content != null) {
const regexes : DBMessageRegex[] | null | undefined = await getRegexesForGuild(serverDB, message.guild)
console.log(regexes?.length)
console.log(message.content)
if(regexes != null && regexes != undefined && regexes.length != 0) {
const matchingRegexes : DBMessageRegex[] | null = await checkMatchingRegexes(regexes, message.content)
matchingRegexes?.forEach((regex) => {
console.log(`${regex.word}`)
})
if(matchingRegexes != null && matchingRegexes.length != 0) {
const channel: TextChannel | null = (await client.channels.fetch(message.channelId)) as TextChannel
if(channel) {
await channel.send(`${message.author} watch your language! Your message has been deleted and this incident has been logged!`)
}
await insertAnyRegexMatches(matchingRegexes, matchDB, message)
await message.delete()
}
}
}
}

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}})
return SQLResult.UPDATED
if(toUpdate != null) {
toUpdate.role_name = role.name
return await db.save(toUpdate)
} else {
return null
}
} 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 { GuildMember, User } from "discord.js";
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 | GuildMember | string): Promise<boolean> {
return (await db.findOne({"where": {user_snowflake: user instanceof User || user instanceof GuildMember ? user.id : user }})) != null
}
export async function insertUser(db: SQLCommon, user: User): Promise<SQLResult> {
const alreadyExists: boolean = await doesUserExist(db, user)
export async function insertUser(db: Repository<DBUser>, user: User | GuildMember | DBUser): Promise<DBUser | null> {
const alreadyExists: boolean = await doesUserExist(db, user instanceof DBUser ? user.user_snowflake : user)
if(alreadyExists) {
return SQLResult.ALREADYEXISTS
return await db.findOne({"where": {user_snowflake: user instanceof DBUser ? user.user_snowflake : user.id }})
}
try {
await db.runParameterized(
"INSERT INTO users VALUES (?, ?, ?)",
[user.id, user.username, user.displayName]
)
const newUser: DBUser = user instanceof DBUser ? user : db.create({
user_snowflake: user.id,
user_name: user instanceof User ? user.username : user.displayName,
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,94 @@
import { VoiceBasedChannel } from "discord.js";
import { IsNull, Repository } from "typeorm";
import { DBCall } from "../storage/entities/DBCall";
import { DBCallUsers } from "../storage/entities/DBCallUsers";
import { DBUser } from "../storage/entities/DBUser";
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
}
export async function returnOrCreateNewCallID(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<number> {
const oldCallID = await getExistingCallID(db, channel)
if(oldCallID !== undefined) {
return oldCallID
} else {
const newCall : DBCall = await db.create({
channel: { channel_snowflake: channel.id },
call_start_time: new Date()
})
return (await db.save(newCall)).call_id;
}
}
export async function setCallEndTime(db: Repository<DBCall>, channel: VoiceBasedChannel) : Promise<DBCall | null> {
const call: DBCall | null = await db.findOne({
"where": {
channel: {
channel_snowflake: channel.id
},
call_end_time: IsNull()
}
})
if (call == null) {
return null
}
call.call_end_time = new Date()
return await db.save(call)
}
export async function numberOfUsersInCall(db: Repository<DBCallUsers>, call: DBCall | number) : Promise<number> {
const activeCallUsers : DBCallUsers[] = await db.find({
"where": {
call: (call instanceof DBCall) ? call : { call_id: call },
call_leave_time: IsNull()
}
})
return activeCallUsers.length
}
export async function registerUserInCall(db: Repository<DBCallUsers>, call: DBCall | number, user: DBUser) : Promise<DBCallUsers> {
return await db.save({
call: (call instanceof DBCall) ? call : {call_id: call},
user: user,
call_join_time: new Date()
})
}
export async function deregisterUserInCall(db: Repository<DBCallUsers>, call: DBCall | number, user: DBUser | string) : Promise<DBCallUsers | null> {
const callUser : DBCallUsers | null = await db.findOne({
where: {
call: (call instanceof DBCall) ? call : {call_id: call},
user: (user instanceof DBUser) ? user : {user_snowflake: user},
call_leave_time: IsNull()
}
})
if(callUser == null) {
return null
}
callUser.call_leave_time = new Date()
return await db.save(callUser)
}

View File

@@ -1,63 +1,112 @@
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";
import { mkdirSync, createWriteStream } from "fs";
import { config } from "../../config";
import path from "path";
import { Readable } from "stream"
import { finished } from "stream/promises";
import { checkYourProfanity } from "../discord/regex_matching";
import { DBServer } from "../storage/entities/DBServer";
import { DBMessageRegex } from "../storage/entities/DBMessageRegex";
import { DBMessageRegexMatches } from "../storage/entities/DBMessageRegexMatches";
export function setupMessageCapture(client: Client, db: SQLCommon) {
export function setupMessageCapture(client: Client,
serverDB: Repository<DBServer>,
channelDB: Repository<DBChannel>,
userDB: Repository<DBUser>,
messageDB: Repository<DBMessage>,
mccDB: Repository<DBMessageContentChanges>,
maDB: Repository<DBMessageAttachments>,
regexesDB: Repository<DBMessageRegex>,
matchesDB: Repository<DBMessageRegexMatches>
) {
client.on(Events.MessageCreate, async (message) => {
processMessageCreate(db, message)
console.log("MESSAGE CREATE")
await processMessageCreate(channelDB, userDB, messageDB, maDB, message)
await checkYourProfanity(client, serverDB, matchesDB, regexesDB, message)
})
client.on(Events.MessageUpdate, async (oldMessage, newMessage) => {
await processMessageModify(db, newMessage)
console.log("MESSAGE EDITED")
console.log(`Old Message ID: ${oldMessage.id}`)
console.log(`New Message ID: ${newMessage.id}`)
await processMessageModify(messageDB, mccDB, maDB, newMessage)
await checkYourProfanity(client, serverDB, matchesDB, regexesDB, 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)
setInterval(async () => {
console.log("STARTING DOWNLOAD CYCLE")
mkdirSync(config.MEDIA_ATTACHMENT_FOLDER, {recursive: true})
if (channelOk == SQLResult.ALREADYEXISTS || channelOk == SQLResult.CREATED ||
userOk == SQLResult.ALREADYEXISTS || userOk == SQLResult.CREATED) {
let attachmentsToProcess: DBMessageAttachments[] | null = await maDB.find({
relations: {
message: true
},
where: {
attachment_downloaded: false
}
})
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)
})
if (attachmentsToProcess != null) {
attachmentsToProcess.forEach(async (attachment: DBMessageAttachments) => {
mkdirSync(config.MEDIA_ATTACHMENT_FOLDER + path.sep + attachment.message.message_snowflake, {recursive: true})
await Promise.all(allAttachments).catch((error) => {
// TODO Winston should handle this
console.log("MESSAGE ATTACHMENT INSERT ERROR")
console.log(error)
const response = await fetch(attachment.attachment_url)
if (response.body !== null) {
const fileStream = createWriteStream(
config.MEDIA_ATTACHMENT_FOLDER + path.sep + attachment.message.message_snowflake +
path.sep + attachment.attachment_snowflake + '_' + attachment.attachment_name,
{ flags: "wx" }
)
await finished(Readable.fromWeb(response.body).pipe(fileStream))
attachment.attachment_downloaded = true
maDB.save(attachment)
}
})
}
}, 30000)
}
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 != null && userOk != null) {
await insertMessage(messageDB, maDB, message)
}
}
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)
})
}
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,126 @@
import { Client, Events, GuildMember, VoiceState } from "discord.js"
import { Repository } from "typeorm"
import { DBCall } from "../storage/entities/DBCall"
import { DBChannel } from "../storage/entities/DBChannel"
import { insertChannel } from "../discord/channels"
import { deregisterUserInCall, getExistingCallID, numberOfUsersInCall, registerUserInCall, returnOrCreateNewCallID, setCallEndTime } from "../discord/voice"
import { createWriteStream, mkdirSync } from 'node:fs'
import { config } from '../../config'
import path from "node:path"
import { EndBehaviorType, entersState, getVoiceConnection, joinVoiceChannel, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice"
import { OggLogicalBitstream, OpusHead } from "prism-media/dist/opus"
import { DBUser } from "../storage/entities/DBUser"
import { insertUser } from "../discord/users"
import { DBCallUsers } from "../storage/entities/DBCallUsers"
export function setupVoice(client: Client, callDB: Repository<DBCall>, channelDB: Repository<DBChannel>, userDB: Repository<DBUser>, callUserDB: Repository<DBCallUsers>) {
client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceState) => {
if(oldState.channel == null && newState.channel != null) {
// TODO Null Type Safety Risk?
if (newState.member?.id == client.user?.id) {
return
}
await insertChannel(channelDB, newState.channel)
let existingCallID : number | undefined = await getExistingCallID(callDB, newState.channel)
console.log(`Call ID Pre Existing Call Check: ${existingCallID}`)
if (existingCallID === undefined) {
existingCallID = await returnOrCreateNewCallID(callDB, newState.channel)
console.log(`Call does not exist new callID value: ${existingCallID}`)
mkdirSync(config.MEDIA_VOICE_FOLDER + path.sep + existingCallID, {recursive: true})
// TODO NULL armor here is probably just going to blow up the call to joinVoiceChannel with no error catching
const connection : VoiceConnection = joinVoiceChannel({
channelId: newState.channelId ?? "",
guildId: newState.guild.id,
selfDeaf: false,
selfMute: true,
adapterCreator: newState.guild.voiceAdapterCreator
})
try {
await entersState(connection, VoiceConnectionStatus.Ready, 20e3)
const receiver = connection.receiver
if (receiver.speaking.listenerCount("start") == 0) {
receiver.speaking.on("start", (userID: string) => {
if(!receiver.subscriptions.has(userID)) {
receiver.subscribe(userID, {
end: {
behavior: EndBehaviorType.AfterSilence,
duration: 500
}
})
.pipe(new OggLogicalBitstream({
opusHead: new OpusHead({
channelCount: 2,
sampleRate: 48000
}),
pageSizeControl: {
maxPackets: 10
}
}))
.pipe(createWriteStream(
config.MEDIA_VOICE_FOLDER + path.sep +
existingCallID + path.sep +
`${Date.now()}-${userID}.ogg`
))
} else {
console.log(`Attempted to create new user subscriber for ${userID} even though one already exist, receiver arm if statement protected against this`)
}
})
receiver.speaking.on("end", (userID: string) => {
console.log(`User ${userID} stopped speaking`)
})
}
} catch (error) {
//TODO Winston
console.log(error)
}
}
const member : GuildMember | null = newState.member
// In theory, member should never be null, because the Gateway Intents necessary
// to make it work are provided
if(member != null) {
const insertedUser: DBUser | null = await insertUser(userDB, member)
if(insertedUser != null) {
await registerUserInCall(callUserDB, existingCallID, insertedUser)
}
}
} else if (oldState.channel != null && newState.channel == null) {
if(oldState.member?.id == client.user?.id) {
return // If the user is breadbot, ignore and exit
}
const connection = getVoiceConnection(oldState.guild.id)
if(oldState !== null && oldState.member !== null && connection?.receiver.subscriptions.has(oldState.member.id)) {
console.log(`Remove receiver subscription ${connection.receiver.subscriptions.delete(oldState.member.id)}`)
}
const existingCall : number | undefined = await getExistingCallID(callDB, oldState.channel)
if (existingCall !== undefined && oldState.member) {
await deregisterUserInCall(callUserDB, existingCall, oldState.member?.id)
const usersInCall: number = await numberOfUsersInCall(callUserDB, existingCall)
if (usersInCall == 0) {
connection?.destroy()
await setCallEndTime(callDB, oldState.channel)
}
}
}
})
}

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,23 @@
import { Column, Entity, OneToOne, PrimaryGeneratedColumn } from "typeorm";
import { DBRole } from "./DBRole";
@Entity()
export class DBBreadAsleep {
@PrimaryGeneratedColumn()
bread_asleep_id: number
@OneToOne(() => DBRole, (role: DBRole) => role.bread_asleep_config)
role: DBRole
@Column()
start_time: string
@Column()
end_time: string
@Column()
lockout_duration: number
@Column({nullable: true})
warning_lockout_until: Date | null
}

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: "text"})
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: "text"})
message_snowflake: string
@ManyToOne(() => DBChannel, (channel: DBChannel) => channel.messages)
channel: DBChannel
@ManyToOne(() => DBUser, (user: DBUser) => user.messages)
user: DBUser
@Column()
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, cascade: true})
attachments: DBMessageAttachments[] | null
@OneToMany(() => 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": "text"})
attachment_snowflake: string
@ManyToOne(() => DBMessage, (message: DBMessage) => message.attachments)
message: DBMessage
@Column()
attachment_name: string
@Column({type: 'text', nullable: true})
attachment_description: string | null
@Column({type: "datetime"})
attachment_timestamp: Date
@Column({type: 'text', nullable: true})
attachment_mime_type: string | null
@Column()
attachment_url: string
@Column({default: false})
attachment_downloaded: 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()
message_change_old_content: string
}

View File

@@ -0,0 +1,21 @@
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()
word: string
@Column()
regex: string
@OneToMany(() => DBMessageRegexMatches, (mrm: DBMessageRegexMatches) => mrm.regex, {nullable: true})
matches: DBMessageRegexMatches[] | null
}

View File

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

View File

@@ -0,0 +1,21 @@
import { Column, Entity, ManyToOne, OneToOne, PrimaryColumn } from "typeorm";
import { DBServer } from "./DBServer";
import { DBBreadAsleep } from "./DBBreadAsleep";
@Entity()
export class DBRole {
@PrimaryColumn({type: "text"})
role_snowflake: string
@ManyToOne(() => DBServer, (server: DBServer) => server.roles)
server: DBServer
@Column()
role_name: string
@Column()
is_deleted: boolean
@OneToOne(() => DBBreadAsleep, (ba: DBBreadAsleep) => ba.role, {nullable: true})
bread_asleep_config: DBBreadAsleep
}

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: "text"})
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: "text"})
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,25 +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);",
"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);"
]
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

@@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Current File",
"request": "launch",
"mainClass": "${file}"
},
{
"type": "java",
"name": "RegexGenerator",
"request": "launch",
"mainClass": "RegexGenerator",
"projectName": "ProfanityFilter_ceb3bd68",
"args": [
"ass",
"shit"
]
}
]
}

View File

@@ -0,0 +1,7 @@
{
"java.project.sourcePaths": ["src"],
"java.project.outputPath": "bin",
"java.project.referencedLibraries": [
"lib/**/*.jar"
]
}

View File

@@ -0,0 +1,18 @@
## Getting Started
Welcome to the VS Code Java world. Here is a guideline to help you get started to write Java code in Visual Studio Code.
## Folder Structure
The workspace contains two folders by default, where:
- `src`: the folder to maintain sources
- `lib`: the folder to maintain dependencies
Meanwhile, the compiled output files will be generated in the `bin` folder by default.
> If you want to customize the folder structure, open `.vscode/settings.json` and update the related settings there.
## Dependency Management
The `JAVA PROJECTS` view allows you to manage your dependencies. More details can be found [here](https://github.com/microsoft/vscode-java-dependency#manage-dependencies).

View File

@@ -0,0 +1,34 @@
package generators;
public class ExceptionGenerator {
public static void main(String[] args) {
//position 0 should be the base word, the rest should be exceptions
String base = args[0].toLowerCase();
String prefixes = "(?<!";
String regex = RegexGenerator.regexGenerator(base);
String suffixes = "(?!";
for (int i = 1; i < args.length; i++) {
args[i] = "$" + args[i] + "$";
String prefix = args[i].toLowerCase().split(base)[0];
String suffix = args[i].toLowerCase().split(base)[1];
if (!prefix.equals("$")) {
prefixes += RegexGenerator.regexGenerator(prefix) + "|";
}
if (!suffix.equals("$")) {
suffixes += RegexGenerator.regexGenerator(suffix) + "|";
}
}
prefixes = prefixes.substring(0, prefixes.length() - 1);
suffixes = suffixes.substring(0, suffixes.length() - 1);
System.out.println(prefixes + ")" + regex + suffixes + ")");
}
}

View File

@@ -0,0 +1,77 @@
package generators;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class RegexGenerator {
private static Map<Character, String> dictionary;
static {
Map<Character, String> map = new HashMap<>();
map.put('a', "[a@*]");
map.put('b', "([b38]|\\\\|3)");
map.put('c', "[c(k]");
map.put('d', "[d]");
map.put('e', "[e3*]");
map.put('f', "([f]|ph)");
map.put('g', "[g]");
map.put('h', "[h]");
map.put('i', "[il1!*]");
map.put('j', "[j]");
map.put('k', "[kc]");
map.put('l', "[li]");
map.put('m', "([m]|rn)");
map.put('n', "[n]");
map.put('o', "[o0pq*]");
map.put('p', "[p]");
map.put('q', "[q]");
map.put('r', "[r]");
map.put('s', "[sz5$]");
map.put('t', "[t7+]");
map.put('u', "[uv*]");
map.put('v', "[v]");
map.put('w', "([w]|vv)");
map.put('x', "[x]");
map.put('y', "[y]");
map.put('z', "[z]");
map.put(' ', "");
map.put('1', "([1]|one)");
map.put('2', "([2]|two)");
map.put('3', "([3]|three)");
map.put('4', "([4]|four)");
map.put('5', "([5]|five)");
map.put('6', "([6]|six)");
map.put('7', "([7]|seven)");
map.put('8', "([8]|eight)");
map.put('9', "([9]|nine)");
map.put('0', "([0]|zero)");
dictionary = Collections.unmodifiableMap(map);
}
public static void main(String[] args) {
for (String s : args) {
String regex = "(\\\\b|^)";
for (char c : s.toLowerCase().toCharArray()) {
regex += dictionary.get(c) + "+[\\\\s\\\\n\\\\W]*";
}
regex += "(\\\\b|$)";
System.out.println(regex);
}
}
public static String regexGenerator(String str) {
String regex = "(\\\\b|^)";
for (char c : str.toLowerCase().toCharArray()) {
regex += dictionary.get(c) + "+[\\\\s\\\\n\\\\W]*";
}
regex += "(\\\\b|$)";
return regex;
}
}

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