118 Commits

Author SHA1 Message Date
81f91df8ac Adding a fix for when new calls are starting in voice channels that haven't been registered yet 2024-01-08 06:47:22 -05:00
6c6c2655f2 Adding another protection to try to avoid multiple recordings of the same audio happening 2024-01-07 16:23:42 -05:00
6012351b3e Attempting to protect against call echo AND fix new call registry which wasn't working correctly it seems... 2024-01-07 15:30:15 -05:00
934fa4d37f Setting auto commit on the db connection to true because apparently it defaults to false. Meaning nothing gets written to the DB 2024-01-06 15:48:08 -05:00
44982fd439 Pushing up an attempted fix for breadmixer not being able to submit transcriptions to the database 2024-01-06 15:35:12 -05:00
46fdebd503 Another minor fix for breadmixer, specifically the final ffmpeg call 2024-01-05 19:34:16 -05:00
c294489a60 Attempted fixes for breadmixers ffmpeg issues 2024-01-05 19:32:17 -05:00
2cf2ec7d88 Trying to figure out what is wrong with breadmixer running the FFMPEG command that I know for certain works 2024-01-05 19:06:24 -05:00
2f03a0de4c Fix for breadbot filter strings 2024-01-05 18:54:45 -05:00
3b33bb6ee4 Cleanup, database structure backup, and an additional log item for breadmixer to try to figure out where the ffmpeg command structure is wrong 2024-01-05 18:51:11 -05:00
f7e0c4b15a Some cleanup, and transitioning breadbot.js to use winston for logging 2024-01-04 20:27:31 -05:00
9fc901a2ef Pushing changes to sqlutil to add winston logging 2024-01-04 19:49:44 -05:00
b7283c17b6 A minor change to how servers are registered, bad logic 2024-01-03 21:06:50 -05:00
40f6ea7cd7 A minor modification to how edited messages are handled to deal with how discord changes links to gifs and other media to what I assume are embeds 2024-01-02 21:50:47 -05:00
cb94d94943 Drastically lowering the end time duration of a speaking session in voice calls to see if event emitter warnings go away 2024-01-02 21:32:45 -05:00
00b834c40b Adding another element to this to detect when someone stops speaking 2024-01-02 21:19:46 -05:00
a294b8c668 Minor fixes and adding a forgotten component of ending a call 2024-01-02 20:55:16 -05:00
263405141b A number of changes to address issues and missing pieces from the voice call monitoring components of Breadbot 2024-01-02 20:39:23 -05:00
089bb99119 Commands for breadbot seem to be missing, modifying deploy-command to fix this 2023-12-31 09:42:39 -05:00
2f51c18a69 Trying some upgrades to command loading 2023-12-31 09:39:25 -05:00
7afbb169e2 Another sql syntax fix 2023-12-30 18:53:32 -05:00
bdb9d4b807 Fixing a connection pool config to allow multiple statements per query 2023-12-30 18:37:28 -05:00
ed67193761 Fixing minor SQL syntax error 2023-12-30 18:34:36 -05:00
18c9be444d Fixing a minor issue when adding messages 2023-12-30 18:30:10 -05:00
40956e5c9e Adding message modify, delete, and attachment tracking (untested) 2023-12-30 17:32:44 -05:00
4de56c1af8 Sort returns none, which was deleting the file dictionary items 2023-12-25 20:09:44 -05:00
7b9a7fb339 Verbosity for breadmixer to try to figure out whats wrong 2023-12-25 19:23:20 -05:00
3039bf65bd Adding breadmixer changes 2023-12-16 12:51:35 -05:00
850e7288d1 A little more work on breadmixer 2023-11-29 20:59:50 -05:00
3bbba3217a More fixes for dragging milliseconds out of the time diff 2023-11-24 20:50:22 -05:00
7b0fde230d Really need milliseconds for seconds from starttime to be accurate 2023-11-24 20:46:42 -05:00
511f4bf69e Fixing datatype coersion problems 2023-11-24 20:43:24 -05:00
78f1232f5b Adding BreadMixer file indexing 2023-11-24 20:41:29 -05:00
41683fe098 Ok, trying s instead of d, even though that's technically not correct 2023-11-24 20:16:03 -05:00
61b617db04 I think we're back to typal pickiness 2023-11-24 20:13:42 -05:00
740213712a Ok, the parameterization for queries is SUPER picky 2023-11-24 20:12:23 -05:00
8cdd7d3974 It's not type coersion it's not seeing something as a tuple 2023-11-24 20:11:05 -05:00
e7681bf7cd Variable type coersion seems to be a problem with parameterized queries 2023-11-24 20:09:55 -05:00
7bee11e4f4 Initial work on BreadMixer 2023-11-24 19:56:03 -05:00
2954d1b304 Found something, might have been a node-crc dependency issue 2023-11-24 15:54:38 -05:00
4868f6930e Trying a PCM write 2023-11-24 15:17:40 -05:00
42f04a552a Attempting to write raw OPUS to file 2023-11-24 15:02:03 -05:00
00de13acd2 Switched back to the old version and turning CRC back on 2023-11-22 21:57:26 -05:00
9a5942facc Trying something because the files I'm getting are playable, but only under specify conditions and not with FFMEG 2023-11-22 21:40:34 -05:00
bdc0ed0fad Setting CRC false on the Ogg bitstream object to see if the problem with the CRC component goes away 2023-11-21 19:49:13 -05:00
55490c4b04 Adding node-crc dependency 2023-11-21 19:33:30 -05:00
66cec55bb6 Found a way to be more like the example code. Still not sure where the 2.0 alpha of prism comes from, but what the example uses I now have 2023-11-21 19:25:51 -05:00
16b41fad2b Modifying versions and restoring the PCM version of audio saving 2023-11-21 18:46:06 -05:00
ab3ef81df9 Trying to write just straight ogg rather than convert to PCM, which seems to be the issue 2023-11-21 18:06:32 -05:00
0f68356254 Possibly working version of joining voice call and collecting audio 2023-11-21 17:38:01 -05:00
855a91e340 Trying something I found on Stack Overflow 2023-11-19 19:49:28 -05:00
8ef2ede2c0 Trying another logging check to figure out this cache 2023-11-19 19:46:21 -05:00
32954a2536 Fixed the problem with registering a new call id 2023-11-19 19:42:21 -05:00
a689783b36 Another commit 2023-11-19 19:39:07 -05:00
9b0f381673 I DID PRINT AGAIN, JAVASCRIPT IS NOT PYTHON 2023-11-19 19:31:29 -05:00
1eb4da8729 More debugging 2023-11-19 19:29:57 -05:00
85fc4805dc Trying to fix the call registration return 2023-11-19 19:25:00 -05:00
f7bcbe348b Python is not Javascript, print doesn't exist 2023-11-19 19:22:00 -05:00
2307119ad7 More random prints 2023-11-19 19:20:32 -05:00
d3daa299c0 Trying another sqlutil fix for inserting call state 2023-11-19 19:14:32 -05:00
abf8f10668 Modifications to fix some bugs with inputting ID 2023-11-19 19:10:55 -05:00
acaedb434c Adding a bunch of random minor changes to sqlutil to fix a lambda parameter problem 2023-11-19 19:00:41 -05:00
e64811a179 More debug messages 2023-11-19 18:57:34 -05:00
113f6cbbd5 Adding a random debug message to see if what I think is happening is actually happening 2023-11-19 18:52:51 -05:00
7c4b5c52f1 Fixing a minor object reference issue 2023-11-19 18:46:52 -05:00
b5ded6acaf Adding a bunch of debug logging 2023-11-19 18:44:22 -05:00
08a9d0addd Another slight modification to make sure voice audio data is stored in a sensible location 2023-11-19 18:37:26 -05:00
aa1789da97 Adding a gitignore entry for media capture testing 2023-11-19 18:36:03 -05:00
9e0c78d939 Major modifications to try out actual audio recording 2023-11-19 18:32:16 -05:00
d82c569363 Added a large amount of untested voice management code, stopping just short of actually connecting to/disconnecting from calls 2023-11-14 21:50:42 -05:00
6effe2983e Some final updates for today, trying to keep track of how many people are in a particular voice channel 2023-11-12 14:54:42 -05:00
a729545e01 Fixing comparison states 2023-11-12 11:01:07 -05:00
7e79b2c3b4 Adding an actual detection that says what user joined where 2023-11-12 10:58:09 -05:00
87be8ca8db Doing to some state value checking now that the event works 2023-11-12 10:49:22 -05:00
5507fea98e I think some misplaced brackets were causing the issues 2023-11-12 10:16:41 -05:00
92df0cf4a7 Trying something simpler for VoiceStateChange notification just to see if the event works 2023-11-12 10:12:57 -05:00
944f8d4e72 I'm pretty sure I used the wrong intent 2023-11-12 10:07:52 -05:00
27e9fdb743 Missing an intent to capture the event that I want 2023-11-12 10:03:57 -05:00
b9a699c4c2 Adding preliminary test for Voice State Changed 2023-11-12 10:01:41 -05:00
734aa58e67 Adding a 'Database Backup', it's not perfect, the queries are out of order, but it'll do for now if something blows up 2023-11-11 22:31:59 -05:00
dc323ffeb1 Some final cleanup, and README adjustments 2023-11-11 22:18:54 -05:00
9fd8f3c289 Cleanup, and more logic fixes 2023-11-11 22:09:26 -05:00
c82fceeef1 Fixing some more logic problems 2023-11-11 22:02:31 -05:00
01a72ca544 Fixing a logic error 2023-11-11 21:58:56 -05:00
8befc32453 Trying a different way of handling the rows variable 2023-11-11 21:57:59 -05:00
4a3e470945 Adding some more console noise for debugging 2023-11-11 21:56:07 -05:00
7c6389d421 A significant amount of reimplementation 2023-11-11 21:51:34 -05:00
ac672e73d0 Pretty sure I have a working chain example 2023-11-11 21:29:05 -05:00
43546d93fe More changes to promise chain testing 2023-11-11 21:23:49 -05:00
6ea91cd1c9 Testing some stuff with promises and return values 2023-11-11 21:20:25 -05:00
0ad30722ae Added more modifications, await doesn't seem to be respected in the way I'm thinking, so more logging! 2023-11-11 20:35:30 -05:00
dfeb3d5148 Trying to fix issues again, this time with the truthiness of promise objects 2023-11-11 20:21:24 -05:00
6cbf6dc718 Trying to unwind some async/await mess 2023-11-11 20:17:59 -05:00
e24cc98fec Fixed some syncronization issues (I think) 2023-11-11 20:09:03 -05:00
a0e979e0bc The field for the message sender is author, not user 2023-11-11 19:52:27 -05:00
69a711a4a4 Added some fancy intents to try to fix BreadBots deafness to new messages 2023-11-11 19:48:08 -05:00
89e73e866b Adding greater console logging for event misfire 2023-11-11 19:43:23 -05:00
77b526ca21 Added preliminary BreadBot message surveillance 2023-11-11 19:39:07 -05:00
881b7f6359 I think I found a missing not directive 2023-11-11 18:37:24 -05:00
ece032ff5a Attempting to fix server already registered handling 2023-11-11 18:31:56 -05:00
dabb635b6b Trying another thing to fix escapement issues 2023-11-11 18:22:07 -05:00
2de2202187 Fixing escapement issues 2023-11-11 18:18:09 -05:00
3928b1f564 Fixed some insertion issue 2023-11-11 18:13:57 -05:00
8865015499 Promises in JavaScript are neat and also a pain 2023-11-11 18:08:42 -05:00
811478f486 More attempts to fix the situation with server registrations 2023-11-11 18:03:09 -05:00
808ca6c442 Fix for isServerRegistered 2023-11-11 17:46:37 -05:00
cb3466c6c6 Adding server to database registration 2023-11-11 17:38:45 -05:00
0d15e7931c Testing a new interaction 2023-11-11 15:33:57 -05:00
e68272236f Pushing up changes to main before I branch 2023-11-11 12:29:37 -05:00
febbd1fa8e Fixed Calendar Delete Issue 2022-11-29 21:37:23 -05:00
0033bea4ab Fix README.md 2022-11-29 19:58:53 -05:00
d120cf9945 Update README.md 2022-11-29 19:57:57 -05:00
24b97bc5b0 Adding some modifications to increase reuseability 2022-11-29 19:47:07 -05:00
7ebdb84c98 Some mild changes, added list calendar 2022-11-25 19:41:08 -05:00
30a2114ff4 Fixed addcalendar, added other file stubs 2022-11-24 17:39:30 -05:00
dce3b6ecc4 Committing a (non-functional) add calendar command 2022-11-22 19:54:42 -05:00
b2b8bccaa6 Committing psuedo functional GCal Pull
It's not exactly a good test, but it's something. Need to do some more
work in the creation and management of calendars.
2022-11-20 19:04:47 -05:00
97af77745f gitignore modified to stop modules from being uploaded 2022-11-13 11:13:41 -05:00
23 changed files with 2548 additions and 1721 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
.env
config.json
node_modules
keyfile.json
media

