DB Storage Re-engineering #1
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,4 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
|
media
|
||||||
|
breadbot.db
|
||||||
.env
|
.env
|
||||||
tools/profanity_filter/bin/Words.json
|
tools/profanity_filter/bin/Words.json
|
||||||
tools/profanity_filter/src/Words.json
|
tools/profanity_filter/src/Words.json
|
||||||
|
bin/config.json
|
||||||
|
bin/Words.json
|
||||||
|
bin/__pycache__
|
||||||
198
bin/breadbot_common.py
Normal file
198
bin/breadbot_common.py
Normal 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
123
bin/breadmixer.py
Normal 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": "="
|
||||||
|
}])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
44
bin/profanity_regex_inserter.py
Normal file
44
bin/profanity_regex_inserter.py
Normal 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
1339
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -3,25 +3,27 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "breadbot.ts",
|
"main": "breadbot.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"watch": "tsc -w",
|
||||||
"dev": "tsx watch src/breadbot.ts",
|
"start": "tsc && node dist/breadbot.js",
|
||||||
"start": "node dist/breadbot.js",
|
"typeorm": "./node_modules/.bin/typeorm"
|
||||||
"build": "tsup src/breadbot.ts --minify"
|
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@discordjs/opus": "^0.9.0",
|
||||||
|
"@discordjs/voice": "^0.18.0",
|
||||||
"discord.js": "^14.20.0",
|
"discord.js": "^14.20.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"sqlite3": "^5.1.7",
|
|
||||||
"@discordjs/opus": "^0.9.0",
|
|
||||||
"@discordjs/voice": "^0.16.0",
|
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"node-crc": "1.3.2",
|
"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": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.10.0",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
|
|||||||
156
src/breadbot.ts
156
src/breadbot.ts
@@ -1,8 +1,53 @@
|
|||||||
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 { utilities } from "./utilties"
|
||||||
import { commands } from "./commands"
|
import { commands } from "./commands"
|
||||||
import { config } from "./config"
|
import { DBCall } from "./utilties/storage/entities/DBCall"
|
||||||
import { SQLCommon } from "./utilties/storage/interfaces"
|
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({
|
export const client : Client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
@@ -14,72 +59,73 @@ export const client : Client = new Client({
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export let db: SQLCommon
|
client.once(Events.ClientReady, async () => {
|
||||||
|
await dataSource.initialize()
|
||||||
|
|
||||||
if (config.DB_MODE == "sqlite") {
|
const serverRepo = dataSource.getRepository(DBServer)
|
||||||
db = new utilities.sqlite.SqliteDB("breadbot_test.db")
|
const channelRepo = dataSource.getRepository(DBChannel)
|
||||||
|
const roleRepo = dataSource.getRepository(DBRole)
|
||||||
db.run("PRAGMA foreign_keys = ON")
|
const userRepo = dataSource.getRepository(DBUser)
|
||||||
|
const messageRepo = dataSource.getRepository(DBMessage)
|
||||||
utilities.tables.makeTables(db)
|
const mccRepo = dataSource.getRepository(DBMessageContentChanges)
|
||||||
|
const maRepo = dataSource.getRepository(DBMessageAttachments)
|
||||||
//TODO I really don't want this to be here.
|
const regexesRepo = dataSource.getRepository(DBMessageRegex)
|
||||||
utilities.events.messages.setupMessageCapture(client, db)
|
const matchesRepo = dataSource.getRepository(DBMessageRegexMatches)
|
||||||
utilities.events.roles.setupRoleCapture(client, db)
|
const callRepo = dataSource.getRepository(DBCall)
|
||||||
}
|
const callUserRepo = dataSource.getRepository(DBCallUsers)
|
||||||
|
const breadAsleepRepo = dataSource.getRepository(DBBreadAsleep)
|
||||||
client.once(Events.ClientReady, () => {
|
|
||||||
// TODO Winston should handle this
|
|
||||||
console.log("Breadbot is ready")
|
|
||||||
|
|
||||||
client.guilds.cache.forEach(async (guild: Guild) => {
|
client.guilds.cache.forEach(async (guild: Guild) => {
|
||||||
await utilities.commands.deployCommands(guild.id)
|
await utilities.commands.deployCommands(guild.id)
|
||||||
|
const server: DBServer | null = await insertGuild(serverRepo, guild)
|
||||||
|
|
||||||
// TODO handle failures?
|
if (server != null) {
|
||||||
await utilities.guilds.insertGuild(db, guild)
|
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) => {
|
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) => {
|
guild.roles.cache.forEach(async (role: Role) => {
|
||||||
await utilities.roles.insertRole(db, role)
|
await insertRole(roleRepo, role)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
client.on(Events.GuildCreate, async (guild : Guild) => {
|
client.on(Events.ChannelCreate, async (channel) => {
|
||||||
await utilities.commands.deployCommands(guild.id)
|
await utilities.channels.insertChannel(channelRepo, channel)
|
||||||
await utilities.guilds.insertGuild(db, guild)
|
|
||||||
|
|
||||||
guild.channels.cache.forEach(async (channel: GuildBasedChannel) => {
|
|
||||||
await utilities.channels.insertChannel(db, 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)
|
client.login(config.DISCORD_TOKEN)
|
||||||
@@ -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!")
|
|
||||||
}
|
|
||||||
55
src/commands/breadasleep.ts
Normal file
55
src/commands/breadasleep.ts
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import * as ping from "./ping";
|
import * as ping from "./ping";
|
||||||
import * as breadalert from "./breadalert"
|
|
||||||
import * as breadthread from "./breadthread"
|
|
||||||
|
|
||||||
export const commands = {
|
export const commands = {
|
||||||
ping,
|
ping
|
||||||
breadalert,
|
|
||||||
breadthread
|
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,16 @@ import dotenv from "dotenv"
|
|||||||
|
|
||||||
dotenv.config()
|
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")
|
throw new Error("Missing environment variables")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
DISCORD_TOKEN,
|
DISCORD_TOKEN,
|
||||||
DISCORD_CLIENT_ID,
|
DISCORD_CLIENT_ID,
|
||||||
DB_MODE
|
DB_MODE,
|
||||||
|
MEDIA_VOICE_FOLDER,
|
||||||
|
MEDIA_ATTACHMENT_FOLDER
|
||||||
}
|
}
|
||||||
@@ -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]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,50 +1,37 @@
|
|||||||
import { SQLCommon } from "../storage/interfaces";
|
|
||||||
import { DMChannel, GuildBasedChannel, PartialDMChannel } from "discord.js";
|
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> {
|
export async function doesChannelExistByID(db: Repository<DBChannel>, channelID: string) : Promise<boolean> {
|
||||||
const queryResult : Object[] = await db.getAllParameterized(
|
return (await db.findOne({"where": {channel_snowflake: channelID}})) != null
|
||||||
"SELECT * FROM channels WHERE channel_snowflake = ?",
|
|
||||||
[channelID]
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryResult.length != 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doesChannelExist(db: SQLCommon, channel : GuildBasedChannel | DMChannel | PartialDMChannel) : Promise<boolean> {
|
export async function doesChannelExist(db: Repository<DBChannel>, channel : GuildBasedChannel | DMChannel | PartialDMChannel) : Promise<boolean> {
|
||||||
const queryResult : Object[] = await db.getAllParameterized(
|
return await doesChannelExistByID(db, channel.id)
|
||||||
"SELECT * FROM channels WHERE channel_snowflake = ?",
|
|
||||||
[channel.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryResult.length != 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
const alreadyExists: boolean = await doesChannelExist(db, channel)
|
||||||
|
|
||||||
if(alreadyExists) {
|
if(alreadyExists) {
|
||||||
return SQLResult.ALREADYEXISTS
|
return await db.findOne({"where": {channel_snowflake: channel.id}})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (channel.isDMBased()) {
|
const newChannel : DBChannel = await db.create({
|
||||||
await db.runParameterized(
|
channel_snowflake: channel.id,
|
||||||
"INSERT INTO channels VALUES (?, ?, ?, ?, ?, ?)",
|
channel_name: channel.isDMBased() ? channel.recipient?.username : channel.name,
|
||||||
[channel.id, null, channel.recipient?.username, channel.isThread(), channel.isDMBased(), channel.isVoiceBased()]
|
is_dm: channel.isDMBased(),
|
||||||
)
|
is_thread: channel.isThread(),
|
||||||
} else {
|
is_voice: channel.isVoiceBased(),
|
||||||
await db.runParameterized(
|
server: channel.isDMBased() ? null : {server_snowflake: channel.guild.id}
|
||||||
"INSERT INTO channels VALUES (?, ?, ?, ?, ?, ?)",
|
})
|
||||||
[channel.id, channel.guild.id, channel.name, channel.isThread(), channel.isDMBased(), channel.isVoiceBased()]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return SQLResult.CREATED
|
return await db.save(newChannel)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//TODO Winston should handle this
|
//TODO Winston should handle this
|
||||||
console.log("CHANNEL INSERT ERROR")
|
console.log("CHANNEL INSERT ERROR")
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,29 @@
|
|||||||
import { Guild } from "discord.js";
|
import { Guild } from "discord.js";
|
||||||
import { SQLCommon } from "../storage/interfaces";
|
import { Repository } from "typeorm";
|
||||||
import { SQLResult } from "../storage/enumerations";
|
import { DBServer } from "../storage/entities/DBServer";
|
||||||
|
|
||||||
export async function doesGuildExist(db: SQLCommon, guild : Guild) : Promise<boolean> {
|
export async function doesGuildExist(db: Repository<DBServer>, guild : Guild) : Promise<boolean> {
|
||||||
const queryResult : Object[] = await db.getAllParameterized(
|
return (await db.findOne({"where": {server_snowflake: guild.id}})) != null
|
||||||
"SELECT * FROM servers WHERE server_snowflake = ?",
|
|
||||||
[guild.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryResult.length != 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
const alreadyExists: boolean = await doesGuildExist(db, guild)
|
||||||
|
|
||||||
if (alreadyExists) {
|
if (alreadyExists) {
|
||||||
return SQLResult.ALREADYEXISTS
|
return await db.findOne({"where": {server_snowflake: guild.id}})
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await db.runParameterized(
|
const server: DBServer = await db.create({
|
||||||
"INSERT INTO servers VALUES (?, ?, ?)",
|
server_snowflake: guild.id,
|
||||||
[guild.id, guild.name, guild.description]
|
server_name: guild.name,
|
||||||
)
|
server_description: guild.description ?? ""
|
||||||
|
})
|
||||||
|
|
||||||
return SQLResult.CREATED
|
return await db.save(server)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Insert Failed")
|
console.log("Insert Failed")
|
||||||
//TODO Winston should handle this
|
//TODO Winston should handle this
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,118 +1,103 @@
|
|||||||
import { Attachment, Message, OmitPartialGroupDMChannel, PartialMessage } from "discord.js";
|
import { Attachment, Message, OmitPartialGroupDMChannel, PartialMessage } from "discord.js";
|
||||||
import { SQLCommon } from "../storage/interfaces";
|
import { Repository } from "typeorm";
|
||||||
import { SQLResult } from "../storage/enumerations";
|
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?
|
// TODO Do partial messages affect other functionality elsewhere?
|
||||||
|
|
||||||
export async function doesMessageExist(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) : Promise<boolean> {
|
export async function doesMessageExist(db: Repository<DBMessage>, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) : Promise<boolean> {
|
||||||
const queryResult: Object[] = await db.getAllParameterized(
|
return (await db.findOne({"where": {message_snowflake: message.id}})) != null
|
||||||
"SELECT * FROM messages WHERE message_snowflake = ?",
|
|
||||||
[message.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryResult.length != 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doesAttachmentExist(db: SQLCommon, attachment: Attachment) : Promise<boolean> {
|
export async function doesAttachmentExist(db: Repository<DBMessageAttachments>, attachment: Attachment) : Promise<boolean> {
|
||||||
const queryResult: Object[] = await db.getAllParameterized(
|
return (await db.findOne({"where": {attachment_snowflake: attachment.id}})) != null
|
||||||
"SELECT * FROM message_attachments WHERE attachment_snowflake = ?",
|
|
||||||
[attachment.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryResult.length != 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function insertMessage(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>>) : Promise<SQLResult> {
|
export async function insertMessage(messageDB: Repository<DBMessage>, maDB: Repository<DBMessageAttachments>, message: OmitPartialGroupDMChannel<Message<boolean>>) : Promise<DBMessage | null> {
|
||||||
const alreadyExists: boolean = await doesMessageExist(db, message)
|
const alreadyExists: boolean = await doesMessageExist(messageDB, message)
|
||||||
|
|
||||||
if(alreadyExists) {
|
if(alreadyExists) {
|
||||||
return SQLResult.ALREADYEXISTS
|
return await messageDB.findOne({"where": {message_snowflake: message.id}})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.runParameterized(
|
const newMessage: DBMessage = await messageDB.create({
|
||||||
"INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?)",
|
message_snowflake: message.id,
|
||||||
[message.id, message.channel.id, message.author.id,
|
channel: {channel_snowflake: message.channel.id},
|
||||||
message.content, message.createdTimestamp, 0]
|
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) {
|
} catch (err) {
|
||||||
//TODO Winston should handle this
|
//TODO Winston should handle this
|
||||||
console.log("MESSAGE INSERTION ERROR")
|
console.log("MESSAGE INSERTION ERROR")
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMessageContentHistory(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>>) : Promise<SQLResult> {
|
export async function updateMessageContentHistory(messageDB: Repository<DBMessage>, mccDB: Repository<DBMessageContentChanges>,
|
||||||
const messageIDExists: boolean = await doesMessageExist(db, message)
|
ma: Repository<DBMessageAttachments>, message: OmitPartialGroupDMChannel<Message<boolean>>) : Promise<DBMessage | null> {
|
||||||
|
let dbMessage: DBMessage | null = await messageDB.findOne({"where": {message_snowflake: message.id}})
|
||||||
|
|
||||||
if(!messageIDExists) {
|
if(dbMessage == null) {
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log([message.id, message.editedTimestamp ?? message.createdTimestamp, message.content, message.id])
|
const contentChange: DBMessageContentChanges = mccDB.create({
|
||||||
await db.runParameterized(
|
message: {message_snowflake: message.id},
|
||||||
"INSERT INTO message_content_changes (message_snowflake, message_change_old_timestamp, message_change_old_content) " +
|
message_change_old_content: dbMessage.message_content,
|
||||||
"SELECT messages.message_snowflake, message_timestamp, message_content FROM messages WHERE message_snowflake = ?;",
|
message_change_old_timestamp: dbMessage.message_timestamp
|
||||||
[message.id]
|
})
|
||||||
)
|
|
||||||
|
|
||||||
await db.runParameterized(
|
dbMessage.message_content = message.content
|
||||||
"UPDATE messages SET message_timestamp = ?, message_content = ? WHERE message_snowflake = ?;",
|
dbMessage.message_timestamp = message.editedAt ?? message.createdAt
|
||||||
[message.editedTimestamp ?? message.createdTimestamp, message.content, message.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
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) {
|
} catch (err) {
|
||||||
//TODO Winston should handle this
|
//TODO Winston should handle this
|
||||||
console.log("MESSAGE MODIFY FAILED")
|
console.log("MESSAGE MODIFY FAILED")
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markMessageDeleted(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) : Promise<SQLResult> {
|
export async function markMessageDeleted(db: Repository<DBMessage>, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) : Promise<DBMessage | null> {
|
||||||
const messageIDExists: boolean = await doesMessageExist(db, message)
|
let dbMessage: DBMessage | null = await db.findOne({"where": {message_snowflake: message.id}})
|
||||||
|
|
||||||
if(!messageIDExists) {
|
if(dbMessage == null) {
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.runParameterized(
|
dbMessage.message_deleted = true
|
||||||
"UPDATE messages SET message_deleted = 1 WHERE message_snowflake = ?",
|
|
||||||
[message.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
return SQLResult.UPDATED
|
return await db.save(dbMessage)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// TODO Winston should handle this
|
// TODO Winston should handle this
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,80 @@
|
|||||||
import { Guild } from "discord.js";
|
import { Client, Guild, Message, OmitPartialGroupDMChannel, PartialMessage, TextChannel } from "discord.js";
|
||||||
import { SQLCommon } from "../storage/interfaces";
|
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: SQLCommon, guild: Guild): Promise<any[]> {
|
export async function getRegexesForGuild(db: Repository<DBServer>, guild: Guild): Promise<DBMessageRegex[] | null | undefined> {
|
||||||
return db.getAllParameterized(
|
return (await db.findOne({
|
||||||
"SELECT * FROM message_regexes WHERE server_snowflake = ?",
|
select: {
|
||||||
[guild.id]
|
regexes: true
|
||||||
)
|
},
|
||||||
|
relations: {
|
||||||
|
regexes: true
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
server_snowflake: guild.id
|
||||||
|
}
|
||||||
|
}))?.regexes
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRoleExclusionSnowflakesForGuild(db: SQLCommon, guild: Guild): Promise<string[]> {
|
export async function checkMatchingRegexes(regexes: DBMessageRegex[], testString: string) : Promise<DBMessageRegex[] | null> {
|
||||||
return (await db.getAllParameterized(
|
let matchedRegexes: DBMessageRegex[] = []
|
||||||
"SELECT role_snowflake FROM message_regex_no_role_check WHERE server_snowflake = ?",
|
|
||||||
[guild.id]
|
regexes.forEach((regex) => {
|
||||||
)).map((o) => (o as any).role_snowflake)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,75 +1,79 @@
|
|||||||
import { Role } from "discord.js";
|
import { Role } from "discord.js";
|
||||||
import { SQLCommon } from "../storage/interfaces";
|
import { Repository } from "typeorm";
|
||||||
import { SQLResult } from "../storage/enumerations";
|
import { DBRole } from "../storage/entities/DBRole";
|
||||||
|
|
||||||
export async function doesRoleExist(db: SQLCommon, role : Role) : Promise<boolean> {
|
export async function doesRoleExist(db: Repository<DBRole>, role : Role) : Promise<boolean> {
|
||||||
const queryResult : Object[] = await db.getAllParameterized(
|
return (await db.findOne({"where": {role_snowflake: role.id}})) != null
|
||||||
"SELECT * FROM roles WHERE role_snowflake = ?",
|
|
||||||
[role.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryResult.length != 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
const alreadyExists: boolean = await doesRoleExist(db, role)
|
||||||
|
|
||||||
if(alreadyExists) {
|
if(alreadyExists) {
|
||||||
return SQLResult.ALREADYEXISTS
|
return await db.findOne({"where": {role_snowflake: role.id}})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.runParameterized(
|
const newRole : DBRole = await db.create({
|
||||||
"INSERT INTO roles VALUES (?, ?, ?, 0)",
|
role_snowflake: role.id,
|
||||||
[role.id, role.guild.id, role.name]
|
server: {server_snowflake: role.guild.id},
|
||||||
)
|
role_name: role.name,
|
||||||
|
is_deleted: false
|
||||||
|
})
|
||||||
|
|
||||||
return SQLResult.CREATED
|
return await db.save(newRole)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("ROLE INSERT ERROR")
|
console.log("ROLE INSERT ERROR")
|
||||||
console.log(err)
|
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)
|
const roleExists: boolean = await doesRoleExist(db, role)
|
||||||
|
|
||||||
if(!roleExists) {
|
if(!roleExists) {
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.runParameterized(
|
const toUpdate: DBRole | null = await db.findOne({"where": {role_snowflake: role.id}})
|
||||||
"UPDATE roles SET role_name = ? WHERE role_snowflake = ?",
|
|
||||||
[role.name, role.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
return SQLResult.UPDATED
|
if(toUpdate != null) {
|
||||||
|
toUpdate.role_name = role.name
|
||||||
|
|
||||||
|
return await db.save(toUpdate)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("ROLE UPDATE FAILED")
|
console.log("ROLE UPDATE FAILED")
|
||||||
console.log(err)
|
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)
|
const roleExists: boolean = await doesRoleExist(db, role)
|
||||||
|
|
||||||
if(!roleExists) {
|
if(!roleExists) {
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.runParameterized(
|
const toUpdate: DBRole | null = await db.findOne({"where": {role_snowflake: role.id}})
|
||||||
"UPDATE roles SET is_deleted = 1 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) {
|
} catch (err) {
|
||||||
console.log("ROLE DELETE FAILED")
|
console.log("ROLE DELETE FAILED")
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,34 +1,30 @@
|
|||||||
import { User } from "discord.js";
|
import { GuildMember, User } from "discord.js";
|
||||||
import { SQLCommon } from "../storage/interfaces";
|
import { Repository } from "typeorm";
|
||||||
import { SQLResult } from "../storage/enumerations";
|
import { DBUser } from "../storage/entities/DBUser";
|
||||||
|
|
||||||
export async function doesUserExist(db: SQLCommon, user: User): Promise<boolean> {
|
export async function doesUserExist(db: Repository<DBUser>, user: User | GuildMember | string): Promise<boolean> {
|
||||||
const queryResult: Object[] = await db.getAllParameterized(
|
return (await db.findOne({"where": {user_snowflake: user instanceof User || user instanceof GuildMember ? user.id : user }})) != null
|
||||||
"SELECT * FROM users WHERE user_snowflake = ?",
|
|
||||||
[user.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryResult.length != 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function insertUser(db: SQLCommon, user: User): Promise<SQLResult> {
|
export async function insertUser(db: Repository<DBUser>, user: User | GuildMember | DBUser): Promise<DBUser | null> {
|
||||||
const alreadyExists: boolean = await doesUserExist(db, user)
|
const alreadyExists: boolean = await doesUserExist(db, user instanceof DBUser ? user.user_snowflake : user)
|
||||||
|
|
||||||
if(alreadyExists) {
|
if(alreadyExists) {
|
||||||
return SQLResult.ALREADYEXISTS
|
return await db.findOne({"where": {user_snowflake: user instanceof DBUser ? user.user_snowflake : user.id }})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.runParameterized(
|
const newUser: DBUser = user instanceof DBUser ? user : db.create({
|
||||||
"INSERT INTO users VALUES (?, ?, ?)",
|
user_snowflake: user.id,
|
||||||
[user.id, user.username, user.displayName]
|
user_name: user instanceof User ? user.username : user.displayName,
|
||||||
)
|
user_displayname: user.displayName
|
||||||
|
})
|
||||||
|
|
||||||
return SQLResult.CREATED
|
return await db.save(newUser)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
//TODO Winston should handle this
|
//TODO Winston should handle this
|
||||||
console.log("USER INSERT ERROR")
|
console.log("USER INSERT ERROR")
|
||||||
console.log(err)
|
console.log(err)
|
||||||
return SQLResult.FAILED
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,63 +1,112 @@
|
|||||||
import { Client, Events, Message, OmitPartialGroupDMChannel, PartialMessage } from "discord.js";
|
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 { insertChannel } from "../discord/channels";
|
||||||
import { insertUser } from "../discord/users";
|
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) => {
|
client.on(Events.MessageCreate, async (message) => {
|
||||||
await 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) => {
|
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) => {
|
client.on(Events.MessageDelete, async (deletedMessage) => {
|
||||||
await processMessageDeleted(db, deletedMessage)
|
await processMessageDeleted(messageDB, deletedMessage)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
async function processMessageCreate(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>>) {
|
setInterval(async () => {
|
||||||
const channelOk: SQLResult = await insertChannel(db, message.channel)
|
console.log("STARTING DOWNLOAD CYCLE")
|
||||||
const userOk: SQLResult = await insertUser(db, message.author)
|
mkdirSync(config.MEDIA_ATTACHMENT_FOLDER, {recursive: true})
|
||||||
|
|
||||||
if (channelOk == SQLResult.ALREADYEXISTS || channelOk == SQLResult.CREATED ||
|
let attachmentsToProcess: DBMessageAttachments[] | null = await maDB.find({
|
||||||
userOk == SQLResult.ALREADYEXISTS || userOk == SQLResult.CREATED) {
|
relations: {
|
||||||
|
message: true
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
attachment_downloaded: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
await insertMessage(db, message)
|
if (attachmentsToProcess != null) {
|
||||||
// TODO observe success of message insertion
|
attachmentsToProcess.forEach(async (attachment: DBMessageAttachments) => {
|
||||||
if(message.attachments.size != 0) {
|
|
||||||
const allAttachments: void[] = message.attachments.map((attachment) => {
|
mkdirSync(config.MEDIA_ATTACHMENT_FOLDER + path.sep + attachment.message.message_snowflake, {recursive: true})
|
||||||
insertAttachment(db, attachment, message)
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(allAttachments).catch((error) => {
|
const response = await fetch(attachment.attachment_url)
|
||||||
// TODO Winston should handle this
|
|
||||||
console.log("MESSAGE ATTACHMENT INSERT ERROR")
|
if (response.body !== null) {
|
||||||
console.log(error)
|
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>>) {
|
async function processMessageModify(
|
||||||
await updateMessageContentHistory(db, newMessage)
|
messageDB: Repository<DBMessage>,
|
||||||
|
mccDB: Repository<DBMessageContentChanges>,
|
||||||
if(newMessage.attachments.size != 0) {
|
maDB: Repository<DBMessageAttachments>,
|
||||||
const allAttachments: void[] = newMessage.attachments.map((attachment) => {
|
newMessage: OmitPartialGroupDMChannel<Message<boolean>>) {
|
||||||
insertAttachment(db, attachment, newMessage)
|
await updateMessageContentHistory(messageDB, mccDB, maDB, newMessage)
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(allAttachments).catch((error) => {
|
|
||||||
// TODO Winston should handle this
|
|
||||||
console.log(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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)
|
await markMessageDeleted(db, deletedMessage)
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,24 @@
|
|||||||
import { Client, Events } from "discord.js";
|
import { Client, Events } from "discord.js";
|
||||||
import { SQLCommon } from "../storage/interfaces";
|
|
||||||
import { SQLResult } from "../storage/enumerations";
|
|
||||||
import { insertRole, markRoleDeleted, updateRole } from "../discord/roles";
|
import { insertRole, markRoleDeleted, updateRole } from "../discord/roles";
|
||||||
import { insertGuild } from "../discord/guilds";
|
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) => {
|
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) {
|
if (serverOk != null) {
|
||||||
await insertRole(db, role)
|
await insertRole(roleDB, role)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
client.on(Events.GuildRoleUpdate, async (role) => {
|
client.on(Events.GuildRoleUpdate, async (role) => {
|
||||||
await updateRole(db, role)
|
await updateRole(roleDB, role)
|
||||||
})
|
})
|
||||||
|
|
||||||
client.on(Events.GuildRoleDelete, async (role) => {
|
client.on(Events.GuildRoleDelete, async (role) => {
|
||||||
await markRoleDeleted(db, role)
|
await markRoleDeleted(roleDB, role)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
import * as commands from "./discord/commands"
|
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 guilds from "./discord/guilds"
|
||||||
import * as channels from "./discord/channels"
|
import * as channels from "./discord/channels"
|
||||||
import * as users from "./discord/users"
|
import * as users from "./discord/users"
|
||||||
import * as breadthread from "./breadbot/breadthread"
|
|
||||||
import * as roles from "./discord/roles"
|
import * as roles from "./discord/roles"
|
||||||
import { events } from "./events"
|
import { events } from "./events"
|
||||||
|
|
||||||
export const utilities = {
|
export const utilities = {
|
||||||
commands,
|
commands,
|
||||||
sqlite,
|
|
||||||
tables,
|
|
||||||
guilds,
|
guilds,
|
||||||
channels,
|
channels,
|
||||||
users,
|
users,
|
||||||
events,
|
events,
|
||||||
breadthread,
|
|
||||||
roles
|
roles
|
||||||
}
|
}
|
||||||
23
src/utilties/storage/entities/DBBreadAsleep.ts
Normal file
23
src/utilties/storage/entities/DBBreadAsleep.ts
Normal 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
|
||||||
|
}
|
||||||
34
src/utilties/storage/entities/DBCall.ts
Normal file
34
src/utilties/storage/entities/DBCall.ts
Normal 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
|
||||||
|
}
|
||||||
21
src/utilties/storage/entities/DBCallTranscriptions.ts
Normal file
21
src/utilties/storage/entities/DBCallTranscriptions.ts
Normal 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
|
||||||
|
}
|
||||||
21
src/utilties/storage/entities/DBCallUsers.ts
Normal file
21
src/utilties/storage/entities/DBCallUsers.ts
Normal 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
|
||||||
|
}
|
||||||
31
src/utilties/storage/entities/DBChannel.ts
Normal file
31
src/utilties/storage/entities/DBChannel.ts
Normal 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
|
||||||
|
}
|
||||||
36
src/utilties/storage/entities/DBMessage.ts
Normal file
36
src/utilties/storage/entities/DBMessage.ts
Normal 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
|
||||||
|
}
|
||||||
29
src/utilties/storage/entities/DBMessageAttachment.ts
Normal file
29
src/utilties/storage/entities/DBMessageAttachment.ts
Normal 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
|
||||||
|
}
|
||||||
17
src/utilties/storage/entities/DBMessageContentChanges.ts
Normal file
17
src/utilties/storage/entities/DBMessageContentChanges.ts
Normal 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
|
||||||
|
}
|
||||||
21
src/utilties/storage/entities/DBMessageRegex.ts
Normal file
21
src/utilties/storage/entities/DBMessageRegex.ts
Normal 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
|
||||||
|
}
|
||||||
15
src/utilties/storage/entities/DBMessageRegexMatches.ts
Normal file
15
src/utilties/storage/entities/DBMessageRegexMatches.ts
Normal 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
|
||||||
|
}
|
||||||
21
src/utilties/storage/entities/DBRole.ts
Normal file
21
src/utilties/storage/entities/DBRole.ts
Normal 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
|
||||||
|
}
|
||||||
25
src/utilties/storage/entities/DBServer.ts
Normal file
25
src/utilties/storage/entities/DBServer.ts
Normal 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
|
||||||
|
}
|
||||||
25
src/utilties/storage/entities/DBUser.ts
Normal file
25
src/utilties/storage/entities/DBUser.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export enum SQLResult {
|
|
||||||
CREATED,
|
|
||||||
UPDATED,
|
|
||||||
DELETED,
|
|
||||||
ALREADYEXISTS,
|
|
||||||
FAILED
|
|
||||||
}
|
|
||||||
@@ -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[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"lib": ["es5", "es6"],
|
||||||
|
"target": "es6",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
@@ -10,6 +11,10 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"sourceMap": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user