mrmBot-Matrix/utils/database/postgresql.js
samhza ed25116851
Assorted db changes (#333)
* postgres: use transaction-scoped sql for upgrade

* database: combine fixGuild and addGuild into getGuild

* postgres, sqlite: simplify upgrade()

* allow running commands in DMs without prefix

this functionality was broken in
16095c0256 but is now fixed

* allow running esmBot without a database

Before this change, the only way to run esmBot without a database is to use the
dummy database driver which is broken but fails silently. THis can lead to a
confusing user experience. For instance, using `&command disable` with the
dummy database driver will tell you that the command has been disabled even
though it has not been.

This change adds support for running esmBot with no database driver by leaving
the DB= config option empty, and explicitly telling the user that some
functionality is now unavailable rather than failing silently like the dummy
driver.

* remove dummy database driver
2022-12-12 11:15:10 -06:00

188 lines
6.7 KiB
JavaScript

import { prefixCache, disabledCmdCache, disabledCache, commands, messageCommands } from "../collections.js";
import * as logger from "../logger.js";
import Postgres from "postgres";
const sql = Postgres(process.env.DB, {
onnotice: () => {}
});
const settingsSchema = `
CREATE TABLE IF NOT EXISTS settings (
id smallint PRIMARY KEY,
version integer NOT NULL, CHECK(id = 1)
);
`;
const schema = `
ALTER TABLE settings ADD COLUMN broadcast text;
CREATE TABLE guilds (
guild_id VARCHAR(30) NOT NULL PRIMARY KEY,
prefix VARCHAR(15) NOT NULL,
disabled text ARRAY NOT NULL,
disabled_commands text ARRAY NOT NULL
);
CREATE TABLE counts (
command VARCHAR NOT NULL PRIMARY KEY,
count integer NOT NULL
);
CREATE TABLE tags (
guild_id VARCHAR(30) NOT NULL,
name text NOT NULL,
content text NOT NULL,
author VARCHAR(30) NOT NULL,
UNIQUE(guild_id, name)
);
`;
const updates = [
"", // reserved
"CREATE TABLE IF NOT EXISTS settings ( id smallint PRIMARY KEY, version integer NOT NULL, CHECK(id = 1) );\nALTER TABLE guilds ADD COLUMN accessed timestamp;",
"ALTER TABLE guilds DROP COLUMN accessed",
"ALTER TABLE settings ADD COLUMN IF NOT EXISTS broadcast text"
];
export async function setup() {
const existingCommands = (await sql`SELECT command FROM counts`).map(x => x.command);
const commandNames = [...commands.keys(), ...messageCommands.keys()];
for (const command of existingCommands) {
if (!commandNames.includes(command)) {
await sql`DELETE FROM counts WHERE command = ${command}`;
}
};
for (const command of commandNames) {
if (!existingCommands.includes(command)) {
await sql`INSERT INTO counts ${sql({ command, count: 0 }, "command", "count")}`;
}
};
}
export async function upgrade(logger) {
try {
await sql.begin(async (sql) => {
await sql.unsafe(settingsSchema);
let version;
const settingsrow = (await sql`SELECT version FROM settings WHERE id = 1`);
if (settingsrow.length == 0) {
version = 0;
} else {
version = settingsrow[0].version;
};
const latestVersion = updates.length - 1;
if (version === 0) {
logger.info(`Initializing PostgreSQL database...`);
await sql.unsafe(schema);
} else if (version < latestVersion) {
logger.info(`Migrating PostgreSQL database, which is currently at version ${version}...`);
while (version < latestVersion) {
version++;
logger.info(`Running version ${version} update script...`);
await sql.unsafe(updates[version]);
}
} else if (version > latestVersion) {
throw new Error(`PostgreSQL database is at version ${version}, but this version of the bot only supports up to version ${latestVersion}.`);
} else {
return;
}
await sql`INSERT INTO settings ${sql({ id: 1, version: latestVersion })} ON CONFLICT (id) DO UPDATE SET version = ${latestVersion}`;
});
} catch (e) {
logger.error(`PostgreSQL migration failed: ${e}`);
logger.error("Unable to start the bot, quitting now.");
return 1;
}
}
export async function getGuild(query) {
let guild;
await sql.begin(async (sql) => {
guild = (await sql`SELECT * FROM guilds WHERE guild_id = ${query}`)[0];
if (guild == undefined) {
guild = { guild_id: query, prefix: process.env.PREFIX, disabled: [], disabled_commands: [] };
await sql`INSERT INTO guilds ${sql(guild)}`;
};
});
return guild;
}
export async function setPrefix(prefix, guild) {
await sql`UPDATE guilds SET prefix = ${prefix} WHERE guild_id = ${guild.id}`;
prefixCache.set(guild.id, prefix);
}
export async function getTag(guild, tag) {
const tagResult = await sql`SELECT * FROM tags WHERE guild_id = ${guild} AND name = ${tag}`;
return tagResult[0] ? { content: tagResult[0].content, author: tagResult[0].author } : undefined;
}
export async function getTags(guild) {
const tagArray = await sql`SELECT * FROM tags WHERE guild_id = ${guild}`;
const tags = {};
for (const tag of tagArray) {
tags[tag.name] = { content: tag.content, author: tag.author };
}
return tags;
}
export async function setTag(name, content, guild) {
await sql`INSERT INTO tags ${sql({ guild_id: guild.id, name, content: content.content, author: content.author }, "guild_id", "name", "content", "author")}`;
}
export async function editTag(name, content, guild) {
await sql`UPDATE tags SET content = ${content.content}, author = ${content.author} WHERE guild_id = ${guild.id} AND name = ${name}`;
}
export async function removeTag(name, guild) {
await sql`DELETE FROM tags WHERE guild_id = ${guild.id} AND name = ${name}`;
}
export async function setBroadcast(msg) {
await sql`UPDATE settings SET broadcast = ${msg} WHERE id = 1`;
}
export async function getBroadcast() {
const result = await sql`SELECT broadcast FROM settings WHERE id = 1`;
return result[0].broadcast;
}
export async function disableCommand(guild, command) {
const guildDB = await this.getGuild(guild);
await sql`UPDATE guilds SET disabled_commands = ${(guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command]).filter((v) => !!v)} WHERE guild_id = ${guild}`;
disabledCmdCache.set(guild, guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command].filter((v) => !!v));
}
export async function enableCommand(guild, command) {
const guildDB = await this.getGuild(guild);
const newDisabled = guildDB.disabled_commands ? guildDB.disabled_commands.filter(item => item !== command) : [];
await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${guild}`;
disabledCmdCache.set(guild, newDisabled);
}
export async function disableChannel(channel) {
const guildDB = await this.getGuild(channel.guildID);
await sql`UPDATE guilds SET disabled_commands = ${[...guildDB.disabled, channel.id]} WHERE guild_id = ${channel.guildID}`;
disabledCache.set(channel.guildID, [...guildDB.disabled, channel.id]);
}
export async function enableChannel(channel) {
const guildDB = await this.getGuild(channel.guildID);
const newDisabled = guildDB.disabled.filter(item => item !== channel.id);
await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${channel.guildID}`;
disabledCache.set(channel.guildID, newDisabled);
}
export async function getCounts() {
const counts = await sql`SELECT * FROM counts`;
const countObject = {};
for (const { command, count } of counts) {
countObject[command] = count;
}
return countObject;
}
export async function addCount(command) {
await sql`INSERT INTO counts ${sql({ command, count: 1 }, "command", "count")} ON CONFLICT (command) DO UPDATE SET count = counts.count + 1 WHERE counts.command = ${command}`;
}
export async function stop() {
await sql.end();
}