View File

@@ -3,3 +3,20 @@
BreadBot helps, BreadBot does the things. Whatever things I'm willing to add to BreadBot.
BreadBot is the breadiest bread.
In all seriousness, BreadBot is my first real attempt at a Discord bot. I'm writing it such that it can really only be used in one server at the moment, although that will hopefully change in the future. The intention is really for it only to be used for the Team 2648 Discord server.
I have a subset of features that I want it to have before I call it "production ready" and a set of features that I want if I'm feeling adventurous and want to put in a lot of work.
"Production Ready" Features
- [ ] Google Calendar Integration for Event Management
- [ ] Create/Manage/Remove Events and Calendars
- [ ] Add Autocomplete for common elements like Calendar Names, Event Names, Timezones, etc.
- [ ] Calendar Announcements for Upcoming Events
- [ ] Poll Creation and Results Announcements
- [ ] Conversation Archiving (Partially working, need to work out edited messages, deleted messages, and attachments)
"Adventurous (Lots of Work)" Features
- [ ] BreadBot Voice Chat Hall Monitor
- [ ] Breadle (BreadBot Idle Bread Baking Game)
- [ ] BreadBot Calendar Send To Email

201
bin/breadmixer.py Normal file
View File

@@ -0,0 +1,201 @@
import argparse
import mysql.connector
import json
import os
import sys
import datetime
import subprocess
import random
import string
from txtai.pipeline import Transcription
from pprint import pprint
argument_parser = argparse.ArgumentParser(description="BreadMixer is used to combine media from Discord Voice Calls")
argument_parser.add_argument("callid", help="The call id that needs to be mixed")
argument_parser.add_argument("config", help="The BreadBot config file location")
argument_parser.add_argument("-f", "--filespercycle", help="The number of files to combine per run of ffmpeg", default=50)
argument_parser.add_argument("-v", "--verbose", help="Make the script tell you more about what it's doing", action="store_true")
args = argument_parser.parse_args()
if args.verbose:
print("Checking config file")
if not os.path.exists(args.config):
print('The file path {path} does not exist'.format(path=args.config))
sys.exit(1)
with open(args.config) as config:
json_config = json.loads(config.read())
config_must_contain = [
"mysql_username",
"mysql_password",
"mysql_db_name",
"mysql_host",
"media_voice_folder"
]
if not all([element in json_config for element in config_must_contain]):
print('One or more of the following config items are missing')
for element in config_must_contain:
print('\t{item}'.format(item=element))
sys.exit(2)
if args.verbose:
print("Connecting to mysql db {dbname}".format(dbname=json_config["mysql_db_name"]))
mydb = mysql.connector.connect(
host=json_config["mysql_host"],
user=json_config["mysql_username"],
password=json_config["mysql_password"],
database=json_config["mysql_db_name"]
)
mydb.autocommit = True
cursor = mydb.cursor()
if args.verbose:
print("Checking to see if call ID {callid} exists".format(callid=args.callid))
cursor.execute("SELECT call_start_time FROM call_states WHERE call_id = %s", [args.callid])
call_start_time = cursor.fetchall()
cursor.close()
if len(call_start_time) == 0:
print('{call_id} does not exist in the database'.format(call_id=args.callid))
sys.exit(3)
if args.verbose:
print("Collecting files from {folder}".format(folder=os.path.join(json_config["media_voice_folder"], args.callid)))
file_dict = {}
for file in os.listdir(os.path.join(json_config["media_voice_folder"], args.callid)):
file_name_no_ext = file.split('.')[0]
timestamp = int(file_name_no_ext.split('-')[0])
user_snowflake = file_name_no_ext.split('-')[1]
file_stamp_as_datetime = datetime.datetime.fromtimestamp(timestamp / 1000)
time_diff = file_stamp_as_datetime - call_start_time[0][0]
file_dict[os.path.join(json_config["media_voice_folder"], args.callid, file)] = dict(
user=user_snowflake,
real_date=file_stamp_as_datetime,
milliseconds_from_starttime=int((time_diff.seconds * 1000) + (time_diff.microseconds / 1000))
)
file_dict_items = [(k, v) for k, v in file_dict.items()]
file_dict_items.sort(key=lambda a: a[1]["milliseconds_from_starttime"])
if args.verbose:
print("Collected files: ")
[print(element) for element in file_dict_items]
list_of_final_merges = []
for i in range(0, len(file_dict_items), args.filespercycle):
input_list = []
filter_list = []
next_endpoint = i + 50 if i + 50 <= len(file_dict_items) else len(file_dict_items)
for j in range(i, next_endpoint, 1):
input_list.append(file_dict_items[j][0])
filter_list.append("[{inputid}]adelay={delay}|{delay}[a{inputid}]".format(
inputid = j - i,
delay = file_dict_items[j][1]["milliseconds_from_starttime"]
))
command_list = ["ffmpeg"]
for input in input_list:
command_list.append("-i")
command_list.append(input)
command_list.append("-filter_complex")
filter_string = "\""
filter_string = filter_string + ';'.join(filter_list)
filter_string = filter_string + ";"
for j in range(i, next_endpoint, 1):
filter_string = filter_string + "[a{inputid}]".format(inputid=j - i)
filter_string = filter_string + "amix=inputs={input_count}:normalize=0[a]\"".format(input_count = next_endpoint - i)
command_list.append(filter_string)
command_list.append("-map")
command_list.append("\"[a]\"")
output_file_name = os.path.join(
json_config["media_voice_folder"],
args.callid,
"intermediate-" + "".join(random.choices(string.ascii_uppercase + string.digits, k=10)) + ".mp3"
)
list_of_final_merges.append(output_file_name)
command_list.append(output_file_name)
ffmpeg_process = subprocess.Popen(' '.join(command_list), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
print(ffmpeg_process.args)
stdout, stderr = ffmpeg_process.communicate()
if (ffmpeg_process.returncode != 0):
print("The ffmpeg process failed")
print(stdout)
print(stderr)
sys.exit(5)
final_command_list = ["ffmpeg"]
for file in list_of_final_merges:
final_command_list.append("-i")
final_command_list.append(file)
final_command_list.append("-filter_complex")
filter_string = "\""
for i in range(len(list_of_final_merges)):
filter_string = filter_string + "[{inputid}]".format(inputid=i)
filter_string = filter_string + "amix=inputs={input_count}:normalize=0[a];[a]volume=3[boosted]\"".format(input_count=len(list_of_final_merges))
final_command_list.append(filter_string)
final_command_list.append("-map")
final_command_list.append("\"[boosted]\"")
final_command_list.append(os.path.join(json_config["media_voice_folder"], args.callid, "output.mp3"))
final_command_process = subprocess.Popen(' '.join(final_command_list), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = final_command_process.communicate()
if (final_command_process.returncode != 0):
print("The final ffmpeg process failed")
print(stdout)
print(stderr)
sys.exit(6)
for file in os.listdir(os.path.join(json_config["media_voice_folder"], args.callid)):
if file.startswith("intermediate"):
os.remove(os.path.join(json_config["media_voice_folder"], args.callid, file))
transcribe = Transcription("openai/whisper-base")
for (k, v) in file_dict.items():
text = transcribe(k)
cursor = mydb.cursor()
cursor.execute("INSERT INTO call_transcriptions (call_id, user_snowflake, speaking_start_time, text) VALUES (%s, %s, %s, %s)", [
args.callid,
v["user"],
v["real_date"],
text
])
cursor.close()

View File

@@ -1,26 +1,70 @@
const fs = require('node:fs');
const path = require('node:path');
const { Client, Events, GatewayIntentBits, Collection } = require('discord.js');
const { token } = require('./config.json');
const { joinVoiceChannel, getVoiceConnection, entersState, VoiceConnectionStatus, EndBehaviorType } = require('@discordjs/voice')
const { token, media_voice_folder, breadbot_logging_config } = require('./config.json');
const winston = require('winston')
const winston_mysql = require('winston-mysql')
const sqlutil = require('./utilities/sqlutil');
const { Console } = require('node:console');
const prism = require('prism-media')
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const logger = winston.createLogger({
level: "silly",
transports: [
new winston.transports.Console({
format: winston.format.simple(),
level: breadbot_logging_config["console_log_level"]
}),
new winston_mysql({
level: breadbot_logging_config["sql_log_level"],
host: breadbot_logging_config["mysql_host"],
user: breadbot_logging_config["mysql_username"],
password: breadbot_logging_config["mysql_password"],
database: breadbot_logging_config["mysql_db_name"],
table: breadbot_logging_config["mysql_table_name"]
})
]
})
sqlutil.buildPool()
const getAllFiles = function(directoryPath, arrayOfFiles) {
const files = fs.readdirSync(directoryPath);
arrayOfFiles = arrayOfFiles || [];
files.forEach(file => {
if (fs.statSync(directoryPath + path.sep + file).isDirectory()) {
arrayOfFiles = getAllFiles(directoryPath + path.sep + file, arrayOfFiles);
}
else {
arrayOfFiles.push(path.join(__dirname, directoryPath, path.sep, file));
}
});
return arrayOfFiles;
};
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMembers, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildVoiceStates] });
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
var activeCalls = []
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
getAllFiles('.' + path.sep + 'commands', [])
.filter(file => file.endsWith('.js'))
.forEach(file => {
const command = require(file);
if ('data' in command && 'execute' in command) {
if ('enabled' in command && command.enabled && 'data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
logger.info(`Loaded command at ${file}`)
}
else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
logger.warn(`The command at ${file} is missing a required "data" or "execute" property or is not enabled`)
}
}
});
client.on(Events.InteractionCreate, async interaction => {
if (!interaction.isChatInputCommand()) return;
@@ -28,7 +72,7 @@ client.on(Events.InteractionCreate, async interaction => {
const command = interaction.client.commands.get(interaction.commandName);
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
logger.error(`No command matching ${interaction.commandName} was found`)
return;
}
@@ -36,13 +80,214 @@ client.on(Events.InteractionCreate, async interaction => {
await command.execute(interaction);
}
catch (error) {
console.error(error);
logger.error(error)
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
});
client.on(Events.GuildCreate, async guild => {
if (guild.available) {
logger.info(`Got into a server, ${guild.name}, ${guild.id}, ${guild.description}`)
sqlutil.registerServerIfMissing(guild.id, guild.name, guild.description).then(server_added => {
if(server_added) {
logger.info(`Server Added ${guild.name}`)
} else {
logger.error(`Server failed to add ${guild.name}`)
}
})
}
})
client.on(Events.VoiceStateUpdate, async (oldState, newState) => {
logger.info("Voice State Update Fired")
if (oldState.channel == null && newState.channel != null) {
if (newState.member.id == client.user.id) {
return //If the user is breadbot, ignore and exit
}
logger.info(`Channel Join Detected ${newState.guild.id} - ${newState.channelId} - ${newState.member.id}`)
//TODO Nothing should happen after this if the channel fails to register
await sqlutil.registerChannelIfMissing(newState.channel.id, newState.guild.id, newState.channel.name)
var existingCallID = await sqlutil.inCall(newState.guild.id, newState.channelId)
logger.info(`Existing call ID ${existingCallID}`)
if (existingCallID == -1) {
logger.info("Joining a call")
var newCallID = await sqlutil.registerNewCall(newState.guild.id, newState.channelId, new Date())
existingCallID = newCallID // To ensure all the stuff that happens after call creation works
logger.info(`Next call ID ${newCallID}`)
fs.mkdirSync(media_voice_folder + path.sep + newCallID, {recursive: true})
const connection = 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", (user_id) => {
if(!receiver.subscriptions.has(user_id)) {
receiver.subscribe(user_id, {
end: {
behavior: EndBehaviorType.AfterSilence,
duration: 500
}
})
.pipe(new prism.opus.OggLogicalBitstream({
opusHead: new prism.opus.OpusHead({
channelCount: 2,
sampleRate: 48000
}),
pageSizeControl: {
maxPackets: 10
}
}))
.pipe(fs.createWriteStream(media_voice_folder + path.sep + newCallID + path.sep + `${Date.now()}-${user_id}.ogg`))
} else {
logger.warn(`Attempted to create new user subscriber for ${user_id} even though one already exists, receiver arm if statement protected against this`)
}
})
receiver.speaking.on("end", (user_id) => {
logger.info(`User ${user_id} stopped speaking`)
})
} else {
logger.warn("Attempted to create a new start and end listener for users who are speaking for some reason, receiver armor if statement protected against this")
}
} catch (error) {
logger.error(error)
}
}
var userRegistered = await sqlutil.registerUserIfMissing(newState.member.id, newState.member.username, newState.member.displayName)
if (userRegistered) {
var markedUserInCall = await sqlutil.registerUserInCall(existingCallID, newState.member.id)
if (!markedUserInCall) {
logger.error(`Something went wrong when marking user in voice call: ${newState.member.id} - ${newState.channelId}`)
}
} else {
logger.error(`Something went wrong when registering user for call: ${newState.member.id} - ${newState.member.username}`)
}
} else if (oldState.channel != null && newState.channel == null) {
if (oldState.member.id == client.user.id) {
return //If the user is breadbot, ignore and exit
}
logger.info(`Channel Exit Detected ${oldState.guild.id} - ${oldState.channelId} - ${oldState.member.id}`)
var existingCallID = await sqlutil.inCall(oldState.guild.id, oldState.channelId)
logger.info(`Existing call ID ${existingCallID}`)
if (existingCallID != -1) {
await sqlutil.deregisterUserInCall(existingCallID, oldState.member.id)
var usersInCall = await sqlutil.getNumberUsersInCall(existingCallID)
if (usersInCall == 0) {
const connection = getVoiceConnection(oldState.guild.id)
connection.disconnect()
var didUpdateEndTime = await sqlutil.updateCallEndTime(existingCallID, new Date())
if (!didUpdateEndTime) {
logger.error(`Failed to mark call ID ${existingCallID} as ended with an end date`)
}
}
} else {
logger.error("Couldn't find a call ID based on the guild and channel info, was Breadbot in the call?")
}
}
})
client.on(Events.MessageCreate, async message => {
console.info("Message Create Fired")
var channel_ok = await sqlutil.registerChannelIfMissing(message.channelId, message.channel.guild.id, message.channel.name)
var user_ok = await sqlutil.registerUserIfMissing(message.author.id, message.author.username, message.author.displayName)
logger.info(`Channel Ok? ${channel_ok} User OK? ${user_ok}`)
if (channel_ok && user_ok) {
await sqlutil.registerMessage(message.id, message.channelId, message.author.id, message.content, message.createdAt).then(async message_add => {
if(message_add) {
logger.info("Message Added")
if (message.attachments.size != 0) {
const all_attachments = message.attachments.map(attachment => sqlutil.registerAttachmentIfMissing(
attachment.id,
message.id,
attachment.name,
attachment.description,
message.createdAt,
attachment.contentType,
attachment.url
))
await Promise.all(all_attachments).catch((error) => {
logger.error(error)
})
}
} else {
logger.error("Failed to log message")
}
})
}
})
client.on(Events.MessageUpdate, async (oldMessage, newMessage) => {
logger.info("Message Update Fired")
logger.info(`Old Message Snowflake: ${oldMessage.id} New Message Snowflake: ${newMessage.id}`)
var editTime = newMessage.editedAt
if (editTime == null) {
editTime = newMessage.createdAt
}
await sqlutil.updateMessageContentIfPresent(newMessage.id, newMessage.content, editTime).then(async (updated) => {
if (updated) {
if (newMessage.attachments.size != 0) {
const all_attachments = newMessage.attachments.map(attachment => sqlutil.registerAttachmentIfMissing(
attachment.id,
newMessage.id,
attachment.name,
attachment.description,
editTime,
attachment.contentType,
attachment.url
))
await Promise.all(all_attachments).catch((error) => {
logger.error(error)
})
}
}
})
})
client.on(Events.MessageDelete, async deletedMessage => {
await sqlutil.markMessageDeletedIfPresent(deletedMessage.id)
})
client.once(Events.ClientReady, c => {
console.log(`Ready! Logged in as ${c.user.tag}`);
logger.info(`Ready! Logged in as ${c.user.tag} - ${c.user.id}`)
});
client.login(token);

