Compare commits
No commits in common. "main" and "typescript_refactor" have entirely different histories.
main
...
typescript
@ -1,49 +0,0 @@
|
||||
{
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2021
|
||||
},
|
||||
"rules": {
|
||||
"arrow-spacing": ["warn", { "before": true, "after": true }],
|
||||
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-spacing": "error",
|
||||
"comma-style": "error",
|
||||
"curly": ["error", "multi-line", "consistent"],
|
||||
"dot-location": ["error", "property"],
|
||||
"handle-callback-err": "off",
|
||||
"indent": ["error", "tab"],
|
||||
"keyword-spacing": "error",
|
||||
"max-nested-callbacks": ["error", { "max": 4 }],
|
||||
"max-statements-per-line": ["error", { "max": 2 }],
|
||||
"no-console": "off",
|
||||
"no-empty-function": "error",
|
||||
"no-floating-decimal": "error",
|
||||
"no-inline-comments": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-multi-spaces": "error",
|
||||
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
|
||||
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
|
||||
"no-trailing-spaces": ["error"],
|
||||
"no-var": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"prefer-const": "error",
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"space-before-blocks": "error",
|
||||
"space-before-function-paren": ["error", {
|
||||
"anonymous": "never",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}],
|
||||
"space-in-parens": "error",
|
||||
"space-infix-ops": "error",
|
||||
"space-unary-ops": "error",
|
||||
"spaced-comment": "error",
|
||||
"yoda": "error"
|
||||
}
|
||||
}
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
.env
|
||||
config.json
|
||||
node_modules
|
||||
keyfile.json
|
||||
.env
|
||||
tools/profanity_filter/bin/Words.json
|
||||
tools/profanity_filter/src/Words.json
|
1120
ChartDB(Breadbot).json
Normal file
1120
ChartDB(Breadbot).json
Normal file
File diff suppressed because it is too large
Load Diff
22
README.md
22
README.md
@ -1,22 +0,0 @@
|
||||
<h1>BreadBot</h1>
|
||||
|
||||
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 (May be Removed)
|
||||
|
||||
"Adventurous (Lots of Work)" Features
|
||||
- [ ] BreadBot Voice Chat Hall Monitor
|
||||
- [ ] Breadle (BreadBot Idle Bread Baking Game)
|
||||
- [ ] BreadBot Calendar Send To Email
|
67
breadbot.js
67
breadbot.js
@ -1,67 +0,0 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { Client, Events, GatewayIntentBits, Collection } = require('discord.js');
|
||||
const { token } = require('./config.json');
|
||||
|
||||
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 allFiles = [];
|
||||
getAllFiles('.' + path.sep + 'commands', allFiles);
|
||||
|
||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
|
||||
client.commands = new Collection();
|
||||
|
||||
const commandFiles = allFiles.filter(file => file.endsWith('.js'));
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const command = require(file);
|
||||
|
||||
if ('data' in command && 'execute' in command) {
|
||||
client.commands.set(command.data.name, command);
|
||||
console.log(`[INFO] Loaded command at ${file}`);
|
||||
}
|
||||
else {
|
||||
console.log(`[WARNING] The command at ${file} is missing a required "data" or "execute" property.`);
|
||||
}
|
||||
}
|
||||
|
||||
client.on(Events.InteractionCreate, async interaction => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
const command = interaction.client.commands.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
console.error(`No command matching ${interaction.commandName} was found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||
}
|
||||
});
|
||||
|
||||
client.once(Events.ClientReady, c => {
|
||||
console.log(`Ready! Logged in as ${c.user.tag}`);
|
||||
});
|
||||
|
||||
client.login(token);
|
@ -1,33 +0,0 @@
|
||||
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;
|
||||
});
|
@ -1,37 +0,0 @@
|
||||
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 ] });
|
||||
});
|
||||
},
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
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 ] });
|
||||
});
|
||||
},
|
||||
};
|
@ -1,20 +0,0 @@
|
||||
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 ] });
|
||||
});
|
||||
},
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Replies with Pong!'),
|
||||
async execute(interaction) {
|
||||
await interaction.reply('Pong!');
|
||||
},
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('server')
|
||||
.setDescription('Provides information about the server.'),
|
||||
async execute(interaction) {
|
||||
// interaction.guild is the object representing the Guild in which the command was run
|
||||
await interaction.reply(`This server is ${interaction.guild.name} and has ${interaction.guild.memberCount} members.`);
|
||||
},
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('user')
|
||||
.setDescription('Provides information about the user.'),
|
||||
async execute(interaction) {
|
||||
// interaction.user is the object representing the User who ran the command
|
||||
// interaction.member is the GuildMember object, which represents the user in the specific guild
|
||||
await interaction.reply(`This command was run by ${interaction.user.username}, who joined on ${interaction.member.joinedAt}.`);
|
||||
},
|
||||
};
|
@ -1,35 +0,0 @@
|
||||
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');
|
||||
});
|
@ -1,63 +0,0 @@
|
||||
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 = 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(`${file}`);
|
||||
if ('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
|
||||
const rest = new REST({ version: '10' }).setToken(token);
|
||||
|
||||
// and deploy your commands!
|
||||
(async () => {
|
||||
try {
|
||||
console.log(`Started refreshing ${commands.length} application (/) commands.`);
|
||||
|
||||
// The put method is used to fully refresh all commands in the guild with the current set
|
||||
const data = await rest.put(
|
||||
Routes.applicationGuildCommands(clientId, guildId),
|
||||
{ body: commands },
|
||||
);
|
||||
|
||||
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
|
||||
}
|
||||
catch (error) {
|
||||
// And of course, make sure you catch and log any errors!
|
||||
console.error(error);
|
||||
}
|
||||
})();
|
6003
package-lock.json
generated
6003
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@ -1,19 +1,24 @@
|
||||
{
|
||||
"name": "breadbot",
|
||||
"name": "breadbot-test",
|
||||
"version": "1.0.0",
|
||||
"description": "A very bready Discord bot",
|
||||
"main": "breadbot.js",
|
||||
"main": "breadbot.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "tsx watch src/breadbot.ts",
|
||||
"start": "node dist/breadbot.js",
|
||||
"build": "tsup src/breadbot.ts --minify"
|
||||
},
|
||||
"author": "Bradley Bickford",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@vvo/tzdb": "^6.77.0",
|
||||
"discord.js": "^14.6.0",
|
||||
"googleapis": "^109.0.1"
|
||||
"discord.js": "^14.20.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"sqlite3": "^5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.27.0"
|
||||
"tsup": "^8.5.0",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
84
src/breadbot.ts
Normal file
84
src/breadbot.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { CacheType, ChatInputCommandInteraction, Client, Events, GatewayIntentBits, Guild, GuildBasedChannel, Interaction, Role } from "discord.js"
|
||||
import { utilities } from "./utilties"
|
||||
import { commands } from "./commands"
|
||||
import { config } from "./config"
|
||||
import { SQLCommon } from "./utilties/storage/interfaces"
|
||||
|
||||
export const client : Client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.MessageContent
|
||||
]
|
||||
})
|
||||
|
||||
export let db: SQLCommon
|
||||
|
||||
if (config.DB_MODE == "sqlite") {
|
||||
db = new utilities.sqlite.SqliteDB("breadbot_test.db")
|
||||
|
||||
db.run("PRAGMA foreign_keys = ON")
|
||||
|
||||
utilities.tables.makeTables(db)
|
||||
|
||||
//TODO I really don't want this to be here.
|
||||
utilities.events.messages.setupMessageCapture(client, db)
|
||||
utilities.events.roles.setupRoleCapture(client, db)
|
||||
}
|
||||
|
||||
client.once(Events.ClientReady, () => {
|
||||
// TODO Winston should handle this
|
||||
console.log("Breadbot is ready")
|
||||
|
||||
client.guilds.cache.forEach(async (guild: Guild) => {
|
||||
await utilities.commands.deployCommands(guild.id)
|
||||
|
||||
// TODO handle failures?
|
||||
await utilities.guilds.insertGuild(db, guild)
|
||||
|
||||
guild.channels.cache.forEach(async (channel: GuildBasedChannel) => {
|
||||
await utilities.channels.insertChannel(db, channel)
|
||||
})
|
||||
|
||||
guild.roles.cache.forEach(async (role: Role) => {
|
||||
await utilities.roles.insertRole(db, role)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
client.on(Events.GuildCreate, async (guild : Guild) => {
|
||||
await utilities.commands.deployCommands(guild.id)
|
||||
await utilities.guilds.insertGuild(db, guild)
|
||||
|
||||
guild.channels.cache.forEach(async (channel: GuildBasedChannel) => {
|
||||
await utilities.channels.insertChannel(db, channel)
|
||||
})
|
||||
})
|
||||
|
||||
client.on(Events.ChannelCreate, async (channel) => {
|
||||
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)
|
55
src/commands/breadalert.ts
Normal file
55
src/commands/breadalert.ts
Normal file
@ -0,0 +1,55 @@
|
||||
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!")
|
||||
}
|
60
src/commands/breadthread.ts
Normal file
60
src/commands/breadthread.ts
Normal file
@ -0,0 +1,60 @@
|
||||
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")
|
||||
}
|
||||
}
|
9
src/commands/index.ts
Normal file
9
src/commands/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as ping from "./ping";
|
||||
import * as breadalert from "./breadalert"
|
||||
import * as breadthread from "./breadthread"
|
||||
|
||||
export const commands = {
|
||||
ping,
|
||||
breadalert,
|
||||
breadthread
|
||||
}
|
11
src/commands/ping.ts
Normal file
11
src/commands/ping.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { CommandInteraction, SlashCommandBuilder } from "discord.js"
|
||||
|
||||
export const enabled : boolean = true
|
||||
|
||||
export const data : SlashCommandBuilder = new SlashCommandBuilder()
|
||||
.setName("ping")
|
||||
.setDescription("Replies with Pong!")
|
||||
|
||||
export async function execute(interaction: CommandInteraction) {
|
||||
return interaction.reply("Pong!")
|
||||
}
|
15
src/config.ts
Normal file
15
src/config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import dotenv from "dotenv"
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const { DISCORD_TOKEN, DISCORD_CLIENT_ID, DB_MODE } = process.env
|
||||
|
||||
if (!DISCORD_TOKEN || !DISCORD_CLIENT_ID || !DB_MODE) {
|
||||
throw new Error("Missing environment variables")
|
||||
}
|
||||
|
||||
export const config = {
|
||||
DISCORD_TOKEN,
|
||||
DISCORD_CLIENT_ID,
|
||||
DB_MODE
|
||||
}
|
57
src/utilties/breadbot/breadthread.ts
Normal file
57
src/utilties/breadbot/breadthread.ts
Normal file
@ -0,0 +1,57 @@
|
||||
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]
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
51
src/utilties/discord/channels.ts
Normal file
51
src/utilties/discord/channels.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { SQLCommon } from "../storage/interfaces";
|
||||
import { DMChannel, GuildBasedChannel, PartialDMChannel } from "discord.js";
|
||||
import { SQLResult } from "../storage/enumerations";
|
||||
|
||||
export async function doesChannelExistByID(db: SQLCommon, channelID: string) : Promise<boolean> {
|
||||
const queryResult : Object[] = await db.getAllParameterized(
|
||||
"SELECT * FROM channels WHERE channel_snowflake = ?",
|
||||
[channelID]
|
||||
)
|
||||
|
||||
return queryResult.length != 0
|
||||
}
|
||||
|
||||
export async function doesChannelExist(db: SQLCommon, channel : GuildBasedChannel | DMChannel | PartialDMChannel) : Promise<boolean> {
|
||||
const queryResult : Object[] = await db.getAllParameterized(
|
||||
"SELECT * FROM channels WHERE channel_snowflake = ?",
|
||||
[channel.id]
|
||||
)
|
||||
|
||||
return queryResult.length != 0
|
||||
}
|
||||
|
||||
export async function insertChannel(db: SQLCommon, channel: GuildBasedChannel | DMChannel | PartialDMChannel) : Promise<SQLResult> {
|
||||
const alreadyExists: boolean = await doesChannelExist(db, channel)
|
||||
|
||||
if(alreadyExists) {
|
||||
return SQLResult.ALREADYEXISTS
|
||||
}
|
||||
|
||||
try {
|
||||
if (channel.isDMBased()) {
|
||||
await db.runParameterized(
|
||||
"INSERT INTO channels VALUES (?, ?, ?, ?, ?)",
|
||||
[channel.id, null, channel.recipient?.username, channel.isThread(), channel.isDMBased()]
|
||||
)
|
||||
} else {
|
||||
await db.runParameterized(
|
||||
"INSERT INTO channels VALUES (?, ?, ?, ?, ?)",
|
||||
[channel.id, channel.guild.id, channel.name, channel.isThread(), channel.isDMBased()]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return SQLResult.CREATED
|
||||
} catch (err) {
|
||||
//TODO Winston should handle this
|
||||
console.log("CHANNEL INSERT ERROR")
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
32
src/utilties/discord/commands.ts
Normal file
32
src/utilties/discord/commands.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { REST, Routes } from "discord.js"
|
||||
import { config } from "../../config"
|
||||
import { commands } from "../../commands"
|
||||
|
||||
const commandsData = Object.values(commands)
|
||||
.filter((command) => command.enabled)
|
||||
.map((command) => command.data)
|
||||
|
||||
const rest : REST = new REST({ version: "10" }).setToken(config.DISCORD_TOKEN)
|
||||
|
||||
export async function deployCommands(guildId: string) {
|
||||
try {
|
||||
// TODO Winston should handle this
|
||||
console.log(`Refreshing slash commands for ${guildId}`)
|
||||
|
||||
await rest.put(
|
||||
Routes.applicationGuildCommands(
|
||||
config.DISCORD_CLIENT_ID,
|
||||
guildId
|
||||
),
|
||||
{
|
||||
body: commandsData
|
||||
}
|
||||
)
|
||||
|
||||
// TODO Winston should handle this
|
||||
console.log(`Successfully reloaded slash commands for ${guildId}`)
|
||||
} catch (error) {
|
||||
// TODO Winston should handle this
|
||||
console.error(error)
|
||||
}
|
||||
}
|
33
src/utilties/discord/guilds.ts
Normal file
33
src/utilties/discord/guilds.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Guild } from "discord.js";
|
||||
import { SQLCommon } from "../storage/interfaces";
|
||||
import { SQLResult } from "../storage/enumerations";
|
||||
|
||||
export async function doesGuildExist(db: SQLCommon, guild : Guild) : Promise<boolean> {
|
||||
const queryResult : Object[] = await db.getAllParameterized(
|
||||
"SELECT * FROM servers WHERE server_snowflake = ?",
|
||||
[guild.id]
|
||||
)
|
||||
|
||||
return queryResult.length != 0
|
||||
}
|
||||
|
||||
export async function insertGuild(db: SQLCommon, guild: Guild) : Promise<SQLResult> {
|
||||
const alreadyExists: boolean = await doesGuildExist(db, guild)
|
||||
|
||||
if (alreadyExists) {
|
||||
return SQLResult.ALREADYEXISTS
|
||||
}
|
||||
try {
|
||||
await db.runParameterized(
|
||||
"INSERT INTO servers VALUES (?, ?, ?)",
|
||||
[guild.id, guild.name, guild.description]
|
||||
)
|
||||
|
||||
return SQLResult.CREATED
|
||||
} catch (err) {
|
||||
console.log("Insert Failed")
|
||||
//TODO Winston should handle this
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
118
src/utilties/discord/messages.ts
Normal file
118
src/utilties/discord/messages.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { Attachment, Message, OmitPartialGroupDMChannel, PartialMessage } from "discord.js";
|
||||
import { SQLCommon } from "../storage/interfaces";
|
||||
import { SQLResult } from "../storage/enumerations";
|
||||
|
||||
// TODO Do partial messages affect other functionality elsewhere?
|
||||
|
||||
export async function doesMessageExist(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) : Promise<boolean> {
|
||||
const queryResult: Object[] = await db.getAllParameterized(
|
||||
"SELECT * FROM messages WHERE message_snowflake = ?",
|
||||
[message.id]
|
||||
)
|
||||
|
||||
return queryResult.length != 0
|
||||
}
|
||||
|
||||
export async function doesAttachmentExist(db: SQLCommon, attachment: Attachment) : Promise<boolean> {
|
||||
const queryResult: Object[] = await db.getAllParameterized(
|
||||
"SELECT * FROM message_attachments WHERE attachment_snowflake = ?",
|
||||
[attachment.id]
|
||||
)
|
||||
|
||||
return queryResult.length != 0
|
||||
}
|
||||
|
||||
export async function insertMessage(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>>) : Promise<SQLResult> {
|
||||
const alreadyExists: boolean = await doesMessageExist(db, message)
|
||||
|
||||
if(alreadyExists) {
|
||||
return SQLResult.ALREADYEXISTS
|
||||
}
|
||||
|
||||
try {
|
||||
await db.runParameterized(
|
||||
"INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[message.id, message.channel.id, message.author.id,
|
||||
message.content, message.createdTimestamp, 0]
|
||||
)
|
||||
|
||||
return SQLResult.CREATED
|
||||
} catch (err) {
|
||||
//TODO Winston should handle this
|
||||
console.log("MESSAGE INSERTION ERROR")
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMessageContentHistory(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>>) : Promise<SQLResult> {
|
||||
const messageIDExists: boolean = await doesMessageExist(db, message)
|
||||
|
||||
if(!messageIDExists) {
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
|
||||
try {
|
||||
console.log([message.id, message.editedTimestamp ?? message.createdTimestamp, message.content, message.id])
|
||||
await db.runParameterized(
|
||||
"INSERT INTO message_content_changes (message_snowflake, message_change_old_timestamp, message_change_old_content) " +
|
||||
"SELECT messages.message_snowflake, message_timestamp, message_content FROM messages WHERE message_snowflake = ?;",
|
||||
[message.id]
|
||||
)
|
||||
|
||||
await db.runParameterized(
|
||||
"UPDATE messages SET message_timestamp = ?, message_content = ? WHERE message_snowflake = ?;",
|
||||
[message.editedTimestamp ?? message.createdTimestamp, message.content, message.id]
|
||||
)
|
||||
|
||||
return SQLResult.UPDATED
|
||||
} catch (err) {
|
||||
//TODO Winston should handle this
|
||||
console.log("MESSAGE MODIFY FAILED")
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
export async function markMessageDeleted(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) : Promise<SQLResult> {
|
||||
const messageIDExists: boolean = await doesMessageExist(db, message)
|
||||
|
||||
if(!messageIDExists) {
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
|
||||
try {
|
||||
await db.runParameterized(
|
||||
"UPDATE messages SET message_deleted = 1 WHERE message_snowflake = ?",
|
||||
[message.id]
|
||||
)
|
||||
|
||||
return SQLResult.UPDATED
|
||||
} catch (err) {
|
||||
// TODO Winston should handle this
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
export async function insertAttachment(db: SQLCommon, attachment: Attachment, message: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) : Promise<SQLResult> {
|
||||
const alreadyExists: boolean = await doesAttachmentExist(db, attachment)
|
||||
|
||||
if(alreadyExists) {
|
||||
return SQLResult.ALREADYEXISTS
|
||||
}
|
||||
|
||||
try {
|
||||
await db.runParameterized(
|
||||
"INSERT INTO message_attachments VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[attachment.id, message.id, attachment.name, attachment.description, message.createdTimestamp,
|
||||
attachment.contentType, attachment.url, 0]
|
||||
)
|
||||
|
||||
return SQLResult.CREATED
|
||||
} catch (err) {
|
||||
// TODO Winston should handle this
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
16
src/utilties/discord/regex_matching.ts
Normal file
16
src/utilties/discord/regex_matching.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Guild } from "discord.js";
|
||||
import { SQLCommon } from "../storage/interfaces";
|
||||
|
||||
export async function getRegexesForGuild(db: SQLCommon, guild: Guild): Promise<any[]> {
|
||||
return db.getAllParameterized(
|
||||
"SELECT * FROM message_regexes WHERE server_snowflake = ?",
|
||||
[guild.id]
|
||||
)
|
||||
}
|
||||
|
||||
export async function getRoleExclusionSnowflakesForGuild(db: SQLCommon, guild: Guild): Promise<string[]> {
|
||||
return (await db.getAllParameterized(
|
||||
"SELECT role_snowflake FROM message_regex_no_role_check WHERE server_snowflake = ?",
|
||||
[guild.id]
|
||||
)).map((o) => (o as any).role_snowflake)
|
||||
}
|
75
src/utilties/discord/roles.ts
Normal file
75
src/utilties/discord/roles.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Role } from "discord.js";
|
||||
import { SQLCommon } from "../storage/interfaces";
|
||||
import { SQLResult } from "../storage/enumerations";
|
||||
|
||||
export async function doesRoleExist(db: SQLCommon, role : Role) : Promise<boolean> {
|
||||
const queryResult : Object[] = await db.getAllParameterized(
|
||||
"SELECT * FROM roles WHERE role_snowflake = ?",
|
||||
[role.id]
|
||||
)
|
||||
|
||||
return queryResult.length != 0
|
||||
}
|
||||
|
||||
export async function insertRole(db: SQLCommon, role: Role) : Promise<SQLResult> {
|
||||
const alreadyExists: boolean = await doesRoleExist(db, role)
|
||||
|
||||
if(alreadyExists) {
|
||||
return SQLResult.ALREADYEXISTS
|
||||
}
|
||||
|
||||
try {
|
||||
await db.runParameterized(
|
||||
"INSERT INTO roles VALUES (?, ?, ?, 0)",
|
||||
[role.id, role.guild.id, role.name]
|
||||
)
|
||||
|
||||
return SQLResult.CREATED
|
||||
} catch (err) {
|
||||
console.log("ROLE INSERT ERROR")
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateRole(db: SQLCommon, role: Role): Promise<SQLResult> {
|
||||
const roleExists: boolean = await doesRoleExist(db, role)
|
||||
|
||||
if(!roleExists) {
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
|
||||
try {
|
||||
await db.runParameterized(
|
||||
"UPDATE roles SET role_name = ? WHERE role_snowflake = ?",
|
||||
[role.name, role.id]
|
||||
)
|
||||
|
||||
return SQLResult.UPDATED
|
||||
} catch (err) {
|
||||
console.log("ROLE UPDATE FAILED")
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
export async function markRoleDeleted(db: SQLCommon, role: Role) : Promise<SQLResult> {
|
||||
const roleExists: boolean = await doesRoleExist(db, role)
|
||||
|
||||
if(!roleExists) {
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
|
||||
try {
|
||||
await db.runParameterized(
|
||||
"UPDATE roles SET is_deleted = 1 WHERE role_snowflake = ?",
|
||||
[role.id]
|
||||
)
|
||||
|
||||
return SQLResult.UPDATED
|
||||
} catch (err) {
|
||||
console.log("ROLE DELETE FAILED")
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
34
src/utilties/discord/users.ts
Normal file
34
src/utilties/discord/users.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { User } from "discord.js";
|
||||
import { SQLCommon } from "../storage/interfaces";
|
||||
import { SQLResult } from "../storage/enumerations";
|
||||
|
||||
export async function doesUserExist(db: SQLCommon, user: User): Promise<boolean> {
|
||||
const queryResult: Object[] = await db.getAllParameterized(
|
||||
"SELECT * FROM users WHERE user_snowflake = ?",
|
||||
[user.id]
|
||||
)
|
||||
|
||||
return queryResult.length != 0
|
||||
}
|
||||
|
||||
export async function insertUser(db: SQLCommon, user: User): Promise<SQLResult> {
|
||||
const alreadyExists: boolean = await doesUserExist(db, user)
|
||||
|
||||
if(alreadyExists) {
|
||||
return SQLResult.ALREADYEXISTS
|
||||
}
|
||||
|
||||
try {
|
||||
await db.runParameterized(
|
||||
"INSERT INTO users VALUES (?, ?, ?)",
|
||||
[user.id, user.username, user.displayName]
|
||||
)
|
||||
|
||||
return SQLResult.CREATED
|
||||
} catch (err) {
|
||||
//TODO Winston should handle this
|
||||
console.log("USER INSERT ERROR")
|
||||
console.log(err)
|
||||
return SQLResult.FAILED
|
||||
}
|
||||
}
|
7
src/utilties/events/index.ts
Normal file
7
src/utilties/events/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import * as roles from "./roles"
|
||||
import * as messages from "./messages"
|
||||
|
||||
export const events = {
|
||||
roles,
|
||||
messages
|
||||
}
|
63
src/utilties/events/messages.ts
Normal file
63
src/utilties/events/messages.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Client, Events, Message, OmitPartialGroupDMChannel, PartialMessage } from "discord.js";
|
||||
import { SQLResult } from "../storage/enumerations";
|
||||
import { SQLCommon } from "../storage/interfaces";
|
||||
import { insertChannel } from "../discord/channels";
|
||||
import { insertUser } from "../discord/users";
|
||||
import { insertAttachment, insertMessage, markMessageDeleted, updateMessageContentHistory } from "../discord/messages";
|
||||
|
||||
export function setupMessageCapture(client: Client, db: SQLCommon) {
|
||||
client.on(Events.MessageCreate, async (message) => {
|
||||
await processMessageCreate(db, message)
|
||||
})
|
||||
|
||||
client.on(Events.MessageUpdate, async (oldMessage, newMessage) => {
|
||||
await processMessageModify(db, newMessage)
|
||||
})
|
||||
|
||||
client.on(Events.MessageDelete, async (deletedMessage) => {
|
||||
await processMessageDeleted(db, deletedMessage)
|
||||
})
|
||||
}
|
||||
|
||||
async function processMessageCreate(db: SQLCommon, message: OmitPartialGroupDMChannel<Message<boolean>>) {
|
||||
const channelOk: SQLResult = await insertChannel(db, message.channel)
|
||||
const userOk: SQLResult = await insertUser(db, message.author)
|
||||
|
||||
if (channelOk == SQLResult.ALREADYEXISTS || channelOk == SQLResult.CREATED ||
|
||||
userOk == SQLResult.ALREADYEXISTS || userOk == SQLResult.CREATED) {
|
||||
|
||||
await insertMessage(db, message)
|
||||
// TODO observe success of message insertion
|
||||
if(message.attachments.size != 0) {
|
||||
const allAttachments: void[] = message.attachments.map((attachment) => {
|
||||
insertAttachment(db, attachment, message)
|
||||
})
|
||||
|
||||
await Promise.all(allAttachments).catch((error) => {
|
||||
// TODO Winston should handle this
|
||||
console.log("MESSAGE ATTACHMENT INSERT ERROR")
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processMessageModify(db: SQLCommon, newMessage: OmitPartialGroupDMChannel<Message<boolean>>) {
|
||||
await updateMessageContentHistory(db, newMessage)
|
||||
|
||||
if(newMessage.attachments.size != 0) {
|
||||
const allAttachments: void[] = newMessage.attachments.map((attachment) => {
|
||||
insertAttachment(db, attachment, newMessage)
|
||||
})
|
||||
|
||||
await Promise.all(allAttachments).catch((error) => {
|
||||
// TODO Winston should handle this
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function processMessageDeleted(db: SQLCommon, deletedMessage: OmitPartialGroupDMChannel<Message<boolean>> | PartialMessage) {
|
||||
await markMessageDeleted(db, deletedMessage)
|
||||
}
|
23
src/utilties/events/roles.ts
Normal file
23
src/utilties/events/roles.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Client, Events } from "discord.js";
|
||||
import { SQLCommon } from "../storage/interfaces";
|
||||
import { SQLResult } from "../storage/enumerations";
|
||||
import { insertRole, markRoleDeleted, updateRole } from "../discord/roles";
|
||||
import { insertGuild } from "../discord/guilds";
|
||||
|
||||
export function setupRoleCapture(client: Client, db: SQLCommon) {
|
||||
client.on(Events.GuildRoleCreate, async (role) => {
|
||||
const serverOk: SQLResult = await insertGuild(db, role.guild)
|
||||
|
||||
if (serverOk == SQLResult.ALREADYEXISTS || serverOk == SQLResult.CREATED) {
|
||||
await insertRole(db, role)
|
||||
}
|
||||
})
|
||||
|
||||
client.on(Events.GuildRoleUpdate, async (role) => {
|
||||
await updateRole(db, role)
|
||||
})
|
||||
|
||||
client.on(Events.GuildRoleDelete, async (role) => {
|
||||
await markRoleDeleted(db, role)
|
||||
})
|
||||
}
|
21
src/utilties/index.ts
Normal file
21
src/utilties/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as commands from "./discord/commands"
|
||||
import * as sqlite from "./storage/sqlite"
|
||||
import * as tables from "./storage/tables"
|
||||
import * as guilds from "./discord/guilds"
|
||||
import * as channels from "./discord/channels"
|
||||
import * as users from "./discord/users"
|
||||
import * as breadthread from "./breadbot/breadthread"
|
||||
import * as roles from "./discord/roles"
|
||||
import { events } from "./events"
|
||||
|
||||
export const utilities = {
|
||||
commands,
|
||||
sqlite,
|
||||
tables,
|
||||
guilds,
|
||||
channels,
|
||||
users,
|
||||
events,
|
||||
breadthread,
|
||||
roles
|
||||
}
|
7
src/utilties/storage/enumerations.ts
Normal file
7
src/utilties/storage/enumerations.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum SQLResult {
|
||||
CREATED,
|
||||
UPDATED,
|
||||
DELETED,
|
||||
ALREADYEXISTS,
|
||||
FAILED
|
||||
}
|
7
src/utilties/storage/interfaces.ts
Normal file
7
src/utilties/storage/interfaces.ts
Normal file
@ -0,0 +1,7 @@
|
||||
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[]>
|
||||
}
|
||||
|
68
src/utilties/storage/sqlite.ts
Normal file
68
src/utilties/storage/sqlite.ts
Normal file
@ -0,0 +1,68 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
29
src/utilties/storage/tables.ts
Normal file
29
src/utilties/storage/tables.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { SQLCommon } from "./interfaces";
|
||||
|
||||
const tables: string[] = [
|
||||
"CREATE TABLE IF NOT EXISTS servers (server_snowflake bigint NOT NULL PRIMARY KEY,server_name text NOT NULL,server_description mediumtext);",
|
||||
"CREATE TABLE IF NOT EXISTS channels (channel_snowflake bigint NOT NULL PRIMARY KEY,server_snowflake bigint,channel_name text,is_thread bit NOT NULL,is_dm bit NOT NULL);",
|
||||
"CREATE TABLE IF NOT EXISTS users (user_snowflake bigint NOT NULL PRIMARY KEY,user_name text NOT NULL,user_displayname text);",
|
||||
"CREATE TABLE IF NOT EXISTS messages (message_snowflake bigint NOT NULL PRIMARY KEY,channel_snowflake bigint NOT NULL,user_snowflake bigint NOT NULL,message_content longtext NOT NULL,message_timestamp datetime NOT NULL,message_deleted bit NOT NULL);",
|
||||
"CREATE TABLE IF NOT EXISTS message_content_changes (message_change_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,message_snowflake bigint NOT NULL,message_change_old_timestamp datetime NOT NULL,message_change_old_content longtext NOT NULL);",
|
||||
"CREATE TABLE IF NOT EXISTS message_attachments (attachment_snowflake bigint NOT NULL PRIMARY KEY,message_snowflake bigint NOT NULL,attachment_name text NOT NULL,attachment_description text,attachment_timestamp datetime NOT NULL,attachment_mime_type text,attachment_url text NOT NULL,attachment_downloaded bit NOT NULL);",
|
||||
"CREATE TABLE IF NOT EXISTS breadthread_autolock (breadthread_autolock_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,channel_snowflake bigint NOT NULL,inactivity_seconds bigint NOT NULL,locked bit NOT NULL);",
|
||||
"CREATE TABLE IF NOT EXISTS roles (role_snowflake bigint NOT NULL PRIMARY KEY,server_snowflake bigint NOT NULL,role_name text NOT NULL,is_deleted bit NOT NULL);",
|
||||
"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);"
|
||||
]
|
||||
|
||||
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)))
|
||||
}
|
||||
|
22
src/utilties/time/conversions.ts
Normal file
22
src/utilties/time/conversions.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export function timeShorthandToSeconds(shorthand: string) : number {
|
||||
let totalSeconds: number = 0
|
||||
|
||||
shorthand.split(" ").forEach((value) => {
|
||||
console.log(value)
|
||||
const unit: string = value.substring(value.length - 1, value.length)
|
||||
|
||||
if (unit == "d" || unit == "D") {
|
||||
totalSeconds += Number.parseInt(value.substring(0, value.length - 1)) * 86400
|
||||
} else if (unit == "h" || unit == "H") {
|
||||
totalSeconds += Number.parseInt(value.substring(0, value.length - 1)) * 3600
|
||||
} else if (unit == "m" || unit == "M") {
|
||||
totalSeconds += Number.parseInt(value.substring(0, value.length - 1)) * 60
|
||||
} else if (unit == "s" || unit == "S") {
|
||||
totalSeconds += Number.parseInt(value.substring(0, value.length - 1))
|
||||
} else {
|
||||
console.log(`An invalid shorthand directive was sent ${value}`)
|
||||
}
|
||||
})
|
||||
|
||||
return totalSeconds
|
||||
}
|
25
tools/profanity_filter/.vscode/launch.json
vendored
Normal file
25
tools/profanity_filter/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "java",
|
||||
"name": "Current File",
|
||||
"request": "launch",
|
||||
"mainClass": "${file}"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "RegexGenerator",
|
||||
"request": "launch",
|
||||
"mainClass": "RegexGenerator",
|
||||
"projectName": "ProfanityFilter_ceb3bd68",
|
||||
"args": [
|
||||
"ass",
|
||||
"shit"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
7
tools/profanity_filter/.vscode/settings.json
vendored
Normal file
7
tools/profanity_filter/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"java.project.sourcePaths": ["src"],
|
||||
"java.project.outputPath": "bin",
|
||||
"java.project.referencedLibraries": [
|
||||
"lib/**/*.jar"
|
||||
]
|
||||
}
|
18
tools/profanity_filter/README.md
Normal file
18
tools/profanity_filter/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
## Getting Started
|
||||
|
||||
Welcome to the VS Code Java world. Here is a guideline to help you get started to write Java code in Visual Studio Code.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
The workspace contains two folders by default, where:
|
||||
|
||||
- `src`: the folder to maintain sources
|
||||
- `lib`: the folder to maintain dependencies
|
||||
|
||||
Meanwhile, the compiled output files will be generated in the `bin` folder by default.
|
||||
|
||||
> If you want to customize the folder structure, open `.vscode/settings.json` and update the related settings there.
|
||||
|
||||
## Dependency Management
|
||||
|
||||
The `JAVA PROJECTS` view allows you to manage your dependencies. More details can be found [here](https://github.com/microsoft/vscode-java-dependency#manage-dependencies).
|
BIN
tools/profanity_filter/bin/generators/ExceptionGenerator.class
Normal file
BIN
tools/profanity_filter/bin/generators/ExceptionGenerator.class
Normal file
Binary file not shown.
BIN
tools/profanity_filter/bin/generators/RegexGenerator.class
Normal file
BIN
tools/profanity_filter/bin/generators/RegexGenerator.class
Normal file
Binary file not shown.
@ -0,0 +1,34 @@
|
||||
package generators;
|
||||
|
||||
public class ExceptionGenerator {
|
||||
public static void main(String[] args) {
|
||||
//position 0 should be the base word, the rest should be exceptions
|
||||
String base = args[0].toLowerCase();
|
||||
|
||||
String prefixes = "(?<!";
|
||||
String regex = RegexGenerator.regexGenerator(base);
|
||||
String suffixes = "(?!";
|
||||
|
||||
for (int i = 1; i < args.length; i++) {
|
||||
args[i] = "$" + args[i] + "$";
|
||||
|
||||
String prefix = args[i].toLowerCase().split(base)[0];
|
||||
|
||||
String suffix = args[i].toLowerCase().split(base)[1];
|
||||
|
||||
if (!prefix.equals("$")) {
|
||||
prefixes += RegexGenerator.regexGenerator(prefix) + "|";
|
||||
}
|
||||
|
||||
if (!suffix.equals("$")) {
|
||||
suffixes += RegexGenerator.regexGenerator(suffix) + "|";
|
||||
}
|
||||
}
|
||||
|
||||
prefixes = prefixes.substring(0, prefixes.length() - 1);
|
||||
suffixes = suffixes.substring(0, suffixes.length() - 1);
|
||||
|
||||
|
||||
System.out.println(prefixes + ")" + regex + suffixes + ")");
|
||||
}
|
||||
}
|
77
tools/profanity_filter/src/generators/RegexGenerator.java
Normal file
77
tools/profanity_filter/src/generators/RegexGenerator.java
Normal file
@ -0,0 +1,77 @@
|
||||
package generators;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class RegexGenerator {
|
||||
private static Map<Character, String> dictionary;
|
||||
|
||||
static {
|
||||
Map<Character, String> map = new HashMap<>();
|
||||
map.put('a', "[a@*]");
|
||||
map.put('b', "([b38]|\\\\|3)");
|
||||
map.put('c', "[c(k]");
|
||||
map.put('d', "[d]");
|
||||
map.put('e', "[e3*]");
|
||||
map.put('f', "([f]|ph)");
|
||||
map.put('g', "[g]");
|
||||
map.put('h', "[h]");
|
||||
map.put('i', "[il1!*]");
|
||||
map.put('j', "[j]");
|
||||
map.put('k', "[kc]");
|
||||
map.put('l', "[li]");
|
||||
map.put('m', "([m]|rn)");
|
||||
map.put('n', "[n]");
|
||||
map.put('o', "[o0pq*]");
|
||||
map.put('p', "[p]");
|
||||
map.put('q', "[q]");
|
||||
map.put('r', "[r]");
|
||||
map.put('s', "[sz5$]");
|
||||
map.put('t', "[t7+]");
|
||||
map.put('u', "[uv*]");
|
||||
map.put('v', "[v]");
|
||||
map.put('w', "([w]|vv)");
|
||||
map.put('x', "[x]");
|
||||
map.put('y', "[y]");
|
||||
map.put('z', "[z]");
|
||||
map.put(' ', "");
|
||||
map.put('1', "([1]|one)");
|
||||
map.put('2', "([2]|two)");
|
||||
map.put('3', "([3]|three)");
|
||||
map.put('4', "([4]|four)");
|
||||
map.put('5', "([5]|five)");
|
||||
map.put('6', "([6]|six)");
|
||||
map.put('7', "([7]|seven)");
|
||||
map.put('8', "([8]|eight)");
|
||||
map.put('9', "([9]|nine)");
|
||||
map.put('0', "([0]|zero)");
|
||||
dictionary = Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
for (String s : args) {
|
||||
String regex = "(\\\\b|^)";
|
||||
|
||||
for (char c : s.toLowerCase().toCharArray()) {
|
||||
regex += dictionary.get(c) + "+[\\\\s\\\\n\\\\W]*";
|
||||
}
|
||||
|
||||
regex += "(\\\\b|$)";
|
||||
|
||||
System.out.println(regex);
|
||||
}
|
||||
}
|
||||
|
||||
public static String regexGenerator(String str) {
|
||||
String regex = "(\\\\b|^)";
|
||||
|
||||
for (char c : str.toLowerCase().toCharArray()) {
|
||||
regex += dictionary.get(c) + "+[\\\\s\\\\n\\\\W]*";
|
||||
}
|
||||
|
||||
regex += "(\\\\b|$)";
|
||||
|
||||
return regex;
|
||||
}
|
||||
}
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"removeComments": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
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,
|
||||
};
|
Loading…
Reference in New Issue
Block a user