Finished adding rest of database access classes

This commit is contained in:
WatDuhHekBro 2021-12-09 01:06:55 -06:00
parent 658348d993
commit 3b0b7bd559
No known key found for this signature in database
GPG Key ID: E128514902DF8A05
13 changed files with 198 additions and 95 deletions

View File

@ -9,6 +9,7 @@ LICENSE
# Specific to this repository
dist/
data/
public/
docs/
*.md
tmp/

View File

@ -44,7 +44,8 @@ Certain variables are set via `.env` at the project root. These are for system c
- `ADMINS`: A comma-separated (with a space) list of bot admin IDs
- `SUPPORT`: A comma-separated (with a space) list of bot support IDs
- `WOLFRAM_API_KEY`: Used for `commands/utility/calc`
- `DEV`: Enables dev mode as long as it isn't a falsy value (`DEV=1` works for example)
- `DEV`: Enables dev mode as long as it isn't a falsy value (`DEV=1` works for example), specific values can be checked to test certain features
- `DEV_DATABASE`: Specifies the file to use for trying out changes on a database (`DEV_DATABASE=test` writes to `data/test.db`)
# Utility Functions

View File

@ -88,17 +88,16 @@ export const LeaderboardCommand = new NamedCommand({
async run({send, guild, channel, client}) {
if (isAuthorized(guild, channel)) {
const users = User.all();
const ids = Object.keys(users);
ids.sort((a, b) => users[b].money - users[a].money);
users.sort((a, b) => b.money - a.money);
const fields = [];
for (let i = 0, limit = Math.min(10, ids.length); i < limit; i++) {
const id = ids[i];
for (let i = 0, limit = Math.min(10, users.length); i < limit; i++) {
const id = users[i].id;
const user = await client.users.fetch(id);
fields.push({
name: `#${i + 1}. ${user.tag}`,
value: pluralise(users[id].money, "Mon", "s")
value: pluralise(users[i].money, "Mon", "s")
});
}

View File

@ -214,7 +214,7 @@ export default new NamedCommand({
any: new RestCommand({
async run({send, guild, args, combined}) {
const role = args[0] as Role;
new Guild(guild!.id).streamingRoles.set(role.id, combined);
new Guild(guild!.id).setStreamingRole(role.id, combined);
send(
`Successfully set the category \`${combined}\` to notify \`${role.name}\`.`
);
@ -229,11 +229,14 @@ export default new NamedCommand({
async run({send, guild, args}) {
const role = args[0] as Role;
const guildStorage = new Guild(guild!.id);
const category = guildStorage.streamingRoles.get(role.id);
delete guildStorage.streamingRoles[role.id];
send(
`Successfully removed the category \`${category}\` to notify \`${role.name}\`.`
);
const category = guildStorage.getStreamingRole(role.id);
if (guildStorage.removeStreamingRole(role.id)) {
send(
`Successfully removed the category \`${category}\` to notify \`${role.name}\`.`
);
} else {
send(`Failed to remove streaming role \`${role.id}\` (\`${category}\`).`);
}
}
})
})
@ -248,8 +251,12 @@ export default new NamedCommand({
const voiceChannel = message.member?.voice.channel;
if (!voiceChannel) return send("You are not in a voice channel.");
const guildStorage = new Guild(guild!.id);
delete guildStorage.defaultChannelNames[voiceChannel.id];
return send(`Successfully removed the default channel name for ${voiceChannel}.`);
if (guildStorage.removeDefaultChannelName(voiceChannel.id)) {
send(`Successfully removed the default channel name for ${voiceChannel}.`);
} else {
send(`Failed to remove the default channel name for ${voiceChannel}`);
}
},
any: new RestCommand({
async run({send, guild, message, combined}) {
@ -262,7 +269,7 @@ export default new NamedCommand({
if (!guild!.me?.permissions.has(Permissions.FLAGS.MANAGE_CHANNELS))
return send("I can't change channel names without the `Manage Channels` permission.");
guildStorage.defaultChannelNames.set(voiceChannel.id, newName);
guildStorage.setDefaultChannelName(voiceChannel.id, newName);
return await send(`Set default channel name to "${newName}".`);
}
})

View File

@ -115,7 +115,7 @@ export default new NamedCommand({
let found = false;
// Check if it's a valid category
for (const [roleID, categoryName] of Object.entries(guildStorage.streamingRoles)) {
for (const [roleID, categoryName] of guildStorage.getStreamingRoleEntries()) {
if (combined === categoryName) {
found = true;
memberStorage.streamCategory = roleID;
@ -133,10 +133,16 @@ export default new NamedCommand({
}
if (!found) {
const categories = [];
for (const [_, category] of guildStorage.getStreamingRoleEntries()) {
categories.push(category);
}
send(
`No valid category found by \`${combined}\`! The available categories are: \`${Object.values(
guildStorage.streamingRoles
).join(", ")}\``
`No valid category found by \`${combined}\`! The available categories are: \`${categories.join(
", "
)}\``
);
}
}

View File

@ -1,4 +1,4 @@
import {NamedCommand, RestCommand} from "onion-lasers";
import {Command, NamedCommand, RestCommand} from "onion-lasers";
import moment from "moment";
import {User} from "../../lib";
import {MessageEmbed} from "discord.js";
@ -9,11 +9,12 @@ export default new NamedCommand({
const user = new User(author.id);
const embed = new MessageEmbed().setTitle(`Todo list for ${author.tag}`).setColor("BLUE");
for (const timestamp in user.todoList) {
const date = new Date(Number(timestamp));
for (const [id, {entry, lastModified}] of user.getTodoEntries()) {
embed.addField(
`${moment(date).format("LT")} ${moment(date).format("LL")} (${moment(date).fromNow()})`,
user.todoList[timestamp]
`\`${id}\`: ${moment(lastModified).format("LT")} ${moment(lastModified).format("LL")} (${moment(
lastModified
).fromNow()})`,
entry
);
}
@ -24,40 +25,29 @@ export default new NamedCommand({
run: "You need to specify a note to add.",
any: new RestCommand({
async run({send, author, combined}) {
const user = new User(author.id);
user.todoList[Date.now().toString()] = combined;
Storage.save();
new User(author.id).addTodoEntry(combined);
send(`Successfully added \`${combined}\` to your todo list.`);
}
})
}),
remove: new NamedCommand({
run: "You need to specify a note to remove.",
any: new RestCommand({
async run({send, author, combined}) {
number: new Command({
async run({send, author, args}) {
const user = new User(author.id);
let isFound = false;
const success = user.removeTodoEntry(args[0]);
for (const timestamp in user.todoList) {
const selectedNote = user.todoList[timestamp];
if (selectedNote === combined) {
delete user.todoList[timestamp];
Storage.save();
isFound = true;
send(`Removed \`${combined}\` from your todo list.`);
}
if (success) {
send(`Removed Note \`${args[0]}\` from your todo list.`);
} else {
send("That item couldn't be found.");
}
if (!isFound) send("That item couldn't be found.");
}
})
}),
clear: new NamedCommand({
async run({send, author}) {
const user = new User(author.id);
user.todoList = {};
Storage.save();
new User(author.id).clearTodoEntries();
send("Cleared todo list.");
}
})

View File

@ -23,15 +23,23 @@ class Config {
this._systemLogsChannel = systemLogsChannel;
db.prepare("UPDATE Settings SET SystemLogsChannel = ? WHERE Tag = 'Main'").run(systemLogsChannel);
}
get webhooks() {
return this._webhooks;
getWebhook(id: string) {
return this._webhooks.get(id);
}
getWebhookEntries() {
return this._webhooks.entries();
}
hasWebhook(id: string) {
return this._webhooks.has(id);
}
// getWebhook, setWebhook, removeWebhook, hasWebhook, getWebhookEntries
setWebhook(id: string, token: string) {
db.prepare("INSERT INTO Webhooks VALUES (?, ?)").run(id, token);
this._webhooks.set(id, token);
db.prepare(
"INSERT INTO Webhooks VALUES (:id, :token) ON CONFLICT (ID) DO UPDATE SET Token = :token WHERE ID = :id"
).run({id, token});
}
removeWebhook(id: string) {
db.prepare("DELETE FROM Webhooks WHERE ID = ?").run(id);
return this._webhooks.delete(id);
}
}

View File

@ -109,7 +109,7 @@ export class Guild {
break;
}
db.prepare(upsert("WelcomeType", "welcomeType")).run({
db.prepare(upsert("WelcomeType", "welcomeTypeInt")).run({
id: this.id,
welcomeTypeInt
});
@ -154,13 +154,53 @@ export class Guild {
hasMessageEmbeds: +hasMessageEmbeds
});
}
get streamingRoles() {
return this._streamingRoles; // Role ID: Category Name
getStreamingRole(id: string) {
return this._streamingRoles.get(id);
}
get defaultChannelNames() {
return this._defaultChannelNames; // Channel ID: Channel Name
getStreamingRoleEntries() {
return this._streamingRoles.entries();
}
hasStreamingRole(id: string) {
return this._streamingRoles.has(id);
}
setStreamingRole(id: string, category: string) {
db.prepare("INSERT INTO StreamingRoles VALUES (?, ?, ?)").run(this.id, id, category);
this._streamingRoles.set(id, category);
}
removeStreamingRole(id: string) {
db.prepare("DELETE FROM StreamingRoles WHERE GuildID = ? AND RoleID = ?").run(this.id, id);
return this._streamingRoles.delete(id);
}
getDefaultChannelName(id: string) {
return this._defaultChannelNames.get(id);
}
getDefaultChannelNameEntries() {
return this._defaultChannelNames.entries();
}
hasDefaultChannelName(id: string) {
return this._defaultChannelNames.has(id);
}
setDefaultChannelName(id: string, name: string) {
db.prepare("INSERT INTO DefaultChannelNames VALUES (?, ?, ?)").run(this.id, id, name);
this._defaultChannelNames.set(id, name);
}
removeDefaultChannelName(id: string) {
db.prepare("DELETE FROM DefaultChannelNames WHERE GuildID = ? AND ChannelID = ?").run(this.id, id);
return this._defaultChannelNames.delete(id);
}
get autoRoles() {
return this._autoRoles; // string array of role IDs
return this._autoRoles;
}
set autoRoles(autoRoles) {
this._autoRoles = autoRoles;
db.prepare("DELETE FROM AutoRoles WHERE GuildID = ?").run(this.id);
const addAutoRoles = db.prepare("INSERT INTO AutoRoles VALUES (?, ?)");
for (const roleID of autoRoles) {
addAutoRoles.run(this.id, roleID);
}
}
}

View File

@ -12,12 +12,12 @@ export class User {
private _timezoneOffset: number | null; // This is for the standard timezone only, not the daylight savings timezone
private _daylightSavingsRegion: "na" | "eu" | "sh" | "none";
private _ecoBetInsurance: number;
private _todoList: Collection<number, string>;
private _todoList: Collection<number, {lastModified: Date; entry: string}>;
constructor(id: string) {
this.id = id;
const data = db.prepare("SELECT * FROM Users WHERE ID = ?").get(id);
const todoList = db.prepare("SELECT Timestamp, Entry FROM TodoLists WHERE UserID = ?").all(id) ?? [];
const todoList = db.prepare("SELECT ID, LastModified, Entry FROM TodoLists WHERE UserID = ?").all(id) ?? [];
if (data) {
const {Money, LastReceived, LastMonday, TimezoneOffset, DaylightSavingsRegion, EcoBetInsurance} = data;
@ -51,8 +51,11 @@ export class User {
this._todoList = new Collection();
for (const {Timestamp, Entry} of todoList) {
this._todoList.set(Timestamp, Entry);
for (const {ID, LastModified, Entry} of todoList) {
this._todoList.set(ID, {
entry: Entry,
lastModified: new Date(LastModified)
});
}
}
@ -147,8 +150,52 @@ export class User {
get todoList() {
return this._todoList;
}
// NOTE: Need to figure out an actual ID system
setTodoEntry(timestamp: number, entry: string) {
db.prepare("INSERT INTO TodoLists VALUES (?, ?, ?)").run(this.id, timestamp, entry);
getTodoEntry(id: number) {
return this._todoList.get(id);
}
getTodoEntries() {
return this._todoList.entries();
}
hasTodoEntry(id: number) {
return this._todoList.has(id);
}
addTodoEntry(entry: string) {
const lastModified = Date.now();
db.prepare("INSERT INTO TodoLists (UserID, Entry, LastModified) VALUES (?, ?, ?)").run(
this.id,
entry,
lastModified
);
const {ID} = db
.prepare("SELECT ID FROM TodoLists WHERE UserID = ? AND Entry = ? AND LastModified = ?")
.get(this.id, entry, lastModified);
this._todoList.set(ID, {
entry,
lastModified: new Date(lastModified)
});
}
setTodoEntry(id: number, entry: string): boolean {
const lastModified = Date.now();
const exists = !!db.prepare("SELECT * FROM TodoLists WHERE UserID = ? AND ID = ?").get(this.id, id);
if (exists) {
db.prepare("INSERT INTO TodoLists VALUES (?, ?, ?, ?)").run(id, this.id, entry, lastModified);
this._todoList.set(id, {
entry,
lastModified: new Date(lastModified)
});
return true;
} else {
return false;
}
}
removeTodoEntry(id: number) {
db.prepare("DELETE FROM TodoLists WHERE UserID = ? AND ID = ?").run(this.id, id);
return this._todoList.delete(id);
}
clearTodoEntries() {
db.prepare("DELETE FROM TodoLists WHERE UserID = ?").run(this.id);
this._todoList.clear();
}
}

View File

@ -4,14 +4,14 @@ import {Permissions} from "discord.js";
client.on("voiceStateUpdate", async (before, after) => {
const channel = before.channel;
const {defaultChannelNames} = new Guild(after.guild.id);
const guild = new Guild(after.guild.id);
if (
channel &&
channel.members.size === 0 &&
defaultChannelNames.has(channel.id) &&
guild.hasDefaultChannelName(channel.id) &&
before.guild.me?.permissions.has(Permissions.FLAGS.MANAGE_CHANNELS)
) {
channel.setName(defaultChannelNames.get(channel.id)!);
channel.setName(guild.getDefaultChannelName(channel.id)!);
}
});

View File

@ -17,7 +17,7 @@ import {join} from "path";
// Guilds: ID, Prefix (TEXT NULLABLE), WelcomeType (INT), WelcomeChannel (TEXT NULLABLE), WelcomeMessage (TEXT NULLABLE), StreamingChannel (TEXT NULLABLE), HasMessageEmbeds (BOOL)
// Members: UserID, GuildID, StreamCategory (TEXT NULLABLE)
// Webhooks: ID, Token (TEXT)
// TodoLists: UserID, Timestamp (TIME), Entry (TEXT)
// TodoLists: ID (INT PRIMARY KEY), UserID, Entry (TEXT), LastModified (TIME)
// StreamingRoles: GuildID, RoleID, Category (TEXT)
// DefaultChannelNames: GuildID, ChannelID, Name (TEXT)
// AutoRoles: GuildID, RoleID
@ -33,7 +33,7 @@ import {join} from "path";
// - Booleans (marked as BOOL) will be stored as an integer, either 0 or 1 (though it just checks for 0).
const DATA_FOLDER = "data";
const DATABASE_FILE = join(DATA_FOLDER, "main.db");
const DATABASE_FILE = join(DATA_FOLDER, `${process.env.DEV_DATABASE ?? "main"}.db`);
// Calling migrations[2]() migrates the database from version 2 to version 3.
// NOTE: Once a migration is written, DO NOT change that migration or it'll break all future migrations.
@ -84,9 +84,10 @@ const migrations: (() => void)[] = [
Token TEXT NOT NULL
)`,
`CREATE TABLE TodoLists (
ID INTEGER NOT NULL PRIMARY KEY ON CONFLICT REPLACE AUTOINCREMENT,
UserID TEXT NOT NULL,
Timestamp INT NOT NULL,
Entry TEXT NOT NULL
Entry TEXT NOT NULL,
LastModified INT NOT NULL
)`,
`CREATE TABLE StreamingRoles (
GuildID TEXT NOT NULL,
@ -108,11 +109,18 @@ const migrations: (() => void)[] = [
if (hasLegacyData) {
const config = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
const {users, guilds} = JSON.parse(readFileSync(STORAGE_FILE, "utf-8"));
db.prepare("INSERT INTO Settings VALUES ('Main', ?)").run(config.systemLogsChannel);
const addWebhooks = db.prepare("INSERT INTO Webhooks VALUES (?, ?)");
const addUsers = db.prepare("INSERT INTO Users VALUES (?, ?, ?, ?, ?, ?, ?)");
const addTodoLists = db.prepare("INSERT INTO TodoLists (UserID, Entry, LastModified) VALUES (?, ?, ?)");
const addGuilds = db.prepare("INSERT INTO Guilds VALUES (?, ?, ?, ?, ?, ?, ?)");
const addMembers = db.prepare("INSERT INTO Members VALUES (?, ?, ?)");
const addStreamingRoles = db.prepare("INSERT INTO StreamingRoles VALUES (?, ?, ?)");
const addDefaultChannelNames = db.prepare("INSERT INTO DefaultChannelNames VALUES (?, ?, ?)");
const addAutoRoles = db.prepare("INSERT INTO AutoRoles VALUES (?, ?)");
for (const [id, token] of Object.entries(config.webhooks)) {
db.prepare("INSERT INTO Webhooks VALUES (?, ?)").run(id, token);
addWebhooks.run(id, token);
}
for (const id in users) {
@ -134,19 +142,11 @@ const migrations: (() => void)[] = [
}
}
db.prepare("INSERT INTO Users VALUES (?, ?, ?, ?, ?, ?, ?)").run(
id,
money,
lastReceived,
lastMonday,
timezone,
dstInfo,
ecoBetInsurance
);
addUsers.run(id, money, lastReceived, lastMonday, timezone, dstInfo, ecoBetInsurance);
for (const timestamp in todoList) {
const entry = todoList[timestamp];
db.prepare("INSERT INTO TodoLists VALUES (?, ?, ?)").run(id, Number(timestamp), entry);
addTodoLists.run(id, entry, Number(timestamp));
}
}
@ -174,7 +174,7 @@ const migrations: (() => void)[] = [
break;
}
db.prepare("INSERT INTO Guilds VALUES (?, ?, ?, ?, ?, ?, ?)").run(
addGuilds.run(
id,
prefix,
welcomeTypeInt,
@ -186,28 +186,28 @@ const migrations: (() => void)[] = [
for (const userID in members) {
const {streamCategory} = members[userID];
db.prepare("INSERT INTO Members VALUES (?, ?, ?)").run(userID, id, streamCategory);
addMembers.run(userID, id, streamCategory);
}
for (const roleID in streamingRoles) {
const category = streamingRoles[roleID];
db.prepare("INSERT INTO StreamingRoles VALUES (?, ?, ?)").run(id, roleID, category);
addStreamingRoles.run(id, roleID, category);
}
for (const channelID in channelNames) {
const channelName = channelNames[channelID];
db.prepare("INSERT INTO DefaultChannelNames VALUES (?, ?, ?)").run(id, channelID, channelName);
addDefaultChannelNames.run(id, channelID, channelName);
}
if (autoRoles) {
for (const roleID of autoRoles) {
db.prepare("INSERT INTO AutoRoles VALUES (?, ?)").run(id, roleID);
addAutoRoles.run(id, roleID);
}
}
}
}
}
// generateSQLMigration(["UPDATE System SET Version = 2"])
// generateSQLMigration([])
];
const isExistingDatabase = existsSync(DATABASE_FILE);
@ -244,6 +244,10 @@ if (isExistingDatabase) {
if (version !== -1) {
for (let v = version; v < migrations.length; v++) {
migrations[v]();
if (v >= 1) {
db.prepare("UPDATE System SET Version = ?").run(v + 1);
}
}
}

View File

@ -61,7 +61,8 @@ client.on("voiceStateUpdate", async (before, after) => {
// Note: isStopStreamEvent can be called twice in a row - If Discord crashes/quits while you're streaming, it'll call once with a null channel and a second time with a channel.
if (isStartStreamEvent || isStopStreamEvent) {
const {streamingChannel, streamingRoles} = new Guild(after.guild.id);
const guild = new Guild(after.guild.id);
const {streamingChannel} = guild;
if (streamingChannel) {
const member = after.member!;
@ -79,9 +80,9 @@ client.on("voiceStateUpdate", async (before, after) => {
const roleID = new Member(member.id, after.guild.id).streamCategory;
// Only continue if they set a valid category.
if (roleID && streamingRoles.has(roleID)) {
if (roleID && guild.hasStreamingRole(roleID)) {
streamNotificationPing = `<@&${roleID}>`;
category = streamingRoles.get(roleID)!;
category = guild.getStreamingRole(roleID)!;
}
streamList.set(member.id, {

View File

@ -38,7 +38,7 @@ export function deleteWebhook(urlOrID: string): boolean {
else if (ID_PATTERN.test(urlOrID)) id = ID_PATTERN.exec(urlOrID)![1];
if (id) {
delete config.webhooks[id];
config.removeWebhook(id);
refreshWebhookCache();
}
@ -55,14 +55,13 @@ client.on("ready", refreshWebhookCache);
export async function refreshWebhookCache(): Promise<void> {
webhookStorage.clear();
for (const [id, token] of Object.entries(Config.webhooks)) {
for (const [id, token] of config.getWebhookEntries()) {
// If there are stored webhook IDs/tokens that don't work, delete those webhooks from storage.
try {
const webhook = await client.fetchWebhook(id, token);
webhookStorage.set(webhook.channelId, webhook);
} catch {
delete Config.webhooks[id];
Config.save();
config.removeWebhook(id);
}
}
}