33
calendarpulltest.js Normal file
View File

@@ -0,0 +1,33 @@
const { google } = require('googleapis');
const { googlePrivateKey, googleClientEmail, googleProjectNumber } = require('./config.json');
const SCOPES = ['https://www.googleapis.com/auth/calendar'];
async function main() {
const jwtClient = new google.auth.JWT(
googleClientEmail,
null,
googlePrivateKey,
SCOPES,
);
const calendar = new google.calendar({
version: 'v3',
project: googleProjectNumber,
auth: jwtClient,
});
calendar.calendarList.list({}, (err, res) => {
if (err) {
console.log('[ERROR]');
console.log(err.errors);
return;
}
console.log(res.data.items.map(x => x.summary + '---' + x.timeZone));
});
}
main().catch(e => {
console.error(e);
throw e;
});

View File

@@ -0,0 +1,37 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { addCalendar } = require('../../utilities/googlecalendar.js');
// const { getTimeZones } = require('@vvo/tzdb');
module.exports = {
data: new SlashCommandBuilder()
.setName('addcalendar')
.setDescription('Creates a new Google Calendar')
.addStringOption(option =>
option
.setName('name')
.setDescription('The new name for the calendar')
.setRequired(true))
.addStringOption(option =>
option
.setName('timezone')
.setDescription('The Time Zone of this new calendar, must be in IANA format')
.setRequired(true)),
// .addChoices(getTimeZones().map(tz => {
// return { name: tz.name, value: tz.name };
// }))),
async execute(interaction) {
await interaction.deferReply({ ephemeral: true });
const name = interaction.options.getString('name');
const timezone = interaction.options.getString('timezone');
// eslint-disable-next-line no-unused-vars
addCalendar(name, timezone, async (success, message, extra) => {
const embedResponse = new EmbedBuilder()
.setColor(success ? 0x00FF00 : 0xFF0000)
.setTitle(message);
await interaction.editReply({ embeds: [ embedResponse ] });
});
},
};

