diff --git a/package.json b/package.json index a28ddde2..be799707 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "prettier": "prettier \"{,!(node_modules)/**/}*.ts\" --config .prettierrc --write" }, "devDependencies": { - "@types/node": "^22.10.5", + "@types/node": "^22.10.7", "nodemon": "^3.1.9", "prettier": "^3.4.2" }, @@ -17,7 +17,7 @@ "@napi-rs/canvas": "^0.1.65", "@octokit/rest": "^20.1.1", "axios": "^1.7.9", - "cron": "^3.3.2", + "cron": "^3.5.0", "discord.js": "^14.16.3", "dotenv": "^16.4.7", "gif-encoder-2": "^1.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a984f06..abfea22c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^1.7.9 version: 1.7.9 cron: - specifier: ^3.3.2 - version: 3.3.2 + specifier: ^3.5.0 + version: 3.5.0 discord.js: specifier: ^14.16.3 version: 14.16.3 @@ -31,14 +31,14 @@ importers: version: 1.0.5 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.10.5)(typescript@5.5.4) + version: 10.9.2(@types/node@22.10.7)(typescript@5.5.4) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 devDependencies: '@types/node': - specifier: ^22.10.5 - version: 22.10.5 + specifier: ^22.10.7 + version: 22.10.7 nodemon: specifier: ^3.1.9 version: 3.1.9 @@ -233,8 +233,8 @@ packages: '@types/luxon@3.4.2': resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} - '@types/node@22.10.5': - resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==} + '@types/node@22.10.7': + resolution: {integrity: sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==} '@types/ws@8.5.13': resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} @@ -296,8 +296,8 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cron@3.3.2: - resolution: {integrity: sha512-7o2PH9vKRd4PxB8c2GsHRozfHYT+gIhZG0DI+vzGOdWo42mofO/ooYnyU0CCh27aKzCrUKMAwAwi7xJ84xKSug==} + cron@3.5.0: + resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==} debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} @@ -736,13 +736,13 @@ snapshots: '@types/luxon@3.4.2': {} - '@types/node@22.10.5': + '@types/node@22.10.7': dependencies: undici-types: 6.20.0 '@types/ws@8.5.13': dependencies: - '@types/node': 22.10.5 + '@types/node': 22.10.7 '@vladfrangu/async_event_emitter@2.4.6': {} @@ -804,7 +804,7 @@ snapshots: create-require@1.1.1: {} - cron@3.3.2: + cron@3.5.0: dependencies: '@types/luxon': 3.4.2 luxon: 3.5.0 @@ -963,14 +963,14 @@ snapshots: ts-mixer@6.0.4: {} - ts-node@10.9.2(@types/node@22.10.5)(typescript@5.5.4): + ts-node@10.9.2(@types/node@22.10.7)(typescript@5.5.4): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.10.5 + '@types/node': 22.10.7 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 diff --git a/resources/strings.json b/resources/strings.json index aff2caf1..36c9e9d4 100644 --- a/resources/strings.json +++ b/resources/strings.json @@ -7,19 +7,20 @@ "error": "One of my developers made an error while making this command! Don't worry, the error is not on your side. Please contact <@360249987927638016>, <@473860522710794250>, or <@173336582265241601> if this keeps occurring.", "maintenance": "I'm currently under maintenance, please try again later.", "description": { - "autopush": "Push textures from a result channel to GitHub.", - "channelpush": "Send textures to council and result channels.", - "behave": "(⌯˃̶᷄ ﹏ ˂̶᷄⌯)", + "autopush": "Push textures from a result channel to GitHub.", + "channelpush": "Send textures to council and result channels.", + "behave": "(⌯˃̶᷄ ﹏ ˂̶᷄⌯)", "feedback": "Create a new suggestion or bug report on our GitHub bug tracker.", "backup": "Back up all files to the GitHub Database repository.", - "hotfix": "Fix something (may change at any time).", - "ping": "Pong!", - "reload": "Reloads all commands.", - "restart": "Restart the bot.", - "say": "Make the bot say something.", - "shutdown": "Stops the bot.", - "status": "Changes the bot's status." - }, + "hotfix": "Fix something (may change at any time).", + "ping": "Pong!", + "reload": "Reloads all commands.", + "restart": "Restart the bot.", + "say": "Make the bot say something.", + "shutdown": "Stops the bot.", + "status": "Changes the bot's status.", + "remove": "Remove files from a pack repository. This does not remove contributions or other pack-specific data." + }, "feedback_sent": "Your ticket will be created shortly on GitHub. Thank you for helping us improve the bot!", "backup_failed": "At least one collection could not be backed up. Please check the developer logs for more information.", "behave": "I'm so sorry! (⌯˃̶᷄ ﹏ ˂̶᷄⌯)", diff --git a/src/commands/submission/remove.ts b/src/commands/submission/remove.ts new file mode 100644 index 00000000..5853277a --- /dev/null +++ b/src/commands/submission/remove.ts @@ -0,0 +1,113 @@ +import strings from "@resources/strings.json"; +import settings from "@resources/settings.json"; + +import { Command } from "@interfaces/discord"; +import { EmbedBuilder, PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; +import { MinecraftEdition, Pack, PackFile, Path, Texture } from "@interfaces/database"; +import axios from "axios"; +import { deleteFromGitHub } from "@functions/pushToGitHub"; +import addDeleteButton from "@helpers/addDeleteButton"; + +const DEBUG = process.env.DEBUG.toLowerCase() === "true"; + +export default { + data: new SlashCommandBuilder() + .setName("remove") + .setDescription(strings.command.description.remove) + .addStringOption((option) => + option + .setName("pack") + .setDescription("Which pack to remove textures from.") + .addChoices( + { name: "All", value: "all" }, + ...Object.values(require("@resources/packs.json")).map((pack: Pack) => ({ + name: pack.name, + value: pack.id, + })), + ) + .setRequired(true), + ) + .addStringOption((option) => + option + .setName("texture") + .setDescription("Texture name or ID to find (first name will be chosen so be careful).") + .setRequired(true), + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDMPermission(false), + async execute(interaction) { + interaction.deferReply(); + const submissions: PackFile = require("@resources/packs.json"); + const pack = interaction.options.getString("pack", true); + const texture = interaction.options.getString("texture", true); + + const cleanedTextureName = texture + .trim() + .replace(".png", "") + .replace("#", "") + .replace(/ /g, "_"); + + const noResultEmbed = new EmbedBuilder() + .setTitle("No results found!") + .setDescription(`No results were found for ${cleanedTextureName}. Have you made a typo?`) + .setColor(settings.colors.red); + + let results: Texture | Texture[]; + try { + results = ( + await axios.get( + `${process.env.API_URL}textures/${encodeURIComponent(cleanedTextureName)}/all`, + ) + ).data; + } catch { + return interaction.editReply({ + embeds: [noResultEmbed], + }); + } + + results = Array.isArray(results) ? results : [results]; + if (!results.length) + return interaction.editReply({ + embeds: [noResultEmbed], + }); + + // take first result + const { name, uses, paths } = results[0]; + + // grouped by edition -> version -> path name + const groupedPaths = uses.reduce>>((acc, cur) => { + acc[cur.edition] ||= {}; + for (const path of paths.filter((p) => p.use === cur.id)) { + // add path to every version it's in + for (const version of path.versions) { + acc[cur.edition][version] ||= []; + acc[cur.edition][version].push(path.name); + } + } + return acc; + }, {}); + + for (const [edition, versions] of Object.entries(groupedPaths)) { + const { org, repo } = submissions[pack].github[edition as MinecraftEdition]; + for (const [version, paths] of Object.entries(versions)) { + try { + await deleteFromGitHub(org, repo, version, `Delete ${name}`, paths); + if (DEBUG) console.log(`Deleted: ${org}/${repo}:${version} (${name})`); + } catch { + // can also be an auth error or really anything but this is most likely + if (DEBUG) console.log(`Branch ${version} doesn't exist for pack ${repo}!`); + } + } + } + + return interaction + .editReply({ + embeds: [ + new EmbedBuilder() + .setTitle(`Successfully removed texture ${name} from pack ${submissions[pack].name}`) + .setColor(settings.colors.green), + ], + }) + .then((msg) => addDeleteButton(msg)); + }, +} as Command; diff --git a/src/functions/pushToGitHub.ts b/src/functions/pushToGitHub.ts index fb6f74f2..dca12b52 100644 --- a/src/functions/pushToGitHub.ts +++ b/src/functions/pushToGitHub.ts @@ -67,6 +67,56 @@ export default async function pushToGitHub( return newCommit.sha; } +/** + * Remove files from a GitHub repository by name + * @author Evorp + * @param org GitHub organization name + * @param repo GitHub repository name + * @param branch branch name + * @param commitMessage + * @param filePaths Paths to delete + * @returns sent commit sha + */ +export async function deleteFromGitHub( + org: string, + repo: string, + branch: string, + commitMessage: string, + filePaths: string[], +) { + const octo = new Octokit({ auth: process.env.GIT_TOKEN }); + const { commitSha, treeSha } = await getCurrentCommit(octo, org, repo, branch); + + const treeItems = filePaths.map( + (path) => + ({ + path, + mode: "100644", + // null means deletion + sha: null, + }) as const, + ); + + const { data: newTreeData } = await octo.git.createTree({ + owner: org, + repo, + tree: treeItems, + base_tree: treeSha, + }); + + const commitData = await createNewCommit( + octo, + org, + repo, + commitMessage, + newTreeData.sha, + commitSha, + ); + + await setBranchToCommit(octo, org, repo, branch, commitData.sha); + return commitData.sha; +} + /** * Get current commit of a branch from a repository * @param octo @@ -186,7 +236,7 @@ async function createNewTree( * @param repo GitHub repository of the organisation * @param message Commit message * @param currentTreeSha - * @param currentCommitSha + * @param lastCommitSha */ async function createNewCommit( octo: Octokit, @@ -194,14 +244,14 @@ async function createNewCommit( repo: string, message: string, currentTreeSha: string, - currentCommitSha: string, + lastCommitSha: string, ) { const { data } = await octo.git.createCommit({ owner: org, repo, message, tree: currentTreeSha, - parents: [currentCommitSha], + parents: [lastCommitSha], }); return data; } diff --git a/src/functions/submission/pushTextures.ts b/src/functions/submission/pushTextures.ts index 4e027381..1e3833af 100644 --- a/src/functions/submission/pushTextures.ts +++ b/src/functions/submission/pushTextures.ts @@ -3,7 +3,7 @@ import { existsSync, rmSync } from "fs"; import formattedDate from "@helpers/formattedDate"; import pushToGitHub from "@functions/pushToGitHub"; -import type { PackFile } from "@interfaces/database"; +import type { MinecraftEdition, PackFile } from "@interfaces/database"; import { join } from "path"; const DEBUG = process.env.DEBUG.toLowerCase() === "true"; const DEV = process.env.DEV.toLowerCase() === "true"; @@ -23,14 +23,13 @@ export default async function pushTextures( const settings = require("@resources/settings.json"); const packs: PackFile = require("@resources/packs.json"); - const editions = Object.keys(settings.versions).filter((k) => k !== "id"); - for (const edition of editions) { - const packGitHub = packs[pack].github[edition]; + for (const edition of Object.keys(settings.versions)) { + const packGitHub = packs[pack].github[edition as MinecraftEdition]; if (!packGitHub) { if (DEBUG) console.log(`${pack} doesn't support ${edition} yet!`); continue; } - for (const branch of settings.versions[edition]) { + for (const branch of settings.versions[edition] as string) { const path = join(basePath, packGitHub.repo, branch); // don't create empty commits