Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"deploy": "node src/deploy.js",
"start": "node src/index.js"
},
"author": "",
Expand Down
1 change: 1 addition & 0 deletions src/amiami/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ class AmiAmiFallbackPreview {
* @see {@link AmiAmiFallbackPreview}
*/
const AmiAmiPreview = {
name: "AmiAmi",
/**
* @param {string} content
* @returns {string[]} matches
Expand Down
21 changes: 21 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { Client, IntentsBitField: {Flags: IntentsFlags} } = require("discord.js");
const { AmiAmiPreview } = require("./amiami");
const { YouTubePreview } = require("./youtube");
const sqlite = require("sqlite3");

const client = new Client({
intents: [IntentsFlags.Guilds, IntentsFlags.GuildMessages, IntentsFlags.MessageContent],
});

client.db = new sqlite.Database(process.env.DB_PATH ?? "./data.db");

client.previews = [
AmiAmiPreview,
YouTubePreview,
];

function getClient() {
return client;
}

module.exports = { getClient };
31 changes: 31 additions & 0 deletions src/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require("dotenv").config();
const { getSettingsCommand } = require("./settings");
const { getClient } = require("./client");


const client = getClient();


client.on("ready", async () => {
console.log(`Deploying commands for ${client.user.tag}.`);

const debugGuild = process.env.DEBUG_GUILD;

if (debugGuild) {
console.log(`Deploying commands to Guild ${debugGuild}`);
} else {
console.log("Deploying commands globally.");
}

await client.application.commands.set([
getSettingsCommand(client),
], debugGuild);

await client.destroy();

console.log("Done.");

process.exit(0);
});

client.login(process.env.DISCORD_TOKEN);
39 changes: 26 additions & 13 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
require("dotenv").config();
const { Client, IntentsBitField: {Flags: IntentsFlags} } = require("discord.js");
const { AmiAmiPreview } = require("./amiami");
const { YouTubePreview } = require("./youtube");

const client = new Client({
intents: [IntentsFlags.Guilds, IntentsFlags.GuildMessages, IntentsFlags.MessageContent],
});
const { getClient } = require("./client");
const { Settings, settingsHandler } = require("./settings");


client.previews = [
AmiAmiPreview,
YouTubePreview,
];
const client = getClient();

client.on("messageCreate", async message => {
if (message.author.bot) return;
if (!message.inGuild()) return;

const settings = await Settings.forGuild(client.db, message.guildId);

for (const group of client.previews) {
const disabled = settings.disabled;

const enabledPreviews = client.previews.filter(preview => !disabled.has(preview.name));

for (const group of enabledPreviews) {
const matches = group.match(message.content);
matches: for (const match of matches) {
for (const generator of group.generators) {
Expand All @@ -35,13 +33,28 @@ client.on("messageCreate", async message => {
}
})

client.on("interactionCreate", async interaction => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === "settings") {
await settingsHandler(interaction);
}
});



async function main() {
for (const matcher of client.previews) {
console.log(`Initializing ${matcher.generators.length} generators for "${matcher.name}".`);
await matcher.init?.();

for (const generator of matcher.generators) {
await generator.init?.();
}
}

console.log(`Initializing settings.`);
await Settings.init(client.db);

await client.login(process.env.DISCORD_TOKEN);
console.log(`Logged into Discord as ${client.user.tag}.`);
}
Expand Down
250 changes: 250 additions & 0 deletions src/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
const { Colors, PermissionFlagsBits, SlashCommandBuilder, EmbedBuilder } = require("discord.js");

/**
* Per-guild settings.
*
* Construct with `Settings.forGuild(db, guildId)`.
*/
class Settings {
/**
* @type {Map<string, Settings>}
*/
static instances = new Map();

/**
* Whether the settings have changed since the last save.
*/
#changed = false;

/**
* @type {{guild: string, disabled: string | null, preferredStyle: string | null}}
*/
#row;
/**
* @type {sqlite.Database}
*/
#db;

constructor(db, row) {
this.#db = db;
this.#row = row;
}

async save() {
if (!this.#changed) return;

return new Promise((res, rej) => {
this.#db.run("UPDATE settings SET disabled = (?), preferredStyle = (?) WHERE guild = (?)", [this.#row.disabled, this.#row.preferredStyle, this.#row.guild], (err) => {
if (err) rej(err);
this.#changed = false;

res();
});
});
}

/**
* @type {string}
*/
get guildId() {
return this.#row.guild;
}

/**
* @type {Set<string>}
*/
get disabled() {
const raw = this.#row.disabled;

return raw ? new Set(raw.split(",")) : new Set();
}

/**
* Disable a preview provider.
*/
disable(previewName) {
const disabled = this.disabled;

if (disabled.has(previewName)) this.#changed = true;

disabled.add(previewName);

this.#row.disabled = Array.from(disabled).join(",");

return this;
}

/**
* Enable a preview provider.
*/
enable(previewName) {
const disabled = this.disabled;

if (disabled.has(previewName)) this.#changed = true;

disabled.delete(previewName);

this.#row[2] = Array.from(disabled).join(",");

return this;
}

get preferredStyle() {
return this.#row.preferredStyle ?? "full";
}

set preferredStyle(value) {
if (value != "full" && value != "compact") throw new TypeError("Prefered style must be 'full' or 'compact'");

this.#changed = true;
this.#row.preferredStyle = value;
}

/**
* @returns {Promise<Settings>}
*/
static async forGuild(db, id) {
if (Settings.instances.has(id)) return Settings.instances.get(id);

let row = await new Promise((res, rej) => db.get("SELECT * FROM settings WHERE guild = (?)", [id], (err, row) => {
if (err) rej(err);
res(row);
}));

if (!row) {
await new Promise((res, rej) => db.run("INSERT INTO settings VALUES (?, ?, ?)", [id, null, null], (err) => {
if (err) rej(err);
res();
}));

row = await new Promise((res, rej) => db.get("SELECT * FROM settings WHERE guild = (?)", [id], (err, row) => {
if (err) rej(err);
res(row);
}));
}

const settings = new Settings(db, row);
Settings.instances.set(id, settings);
return settings;
}


static async init(db) {
return new Promise((res, rej) => db.run("CREATE TABLE IF NOT EXISTS settings (guild TEXT UNIQUE NOT NULL, disabled TEXT, preferredStyle TEXT)", (err) => {
if (err) rej(err);
res();
}));
}
}

function getSettingsCommand(client) {
/**
* @type {string[]}
*/
const previewNames = client.previews.map(preview => preview.name);

return new SlashCommandBuilder()
.setName("settings")
.setDescription("View or modify your settings.")
.setDMPermission(false)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addSubcommand(view => view
.setName("view")
.setDescription("View your settings.")
)
.addSubcommand(disable => disable
.setName("disable")
.setDescription("Disable a preview provider.")
.addStringOption(option => option
.setName("provider")
.setDescription("The provider to disable.")
.setChoices(...previewNames.map(name => ({ name, value: name })))
.setRequired(true)
)
)
.addSubcommand(enable => enable
.setName("enable")
.setDescription("Enable a preview provider.")
.addStringOption(option => option
.setName("provider")
.setDescription("The provider to enable.")
.setChoices(...previewNames.map(name => ({ name, value: name })))
.setRequired(true)
)
)
.addSubcommand(style => style
.setName("style")
.setDescription("Set the preferred preview style.")
.addStringOption(option => option
.setName("style")
.setDescription("The preferred preview style.")
.setChoices({ name: "Full", value: "full" }, { name: "Compact", value: "compact" })
.setRequired(true)
)
);
}

async function settingsHandler(interaction) {
if (!interaction.inGuild()) throw new Error("Must be in a server to use this command.");

const subcommand = interaction.options.getSubcommand();

const settings = await Settings.forGuild(client.db, interaction.guildId);
switch (subcommand) {
case "view": {
const disabled = settings.disabled;
const enabled = interaction.client.previews.filter(preview => !disabled.has(preview.name)).map(preview => preview.name);
const style = settings.preferredStyle;

const embed = new EmbedBuilder()
.setAuthor({
name: interaction.guild.name,
iconURL: interaction.guild.iconURL(),
})
.setTitle("Settings")
.addFields(
{ name: "Disabled", value: disabled.size ? Array.from(disabled).join(", ") : "None", inline: true },
{ name: "Enabled", value: enabled.length ? Array.from(enabled).join(", ") : "None", inline: true },
{ name: "Preferred style", value: style, inline: false },
)
.setColor(Colors.Aqua);

return interaction.reply({ embeds: [embed] });
}

case "disable": {
const provider = interaction.options.getString("provider");
settings.disable(provider);
await settings.save();
return interaction.reply({ embeds: [
new EmbedBuilder()
.setDescription(`Disabled ${provider}.`)
.setColor(Colors.Green)
]});
}

case "enable": {
const provider = interaction.options.getString("provider");
settings.enable(provider);
await settings.save();
return interaction.reply({ embeds: [
new EmbedBuilder()
.setDescription(`Enabled ${provider}.`)
.setColor(Colors.Green)
]});
}

case "style": {
const style = interaction.options.getString("style");
settings.preferredStyle = style;
await settings.save();
return interaction.reply({ embeds: [
new EmbedBuilder()
.setDescription(`Set preferred style to ${style}.`)
.setColor(Colors.Green)
]});
}
}
}

module.exports = { Settings, getSettingsCommand, settingsHandler };
1 change: 1 addition & 0 deletions src/youtube/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ function postToEmbed(post, channel) {
}

const YouTubePreview = {
name: "YouTube (Community Posts)",
match(content) {
return [...content.matchAll(postIdRegex)].map(match => typeof match == "string" ? match : match[0]);
},
Expand Down