View File

View File

@@ -0,0 +1,27 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { deleteCalendar } = require('../../utilities/googlecalendar.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('deletecalendar')
.setDescription('Permanently deletes a calendar and it\'s associated events')
.addStringOption(option =>
option
.setName('name')
.setDescription('The name of the calendar you want to delete')
.setRequired(true)),
async execute(interaction) {
await interaction.deferReply({ ephemeral: true });
const name = interaction.options.getString('name');
// eslint-disable-next-line no-unused-vars
deleteCalendar(name, async (success, message, extra) => {
const embedResponse = new EmbedBuilder()
.setColor(success ? 0x0FF00 : 0xFF0000)
.setTitle(message);
await interaction.editReply({ embeds: [ embedResponse ] });
});
},
};

View File

View File

@@ -0,0 +1,20 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getListOfCalendars } = require('../../utilities/googlecalendar');
module.exports = {
data: new SlashCommandBuilder()
.setName('listcalendars')
.setDescription('Lists the currently available calendars'),
async execute(interaction) {
await interaction.deferReply({ ephemeral: true });
getListOfCalendars({}, async (success, message, extra) => {
const embedResponse = new EmbedBuilder()
.setColor(success ? 0x00FF00 : 0xFF0000)
.setTitle(message)
.setDescription(extra.map(x => x.summary + ' --- ' + x.timeZone).join('\n\n'));
await interaction.editReply({ embeds: [ embedResponse ] });
});
},
};

