diff --git a/src/autocomplete/modify.js b/src/autocomplete/modify.js new file mode 100644 index 0000000..45f6950 --- /dev/null +++ b/src/autocomplete/modify.js @@ -0,0 +1,54 @@ +import { InteractionResponseTypes } from "@discordeno/types" +import REST from "../lib/handlers/RESTHandler" +import { staged } from "../lib/modpack" +import { getAllMods } from "../lib/database" + +function search(arr, search) { + if (!search.length) return false + + var matches = arr.filter(function (str) { + const regex = new RegExp(search, "i") + return str.search(regex) > -1 + }) + + return matches.length > 0 ? matches : false +} + +export default class Autocomplete { + static name = "modify" + + constructor(data) {} + + async run(interaction) { + const entry = interaction.data.options[0].options[0].value + const from_modpack = interaction.data.options[0].options[1]?.value + + if (!from_modpack) { + const keys = Array.from(staged.keys()) + let choices = search(keys, entry) + if (!choices) choices = keys + + return REST.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, + data: { + choices: choices.map((_) => { + return { name: _, value: _ } + }), + }, + }) + } else { + const keys = getAllMods.all().map((_) => _.name) + let choices = search(keys, entry) + if (!choices) choices = keys + + return REST.sendInteractionResponse(interaction.id, interaction.token, { + type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, + data: { + choices: choices.map((_) => { + return { name: _, value: _ } + }), + }, + }) + } + } +} diff --git a/src/commands/changes.js b/src/commands/changes.js index deb7048..2ed4a1e 100644 --- a/src/commands/changes.js +++ b/src/commands/changes.js @@ -16,7 +16,7 @@ export default class Command extends BaseCommand { if (!staged.size) return interaction.respond("No changes to the modpack have been staged.") for (let [key, value] of staged.entries()) { - text += (value.action == "add" ? "+" : "-") + " " + key + " " + value.url + text += (value.action == "add" ? "+" : "-") + " " + key + " (" + value.url + ")\n" } interaction.respond("```diff\n" + text + "\n```") diff --git a/src/commands/meta.js b/src/commands/meta.js index 6e33058..53ab387 100644 --- a/src/commands/meta.js +++ b/src/commands/meta.js @@ -3,6 +3,7 @@ import BaseCommand from "../lib/classes/BaseCommand" const META_CHOICES = [ { name: "loader", value: "loader" }, { name: "minecraft_version", value: "minecraft_version" }, + { name: "version", value: "version" }, ] export default class Command extends BaseCommand { diff --git a/src/commands/mod.js b/src/commands/mod.js index 3565463..a97d2ad 100644 --- a/src/commands/mod.js +++ b/src/commands/mod.js @@ -21,6 +21,7 @@ export default class Command extends BaseCommand { } async run(interaction) { + const then = Date.now() const { data } = interaction const url = data.options.find((_) => _.name == "url").value @@ -45,7 +46,7 @@ export default class Command extends BaseCommand { title: mod.name, description: mod.summary, url: mod.links.websiteUrl, - thumbnail: { url: mod.logo.url }, + image: { url: mod.logo.url }, footer: { text: `Curseforge | ModId ${mod.id} | FileId ${file.id}` }, }, ], diff --git a/src/commands/modify.js b/src/commands/modify.js index 5b24c2f..2088ee9 100644 --- a/src/commands/modify.js +++ b/src/commands/modify.js @@ -1,4 +1,7 @@ import BaseCommand from "../lib/classes/BaseCommand" +import { staged } from "../lib/modpack" +import * as curseforge from "../lib/curseforge" +import { getMod } from "../lib/database" export default class Command extends BaseCommand { static type = 1 @@ -23,6 +26,12 @@ export default class Command extends BaseCommand { name: "remove", description: "Remove a mod", options: [ + { + type: 5, + name: "from_modpack", + description: "Toggle removal from staging or the modpack", + required: true, + }, { type: 3, name: "name", @@ -30,11 +39,6 @@ export default class Command extends BaseCommand { autocomplete: true, required: true, }, - { - type: 5, - name: "from_modpack", - description: "Toggle removal from staging or the modpack", - }, ], }, ] @@ -44,6 +48,59 @@ export default class Command extends BaseCommand { } async run(interaction) { - console.log("modify.js") + const subcommand = interaction.data.options[0].name + const subcommandOptions = interaction.data.options[0].options + + if (subcommand == "add") { + const url = subcommandOptions.find((_) => _.name == "url").value + const parsedUrl = new URL(url) + + if (parsedUrl.host.includes("curseforge.com")) { + const mod = await curseforge.getMod(url) + if (!mod) return interaction.respond("Mod not found.") + + const file = await curseforge.getModFiles(mod.id) + + if (staged.get(mod.name)) return interaction.respond(`**${mod.name}** is already in the staged mods.`, { isPrivate: true }) + if (getMod.get(mod.name)) return interaction.respond(`**${mod.name}** is already in the modpack.`, { isPrivate: true }) + + staged.set(mod.name, { + modId: mod.id, + fileId: file.id, + provider: "curseforge", + url: mod.links.websiteUrl, + action: "add", + }) + + interaction.respond(`Added **${mod.name}** to the staged mods.`) + } else if (parsedUrl.host.includes("modrinth.com")) { + console.log("Modrinth mod detected") + } + } else if (subcommand == "remove") { + const name = subcommandOptions.find((_) => _.name == "name").value + const from_modpack = subcommandOptions.find((_) => _.name == "from_modpack")?.value + + if (!from_modpack) { + const mod = staged.get(name) + if (!mod) return interaction.respond("Mod not found.") + + staged.delete(name) + + interaction.respond(`Removed **${name}** from the staged mods.`) + } else { + const mod = getMod.get(name) + if (!mod) return interaction.respond("Mod not found.") + const modData = await curseforge.getModFromId(mod.modId) + + staged.set(mod.name, { + modId: mod.modId, + url: modData.data.links.websiteUrl, + provider: "curseforge", + action: "remove", + }) + + interaction.respond(`Staged the removal of **${name}**.`) + } + } } } diff --git a/src/commands/publish.js b/src/commands/publish.js index efbc71c..f494793 100644 --- a/src/commands/publish.js +++ b/src/commands/publish.js @@ -1,4 +1,6 @@ import BaseCommand from "../lib/classes/BaseCommand" +import { addMod, getAllMods, getAttribute, removeMod, updateAttribute, updateChangelog } from "../lib/database" +import { staged } from "../lib/modpack" export default class Command extends BaseCommand { static type = 1 @@ -10,6 +12,22 @@ export default class Command extends BaseCommand { } async run(interaction) { - console.log("publish.js") + if (!staged.size) return interaction.respond("There is nothing to publish.") + + const currentVersion = Number(getAttribute.get("version").value) + const nextVersion = currentVersion + 1 + + let changelog = [] + + for (let [key, value] of staged.entries()) { + if (value.action == "remove") removeMod.run(value.modId) + if (value.action == "add") addMod.run(key, Number(value.modId), Number(value.fileId)) + updateChangelog.run(key, nextVersion, value.action == "remove" ? 0 : 1) + staged.delete(key) + } + + updateAttribute.run("version", nextVersion) + + interaction.respond(`Version **${nextVersion}** was published.`) } } diff --git a/src/components/addMod.js b/src/components/addMod.js index 9dc2392..f94b17b 100644 --- a/src/components/addMod.js +++ b/src/components/addMod.js @@ -2,7 +2,7 @@ import { InteractionResponseTypes } from "@discordeno/types" import * as curseforge from "../lib/curseforge" import REST from "../lib/handlers/RESTHandler" import { staged } from "../lib/modpack" -import { cache } from "../lib/cache" +import { getMod } from "../lib/database" export default class Command { static name = "addMod" @@ -18,7 +18,8 @@ export default class Command { if (provider == "curseforge") { const mod = await curseforge.getModFromId(modId) - if (staged.get(mod.data.name)) return interaction.respond(`**${mod.data.name}** is already in the modpack.`, { isPrivate: true }) + if (staged.get(mod.data.name)) return interaction.respond(`**${mod.data.name}** is already in the staged mods.`, { isPrivate: true }) + if (getMod.get(mod.data.name)) return interaction.respond(`**${mod.data.name}** is already in the modpack.`, { isPrivate: true }) staged.set(mod.data.name, { modId, @@ -28,7 +29,7 @@ export default class Command { action: "add", }) - interaction.respond(`Added **${mod.data.name}** to the modpack.`) + interaction.respond(`Added **${mod.data.name}** to the staged mods.`) } else { interaction.respond("Not implemented for provider " + provider) } diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index 6516dea..332a4d5 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -1,6 +1,7 @@ import { logger } from "@discordeno/utils" import CommandHandler from "../lib/handlers/CommandHandler" +import AutocompleteHandler from "../lib/handlers/AutocompleteHandler" import ComponentHandler from "../lib/handlers/ComponentHandler" const Commands = new CommandHandler() @@ -13,7 +14,13 @@ await Components.load((comp) => { console.log("Loaded component response " + comp.name) }) +const Autocomplete = new AutocompleteHandler() +await Autocomplete.load((comp) => { + console.log("Loaded autocomplete response " + comp.name) +}) + export default async function (client, interaction) { - if (interaction.type == 2) Commands.check(client, interaction) - if (interaction.type == 3) Components.check(client, interaction) + if (interaction.type == 2) return Commands.check(client, interaction) + if (interaction.type == 3) return Components.check(client, interaction) + if (interaction.type == 4) return Autocomplete.check(client, interaction) } diff --git a/src/lib/cache.js b/src/lib/cache.js index 8f23074..3f08177 100644 --- a/src/lib/cache.js +++ b/src/lib/cache.js @@ -1,2 +1,3 @@ -//lol -export const cache = new Map() +export let cfModCache = new Map() +export let cfModIdCache = new Map() +export let cfModFilesCache = new Map() diff --git a/src/lib/curseforge.js b/src/lib/curseforge.js index 322ef27..804f3f0 100644 --- a/src/lib/curseforge.js +++ b/src/lib/curseforge.js @@ -1,3 +1,5 @@ +import { cfModCache, cfModFilesCache, cfModIdCache } from "./cache" + const BASE_URL = "https://api.curseforge.com" const SEARCH_MODS = "/v1/mods/search" const GET_FILES = "/v1/mods/{modId}/files" @@ -9,6 +11,8 @@ export async function getMod(url, gameVersion = "1.18.2", modLoaderType = 1) { url = url.split("/") const slug = url[url.length - 1] + if (cfModCache.get(slug)) return cfModCache.get(slug) + const query = new URLSearchParams() query.append("gameId", GAME_ID) query.append("slug", slug) @@ -22,20 +26,29 @@ export async function getMod(url, gameVersion = "1.18.2", modLoaderType = 1) { }, }).catch((e) => console.log(e)) - return (await response.json()).data[0] + const data = (await response.json()).data[0] + cfModCache.set(slug, data) + return data } export async function getModFromId(modId) { + if (cfModIdCache.get(modId)) return cfModIdCache.get(modId) + const response = await fetch(BASE_URL + GET_MOD.replace("{modId}", modId), { headers: { "x-api-key": import.meta.env.CURSEFORGE_API, }, }).catch((e) => console.log(e)) - return await response.json() + const data = await response.json() + cfModIdCache.set(modId, data) + + return data } export async function getModFiles(modId, gameVersion = "1.18.2", modLoaderType = 1) { + if (cfModFilesCache.get(modId)) return cfModFilesCache.get(modId) + const query = new URLSearchParams() if (gameVersion) query.append("gameVersion", gameVersion) if (modLoaderType) query.append("modLoaderType", modLoaderType) @@ -46,7 +59,9 @@ export async function getModFiles(modId, gameVersion = "1.18.2", modLoaderType = }, }).catch((e) => console.log(e)) - return (await response.json()).data[0] + const data = (await response.json()).data[0] + cfModFilesCache.set(modId, data) + return data } export async function downloadMod(id, fileId) {} diff --git a/src/lib/database.js b/src/lib/database.js index b42ea66..ab12dc0 100644 --- a/src/lib/database.js +++ b/src/lib/database.js @@ -1,9 +1,23 @@ import { Database } from "bun:sqlite" const database = new Database("data/database.sqlite") -database.run(`CREATE TABLE IF NOT EXISTS attributes (key INTEGER PRIMARY KEY, value INTEGER NOT NULL);`) +database.run(`CREATE TABLE IF NOT EXISTS attributes (key STRING PRIMARY KEY, value STRING NOT NULL);`) +database.run(`CREATE TABLE IF NOT EXISTS mods (name STRING PRIMARY KEY, modId INTEGER NOT NULL, fileId INTEGER NOT NULL);`) +database.run(`CREATE TABLE IF NOT EXISTS changelog (id INTEGER PRIMARY KEY AUTOINCREMENT, name STRING NOT NULL, version INTEGER NOT NULL, action BOOLEAN NOT NULL);`) -database.run(`CREATE TABLE IF NOT EXISTS mods (url INTEGER PRIMARY KEY);`) +database.run(`INSERT OR IGNORE INTO attributes (key, value) VALUES ('version', 0);`) + +export const addMod = database.prepare(`INSERT INTO mods (name, modId, fileId) VALUES (?1, ?2, ?3);`) +export const removeMod = database.prepare(`DELETE FROM mods WHERE modId = ?1;`) +export const getAllMods = database.prepare(`SELECT * FROM mods;`) +export const getMod = database.prepare(`SELECT * FROM mods WHERE name = ?1;`) + +export const addAttribute = database.prepare(`INSERT INTO attributes (key, value) VALUES (?1, ?2);`) +export const updateAttribute = database.prepare(`UPDATE attributes SET value = ?2 WHERE key = ?1;`) +export const getAttribute = database.prepare(`SELECT value FROM attributes WHERE key = ?1;`) + +export const updateChangelog = database.prepare(`INSERT INTO changelog (name, version, action) VALUES (?1, ?2, ?3);`) +export const getChangelog = database.prepare(`SELECT * FROM changelog WHERE version = ?1;`) process.on("exit", () => { database.close() diff --git a/src/lib/handlers/AutocompleteHandler.js b/src/lib/handlers/AutocompleteHandler.js new file mode 100644 index 0000000..b42e377 --- /dev/null +++ b/src/lib/handlers/AutocompleteHandler.js @@ -0,0 +1,40 @@ +import { readdir } from "node:fs/promises" + +export default class AutocompleteHandler { + constructor() { + this.cache = new Map() + } + + async load(postLoad = () => {}) { + const dir = await readdir("src/autocomplete") + for (let file of dir) { + const cmd = (await this.loadCommand(file)).default + this.cache.set(cmd.name, cmd) + + postLoad(cmd) + } + + return this.cache + } + + async loadCommand(path) { + return await import(process.cwd() + "/src/autocomplete/" + path) + } + + reloadCommand(name) {} + + unloadCommand(name) {} + + check(client, data) { + if (data.type !== 4) return false + this.run(client, data) + } + + run(client, interaction) { + const cmd = this.cache.get(interaction.data.name) + if (!cmd) return false + + const Command = new cmd(client) + return Command.run(interaction) + } +} diff --git a/src/routes/getAllMods.js b/src/routes/getAllMods.js index 4b279aa..fcbc0e5 100644 --- a/src/routes/getAllMods.js +++ b/src/routes/getAllMods.js @@ -1 +1,9 @@ -export default async function route(req) {} +import { getAllMods } from "../lib/database" + +export default async function route(req) { + const mods = getAllMods.all() + + return new Response(JSON.stringify(mods), { + headers: { "content-type": "application/json" }, + }) +} diff --git a/src/routes/getMod.js b/src/routes/getMod.js deleted file mode 100644 index 4b279aa..0000000 --- a/src/routes/getMod.js +++ /dev/null @@ -1 +0,0 @@ -export default async function route(req) {} diff --git a/src/routes/getUpdates.js b/src/routes/getUpdates.js index 4b279aa..21e287c 100644 --- a/src/routes/getUpdates.js +++ b/src/routes/getUpdates.js @@ -1 +1,30 @@ -export default async function route(req) {} +import { getAttribute, getChangelog } from "../lib/database" + +export default async function route(req) { + const from = Number(new URL(req.url).searchParams.get("from")) + if (isNaN(from)) + return new Response("400: Missing 'from' in query", { + status: 400, + }) + + const current = Number(getAttribute.get("version").value) + + if (from > current) + return new Response("400: Requested version is higher than current", { + status: 400, + }) + + if (from == current) + return new Response(null, { + status: 204, + }) + + let diff = [] + + const currentChangelog = getChangelog.all(getAttribute.get("version").value) + const thenChangelog = getChangelog.all(from) + + return new Response(JSON.stringify({ currentChangelog, thenChangelog }), { + headers: { "content-type": "application/json" }, + }) +} diff --git a/src/routes/index.js b/src/routes/index.js index 4b279aa..73a36e3 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1 +1,7 @@ -export default async function route(req) {} +import { getAttribute } from "../lib/database" + +export default async function route(req) { + return new Response(JSON.stringify({ version: getAttribute.get("version").value }), { + headers: { "content-type": "application/json" }, + }) +} diff --git a/src/server.js b/src/server.js index 6a4ea7d..0db3b7f 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,11 @@ +import getChanges from "./routes/getUpdates" +import getAllMods from "./routes/getAllMods" import index from "./routes/index" const ROUTES = { "/": index, + "/getallmods": getAllMods, + "/changes": getChanges, } export default async function server() { @@ -9,7 +13,7 @@ export default async function server() { fetch(req) { const url = new URL(req.url) const route = ROUTES[url.pathname] - if (route) return route() + if (route) return route(req) throw new Error(404) }, error(error) {