View File

View File

View File

@@ -1,6 +1,7 @@
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
enabled: true,
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),

View File

@@ -1,6 +1,7 @@
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
enabled: true,
data: new SlashCommandBuilder()
.setName('server')
.setDescription('Provides information about the server.'),

View File

@@ -1,6 +1,7 @@
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
enabled: true,
data: new SlashCommandBuilder()
.setName('user')
.setDescription('Provides information about the user.'),

178
database_generator.sql Normal file
View File

@@ -0,0 +1,178 @@
-- --------------------------------------------------------
-- Host: 192.168.1.196
-- Server version: 8.0.33 - MySQL Community Server - GPL
-- Server OS: Linux
-- HeidiSQL Version: 12.5.0.6677
-- --------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- Dumping database structure for breadbot_test
CREATE DATABASE IF NOT EXISTS `breadbot_test` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `breadbot_test`;
-- Dumping structure for table breadbot_test.call_states
CREATE TABLE IF NOT EXISTS `call_states` (
`call_id` int NOT NULL AUTO_INCREMENT,
`server_snowflake` bigint unsigned NOT NULL,
`channel_snowflake` bigint unsigned NOT NULL,
`call_start_time` datetime NOT NULL,
`call_end_time` datetime DEFAULT NULL,
`call_consolidated` bit(1) NOT NULL DEFAULT (0),
`call_transcribed` bit(1) NOT NULL DEFAULT (0),
`call_data_cleaned_up` bit(1) NOT NULL DEFAULT (0),
PRIMARY KEY (`call_id`),
KEY `fk_snowflake_recording_to_server` (`server_snowflake`),
KEY `fk_snowflake_recording_to_channel` (`channel_snowflake`),
CONSTRAINT `fk_snowflake_recording_to_channel` FOREIGN KEY (`channel_snowflake`) REFERENCES `channels` (`channel_snowflake`) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT `fk_snowflake_recording_to_server` FOREIGN KEY (`server_snowflake`) REFERENCES `servers` (`server_snowflake`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=40 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.call_transcriptions
CREATE TABLE IF NOT EXISTS `call_transcriptions` (
`transcription_id` int NOT NULL AUTO_INCREMENT,
`call_id` int NOT NULL,
`user_snowflake` bigint unsigned NOT NULL,
`speaking_start_time` datetime NOT NULL,
`text` longtext NOT NULL,
PRIMARY KEY (`transcription_id`),
KEY `fk_call_id_states_to_transcriptions` (`call_id`),
KEY `fk_snowflake_call_states_to_users` (`user_snowflake`),
CONSTRAINT `fk_call_id_states_to_transcriptions` FOREIGN KEY (`call_id`) REFERENCES `call_states` (`call_id`) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT `fk_snowflake_call_states_to_users` FOREIGN KEY (`user_snowflake`) REFERENCES `users` (`user_snowflake`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.call_users
CREATE TABLE IF NOT EXISTS `call_users` (
`call_users_id` int NOT NULL AUTO_INCREMENT,
`call_id` int NOT NULL,
`user_snowflake` bigint unsigned NOT NULL,
PRIMARY KEY (`call_users_id`) USING BTREE,
KEY `fk_call_id_call_user_to_state` (`call_id`),
KEY `fk_snowflake_call_user_to_users` (`user_snowflake`),
CONSTRAINT `fk_call_id_call_user_to_state` FOREIGN KEY (`call_id`) REFERENCES `call_states` (`call_id`) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT `fk_snowflake_call_user_to_users` FOREIGN KEY (`user_snowflake`) REFERENCES `users` (`user_snowflake`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.channels
CREATE TABLE IF NOT EXISTS `channels` (
`channel_snowflake` bigint unsigned NOT NULL,
`server_snowflake` bigint unsigned NOT NULL,
`channel_name` text,
PRIMARY KEY (`channel_snowflake`),
KEY `fk_snowflake_channel_to_server` (`server_snowflake`),
CONSTRAINT `fk_snowflake_channel_to_server` FOREIGN KEY (`server_snowflake`) REFERENCES `servers` (`server_snowflake`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.messages
CREATE TABLE IF NOT EXISTS `messages` (
`message_snowflake` bigint unsigned NOT NULL,
`channel_snowflake` bigint unsigned NOT NULL,
`user_snowflake` bigint unsigned NOT NULL,
`message_content` text NOT NULL,
`message_timestamp` datetime NOT NULL,
`message_deleted` bit(1) NOT NULL DEFAULT (0),
PRIMARY KEY (`message_snowflake`),
KEY `fk_snowflake_message_to_channel` (`channel_snowflake`),
KEY `fk_snowflake_message_to_user` (`user_snowflake`),
CONSTRAINT `fk_snowflake_message_to_channel` FOREIGN KEY (`channel_snowflake`) REFERENCES `channels` (`channel_snowflake`) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT `fk_snowflake_message_to_user` FOREIGN KEY (`user_snowflake`) REFERENCES `users` (`user_snowflake`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.message_attachments
CREATE TABLE IF NOT EXISTS `message_attachments` (
`attachment_snowflake` bigint unsigned NOT NULL,
`message_snowflake` bigint unsigned NOT NULL DEFAULT (0),
`attachment_name` text NOT NULL,
`attachment_description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`attachment_timestamp` datetime NOT NULL,
`attachment_mime_type` tinytext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`attachment_url` text NOT NULL,
`attachment_downloaded` bit(1) NOT NULL DEFAULT (0),
PRIMARY KEY (`attachment_snowflake`) USING BTREE,
KEY `fk_snowflake_messages_to_attachments` (`message_snowflake`),
CONSTRAINT `fk_snowflake_messages_to_attachments` FOREIGN KEY (`message_snowflake`) REFERENCES `messages` (`message_snowflake`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.message_content_changes
CREATE TABLE IF NOT EXISTS `message_content_changes` (
`message_change_id` int unsigned NOT NULL AUTO_INCREMENT,
`message_snowflake` bigint unsigned NOT NULL,
`message_change_old_timestamp` datetime NOT NULL,
`message_change_old_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
PRIMARY KEY (`message_change_id`),
KEY `fk_snowflake_message_to_message_changes` (`message_snowflake`),
CONSTRAINT `fk_snowflake_message_to_message_changes` FOREIGN KEY (`message_snowflake`) REFERENCES `messages` (`message_snowflake`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.servers
CREATE TABLE IF NOT EXISTS `servers` (
`server_snowflake` bigint unsigned NOT NULL,
`server_name` text NOT NULL,
`server_description` mediumtext,
PRIMARY KEY (`server_snowflake`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.users
CREATE TABLE IF NOT EXISTS `users` (
`user_snowflake` bigint unsigned NOT NULL,
`user_name` text NOT NULL,
`user_displayname` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
PRIMARY KEY (`user_snowflake`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.winston_breadbot
CREATE TABLE IF NOT EXISTS `winston_breadbot` (
`id` int NOT NULL AUTO_INCREMENT,
`level` varchar(16) NOT NULL,
`message` varchar(2048) NOT NULL,
`meta` varchar(2048) NOT NULL,
`timestamp` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
-- Dumping structure for table breadbot_test.winston_sqlutil
CREATE TABLE IF NOT EXISTS `winston_sqlutil` (
`id` int NOT NULL AUTO_INCREMENT,
`level` varchar(16) NOT NULL,
`message` varchar(2048) NOT NULL,
`meta` varchar(2048) NOT NULL,
`timestamp` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Data exporting was unselected.
/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;

35
datetesting.js Normal file
View File

@@ -0,0 +1,35 @@
const readline = require('readline');
const r1 = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
let date1 = null;
let date2 = null;
let result = null;
r1.on('line', line => {
if (line.startsWith('date1')) {
date1 = new Date(line.split(':')[1]);
}
else if (line.startsWith('date2')) {
date2 = new Date(line.split(':')[1]);
}
else if (line.startsWith('result')) {
if (date1 !== null && date2 !== null) {
result = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate(),
date2.getHours(), date2.getMinutes(), date2.getSeconds());
console.log(result);
}
}
else {
console.log('Bad command');
}
});
r1.once('close', () => {
console.log('Closing');
});

View File

@@ -1,15 +1,43 @@
const { REST, Routes } = require('discord.js');
const { clientId, guildId, token } = require('./config.json');
const fs = require('node:fs');
const path = require('node:path');
const getAllFiles = function(directoryPath, arrayOfFiles) {
const files = fs.readdirSync(directoryPath);
arrayOfFiles = arrayOfFiles || [];
files.forEach(file => {
if (fs.statSync(directoryPath + path.sep + file).isDirectory()) {
arrayOfFiles = getAllFiles(directoryPath + path.sep + file, arrayOfFiles);
}
else {
arrayOfFiles.push(path.join(__dirname, directoryPath, path.sep, file));
}
});
return arrayOfFiles;
};
const commands = [];
const allFiles = [];
getAllFiles('.' + path.sep + 'commands', allFiles);
allFiles.forEach(file => {
console.log(file);
});
// Grab all the command files from the commands directory you created earlier
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
const commandFiles = allFiles.filter(file => file.endsWith('.js'));
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
const command = require(`${file}`);
if ('enabled' in command && command.enabled && 'data' in command && 'execute' in command) {
commands.push(command.data.toJSON());
}
else {
console.log(`Skipping ${file} as it is missing one or more required exported fields`);
}
}
// Construct and prepare an instance of the REST module

3003
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,18 @@
"author": "Bradley Bickford",
"license": "ISC",
"dependencies": {
"@discordjs/opus": "^0.9.0",
"@discordjs/voice": "^0.16.0",
"@vvo/tzdb": "^6.77.0",
"discord.js": "^14.6.0",
"googleapis": "^109.0.1"
"googleapis": "^109.0.1",
"libsodium-wrappers": "^0.7.13",
"mysql": "^2.18.1",
"mysql2": "^3.6.3",
"node-crc": "1.3.2",
"prism-media": "^2.0.0-alpha.0",
"winston": "^3.11.0",
"winston-mysql": "^1.1.1"
},
"devDependencies": {
"eslint": "^8.27.0"

18
promise_chain_test.js Normal file
View File

@@ -0,0 +1,18 @@
const mysql = require('mysql2')
const { mysql_username, mysql_password } = require('./config.json')
var connection_pool = mysql.createPool({
host: "10.26.48.207",
user: mysql_username,
password: mysql_password,
database: 'breadbot_test',
connectionLimit: 10
}).promise()
connection_pool.query("SELECT * FROM servers").then(async ([rows, fields]) => {
console.log(rows)
return await connection_pool.query("SELECT * FROM channels").then(([rows, fields]) => {
console.log(rows)
return "SOME TEXT"
})
}).catch(() => {return "SOME FAIL TEXT"}).then(console.log)

122
utilities/googlecalendar.js Normal file
View File

@@ -0,0 +1,122 @@
const { google } = require('googleapis');
const { googlePrivateKey, googleClientEmail, googleProjectNumber } = require('../config.json');
const { stdout } = require('node:process');
const SCOPES = ['https://www.googleapis.com/auth/calendar'];
async function getCalendarReference() {
const jwtClient = new google.auth.JWT(
googleClientEmail,
'../keyfile.json',
googlePrivateKey,
SCOPES,
);
return new google.calendar({
version: 'v3',
project: googleProjectNumber,
auth: jwtClient,
});
}
async function doesCalendarExist(calendarName) {
const calendarReference = await getCalendarReference();
const listResults = await calendarReference.calendarList.list({});
for (const item of listResults.data.items) {
if (item.summary === calendarName) {
return item;
}
}
return null;
}
async function getListOfCalendars(options, callback) {
const calendarReference = await getCalendarReference();
calendarReference.calendarList.list(options, async (err, res) => {
if (err) {
callback(false, 'Failed to retrieve the list of calendars\nAsk Bradley to check Breadbot console');
stdout.write('[ERROR]:');
console.log(err.errors);
return;
}
callback(true, 'Calendar List', res.data.items);
});
}
async function addCalendar(calendarName, timezone, callback) {
const calendarReference = await getCalendarReference();
calendarReference.calendars.insert({
resource: {
summary: calendarName,
timeZone: timezone,
},
},
// eslint-disable-next-line no-unused-vars
async (err, res) => {
if (err) {
callback(false, 'Failed to create new calendar ' + calendarName + '\nAsk Bradley to check Breadbat console', err);
stdout.write('[ERROR]: ');
console.log(err.errors);
return;
}
callback(true, 'Successfully created new calendar ' + calendarName, null);
});
}
async function deleteCalendar(calendarName, callback) {
const exists = await doesCalendarExist(calendarName);
if (exists) {
const calendarReference = await getCalendarReference();
calendarReference.calendars.delete({
calendarId: exists.id,
},
// eslint-disable-next-line no-unused-vars
async (err, res) => {
if (err) {
callback(false, 'Failed to delete ' + calendarName + '\nAsk Bradley to check Breadbot console', err);
stdout.write('[ERROR]: ');
console.log(err);
return;
}
callback(true, 'Successfully deleted ' + calendarName, null);
});
}
else {
callback(false, 'The calendar name specified doesn\'t exist', null);
}
}
async function addEvent(calendarName, eventName, location, description, startDate, startTime, endDate, endTime) {
const exists = await doesCalendarExist(calendarName);
if (exists) {
const calendarReference = await getCalendarReference();
calendarReference.events.insert({
calendarId: exists.id,
resource: {
summary: eventName,
location: location,
description: description,
start: {
}
},
})
}
else {
callback(false, 'The calendar name specified doesn\'t exist', null);
}
}
module.exports = {
getCalendarReference,
getListOfCalendars,
doesCalendarExist,
deleteCalendar,
addCalendar,
};

241
utilities/sqlutil.js Normal file
View File

@@ -0,0 +1,241 @@
const mysql = require('mysql2')
const winston = require('winston')
const winston_mysql = require('winston-mysql')
const {
mysql_username,
mysql_password,
mysql_host,
mysql_db_name,
sqlutil_logging_config
} = require('../config.json')
var connection_pool = null
var logger = null
async function buildPool() {
if (connection_pool == null) {
connection_pool = mysql.createPool({
host: mysql_host,
user: mysql_username,
password: mysql_password,
database: mysql_db_name,
connectionLimit: 10,
multipleStatements: true
}).promise()
}
if (logger == null) {
logger = winston.createLogger({
level: "silly",
transports: [
new winston.transports.Console({
format: winston.format.simple(),
level: sqlutil_logging_config["console_log_level"]
}),
new winston_mysql({
level: sqlutil_logging_config["sql_log_level"],
host: sqlutil_logging_config["mysql_host"],
user: sqlutil_logging_config["mysql_username"],
password: sqlutil_logging_config["mysql_password"],
database: sqlutil_logging_config["mysql_db_name"],
table: sqlutil_logging_config["mysql_table_name"]
})
]
})
}
}
async function registerServerIfMissing(server_snowflake, server_name, server_description) {
return connection_pool.query("SELECT * FROM servers WHERE server_snowflake = ?;", [server_snowflake]).then(async ([rows, fields]) => {
if (rows.length != 0) {
return true
} else {
return await connection_pool.query("INSERT INTO servers VALUES (?, ?, ?)", [server_snowflake, server_name, server_description]).then(([rows, fields]) => {
return true
})
}
}).catch((error) => {
logger.error(error)
return false
})
}
async function registerChannelIfMissing(channel_snowflake, server_snowflake, channel_name) {
return connection_pool.query("SELECT * FROM channels WHERE channel_snowflake = ?;", [channel_snowflake]).then(async ([rows, fields]) => {
if (rows.length != 0) {+
logger.info("Channel already registered")
return true
} else {
logger.info("Channel Not registered, registering")
return await connection_pool.query("INSERT INTO channels VALUES (?, ?, ?)", [channel_snowflake, server_snowflake, channel_name]).then(([rows, fields]) => {
return true
})
}
}).catch((error) => {
logger.error(error)
return false
})
}
async function updateMessageContentIfPresent(message_snowflake, message_content, message_timestamp) {
return connection_pool.query("SELECT message_snowflake FROM messages WHERE message_snowflake = ?", [message_snowflake]).then(async ([rows, fields]) => {
if (rows.length == 0) {
logger.info("Message specified doesn't exist, probably created before breadbot was here")
return false
} else {
return await connection_pool.query(
"INSERT INTO message_content_changes (message_snowflake, message_change_old_timestamp, message_change_old_content) " +
"SELECT message_snowflake, message_timestamp, message_content FROM messages WHERE message_snowflake = ?;" +
"UPDATE messages SET message_timestamp = ?, message_content = ? WHERE message_snowflake = ?;",
[message_snowflake, message_timestamp, message_content, message_snowflake]
).then(([rows, fields]) => {
return true
})
}
}).catch((error) => {
logger.error(error)
return false
})
}
async function markMessageDeletedIfPresent(message_snowflake) {
return connection_pool.query("SELECT message_snowflake FROM messages WHERE message_snowflake = ?", [message_snowflake]).then(async ([rows, fields]) => {
if (rows.length == 0) {
logger.info("Message specified doesn't exists, probably created before breadbot was here")
return false
} else {
return await connection_pool.query(
"UPDATE messages SET message_deleted = 1 WHERE message_snowflake = ?", [message_snowflake]
).then(([rows, fields]) => {
return true
})
}
}).catch((error) => {
logger.error(error)
return false
})
}
async function registerAttachmentIfMissing(attachment_snowflake, message_snowflake, attachment_name, attachment_description, attachment_timestamp, attachment_mime_type, attachment_url) {
return connection_pool.query("SELECT attachment_snowflake FROM message_attachments WHERE attachment_snowflake = ?", [attachment_snowflake]).then(async ([rows, fields]) => {
if (rows.length != 0) {
logger.info("Attachment alreaedy exists")
return true
} else {
return await connection_pool.query(
"INSERT INTO message_attachments (attachment_snowflake, message_snowflake, attachment_name, attachment_description, attachment_timestamp, attachment_mime_type, attachment_url) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)",
[attachment_snowflake, message_snowflake, attachment_name, attachment_description, attachment_timestamp, attachment_mime_type, attachment_url]
).then(([rows, fields]) => {
return true
})
}
}).catch((error) => {
logger.error(error)
return false
})
}
async function registerUserIfMissing(user_snowflake, user_name, user_displayname) {
return connection_pool.query("SELECT * FROM users WHERE user_snowflake = ?;", [user_snowflake]).then(async ([rows, fields]) => {
if (rows.length != 0) {
return true
} else {
return await connection_pool.query("INSERT INTO users VALUES (?, ?, ?)", [user_snowflake, user_name, user_displayname]).then(([rows, fields]) => {
return true
})
}
}).catch((error) => {
logger.error(error)
return false
})
}
async function registerMessage(message_snowflake, channel_snowflake, user_snowflake, message_content, message_timestamp) {
return connection_pool.query("INSERT INTO messages VALUES (?, ?, ?, ?, ?, 0)", [message_snowflake, channel_snowflake, user_snowflake, message_content, message_timestamp]).then(([rows, fields]) => {
return true
}).catch((error) => {
logger.error(error)
return false
})
}
async function inCall(server_snowflake, channel_snowflake) {
return connection_pool.query("SELECT call_id FROM call_states WHERE server_snowflake = ? AND channel_snowflake = ? AND call_end_time IS NULL", [server_snowflake, channel_snowflake]).then(async ([rows, fields]) => {
if (rows.length == 0) {
return -1;
} else {
return rows[0].call_id
}
}).catch((error) => {
logger.error(error)
return -1;
})
}
async function registerNewCall(server_snowflake, channel_snowflake, call_start_time) {
return connection_pool.query("INSERT INTO call_states (server_snowflake, channel_snowflake, call_start_time, call_end_time, call_consolidated, call_transcribed, call_data_cleaned_up) VALUES (?, ?, ?, NULL, 0, 0, 0)", [server_snowflake, channel_snowflake, call_start_time]).then(async ([rows, fields]) => {
if (typeof rows.insertId === 'undefined') {
return -1;
} else {
return rows.insertId
}
}).catch((error) => {
logger.error(error)
return -1;
})
}
async function registerUserInCall(call_id, user_snowflake) {
return connection_pool.query("INSERT INTO call_users (call_id, user_snowflake) VALUES (?, ?)", [call_id, user_snowflake]).then(([rows, fields]) => {
return true
}).catch((error) => {
logger.error(error)
return false
})
}
async function deregisterUserInCall(call_id, user_snowflake) {
return connection_pool.query("DELETE FROM call_users WHERE call_id = ? AND user_snowflake = ?", [call_id, user_snowflake]).then(([rows, field]) => {
return true
}).catch((error) => {
logger.error(error)
return false
})
}
async function getNumberUsersInCall(call_id) {
return connection_pool.query("SELECT COUNT(call_users_id) AS users_in_call FROM call_users WHERE call_id = ?", [call_id]).then(([rows, fields]) => {
return rows[0].users_in_call
}).catch((error) => {
logger.error(error)
return -1
})
}
async function updateCallEndTime(call_id, call_end_time) {
return await connection_pool.query("UPDATE call_states SET call_end_time = ? WHERE call_id = ?", [call_end_time, call_id]).then(async ([rows, fields]) => {
return true
}).catch((error) => {
logger.error(error)
return false;
})
}
module.exports = {
buildPool,
registerMessage,
registerServerIfMissing,
registerChannelIfMissing,
registerUserIfMissing,
registerNewCall,
updateCallEndTime,
inCall,
updateMessageContentIfPresent,
markMessageDeletedIfPresent,
registerAttachmentIfMissing,
registerUserInCall,
deregisterUserInCall,
getNumberUsersInCall
}