From 7d2e157148b0031b4830736ba5a359e29e4519e4 Mon Sep 17 00:00:00 2001 From: ITOH Date: Wed, 26 Jan 2022 19:02:34 +0100 Subject: [PATCH 1/2] move plugins to the main repo --- plugins/cache/LICENSE | 201 ++++++++ plugins/cache/README.md | 21 + plugins/cache/deps.ts | 1 + plugins/cache/mod.ts | 159 +++++++ plugins/cache/src/addCacheCollections.ts | 39 ++ plugins/cache/src/dispatchRequirements.ts | 99 ++++ plugins/cache/src/setupCacheEdits.ts | 120 +++++ plugins/cache/src/setupCacheRemovals.ts | 124 +++++ plugins/cache/src/sweepers.ts | 76 +++ plugins/fileloader/LICENSE | 201 ++++++++ plugins/fileloader/README.md | 32 ++ plugins/fileloader/deps.ts | 1 + plugins/fileloader/mod.ts | 99 ++++ plugins/helpers/LICENSE | 201 ++++++++ plugins/helpers/README.md | 1 + plugins/helpers/deps.ts | 1 + plugins/helpers/mod.ts | 123 +++++ plugins/helpers/src/channels.ts | 46 ++ plugins/helpers/src/disconnectMember.ts | 6 + plugins/helpers/src/getMembersPaginated.ts | 73 +++ plugins/helpers/src/moveMember.ts | 13 + .../helpers/src/sendAutoCompleteChoices.ts | 19 + plugins/helpers/src/sendDirectMessage.ts | 27 ++ plugins/helpers/src/suppressEmbeds.ts | 17 + plugins/helpers/src/threads.ts | 38 ++ plugins/permissions/LICENSE | 201 ++++++++ plugins/permissions/README.md | 25 + plugins/permissions/deps.ts | 2 + plugins/permissions/mod.ts | 48 ++ .../permissions/src/channels/deleteChannel.ts | 37 ++ .../src/channels/deleteChannelOverwrite.ts | 16 + .../permissions/src/channels/editChannel.ts | 123 +++++ .../src/channels/editChannelOverwrite.ts | 15 + .../permissions/src/channels/followChannel.ts | 15 + .../src/channels/getChannelWebhooks.ts | 15 + plugins/permissions/src/channels/mod.ts | 22 + plugins/permissions/src/channels/stage.ts | 56 +++ .../permissions/src/channels/swapChannels.ts | 12 + .../src/channels/threads/addToThread.ts | 26 + .../channels/threads/getArchivedThreads.ts | 22 + .../src/channels/threads/getThreadMembers.ts | 16 + .../src/channels/threads/joinThread.ts | 15 + .../src/channels/threads/leaveThread.ts | 15 + .../permissions/src/channels/threads/mod.ts | 16 + .../channels/threads/removeThreadMember.ts | 33 ++ plugins/permissions/src/components.ts | 178 +++++++ .../permissions/src/connectToVoiceChannels.ts | 43 ++ plugins/permissions/src/discovery.ts | 49 ++ plugins/permissions/src/editMember.ts | 67 +++ plugins/permissions/src/emojis.ts | 38 ++ plugins/permissions/src/guilds/createGuild.ts | 22 + plugins/permissions/src/guilds/deleteGuild.ts | 14 + plugins/permissions/src/guilds/editGuild.ts | 12 + plugins/permissions/src/guilds/events.ts | 144 ++++++ .../permissions/src/guilds/getAuditLogs.ts | 12 + plugins/permissions/src/guilds/getBan.ts | 12 + plugins/permissions/src/guilds/getBans.ts | 12 + .../permissions/src/guilds/getPruneCount.ts | 12 + .../permissions/src/guilds/getVanityUrl.ts | 12 + plugins/permissions/src/guilds/mod.ts | 26 + .../permissions/src/guilds/welcomeScreen.ts | 16 + plugins/permissions/src/guilds/widget.ts | 16 + plugins/permissions/src/integrations.ts | 27 ++ .../permissions/src/interactions/commands.ts | 201 ++++++++ .../src/interactions/editFollowupMessage.ts | 61 +++ plugins/permissions/src/interactions/mod.ts | 31 ++ plugins/permissions/src/invites.ts | 47 ++ plugins/permissions/src/members/ban.ts | 27 ++ plugins/permissions/src/members/editBot.ts | 12 + plugins/permissions/src/members/editMember.ts | 22 + plugins/permissions/src/members/kickMember.ts | 12 + plugins/permissions/src/members/mod.ts | 14 + .../permissions/src/members/pruneMembers.ts | 12 + plugins/permissions/src/messages/create.ts | 200 ++++++++ plugins/permissions/src/messages/delete.ts | 80 ++++ plugins/permissions/src/messages/get.ts | 44 ++ plugins/permissions/src/messages/mod.ts | 14 + plugins/permissions/src/messages/pin.ts | 37 ++ plugins/permissions/src/messages/reactions.ts | 92 ++++ plugins/permissions/src/misc/mod.ts | 45 ++ plugins/permissions/src/permissions.ts | 450 ++++++++++++++++++ plugins/permissions/src/roles/add.ts | 32 ++ plugins/permissions/src/roles/create.ts | 16 + plugins/permissions/src/roles/delete.ts | 15 + plugins/permissions/src/roles/edit.ts | 31 ++ plugins/permissions/src/roles/mod.ts | 14 + plugins/permissions/src/roles/remove.ts | 32 ++ .../permissions/src/webhooks/createWebhook.ts | 22 + .../permissions/src/webhooks/deleteWebhook.ts | 12 + .../permissions/src/webhooks/editWebhook.ts | 23 + plugins/permissions/src/webhooks/message.ts | 71 +++ plugins/permissions/src/webhooks/mod.ts | 12 + .../permissions/src/webhooks/sendWebhook.ts | 67 +++ 93 files changed, 4928 insertions(+) create mode 100644 plugins/cache/LICENSE create mode 100644 plugins/cache/README.md create mode 100644 plugins/cache/deps.ts create mode 100644 plugins/cache/mod.ts create mode 100644 plugins/cache/src/addCacheCollections.ts create mode 100644 plugins/cache/src/dispatchRequirements.ts create mode 100644 plugins/cache/src/setupCacheEdits.ts create mode 100644 plugins/cache/src/setupCacheRemovals.ts create mode 100644 plugins/cache/src/sweepers.ts create mode 100644 plugins/fileloader/LICENSE create mode 100644 plugins/fileloader/README.md create mode 100644 plugins/fileloader/deps.ts create mode 100644 plugins/fileloader/mod.ts create mode 100644 plugins/helpers/LICENSE create mode 100644 plugins/helpers/README.md create mode 100644 plugins/helpers/deps.ts create mode 100644 plugins/helpers/mod.ts create mode 100644 plugins/helpers/src/channels.ts create mode 100644 plugins/helpers/src/disconnectMember.ts create mode 100644 plugins/helpers/src/getMembersPaginated.ts create mode 100644 plugins/helpers/src/moveMember.ts create mode 100644 plugins/helpers/src/sendAutoCompleteChoices.ts create mode 100644 plugins/helpers/src/sendDirectMessage.ts create mode 100644 plugins/helpers/src/suppressEmbeds.ts create mode 100644 plugins/helpers/src/threads.ts create mode 100644 plugins/permissions/LICENSE create mode 100644 plugins/permissions/README.md create mode 100644 plugins/permissions/deps.ts create mode 100644 plugins/permissions/mod.ts create mode 100644 plugins/permissions/src/channels/deleteChannel.ts create mode 100644 plugins/permissions/src/channels/deleteChannelOverwrite.ts create mode 100644 plugins/permissions/src/channels/editChannel.ts create mode 100644 plugins/permissions/src/channels/editChannelOverwrite.ts create mode 100644 plugins/permissions/src/channels/followChannel.ts create mode 100644 plugins/permissions/src/channels/getChannelWebhooks.ts create mode 100644 plugins/permissions/src/channels/mod.ts create mode 100644 plugins/permissions/src/channels/stage.ts create mode 100644 plugins/permissions/src/channels/swapChannels.ts create mode 100644 plugins/permissions/src/channels/threads/addToThread.ts create mode 100644 plugins/permissions/src/channels/threads/getArchivedThreads.ts create mode 100644 plugins/permissions/src/channels/threads/getThreadMembers.ts create mode 100644 plugins/permissions/src/channels/threads/joinThread.ts create mode 100644 plugins/permissions/src/channels/threads/leaveThread.ts create mode 100644 plugins/permissions/src/channels/threads/mod.ts create mode 100644 plugins/permissions/src/channels/threads/removeThreadMember.ts create mode 100644 plugins/permissions/src/components.ts create mode 100644 plugins/permissions/src/connectToVoiceChannels.ts create mode 100644 plugins/permissions/src/discovery.ts create mode 100644 plugins/permissions/src/editMember.ts create mode 100644 plugins/permissions/src/emojis.ts create mode 100644 plugins/permissions/src/guilds/createGuild.ts create mode 100644 plugins/permissions/src/guilds/deleteGuild.ts create mode 100644 plugins/permissions/src/guilds/editGuild.ts create mode 100644 plugins/permissions/src/guilds/events.ts create mode 100644 plugins/permissions/src/guilds/getAuditLogs.ts create mode 100644 plugins/permissions/src/guilds/getBan.ts create mode 100644 plugins/permissions/src/guilds/getBans.ts create mode 100644 plugins/permissions/src/guilds/getPruneCount.ts create mode 100644 plugins/permissions/src/guilds/getVanityUrl.ts create mode 100644 plugins/permissions/src/guilds/mod.ts create mode 100644 plugins/permissions/src/guilds/welcomeScreen.ts create mode 100644 plugins/permissions/src/guilds/widget.ts create mode 100644 plugins/permissions/src/integrations.ts create mode 100644 plugins/permissions/src/interactions/commands.ts create mode 100644 plugins/permissions/src/interactions/editFollowupMessage.ts create mode 100644 plugins/permissions/src/interactions/mod.ts create mode 100644 plugins/permissions/src/invites.ts create mode 100644 plugins/permissions/src/members/ban.ts create mode 100644 plugins/permissions/src/members/editBot.ts create mode 100644 plugins/permissions/src/members/editMember.ts create mode 100644 plugins/permissions/src/members/kickMember.ts create mode 100644 plugins/permissions/src/members/mod.ts create mode 100644 plugins/permissions/src/members/pruneMembers.ts create mode 100644 plugins/permissions/src/messages/create.ts create mode 100644 plugins/permissions/src/messages/delete.ts create mode 100644 plugins/permissions/src/messages/get.ts create mode 100644 plugins/permissions/src/messages/mod.ts create mode 100644 plugins/permissions/src/messages/pin.ts create mode 100644 plugins/permissions/src/messages/reactions.ts create mode 100644 plugins/permissions/src/misc/mod.ts create mode 100644 plugins/permissions/src/permissions.ts create mode 100644 plugins/permissions/src/roles/add.ts create mode 100644 plugins/permissions/src/roles/create.ts create mode 100644 plugins/permissions/src/roles/delete.ts create mode 100644 plugins/permissions/src/roles/edit.ts create mode 100644 plugins/permissions/src/roles/mod.ts create mode 100644 plugins/permissions/src/roles/remove.ts create mode 100644 plugins/permissions/src/webhooks/createWebhook.ts create mode 100644 plugins/permissions/src/webhooks/deleteWebhook.ts create mode 100644 plugins/permissions/src/webhooks/editWebhook.ts create mode 100644 plugins/permissions/src/webhooks/message.ts create mode 100644 plugins/permissions/src/webhooks/mod.ts create mode 100644 plugins/permissions/src/webhooks/sendWebhook.ts diff --git a/plugins/cache/LICENSE b/plugins/cache/LICENSE new file mode 100644 index 000000000..80a84a261 --- /dev/null +++ b/plugins/cache/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 - 2022 Discordeno + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/cache/README.md b/plugins/cache/README.md new file mode 100644 index 000000000..994e34f77 --- /dev/null +++ b/plugins/cache/README.md @@ -0,0 +1,21 @@ +# cache-plugin + +This is an official plugin maintained by Discordeno. This plugin provides +automatic caching. Remember Discordeno does not cache by default. This plugin is +NOT recommended for big bot developers but this is useful for smaller bots who +just want simple functionality. + +## Usage + +```ts +// MOVE TO DEPS.TS AND USE SPECIFIC VERSION +import { enableCachePlugin, enableCacheSweepers } from "https://deno.land/x/discordeno_cache_plugin/mod.ts"; + +// Create the bot object, THIS WILL NEED YOUR OPTIONS. +const baseBot = createBot({}); +// Enables the cache plugin on this bot +const bot = enableCachePlugin(baseBot); +enableCacheSweepers(bot); +// Start your bot +await startBot(bot); +``` diff --git a/plugins/cache/deps.ts b/plugins/cache/deps.ts new file mode 100644 index 000000000..ac326e73f --- /dev/null +++ b/plugins/cache/deps.ts @@ -0,0 +1 @@ +export * from "../../mod.ts"; diff --git a/plugins/cache/mod.ts b/plugins/cache/mod.ts new file mode 100644 index 000000000..001920d2b --- /dev/null +++ b/plugins/cache/mod.ts @@ -0,0 +1,159 @@ +import { + Bot, + Collection, + GuildEmojisUpdate, + SnakeCasedPropertiesDeep, +} from "./deps.ts"; +import { setupCacheRemovals } from "./src/setupCacheRemovals.ts"; +import { + addCacheCollections, + BotWithCache, +} from "./src/addCacheCollections.ts"; +import { setupCacheEdits } from "./src/setupCacheEdits.ts"; + +// PLUGINS MUST TAKE A BOT ARGUMENT WHICH WILL BE MODIFIED +export function enableCachePlugin(rawBot: B): BotWithCache { + // MARK THIS PLUGIN BEING USED + rawBot.enabledPlugins.add("CACHE"); + + // CUSTOMIZATION GOES HERE + const bot = addCacheCollections(rawBot); + + // Get the unmodified transformer. + const { guild, user, member, channel, message, presence, role } = + bot.transformers; + // Override the transformer + bot.transformers.guild = function (_, payload) { + // Run the unmodified transformer + const result = guild(bot, payload); + // Cache the result + if (result) { + bot.guilds.set(result.id, result); + + const channels = payload.guild.channels || []; + + channels.forEach((channel) => { + bot.transformers.channel(bot, { channel, guildId: result.id }); + }); + } + + // Return the result + return result; + }; + + // Override the transformer + bot.transformers.user = function (...args) { + // Run the unmodified transformer + const result = user(...args); + // Cache the result + if (result) { + bot.users.set(result.id, result); + } + // Return the result + return result; + }; + + // Override the transformer + bot.transformers.member = function (...args) { + // Run the unmodified transformer + const result = member(...args); + // Cache the result + if (result) { + bot.members.set( + bot.transformers.snowflake(`${result.id}${result.guildId}`), + result, + ); + } + // Return the result + return result; + }; + + // Override the transformer + bot.transformers.channel = function (...args) { + // Run the unmodified transformer + const result = channel(...args); + // Cache the result + if (result) { + bot.channels.set(result.id, result); + } + // Return the result + return result; + }; + + // Override the transformer + bot.transformers.message = function (_, payload) { + // Run the unmodified transformer + const result = message(bot, payload); + // Cache the result + if (result) { + bot.messages.set(result.id, result); + // CACHE THE USER + const user = bot.transformers.user(bot, payload.author); + bot.users.set(user.id, user); + + if (payload.guild_id && payload.member) { + const guildId = bot.transformers.snowflake(payload.guild_id); + // CACHE THE MEMBER + bot.members.set( + bot.transformers.snowflake(`${payload.author.id}${payload.guild_id}`), + bot.transformers.member(bot, payload.member, guildId, user.id), + ); + } + } + + // Return the result + return result; + }; + + // Override the transformer + bot.transformers.presence = function (...args) { + // Run the unmodified transformer + const result = presence(...args); + // Cache the result + if (result) { + bot.presences.set(result.user.id, result); + } + // Return the result + return result; + }; + + // Override the transformer + bot.transformers.role = function (...args) { + // Run the unmodified transformer + const result = role(...args); + // Cache the result + if (result) { + bot.guilds.get(result.guildId)?.roles.set(result.id, result); + } + // Return the result + return result; + }; + + const { GUILD_EMOJIS_UPDATE } = bot.handlers; + bot.handlers.GUILD_EMOJIS_UPDATE = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + + const guild = bot.guilds.get(bot.transformers.snowflake(payload.guild_id)); + if (guild) { + guild.emojis = new Collection(payload.emojis.map((e) => { + const emoji = bot.transformers.emoji(bot, e); + return [emoji.id!, emoji]; + })); + } + + GUILD_EMOJIS_UPDATE(bot, data, shardId); + }; + + setupCacheRemovals(bot); + setupCacheEdits(bot); + + // PLUGINS MUST RETURN THE BOT + return bot; +} + +export default enableCachePlugin; +export * from "./src/addCacheCollections.ts"; +export * from "./src/dispatchRequirements.ts"; +export * from "./src/setupCacheEdits.ts"; +export * from "./src/setupCacheRemovals.ts"; +export * from "./src/sweepers.ts"; diff --git a/plugins/cache/src/addCacheCollections.ts b/plugins/cache/src/addCacheCollections.ts new file mode 100644 index 000000000..34ecb2611 --- /dev/null +++ b/plugins/cache/src/addCacheCollections.ts @@ -0,0 +1,39 @@ +import { + Bot, + Collection, + DiscordenoChannel, + DiscordenoGuild, + DiscordenoMember, + DiscordenoMessage, + DiscordenoPresence, + DiscordenoUser, +} from "../deps.ts"; + +export type BotWithCache = B & CacheProps; + +export interface CacheProps extends Bot { + guilds: Collection; + users: Collection; + members: Collection; + channels: Collection; + messages: Collection; + presences: Collection; + dispatchedGuildIds: Set; + dispatchedChannelIds: Set; + activeGuildIds: Set; +} + +export function addCacheCollections(bot: B): BotWithCache { + const cacheBot = bot as BotWithCache; + cacheBot.guilds = new Collection(); + cacheBot.users = new Collection(); + cacheBot.members = new Collection(); + cacheBot.channels = new Collection(); + cacheBot.messages = new Collection(); + cacheBot.presences = new Collection(); + cacheBot.dispatchedGuildIds = new Set(); + cacheBot.dispatchedChannelIds = new Set(); + cacheBot.activeGuildIds = new Set(); + + return bot as BotWithCache; +} diff --git a/plugins/cache/src/dispatchRequirements.ts b/plugins/cache/src/dispatchRequirements.ts new file mode 100644 index 000000000..650107211 --- /dev/null +++ b/plugins/cache/src/dispatchRequirements.ts @@ -0,0 +1,99 @@ +import { Bot, GatewayPayload } from "../deps.ts"; +import { BotWithCache } from "./addCacheCollections.ts"; + +const processing = new Set(); + +export async function dispatchRequirements( + bot: BotWithCache, + data: GatewayPayload, +) { + // DELETE MEANS WE DONT NEED TO FETCH. CREATE SHOULD HAVE DATA TO CACHE + if (data.t && ["GUILD_CREATE", "GUILD_DELETE"].includes(data.t)) return; + + const id = bot.utils.snowflakeToBigint( + (data.t && ["GUILD_UPDATE"].includes(data.t) + ? // deno-lint-ignore no-explicit-any + (data.d as any)?.id + : // deno-lint-ignore no-explicit-any + (data.d as any)?.guild_id) ?? "", + ); + + if (!id || bot.activeGuildIds.has(id)) return; + + // If this guild is in cache, it has not been swept and we can cancel + if (bot.guilds.has(id)) { + bot.activeGuildIds.add(id); + return; + } + + if (processing.has(id)) { + bot.events.debug( + `[DISPATCH] New Guild ID already being processed: ${id} in ${data.t} event`, + ); + + let runs = 0; + do { + await bot.utils.delay(500); + runs++; + } while (processing.has(id) && runs < 40); + + if (!processing.has(id)) return; + + return bot.events.debug( + `[DISPATCH] Already processed guild was not successfully fetched: ${id} in ${data.t} event`, + ); + } + + processing.add(id); + + // New guild id has appeared, fetch all relevant data + bot.events.debug( + `[DISPATCH] New Guild ID has appeared: ${id} in ${data.t} event`, + ); + + const guild = (await bot.helpers + .getGuild(id, { + counts: true, + }) + .catch(console.log)); + + if (!guild) { + processing.delete(id); + return bot.events.debug(`[DISPATCH] Guild ID ${id} failed to fetch.`); + } + + bot.events.debug(`[DISPATCH] Guild ID ${id} has been found. ${guild.name}`); + + const [channels, botMember] = await Promise.all([ + bot.helpers.getChannels(id), + bot.helpers.getMember(id, bot.id), + ]).catch((error) => { + bot.events.debug(error); + return []; + }); + + if (!botMember || !channels) { + processing.delete(id); + return bot.events.debug( + `[DISPATCH] Guild ID ${id} Name: ${guild.name} failed. Unable to get botMember or channels`, + ); + } + + // Add to cache + bot.guilds.set(id, guild); + bot.dispatchedGuildIds.delete(id); + channels.forEach((channel) => { + bot.dispatchedChannelIds.delete(channel.id); + bot.channels.set(channel.id, channel); + }); + bot.members.set( + bot.transformers.snowflake(`${botMember.id}${guild.id}`), + botMember, + ); + + processing.delete(id); + + bot.events.debug( + `[DISPATCH] Guild ID ${id} Name: ${guild.name} completely loaded.`, + ); +} diff --git a/plugins/cache/src/setupCacheEdits.ts b/plugins/cache/src/setupCacheEdits.ts new file mode 100644 index 000000000..0a1b50d35 --- /dev/null +++ b/plugins/cache/src/setupCacheEdits.ts @@ -0,0 +1,120 @@ +import type { + Bot, + GuildMemberAdd, + GuildMemberRemove, + MessageReactionAdd, + MessageReactionRemove, + MessageReactionRemoveAll, + SnakeCasedPropertiesDeep, +} from "../deps.ts"; +import type { BotWithCache } from "./addCacheCollections.ts"; + +export function setupCacheEdits(bot: BotWithCache) { + const { + GUILD_MEMBER_ADD, + GUILD_MEMBER_REMOVE, + MESSAGE_REACTION_ADD, + MESSAGE_REACTION_REMOVE, + MESSAGE_REACTION_REMOVE_ALL, + } = bot.handlers; + + bot.handlers.GUILD_MEMBER_ADD = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + + const guild = bot.guilds.get(bot.transformers.snowflake(payload.guild_id)); + + if (guild) guild.memberCount++; + + GUILD_MEMBER_ADD(bot, data, shardId); + }; + + bot.handlers.GUILD_MEMBER_REMOVE = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + + const guild = bot.guilds.get(bot.transformers.snowflake(payload.guild_id)); + + if (guild) guild.memberCount--; + + GUILD_MEMBER_REMOVE(bot, data, shardId); + }; + + bot.handlers.MESSAGE_REACTION_ADD = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + + const messageId = bot.transformers.snowflake(payload.message_id) + const message = bot.messages.get(messageId); + + const emoji = bot.transformers.emoji(bot, payload.emoji); + + // if the message is cached + if (message) { + const reactions = message.reactions?.map((r) => r.emoji.name); + const toSet = { + count: 1, + me: bot.transformers.snowflake(payload.user_id) === bot.id, + emoji: emoji, + }; + + // if theres no reaction add it + if (!message.reactions || !reactions) { + message.reactions = [toSet]; + } else if (!reactions.includes(emoji.name)) { + message.reactions?.push(toSet); + } else { // otherwise the reaction has already been added so +1 to the reaction count + const current = message.reactions?.[reactions.indexOf(emoji.name)]; + + // rewrite + if (current && message.reactions?.[message.reactions.indexOf(current)]) { + message.reactions[message.reactions.indexOf(current)].count++; + } + } + } + + MESSAGE_REACTION_ADD(bot, data, shardId); + } + + bot.handlers.MESSAGE_REACTION_REMOVE = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + + const messageId = bot.transformers.snowflake(payload.message_id) + const message = bot.messages.get(messageId); + + const emoji = bot.transformers.emoji(bot, payload.emoji); + + // if the message is cached + if (message) { + const reactions = message.reactions?.map((r) => r.emoji.name); + + if (reactions?.indexOf(emoji.name) !== undefined) { + const current = message.reactions?.[reactions.indexOf(emoji.name)]; + + if (current) { + if (current.count > 0) { + current.count--; + } + // delete when count is 0 + if (current.count === 0) { + message.reactions?.splice(reactions?.indexOf(emoji.name), 1); + } + // when someone deleted a reaction that doesn't exist in the cache just pass + } + } + } + + MESSAGE_REACTION_REMOVE(bot, data, shardId); + } + + bot.handlers.MESSAGE_REACTION_REMOVE_ALL = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + + const messageId = bot.transformers.snowflake(payload.message_id); + const message = bot.messages.get(messageId); + + if (message) { + // when an admin deleted all the reactions of a message + message.reactions = undefined; + } + + MESSAGE_REACTION_REMOVE_ALL(bot, data, shardId); + } +} diff --git a/plugins/cache/src/setupCacheRemovals.ts b/plugins/cache/src/setupCacheRemovals.ts new file mode 100644 index 000000000..001ce5129 --- /dev/null +++ b/plugins/cache/src/setupCacheRemovals.ts @@ -0,0 +1,124 @@ +import { +Bot, + Channel, + Collection, + GuildBanAddRemove, + GuildEmojisUpdate, + GuildMemberRemove, + GuildRoleDelete, + MessageDelete, + MessageDeleteBulk, +} from "../deps.ts"; +import { SnakeCasedPropertiesDeep, UnavailableGuild } from "../deps.ts"; +import { BotWithCache } from "./addCacheCollections.ts"; + +export function setupCacheRemovals(bot: BotWithCache) { + const { + CHANNEL_DELETE, + GUILD_BAN_ADD, + GUILD_DELETE, + GUILD_EMOJIS_UPDATE, + GUILD_MEMBER_REMOVE, + GUILD_ROLE_DELETE, + MESSAGE_DELETE_BULK, + } = bot.handlers; + + bot.handlers.GUILD_DELETE = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + const id = bot.transformers.snowflake(payload.id); + + bot.guilds.delete(id); + bot.channels.forEach((channel) => { + if (channel.guildId === id) bot.channels.delete(channel.id); + }); + bot.members.forEach((member) => { + if (member.guildId === id) bot.members.delete(member.id); + }); + bot.messages.forEach((message) => { + if (message.guildId === id) bot.messages.delete(message.id); + }); + GUILD_DELETE(bot, data, shardId); + }; + + bot.handlers.CHANNEL_DELETE = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + // HANDLER BEFORE DELETING, BECAUSE HANDLER RUNS TRANSFORMER WHICH RECACHES + CHANNEL_DELETE(bot, data, shardId); + + const id = bot.transformers.snowflake(payload.id); + bot.channels.delete(id); + bot.messages.forEach((message) => { + if (message.channelId === id) bot.messages.delete(message.id); + }); + }; + + bot.handlers.GUILD_MEMBER_REMOVE = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + bot.members.delete(bot.transformers.snowflake(payload.user.id)); + GUILD_MEMBER_REMOVE(bot, data, shardId); + }; + + bot.handlers.GUILD_BAN_ADD = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + bot.members.delete(bot.transformers.snowflake(payload.user.id)); + GUILD_BAN_ADD(bot, data, shardId); + }; + + bot.handlers.GUILD_EMOJIS_UPDATE = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + + const guild = bot.guilds.get(bot.transformers.snowflake(payload.guild_id)); + if (guild) { + guild.emojis = new Collection(payload.emojis.map((e) => { + const emoji = bot.transformers.emoji(bot, e); + return [emoji.id!, emoji]; + })); + } + + GUILD_EMOJIS_UPDATE(bot, data, shardId); + }; + + bot.handlers.MESSAGE_DELETE = function (_, data) { + const payload = data.d as SnakeCasedPropertiesDeep; + const id = bot.transformers.snowflake(payload.id); + const message = bot.messages.get(id); + bot.events.messageDelete(bot, { + id, + channelId: bot.transformers.snowflake(payload.channel_id), + guildId: payload.guild_id + ? bot.transformers.snowflake(payload.guild_id) + : undefined, + }, message); + bot.messages.delete(id); + }; + + bot.handlers.MESSAGE_DELETE_BULK = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + payload.ids.forEach((id) => + bot.messages.delete(bot.transformers.snowflake(id)) + ); + MESSAGE_DELETE_BULK(bot, data, shardId); + }; + + bot.handlers.GUILD_ROLE_DELETE = function (_, data, shardId) { + const payload = data.d as SnakeCasedPropertiesDeep; + const guild = bot.guilds.get( + bot.transformers.snowflake(payload.guild_id), + ); + const id = bot.transformers.snowflake(payload.role_id); + + if (guild) { + guild.roles.delete(id); + bot.members.forEach((member) => { + // SKIP MEMBERS IN OTHER GUILDS + if (member.guildId !== guild.id) return; + // SKIP MEMBERS WHO DON'T HAVE ROLE + if (!member.roles.includes(id)) return; + // EDIT THE MEMBERS ROLES + member.roles = member.roles.filter((roleId) => roleId !== id); + }); + } + + GUILD_ROLE_DELETE(bot, data, shardId); + }; +} diff --git a/plugins/cache/src/sweepers.ts b/plugins/cache/src/sweepers.ts new file mode 100644 index 000000000..bb4085732 --- /dev/null +++ b/plugins/cache/src/sweepers.ts @@ -0,0 +1,76 @@ +import { Bot } from "../deps.ts"; +import { BotWithCache } from "./addCacheCollections.ts"; +import { dispatchRequirements } from "./dispatchRequirements.ts"; + +/** Enables sweepers for your bot but will require, enabling cache first. */ +export function enableCacheSweepers(bot: BotWithCache) { + bot.guilds.startSweeper({ + filter: function (guild, _, bot: BotWithCache) { + // Reset activity for next interval + if (bot.activeGuildIds.delete(guild.id)) return false; + + // This is inactive guild. Not a single thing has happened for atleast 30 minutes. + // Not a reaction, not a message, not any event! + bot.dispatchedGuildIds.add(guild.id); + + return true; + }, + interval: 3660000, + bot, + }); + bot.channels.startSweeper({ + filter: function channelSweeper( + channel, + key, + bot: BotWithCache, + ) { + // If this is in a guild and the guild was dispatched, then we can dispatch the channel + if (channel.guildId && bot.dispatchedGuildIds.has(channel.guildId)) { + bot.dispatchedChannelIds.add(channel.id); + return true; + } + + // THE KEY DM CHANNELS ARE STORED BY IS THE USER ID. If the user is not cached, we dont need to cache their dm channel. + if (!channel.guildId && !bot.members.has(key)) return true; + + return false; + }, + interval: 3660000, + bot, + }); + + bot.members.startSweeper({ + filter: function memberSweeper(member, _, bot: BotWithCache) { + // Don't sweep the bot else strange things will happen + if (member.id === bot.id) return false; + + // Only sweep members who were not active the last 30 minutes + return Date.now() - member.cachedAt > 1800000; + }, + interval: 300000, + bot, + }); + + bot.messages.startSweeper({ + filter: function messageSweeper(message) { + // DM messages aren't needed + if (!message.guildId) return true; + + // Only delete messages older than 10 minutes + return Date.now() - message.timestamp > 600000; + }, + interval: 300000, + bot, + }); + + bot.presences.startSweeper({ filter: () => true, interval: 300000, bot }); + + // DISPATCH REQUIREMENTS + const handleDiscordPayloadOld = bot.gateway.handleDiscordPayload; + bot.gateway.handleDiscordPayload = async function (_, data, shardId) { + // RUN DISPATCH CHECK + await dispatchRequirements(bot, data); + // RUN OLD HANDLER + handleDiscordPayloadOld(_, data, shardId); + }; +} diff --git a/plugins/fileloader/LICENSE b/plugins/fileloader/LICENSE new file mode 100644 index 000000000..80a84a261 --- /dev/null +++ b/plugins/fileloader/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 - 2022 Discordeno + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/fileloader/README.md b/plugins/fileloader/README.md new file mode 100644 index 000000000..e8b29893a --- /dev/null +++ b/plugins/fileloader/README.md @@ -0,0 +1,32 @@ +# fileloader-plugin + +> Please check `./deps.ts` for the compatible `discordeno` version! + +This plugin leverages the ability to write files, and then import them. + +## Code Example + +```typescript +import { createBot, enableFileLoaderPlugin, startBot } from './deps.ts' // Import discordeno and this plugin. + +console.log('Starting Up the Bot, this might take awhile...'); + +const bot = enableFileLoaderPlugin(createBot({ + token: '', // Your bot's token + botId: 0n, // Your bot's "Application Id", + intents: [], + events: { + ready() { + console.log('Bot Ready'); + } + } +})); + +bot.fastFileLoader([ + // './src/commands', etc. This works just like `import [something] from [somewhere]` +]); + +startBot(bot); +``` + +Make sure to ignore `fileloader.ts` in git as it is (re)generated whever you (re)start the bot. diff --git a/plugins/fileloader/deps.ts b/plugins/fileloader/deps.ts new file mode 100644 index 000000000..ac326e73f --- /dev/null +++ b/plugins/fileloader/deps.ts @@ -0,0 +1 @@ +export * from "../../mod.ts"; diff --git a/plugins/fileloader/mod.ts b/plugins/fileloader/mod.ts new file mode 100644 index 000000000..4286658a3 --- /dev/null +++ b/plugins/fileloader/mod.ts @@ -0,0 +1,99 @@ +import { Bot } from './deps.ts'; + +// iMpOrTaNt to make sure files can be reloaded properly! +export let uniqueFilePathCounter = 0; +export let paths: string[] = []; + +/** Recursively generates an array of unique paths to import using `fileLoader()` + * (**Is** windows compatible) +*/ +export async function importDirectory(path: string) { + path = path.replaceAll("\\", "/"); + const files = Deno.readDirSync(Deno.realPathSync(path)); + + for (const file of files) { + if (!file.name) continue; + + const currentPath = `${path}/${file.name}`; + if (file.isFile) { + if (!currentPath.endsWith(".ts")) continue; + paths.push( + `import "${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf("/"))}/${currentPath.substring( + currentPath.indexOf("src/") + )}#${uniqueFilePathCounter}";` + ); + continue; + } + + // Recursive function! + await importDirectory(currentPath); + } + + uniqueFilePathCounter++; +} + +/** Writes, then imports all everything in fileloader.ts */ +export async function fileLoader() { + await Deno.writeTextFile("fileloader.ts", paths.join("\n").replaceAll("\\", "/")); + await import( + `${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf("/"))}/fileloader.ts#${uniqueFilePathCounter}` + ); + paths = []; +} + +/** This function will import the specified directories */ +export async function fastFileLoader( + /** An array of directories to import recursively. */ + paths: string[], + /** A function that will run before recursively setting a part of `paths`. + * `path` contains the path that will be imported, useful for logging + */ + between?: (path: string, uniqueFilePathCounter: number, paths: string[]) => void, + /** A function that runs before **actually** importing all the files. */ + before?: (uniqueFilePathCounter: number, paths: string[]) => void +) { + await Promise.all( + [...paths].map((path) => { + if (between) between(path, uniqueFilePathCounter, paths); + importDirectory(path) + }) + ); + + if (before) before(uniqueFilePathCounter, paths); + + await fileLoader(); +} + +/** Extend the Bot with the Plugin's added functions */ +export interface BotWithFileLoader extends Bot { + /** Recursively generates an array of unique paths to import using `fileLoader()` + * (**Is** windows compatible) + */ + importDirectory: (path: string) => void, + /** Writes, then imports all everything in fileloader.ts */ + fileLoader: () => void, + /** This function will import the specified directories */ + fastFileLoader: ( + /** An array of directories to import recursively. */ + paths: string[], + /** A function that will run before recursively setting a part of `paths`. + * `path` contains the path that will be imported, useful for logging + */ + between?: (path: string, uniqueFilePathCounter: number, paths: string[]) => void, + /** A function that runs before **actually** importing all the files. */ + before?: (uniqueFilePathCounter: number, paths: string[]) => void + ) => void, +} + +/** Pass in a (compatible) bot instance, and get sweet file loader goodness. + * Remember to capture the output of this function! + */ +export function enableFileLoaderPlugin(rawBot: Bot): BotWithFileLoader { + const bot = rawBot as BotWithFileLoader; + + bot.importDirectory = importDirectory; + bot.fileLoader = fileLoader; + bot.fastFileLoader = fastFileLoader; + + return bot; +} \ No newline at end of file diff --git a/plugins/helpers/LICENSE b/plugins/helpers/LICENSE new file mode 100644 index 000000000..049d614f1 --- /dev/null +++ b/plugins/helpers/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright Copyright 2021 - 2022 Discordeno + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/helpers/README.md b/plugins/helpers/README.md new file mode 100644 index 000000000..e8c34b8ba --- /dev/null +++ b/plugins/helpers/README.md @@ -0,0 +1 @@ +# helpers-plugin \ No newline at end of file diff --git a/plugins/helpers/deps.ts b/plugins/helpers/deps.ts new file mode 100644 index 000000000..ac326e73f --- /dev/null +++ b/plugins/helpers/deps.ts @@ -0,0 +1 @@ +export * from "../../mod.ts"; diff --git a/plugins/helpers/mod.ts b/plugins/helpers/mod.ts new file mode 100644 index 000000000..c5ddf4741 --- /dev/null +++ b/plugins/helpers/mod.ts @@ -0,0 +1,123 @@ +import { + ApplicationCommandOptionChoice, + Bot, + Collection, + CreateMessage, + DiscordenoChannel, + DiscordenoMember, + DiscordenoMessage, + FinalHelpers, + ListGuildMembers, + ModifyThread, +} from "./deps.ts"; +import { cloneChannel } from "./src/channels.ts"; +import { sendAutocompleteChoices } from "./src/sendAutoCompleteChoices.ts"; +import { sendDirectMessage } from "./src/sendDirectMessage.ts"; +import { suppressEmbeds } from "./src/suppressEmbeds.ts"; +import { + archiveThread, + editThread, + lockThread, + unarchiveThread, + unlockThread, +} from "./src/threads.ts"; +import { disconnectMember } from "./src/disconnectMember.ts"; +import { getMembersPaginated } from "./src/getMembersPaginated.ts"; +import { moveMember } from "./src/moveMember.ts"; + +export interface BotWithHelpersPlugin extends Bot { + helpers: FinalHelpers & { + sendDirectMessage: ( + userId: bigint, + content: string | CreateMessage + ) => Promise; + suppressEmbeds: ( + channelId: bigint, + messageId: bigint + ) => Promise; + archiveThread: (threadId: bigint) => Promise; + unarchiveThread: (threadId: bigint) => Promise; + lockThread: (threadId: bigint) => Promise; + unlockThread: (threadId: bigint) => Promise; + editThread: ( + threadId: bigint, + options: ModifyThread, + reason?: string + ) => Promise; + cloneChannel: ( + channel: DiscordenoChannel, + reason?: string + ) => Promise; + sendAutocompleteChoices: ( + interactionId: bigint, + interactionToken: string, + choices: ApplicationCommandOptionChoice[] + ) => Promise; + disconnectMember: ( + guildId: bigint, + memberId: bigint + ) => Promise; + getMembersPaginated: ( + guildId: bigint, + options: ListGuildMembers & { memberCount: number } + ) => Promise>; + moveMember: ( + guildId: bigint, + memberId: bigint, + channelId: bigint + ) => Promise; + }; +} + +export function enableHelpersPlugin(rawBot: Bot): BotWithHelpersPlugin { + const bot = rawBot as BotWithHelpersPlugin; + + bot.helpers.sendDirectMessage = ( + userId: bigint, + content: string | CreateMessage + ) => sendDirectMessage(bot, userId, content); + bot.helpers.suppressEmbeds = (channelId: bigint, messageId: bigint) => + suppressEmbeds(bot, channelId, messageId); + bot.helpers.archiveThread = (threadId: bigint) => + archiveThread(bot, threadId); + bot.helpers.unarchiveThread = (threadId: bigint) => + unarchiveThread(bot, threadId); + bot.helpers.lockThread = (threadId: bigint) => lockThread(bot, threadId); + bot.helpers.unlockThread = (threadId: bigint) => unlockThread(bot, threadId); + bot.helpers.editThread = ( + threadId: bigint, + options: ModifyThread, + reason?: string + ) => editThread(bot, threadId, options, reason); + bot.helpers.cloneChannel = (channel: DiscordenoChannel, reason?: string) => + cloneChannel(bot, channel, reason); + bot.helpers.sendAutocompleteChoices = ( + interactionId: bigint, + interactionToken: string, + choices: ApplicationCommandOptionChoice[] + ) => sendAutocompleteChoices(bot, interactionId, interactionToken, choices); + bot.helpers.disconnectMember = (guildId: bigint, memberId: bigint) => + disconnectMember(bot, guildId, memberId); + bot.helpers.getMembersPaginated = ( + guildId: bigint, + options: ListGuildMembers & { memberCount: number } + ) => getMembersPaginated(bot, guildId, options); + bot.helpers.moveMember = ( + guildId: bigint, + memberId: bigint, + channelId: bigint + ) => moveMember(bot, guildId, memberId, channelId); + + return bot as BotWithHelpersPlugin; +} + +// EXPORT EVERYTHING HERE SO USERS CAN OPT TO USE FUNCTIONS DIRECTLY +export * from "./src/channels.ts"; +export * from "./src/disconnectMember.ts"; +export * from "./src/getMembersPaginated.ts"; +export * from "./src/moveMember.ts"; +export * from "./src/sendAutoCompleteChoices.ts"; +export * from "./src/sendDirectMessage.ts"; +export * from "./src/suppressEmbeds.ts"; +export * from "./src/threads.ts"; +export default enableHelpersPlugin; \ No newline at end of file diff --git a/plugins/helpers/src/channels.ts b/plugins/helpers/src/channels.ts new file mode 100644 index 000000000..47f1f93b4 --- /dev/null +++ b/plugins/helpers/src/channels.ts @@ -0,0 +1,46 @@ +import { + Bot, + CreateGuildChannel, + DiscordenoChannel, + separateOverwrites, +} from "../deps.ts"; + +/** Create a copy of a channel */ +export async function cloneChannel( + bot: Bot, + channel: DiscordenoChannel, + reason?: string, +) { + if (!channel.guildId) { + throw new Error(`Cannot clone a channel outside a guild`); + } + + const createChannelOptions: CreateGuildChannel = { + type: channel.type, + bitrate: channel.bitrate, + userLimit: channel.userLimit, + rateLimitPerUser: channel.rateLimitPerUser, + position: channel.position, + parentId: channel.parentId, + nsfw: channel.nsfw, + name: channel.name!, + topic: channel.topic || undefined, + permissionOverwrites: channel.permissionOverwrites.map((overwrite) => { + const [type, id, allow, deny] = separateOverwrites(overwrite); + + return { + id, + type, + allow: bot.utils.calculatePermissions(BigInt(allow)), + deny: bot.utils.calculatePermissions(BigInt(deny)), + }; + }), + }; + + //Create the channel (also handles permissions) + return await bot.helpers.createChannel( + channel.guildId!, + createChannelOptions, + reason, + ); +} diff --git a/plugins/helpers/src/disconnectMember.ts b/plugins/helpers/src/disconnectMember.ts new file mode 100644 index 000000000..bbfe094a7 --- /dev/null +++ b/plugins/helpers/src/disconnectMember.ts @@ -0,0 +1,6 @@ +import { Bot } from "../deps.ts"; + +/** Kicks a member from a voice channel */ +export function disconnectMember(bot: Bot, guildId: bigint, memberId: bigint) { + return bot.helpers.editMember(guildId, memberId, { channelId: null }); +} diff --git a/plugins/helpers/src/getMembersPaginated.ts b/plugins/helpers/src/getMembersPaginated.ts new file mode 100644 index 000000000..769470fd1 --- /dev/null +++ b/plugins/helpers/src/getMembersPaginated.ts @@ -0,0 +1,73 @@ +import { + Bot, + Collection, + DiscordenoMember, + GuildMemberWithUser, + ListGuildMembers, +} from "../deps.ts"; + +/** + * Highly recommended to **NOT** use this function to get members instead use fetchMembers(). + * REST(this function): 50/s global(across all shards) rate limit with ALL requests this included + * GW(fetchMembers): 120/m(PER shard) rate limit. Meaning if you have 8 shards your limit is 960/m. + */ +export async function getMembersPaginated( + bot: Bot, + guildId: bigint, + options: ListGuildMembers & { memberCount: number } +) { + const members = new Collection(); + + let membersLeft = options?.limit ?? options.memberCount; + let loops = 1; + while ( + (options?.limit ?? options.memberCount) > members.size && + membersLeft > 0 + ) { + bot.events.debug("Running while loop in getMembers function."); + + if (options?.limit && options.limit > 1000) { + console.log( + `Paginating get members from REST. #${loops} / ${Math.ceil( + (options?.limit ?? 1) / 1000 + )}` + ); + } + + const result = await bot.rest.runMethod( + bot.rest, + "get", + `${bot.constants.endpoints.GUILD_MEMBERS(guildId)}?limit=${ + membersLeft > 1000 ? 1000 : membersLeft + }${options?.after ? `&after=${options.after}` : ""}` + ); + + const discordenoMembers = result.map((member) => + bot.transformers.member( + bot, + member, + guildId, + bot.transformers.snowflake(member.user.id) + ) + ); + + if (!discordenoMembers.length) break; + + discordenoMembers.forEach((member) => { + bot.events.debug(`Running forEach loop in get_members file.`); + members.set(member.id, member); + }); + + options = { + limit: options?.limit, + after: discordenoMembers[discordenoMembers.length - 1].id.toString(), + memberCount: options.memberCount, + }; + + membersLeft -= 1000; + + loops++; + } + + return members; +} diff --git a/plugins/helpers/src/moveMember.ts b/plugins/helpers/src/moveMember.ts new file mode 100644 index 000000000..9ac51d32a --- /dev/null +++ b/plugins/helpers/src/moveMember.ts @@ -0,0 +1,13 @@ +import { Bot } from "../deps.ts"; + +/** + * Move a member from a voice channel to another. + */ +export function moveMember( + bot: Bot, + guildId: bigint, + memberId: bigint, + channelId: bigint +) { + return bot.helpers.editMember(guildId, memberId, { channelId }); +} diff --git a/plugins/helpers/src/sendAutoCompleteChoices.ts b/plugins/helpers/src/sendAutoCompleteChoices.ts new file mode 100644 index 000000000..5baf2ee6d --- /dev/null +++ b/plugins/helpers/src/sendAutoCompleteChoices.ts @@ -0,0 +1,19 @@ +import { + ApplicationCommandOptionChoice, + Bot, + InteractionResponseTypes, +} from "../deps.ts"; + +export async function sendAutocompleteChoices( + bot: Bot, + interactionId: bigint, + interactionToken: string, + choices: ApplicationCommandOptionChoice[] +): Promise { + await bot.helpers.sendInteractionResponse(interactionId, interactionToken, { + type: InteractionResponseTypes.ApplicationCommandAutocompleteResult, + data: { + choices: choices, + }, + }); +} diff --git a/plugins/helpers/src/sendDirectMessage.ts b/plugins/helpers/src/sendDirectMessage.ts new file mode 100644 index 000000000..90221800d --- /dev/null +++ b/plugins/helpers/src/sendDirectMessage.ts @@ -0,0 +1,27 @@ +import { Bot, Collection, CreateMessage } from "../deps.ts"; + +/** Maps the for dm channels */ +export const dmChannelIds = new Collection(); + +/** Sends a direct message to a user. This can take two API calls. The first call is to create a dm channel. Then sending the message to that channel. Channel ids are cached as needed to prevent duplicate requests. */ +export async function sendDirectMessage( + bot: Bot, + userId: bigint, + content: string | CreateMessage, +) { + if (typeof content === "string") content = { content }; + + // GET CHANNEL ID FROM CACHE OR CREATE THE CHANNEL FOR THIS USER + const cachedChannelId = dmChannelIds.get(userId); + // IF ID IS CACHED SEND MESSAGE DIRECTLY + if (cachedChannelId) return bot.helpers.sendMessage(cachedChannelId, content); + + // CREATE A NEW DM CHANNEL AND PULCK ITS ID + const channel = (await bot.helpers.getDmChannel(userId)); + + // CACHE IT FOR FUTURE REQUESTS + dmChannelIds.set(userId, channel.id); + + // CACHE CHANNEL IF NEEDED + return bot.helpers.sendMessage(channel.id, content); +} diff --git a/plugins/helpers/src/suppressEmbeds.ts b/plugins/helpers/src/suppressEmbeds.ts new file mode 100644 index 000000000..607173ba0 --- /dev/null +++ b/plugins/helpers/src/suppressEmbeds.ts @@ -0,0 +1,17 @@ +import { Bot, Message } from "../deps.ts"; + +/** Suppress all the embeds in this message */ +export async function suppressEmbeds( + bot: Bot, + channelId: bigint, + messageId: bigint, +) { + const result = await bot.rest.runMethod( + bot.rest, + "patch", + bot.constants.endpoints.CHANNEL_MESSAGE(channelId, messageId), + { flags: 4 }, + ); + + return bot.transformers.message(bot, result); +} diff --git a/plugins/helpers/src/threads.ts b/plugins/helpers/src/threads.ts new file mode 100644 index 000000000..39b95bace --- /dev/null +++ b/plugins/helpers/src/threads.ts @@ -0,0 +1,38 @@ +import { Bot, Channel, ModifyThread } from "../deps.ts"; + +/** Sets a thread channel to be archived. */ +export async function archiveThread(bot: Bot, threadId: bigint) { + return await editThread(bot, threadId, { archived: true }); +} + +/** Sets a thread channel to be unarchived. */ +export async function unarchiveThread(bot: Bot, threadId: bigint) { + return await editThread(bot, threadId, { archived: false }); +} + +/** Sets a thread channel to be locked. */ +export async function lockThread(bot: Bot, threadId: bigint) { + return await editThread(bot, threadId, { locked: true }); +} + +/** Sets a thread channel to be unlocked. */ +export async function unlockThread(bot: Bot, threadId: bigint) { + return await editThread(bot, threadId, { locked: false }); +} + +/** Update a thread's settings. Requires the `MANAGE_CHANNELS` permission for the guild. */ +export async function editThread(bot: Bot, threadId: bigint, options: ModifyThread, reason?: string) { + const result = await bot.rest.runMethod(bot.rest, "patch", bot.constants.endpoints.CHANNEL_BASE(threadId), { + name: options.name, + archived: options.archived, + auto_archive_duration: options.autoArchiveDuration, + locked: options.locked, + rate_limit_per_user: options.rateLimitPerUser, + reason, + }); + + return bot.transformers.channel(bot, { + channel: result, + guildId: result.guild_id ? bot.transformers.snowflake(result.guild_id) : undefined, + }); +} \ No newline at end of file diff --git a/plugins/permissions/LICENSE b/plugins/permissions/LICENSE new file mode 100644 index 000000000..80a84a261 --- /dev/null +++ b/plugins/permissions/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 - 2022 Discordeno + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/plugins/permissions/README.md b/plugins/permissions/README.md new file mode 100644 index 000000000..040eb09f4 --- /dev/null +++ b/plugins/permissions/README.md @@ -0,0 +1,25 @@ +# permissions-plugin + +This is an official plugin maintained by Discordeno. This plugin provides automatic permission checking and useful permission checking utility functions. Highly recommended to install this plugin for all users as you can use the utility functions. Enabling the permission plugin should not be done for big bot developers as it requires the cache plugin which will not work in a performance optimized fashion. This is designed mainly for the small beginner devs. + +## Requirements + +- [Cache Plugin](https://github.com/discordeno/cache-plugin) + +## Usage + +```ts +// MOVE TO DEPS.TS AND USE SPECIFIC VERSION +import enableCachePlugin from "https://deno.land/x/discordeno_cache_plugin/mod.ts"; +import enablePermissionPlugin from "https://deno.land/x/discordeno_permission_plugin/mod.ts"; + +// Create the bot object, THIS WILL NEED YOUR OPTIONS. +const bot = createBot({}); +// REQUIRED: Enables the cache plugin on this bot +enableCachePlugin(bot); +// Enables the permission plugin on this bot +enablePermissionPlugin(bot); +// Start your bot +await startBot(bot); +``` + diff --git a/plugins/permissions/deps.ts b/plugins/permissions/deps.ts new file mode 100644 index 000000000..9fc558c42 --- /dev/null +++ b/plugins/permissions/deps.ts @@ -0,0 +1,2 @@ +export * from "../../mod.ts"; +export type { BotWithCache } from "../cache/mod.ts"; diff --git a/plugins/permissions/mod.ts b/plugins/permissions/mod.ts new file mode 100644 index 000000000..05204954c --- /dev/null +++ b/plugins/permissions/mod.ts @@ -0,0 +1,48 @@ +import { BotWithCache } from "./deps.ts"; +import setupChannelPermChecks from "./src/channels/mod.ts"; +import setupDiscoveryPermChecks from "./src/discovery.ts"; +import setupEditMember from "./src/editMember.ts"; +import setupEmojiPermChecks from "./src/emojis.ts"; +import setupGuildPermChecks from "./src/guilds/mod.ts"; +import setupIntegrationPermChecks from "./src/integrations.ts"; +import setupInteractionPermChecks from "./src/interactions/mod.ts"; +import setupInvitesPermChecks from "./src/invites.ts"; +import setupMemberPermChecks from "./src/members/mod.ts"; +import setupMessagePermChecks from "./src/messages/mod.ts"; +import setupMiscPermChecks from "./src/misc/mod.ts"; +import setupRolePermChecks from "./src/roles/mod.ts"; +import setupWebhooksPermChecks from "./src/webhooks/mod.ts"; + +// PLUGINS MUST TAKE A BOT ARGUMENT WHICH WILL BE MODIFIED +export function enablePermissionsPlugin(bot: BotWithCache) { + // PERM CHECKS REQUIRE CACHE DUH! + if (!bot.enabledPlugins?.has("CACHE")) { + throw new Error("The PERMISSIONS plugin requires the CACHE plugin first."); + } + + // MARK THIS PLUGIN BEING USED + bot.enabledPlugins.add("PERMISSIONS"); + + // BEGIN OVERRIDING HELPER FUNCTIONS + setupChannelPermChecks(bot); + setupDiscoveryPermChecks(bot); + setupEmojiPermChecks(bot); + setupEditMember(bot); + setupGuildPermChecks(bot); + setupIntegrationPermChecks(bot); + setupInteractionPermChecks(bot); + setupInvitesPermChecks(bot); + setupMemberPermChecks(bot); + setupMessagePermChecks(bot); + setupMiscPermChecks(bot); + setupRolePermChecks(bot); + setupWebhooksPermChecks(bot); + + // PLUGINS MUST RETURN THE BOT + return bot; +} + +// EXPORT ALL UTIL FUNCTIONS +export * from "./src/permissions.ts"; +// DEFAULT MAKES IT SLIGHTLY EASIER TO USE +export default enablePermissionsPlugin; diff --git a/plugins/permissions/src/channels/deleteChannel.ts b/plugins/permissions/src/channels/deleteChannel.ts new file mode 100644 index 000000000..cdc0ff571 --- /dev/null +++ b/plugins/permissions/src/channels/deleteChannel.ts @@ -0,0 +1,37 @@ +import { BotWithCache, ChannelTypes } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function deleteChannel(bot: BotWithCache) { + const deleteChannelOld = bot.helpers.deleteChannel; + + bot.helpers.deleteChannel = function (channelId, reason) { + const channel = bot.channels.get(channelId); + + if (channel?.guildId) { + const guild = bot.guilds.get(channel.guildId); + if (!guild) throw new Error("GUILD_NOT_FOUND"); + + if (guild.rulesChannelId === channelId) { + throw new Error("RULES_CHANNEL_CANNOT_BE_DELETED"); + } + + if (guild.publicUpdatesChannelId === channelId) { + throw new Error("UPDATES_CHANNEL_CANNOT_BE_DELETED"); + } + + const isThread = [ + ChannelTypes.GuildNewsThread, + ChannelTypes.GuildPublicThread, + ChannelTypes.GuildPrivateThread, + ].includes(channel.type); + + requireBotGuildPermissions( + bot, + guild, + isThread ? ["MANAGE_THREADS"] : ["MANAGE_CHANNELS"], + ); + } + + return deleteChannelOld(channelId, reason); + }; +} diff --git a/plugins/permissions/src/channels/deleteChannelOverwrite.ts b/plugins/permissions/src/channels/deleteChannelOverwrite.ts new file mode 100644 index 000000000..58801e8c3 --- /dev/null +++ b/plugins/permissions/src/channels/deleteChannelOverwrite.ts @@ -0,0 +1,16 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export default function deleteChannelOverwrite(bot: BotWithCache) { + const deleteChannelOverwriteOld = bot.helpers.deleteChannelOverwrite; + + bot.helpers.deleteChannelOverwrite = function (channelId, overwriteId) { + const channel = bot.channels.get(channelId); + + if (channel?.guildId) { + requireBotChannelPermissions(bot, channelId, ["MANAGE_ROLES"]); + } + + return deleteChannelOverwriteOld(channelId, overwriteId); + }; +} diff --git a/plugins/permissions/src/channels/editChannel.ts b/plugins/permissions/src/channels/editChannel.ts new file mode 100644 index 000000000..41d83a19d --- /dev/null +++ b/plugins/permissions/src/channels/editChannel.ts @@ -0,0 +1,123 @@ +import { PermissionStrings } from "../../deps.ts"; +import { BotWithCache, ChannelTypes, GuildFeatures } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export default function editChannel(bot: BotWithCache) { + const editChannelOld = bot.helpers.editChannel; + + bot.helpers.editChannel = function (channelId, options, reason) { + const channel = bot.channels.get(channelId); + + if (channel?.guildId) { + const guild = bot.guilds.get(channel.guildId); + + if (options.rateLimitPerUser && options.rateLimitPerUser > 21600) { + throw new Error( + "Amount of seconds a user has to wait before sending another message must be between 0-21600", + ); + } + + if (options.name) { + if (!bot.utils.validateLength(options.name, { min: 1, max: 100 })) { + throw new Error( + "The channel name must be between 1-100 characters.", + ); + } + } + + const isThread = [ + ChannelTypes.GuildNewsThread, + ChannelTypes.GuildPublicThread, + ChannelTypes.GuildPrivateThread, + ].includes(channel.type); + + const requiredPerms: PermissionStrings[] = []; + if (isThread) { + if ( + options.invitable !== undefined && + channel.type !== ChannelTypes.GuildPrivateThread + ) { + throw new Error( + "Invitable option is only allowed on private threads.", + ); + } + + // UNARCHIVING AN UNLOCKED CHANNEL SIMPLY REQUIRES SEND + if (!channel.locked && options.archived === false) { + requiredPerms.push("SEND_MESSAGES"); + // MORE THAN ARCHIVE WAS MODIFIED + if (Object.keys(options).length > 1) { + requiredPerms.push("MANAGE_THREADS"); + } + } else { + requiredPerms.push("MANAGE_THREADS"); + } + } else { + requiredPerms.push("MANAGE_CHANNELS"); + + if (options.permissionOverwrites) { + requiredPerms.push("MANAGE_ROLES"); + } + + if (options.type) { + if ( + [ChannelTypes.GuildNews, ChannelTypes.GuildText].includes( + options.type, + ) + ) { + throw new Error("Only news and text types can be modified."); + } + + if (guild && !guild.features.includes(GuildFeatures.News)) { + throw new Error( + "The NEWS feature is missing in this guild to be able to modify the channel type.", + ); + } + } + + if (options.topic) { + if (!bot.utils.validateLength(options.topic, { min: 1, max: 1024 })) { + throw new Error("The topic must be a number between 1 and 1024"); + } + } + + if (options.userLimit && options.userLimit > 99) { + throw new Error("The user limit must be less than 99."); + } + + if (options.parentId) { + const category = bot.channels.get(options.parentId); + if (category && category.type !== ChannelTypes.GuildCategory) { + throw new Error( + "The parent id must be for a category channel type.", + ); + } + } + } + + requireBotChannelPermissions( + bot, + channel, + requiredPerms, + ); + + if (options.autoArchiveDuration) { + if (guild) { + if ( + !guild.features.includes( + options.autoArchiveDuration === 4320 + ? GuildFeatures.ThreeDayThreadArchive + : GuildFeatures.SevenDayThreadArchive, + ) + ) { + throw new Error( + "The 3 day and 7 day archive durations require the server to be boosted", + ); + } + } + } + } + + return editChannelOld(channelId, options, reason); + }; +} diff --git a/plugins/permissions/src/channels/editChannelOverwrite.ts b/plugins/permissions/src/channels/editChannelOverwrite.ts new file mode 100644 index 000000000..287b5e8ba --- /dev/null +++ b/plugins/permissions/src/channels/editChannelOverwrite.ts @@ -0,0 +1,15 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export default function editChannelOverwrite(bot: BotWithCache) { + const editChannelOverwriteOld = bot.helpers.editChannelOverwrite; + + bot.helpers.editChannelOverwrite = function (channelId, overwriteId, options) { + const channel = bot.channels.get(channelId); + if (channel?.guildId) { + requireBotChannelPermissions(bot, channelId, ["MANAGE_ROLES"]); + } + + return editChannelOverwriteOld(channelId, overwriteId, options); + }; +} diff --git a/plugins/permissions/src/channels/followChannel.ts b/plugins/permissions/src/channels/followChannel.ts new file mode 100644 index 000000000..b5bf0fd7c --- /dev/null +++ b/plugins/permissions/src/channels/followChannel.ts @@ -0,0 +1,15 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export default function followChannel(bot: BotWithCache) { + const followChannelOld = bot.helpers.followChannel; + + bot.helpers.followChannel = function (sourceChannelId, targetChannelId) { + const channel = bot.channels.get(targetChannelId); + if (channel?.guildId) { + requireBotChannelPermissions(bot, channel, ["MANAGE_WEBHOOKS"]); + } + + return followChannelOld(sourceChannelId, targetChannelId); + }; +} diff --git a/plugins/permissions/src/channels/getChannelWebhooks.ts b/plugins/permissions/src/channels/getChannelWebhooks.ts new file mode 100644 index 000000000..6f4e672ee --- /dev/null +++ b/plugins/permissions/src/channels/getChannelWebhooks.ts @@ -0,0 +1,15 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export default function getChannelWebhooks(bot: BotWithCache) { + const getChannelWebhooksOld = bot.helpers.getChannelWebhooks; + + bot.helpers.getChannelWebhooks = function (channelId) { + const channel = bot.channels.get(channelId); + if (channel?.guildId) { + requireBotChannelPermissions(bot, channelId, ["MANAGE_WEBHOOKS"]); + } + + return getChannelWebhooksOld(channelId); + }; +} diff --git a/plugins/permissions/src/channels/mod.ts b/plugins/permissions/src/channels/mod.ts new file mode 100644 index 000000000..f02df1e21 --- /dev/null +++ b/plugins/permissions/src/channels/mod.ts @@ -0,0 +1,22 @@ +import { BotWithCache } from "../../deps.ts"; +import setupThreadPermChecks from "./threads/mod.ts"; +import setupStagePermChecks from "./stage.ts"; +import deleteChannel from "./deleteChannel.ts"; +import deleteChannelOverwrite from "./deleteChannelOverwrite.ts"; +import editChannel from "./editChannel.ts"; +import editChannelOverwrite from "./editChannelOverwrite.ts"; +import followChannel from "./followChannel.ts"; +import getChannelWebhooks from "./getChannelWebhooks.ts"; +import swapChannels from "./swapChannels.ts"; + +export default function setupChannelPermChecks(bot: BotWithCache) { + setupThreadPermChecks(bot); + setupStagePermChecks(bot); + deleteChannel(bot); + deleteChannelOverwrite(bot); + editChannel(bot); + editChannelOverwrite(bot); + followChannel(bot); + getChannelWebhooks(bot); + swapChannels(bot); +} \ No newline at end of file diff --git a/plugins/permissions/src/channels/stage.ts b/plugins/permissions/src/channels/stage.ts new file mode 100644 index 000000000..db6b75b9b --- /dev/null +++ b/plugins/permissions/src/channels/stage.ts @@ -0,0 +1,56 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export function createStageInstance(bot: BotWithCache) { + const createStageInstanceOld = bot.helpers.createStageInstance; + + bot.helpers.createStageInstance = function (channelId, topic, privacyLevel) { + if (!bot.utils.validateLength(topic, { max: 120, min: 1 })) { + throw new Error( + "The topic length for creating a stage instance must be between 1-120.", + ); + } + + requireBotChannelPermissions(bot, channelId, [ + "MANAGE_CHANNELS", + "MUTE_MEMBERS", + "MOVE_MEMBERS", + ]); + + return createStageInstanceOld(channelId, topic, privacyLevel); + }; +} + +export function deleteStageInstance(bot: BotWithCache) { + const deleteStageInstanceOld = bot.helpers.deleteStageInstance; + + bot.helpers.deleteStageInstance = function (channelId) { + requireBotChannelPermissions(bot, channelId, [ + "MANAGE_CHANNELS", + "MUTE_MEMBERS", + "MOVE_MEMBERS", + ]); + + return deleteStageInstanceOld(channelId); + }; +} + +export function updateStageInstance(bot: BotWithCache) { + const updateStageInstanceOld = bot.helpers.updateStageInstance; + + bot.helpers.updateStageInstance = function (channelId, data) { + requireBotChannelPermissions(bot, channelId, [ + "MANAGE_CHANNELS", + "MUTE_MEMBERS", + "MOVE_MEMBERS", + ]); + + return updateStageInstanceOld(channelId, data); + }; +} + +export default function setupStagePermChecks(bot: BotWithCache) { + createStageInstance(bot); + deleteStageInstance(bot); + updateStageInstance(bot); +} \ No newline at end of file diff --git a/plugins/permissions/src/channels/swapChannels.ts b/plugins/permissions/src/channels/swapChannels.ts new file mode 100644 index 000000000..0dea16998 --- /dev/null +++ b/plugins/permissions/src/channels/swapChannels.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function swapChannels(bot: BotWithCache) { + const swapChannelsOld = bot.helpers.swapChannels; + + bot.helpers.swapChannels = function (guildId, channelPositions) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_CHANNELS"]); + + return swapChannelsOld(guildId, channelPositions); + }; +} diff --git a/plugins/permissions/src/channels/threads/addToThread.ts b/plugins/permissions/src/channels/threads/addToThread.ts new file mode 100644 index 000000000..c052477e1 --- /dev/null +++ b/plugins/permissions/src/channels/threads/addToThread.ts @@ -0,0 +1,26 @@ +import { BotWithCache } from "../../../deps.ts"; +import { requireBotChannelPermissions } from "../../permissions.ts"; + +export default function addToThread(bot: BotWithCache) { + const addToThreadOld = bot.helpers.addToThread; + + bot.helpers.addToThread = async function (threadId, userId) { + if (userId === bot.id) { + throw new Error( + "To add the bot to a thread, you must use bot.helpers.joinThread()", + ); + } + + const channel = bot.channels.get(threadId); + + if (channel) { + if (channel.archived) { + throw new Error("Cannot add user to thread if thread is archived."); + } + + await requireBotChannelPermissions(bot, channel, ["SEND_MESSAGES"]); + } + + return addToThreadOld(threadId, userId); + }; +} diff --git a/plugins/permissions/src/channels/threads/getArchivedThreads.ts b/plugins/permissions/src/channels/threads/getArchivedThreads.ts new file mode 100644 index 000000000..481e93dea --- /dev/null +++ b/plugins/permissions/src/channels/threads/getArchivedThreads.ts @@ -0,0 +1,22 @@ +import { BotWithCache } from "../../../deps.ts"; +import { requireBotChannelPermissions } from "../../permissions.ts"; + +export default function getArchivedThreads(bot: BotWithCache) { + const getArchivedThreadsOld = bot.helpers.getArchivedThreads; + + bot.helpers.getArchivedThreads = async function (channelId, options) { + const channel = await bot.channels.get(channelId); + + if (channel) { + await requireBotChannelPermissions( + bot, + channel, + options?.type === "private" + ? ["READ_MESSAGE_HISTORY", "MANAGE_THREADS"] + : ["READ_MESSAGE_HISTORY"], + ); + } + + return getArchivedThreadsOld(channelId, options); + }; +} diff --git a/plugins/permissions/src/channels/threads/getThreadMembers.ts b/plugins/permissions/src/channels/threads/getThreadMembers.ts new file mode 100644 index 000000000..0d2881895 --- /dev/null +++ b/plugins/permissions/src/channels/threads/getThreadMembers.ts @@ -0,0 +1,16 @@ +import { BotWithCache, GatewayIntents } from "../../../deps.ts"; + +export default function getThreadMembers(bot: BotWithCache) { + const getThreadMembersOld = bot.helpers.getThreadMembers; + + bot.helpers.getThreadMembers = function (threadId) { + const hasIntent = bot.intents & GatewayIntents.GuildMembers; + if (!hasIntent) { + throw new Error( + "The get thread members endpoint requires GuildMembers intent.", + ); + } + + return getThreadMembersOld(threadId); + }; +} diff --git a/plugins/permissions/src/channels/threads/joinThread.ts b/plugins/permissions/src/channels/threads/joinThread.ts new file mode 100644 index 000000000..521bf9f7d --- /dev/null +++ b/plugins/permissions/src/channels/threads/joinThread.ts @@ -0,0 +1,15 @@ +import { BotWithCache } from "../../../deps.ts"; + +export default function joinThread(bot: BotWithCache) { + const joinThreadOld = bot.helpers.joinThread; + + bot.helpers.joinThread = function (threadId) { + const channel = bot.channels.get(threadId); + + if (channel && !channel.archived) { + throw new Error("You can not join an archived channel."); + } + + return joinThreadOld(threadId); + }; +} diff --git a/plugins/permissions/src/channels/threads/leaveThread.ts b/plugins/permissions/src/channels/threads/leaveThread.ts new file mode 100644 index 000000000..24cd71599 --- /dev/null +++ b/plugins/permissions/src/channels/threads/leaveThread.ts @@ -0,0 +1,15 @@ +import { BotWithCache } from "../../../deps.ts"; + +export default function leaveThread(bot: BotWithCache) { + const leaveThreadOld = bot.helpers.leaveThread; + + bot.helpers.leaveThread = function (threadId) { + const channel = bot.channels.get(threadId); + + if (channel && !channel.archived) { + throw new Error("You can not leave an archived channel."); + } + + return leaveThreadOld(threadId); + }; +} diff --git a/plugins/permissions/src/channels/threads/mod.ts b/plugins/permissions/src/channels/threads/mod.ts new file mode 100644 index 000000000..1bf3e027f --- /dev/null +++ b/plugins/permissions/src/channels/threads/mod.ts @@ -0,0 +1,16 @@ +import { BotWithCache } from "../../../deps.ts"; +import addToThread from "./addToThread.ts"; +import getArchivedThreads from "./getArchivedThreads.ts"; +import getThreadMembers from "./getThreadMembers.ts"; +import joinThread from "./joinThread.ts"; +import leaveThread from "./leaveThread.ts"; +import removeThreadMember from "./removeThreadMember.ts"; + +export default function setupThreadPermChecks(bot: BotWithCache) { + addToThread(bot); + getArchivedThreads(bot); + getThreadMembers(bot); + joinThread(bot); + leaveThread(bot); + removeThreadMember(bot); +} \ No newline at end of file diff --git a/plugins/permissions/src/channels/threads/removeThreadMember.ts b/plugins/permissions/src/channels/threads/removeThreadMember.ts new file mode 100644 index 000000000..14e935934 --- /dev/null +++ b/plugins/permissions/src/channels/threads/removeThreadMember.ts @@ -0,0 +1,33 @@ +import { BotWithCache, ChannelTypes } from "../../../deps.ts"; +import { requireBotChannelPermissions } from "../../permissions.ts"; + +export default function removeThreadMember(bot: BotWithCache) { + const removeThreadMemberOld = bot.helpers.removeThreadMember; + + bot.helpers.removeThreadMember = async function (threadId, userId) { + if (userId === bot.id) { + throw new Error( + "To remove the bot from a thread, you must use bot.helpers.leaveThread()", + ); + } + + const channel = bot.channels.get(threadId); + + if (channel) { + if (channel.archived) { + throw new Error( + "Cannot remove user from thread if thread is archived.", + ); + } + + if ( + !(bot.id === channel.ownerId && + channel.type === ChannelTypes.GuildPrivateThread) + ) { + await requireBotChannelPermissions(bot, channel, ["MANAGE_MESSAGES"]); + } + } + + return removeThreadMemberOld(threadId, userId); + }; +} diff --git a/plugins/permissions/src/components.ts b/plugins/permissions/src/components.ts new file mode 100644 index 000000000..81d58aa2c --- /dev/null +++ b/plugins/permissions/src/components.ts @@ -0,0 +1,178 @@ +import { + Bot, + ButtonStyles, + MessageComponents, + MessageComponentTypes, +} from "../deps.ts"; + +export function validateComponents(bot: Bot, components: MessageComponents) { + if (!components?.length) return; + + let actionRowCounter = 0; + + for (const component of components) { + actionRowCounter++; + // Max of 5 ActionRows per message + if (actionRowCounter > 5) throw new Error("Too many action rows."); + + // Max of 5 Buttons (or any component type) within an ActionRow + if (component.components?.length > 5) { + throw new Error("Too many components."); + } else if ( + component.components?.length > 1 && + component.components.some((subcomponent) => + subcomponent.type === MessageComponentTypes.SelectMenu + ) + ) { + throw new Error("Select component must be alone."); + } + + for (const subcomponent of component.components) { + if ( + subcomponent.customId && + !bot.utils.validateLength(subcomponent.customId, { max: 100 }) + ) { + throw new Error("The custom id in the component is too big."); + } + + // 5 Link buttons can not have a customId + if (subcomponent.type === MessageComponentTypes.Button) { + if (subcomponent.style === ButtonStyles.Link && subcomponent.customId) { + throw new Error("Link buttons can not have custom ids."); + } + // Other buttons must have a customId + if ( + !subcomponent.customId && subcomponent.style !== ButtonStyles.Link + ) { + throw new Error( + "The button requires a custom id if it is not a link button.", + ); + } + + if (!bot.utils.validateLength(subcomponent.label, { max: 80 })) { + throw new Error("The label can not be longer than 80 characters."); + } + + subcomponent.emoji = makeEmojiFromString(subcomponent.emoji); + } + + if (subcomponent.type === MessageComponentTypes.SelectMenu) { + if ( + subcomponent.placeholder && + !bot.utils.validateLength(subcomponent.placeholder, { max: 100 }) + ) { + throw new Error( + "The component placeholder can not be longer than 100 characters.", + ); + } + + if (subcomponent.minValues) { + if (subcomponent.minValues < 1) { + throw new Error( + "The min values must be more than 1 in a select component.", + ); + } + + if (subcomponent.minValues > 25) { + throw new Error( + "The min values must be less than 25 in a select component.", + ); + } + + if (!subcomponent.maxValues) { + subcomponent.maxValues = subcomponent.minValues; + } + if (subcomponent.minValues > subcomponent.maxValues) { + throw new Error( + "The select component can not have a min values higher than a max values.", + ); + } + } + + if (subcomponent.maxValues) { + if (subcomponent.maxValues < 1) { + throw new Error( + "The max values must be more than 1 in a select component.", + ); + } + + if (subcomponent.maxValues > 25) { + throw new Error( + "The max values must be less than 25 in a select component.", + ); + } + } + + if (subcomponent.options.length < 1) { + throw new Error("You need atleast 1 option in the select component."); + } + + if (subcomponent.options.length > 25) { + throw new Error( + "You can not have more than 25 options in the select component.", + ); + } + + let defaults = 0; + + for (const option of subcomponent.options) { + if (option.default) { + defaults++; + if (defaults > (subcomponent.maxValues || 25)) { + throw new Error("You chose too many default options."); + } + } + + if (!bot.utils.validateLength(option.label, { max: 25 })) { + throw new Error( + "The select component label can not exceed 25 characters.", + ); + } + + if (!bot.utils.validateLength(option.value, { max: 100 })) { + throw new Error( + "The select component value can not exceed 100 characters.", + ); + } + + if ( + option.description && + !bot.utils.validateLength(option.description, { max: 50 }) + ) { + throw new Error( + "The select option description can not exceed 50 characters.", + ); + } + + option.emoji = makeEmojiFromString(option.emoji); + } + } + } + } +} + +function makeEmojiFromString( + emoji?: + | string + | { + id?: string | undefined; + name?: string | undefined; + animated?: boolean | undefined; + }, +) { + if (typeof emoji !== "string") return emoji; + + // A snowflake id was provided + if (/^[0-9]+$/.test(emoji)) { + emoji = { + id: emoji, + }; + } else { + // A unicode emoji was provided + emoji = { + name: emoji, + }; + } + + return emoji; +} diff --git a/plugins/permissions/src/connectToVoiceChannels.ts b/plugins/permissions/src/connectToVoiceChannels.ts new file mode 100644 index 000000000..24deb6f23 --- /dev/null +++ b/plugins/permissions/src/connectToVoiceChannels.ts @@ -0,0 +1,43 @@ +import { BotWithCache, ChannelTypes, PermissionStrings } from "../deps.ts"; +import { requireBotChannelPermissions } from "./permissions.ts"; + +export default function connectToVoiceChannel(bot: BotWithCache) { + const connectToVoiceChannelOld = bot.helpers.connectToVoiceChannel; + + bot.helpers.connectToVoiceChannel = async function ( + guildId, + channelId, + options + ) { + const channel = await bot.channels.get(channelId); + if (!channel) throw new Error("CHANNEL_NOT_FOUND"); + + if ( + [ChannelTypes.GuildStageVoice, ChannelTypes.GuildVoice].includes( + channel.type + ) + ) + throw new Error("INVALID_CHANNEL_TYPE"); + + const guild = channel?.guildId && bot.guilds.get(channel.guildId); + if (!guild) throw new Error("GUILD_NOT_FOUND"); + + // Permissions needed for the bot to connect + // CONNECT is needed + const permsNeeded: PermissionStrings[] = ["CONNECT"]; + + // Check if there is space for the bot if channel has user limit + // Having MANAGE_CHANNELS permissions bypasses the limit + // --> Add MANAGE_CHANNELS perm to the check if it is needed + if ( + channel.userLimit && + guild.voiceStates.filter((vs) => vs.channelId === channelId).size >= + channel.userLimit + ) + permsNeeded.push("MANAGE_CHANNELS"); + + await requireBotChannelPermissions(bot, channel, permsNeeded); + + return connectToVoiceChannelOld(guildId, channelId, options); + }; +} diff --git a/plugins/permissions/src/discovery.ts b/plugins/permissions/src/discovery.ts new file mode 100644 index 000000000..caec07517 --- /dev/null +++ b/plugins/permissions/src/discovery.ts @@ -0,0 +1,49 @@ +import { BotWithCache } from "../deps.ts"; +import { requireBotGuildPermissions } from "./permissions.ts"; + +export function addDiscoverySubcategory(bot: BotWithCache) { + const addDiscoverySubcategoryOld = bot.helpers.addDiscoverySubcategory; + + bot.helpers.addDiscoverySubcategory = function (guildId, categoryId) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return addDiscoverySubcategoryOld(guildId, categoryId); + }; +} + +export function removeDiscoverySubcategory(bot: BotWithCache) { + const removeDiscoverySubcategoryOld = bot.helpers.removeDiscoverySubcategory; + + bot.helpers.removeDiscoverySubcategory = function (guildId, categoryId) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return removeDiscoverySubcategoryOld(guildId, categoryId); + }; +} + +export function getDiscovery(bot: BotWithCache) { + const getDiscoveryOld = bot.helpers.getDiscovery; + + bot.helpers.getDiscovery = function (guildId) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return getDiscoveryOld(guildId); + }; +} + +export function editDiscovery(bot: BotWithCache) { + const editDiscoveryOld = bot.helpers.editDiscovery; + + bot.helpers.editDiscovery = function (guildId, data) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return editDiscoveryOld(guildId, data); + }; +} + +export default function setupDiscoveryPermChecks(bot: BotWithCache) { + addDiscoverySubcategory(bot); + editDiscovery(bot); + getDiscovery(bot); + removeDiscoverySubcategory(bot); +} diff --git a/plugins/permissions/src/editMember.ts b/plugins/permissions/src/editMember.ts new file mode 100644 index 000000000..362ba0ce0 --- /dev/null +++ b/plugins/permissions/src/editMember.ts @@ -0,0 +1,67 @@ +import { BotWithCache, PermissionStrings } from "../deps.ts"; +import { + requireBotChannelPermissions, + requireBotGuildPermissions, +} from "./permissions.ts"; + +export default function editMember(bot: BotWithCache) { + const editMemberOld = bot.helpers.editMember; + + bot.helpers.editMember = async function (guildId, memberId, options) { + const requiredPerms: Set = new Set(); + + if (options.nick) { + if (options.nick.length > 32) { + throw new Error("NICKNAMES_MAX_LENGTH"); + } + requiredPerms.add("MANAGE_NICKNAMES"); + } + + if (options.roles) requiredPerms.add("MANAGE_ROLES"); + + if ( + options.mute !== undefined || options.deaf !== undefined || + options.channelId !== undefined + ) { + const memberVoiceState = (await bot.guilds.get(guildId)) + ?.voiceStates.get(memberId); + + if (!memberVoiceState?.channelId) { + throw new Error("MEMBER_NOT_IN_VOICE_CHANNEL"); + } + + if (options.mute !== undefined) { + requiredPerms.add("MUTE_MEMBERS"); + } + + if (options.deaf !== undefined) { + requiredPerms.add("DEAFEN_MEMBERS"); + } + + if (options.channelId) { + const requiredVoicePerms: Set = new Set([ + "CONNECT", + "MOVE_MEMBERS", + ]); + if (memberVoiceState) { + await requireBotChannelPermissions( + bot, + memberVoiceState?.channelId, + [ + ...requiredVoicePerms, + ], + ); + } + await requireBotChannelPermissions(bot, options.channelId, [ + ...requiredVoicePerms, + ]); + } + } + + await requireBotGuildPermissions(bot, guildId, [ + ...requiredPerms, + ]); + + return editMemberOld(guildId, memberId, options); + }; +} diff --git a/plugins/permissions/src/emojis.ts b/plugins/permissions/src/emojis.ts new file mode 100644 index 000000000..853543577 --- /dev/null +++ b/plugins/permissions/src/emojis.ts @@ -0,0 +1,38 @@ +import { BotWithCache } from "../deps.ts"; +import { requireBotGuildPermissions } from "./permissions.ts"; + +export function createEmoji(bot: BotWithCache) { + const createEmojiOld = bot.helpers.createEmoji; + + bot.helpers.createEmoji = function (guildId, id) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_EMOJIS"]); + + return createEmojiOld(guildId, id); + }; +} + +export function deleteEmoji(bot: BotWithCache) { + const deleteEmojiOld = bot.helpers.deleteEmoji; + + bot.helpers.deleteEmoji = function (guildId, id) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_EMOJIS"]); + + return deleteEmojiOld(guildId, id); + }; +} + +export function editEmoji(bot: BotWithCache) { + const editEmojiOld = bot.helpers.editEmoji; + + bot.helpers.editEmoji = function (guildId, id, options) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_EMOJIS"]); + + return editEmojiOld(guildId, id, options); + }; +} + +export default function setupEmojiPermChecks(bot: BotWithCache) { + createEmoji(bot); + deleteEmoji(bot); + editEmoji(bot) +} diff --git a/plugins/permissions/src/guilds/createGuild.ts b/plugins/permissions/src/guilds/createGuild.ts new file mode 100644 index 000000000..a70a2c701 --- /dev/null +++ b/plugins/permissions/src/guilds/createGuild.ts @@ -0,0 +1,22 @@ +import { BotWithCache } from "../../deps.ts"; + +export default function createGuild(bot: BotWithCache) { + const createGuildOld = bot.helpers.createGuild; + + bot.helpers.createGuild = function (options) { + if (bot.guilds.size > 10) { + throw new Error( + "A bot can not create a guild if it is already in 10 guilds.", + ); + } + + if ( + options.name && + !bot.utils.validateLength(options.name, { min: 2, max: 100 }) + ) { + throw new Error("The guild name must be between 2 and 100 characters."); + } + + return createGuildOld(options); + }; +} diff --git a/plugins/permissions/src/guilds/deleteGuild.ts b/plugins/permissions/src/guilds/deleteGuild.ts new file mode 100644 index 000000000..a56a8abd9 --- /dev/null +++ b/plugins/permissions/src/guilds/deleteGuild.ts @@ -0,0 +1,14 @@ +import { BotWithCache } from "../../deps.ts"; + +export default function deleteGuild(bot: BotWithCache) { + const deleteGuildOld = bot.helpers.deleteGuild; + + bot.helpers.deleteGuild = function (guildId) { + const guild = bot.guilds.get(guildId); + if (guild && guild.ownerId !== bot.id) { + throw new Error("A bot can only delete a guild it owns."); + } + + return deleteGuildOld(guildId); + }; +} diff --git a/plugins/permissions/src/guilds/editGuild.ts b/plugins/permissions/src/guilds/editGuild.ts new file mode 100644 index 000000000..bf1cb3f7a --- /dev/null +++ b/plugins/permissions/src/guilds/editGuild.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function editGuild(bot: BotWithCache) { + const editGuildOld = bot.helpers.editGuild; + + bot.helpers.editGuild = function (guildId, options, shardId) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]) + + return editGuildOld(guildId, options, shardId); + }; +} diff --git a/plugins/permissions/src/guilds/events.ts b/plugins/permissions/src/guilds/events.ts new file mode 100644 index 000000000..af1938c11 --- /dev/null +++ b/plugins/permissions/src/guilds/events.ts @@ -0,0 +1,144 @@ +import { BotWithCache, ScheduledEventEntityType } from "../../deps.ts"; +import { + requireBotChannelPermissions, + requireBotGuildPermissions, +} from "../permissions.ts"; + +export function createScheduledEvent(bot: BotWithCache) { + const createScheduledEventOld = bot.helpers.createScheduledEvent; + + bot.helpers.createScheduledEvent = function (guildId, options) { + if (options.entityType === ScheduledEventEntityType.StageInstance) { + if (!options.channelId) { + throw new Error( + "A channel id is required for creating a stage scheduled event.", + ); + } + + requireBotChannelPermissions(bot, options.channelId, [ + "MANAGE_CHANNELS", + "MUTE_MEMBERS", + "MOVE_MEMBERS", + ]); + + // MANAGE_EVENTS at the guild level or at least MANAGE_EVENTS for the channel_id associated with the event + try { + requireBotGuildPermissions(bot, guildId, [ + "MANAGE_EVENTS", + ]); + } catch { + requireBotChannelPermissions(bot, options.channelId, [ + "MANAGE_EVENTS", + ]); + } + + return createScheduledEventOld(guildId, options); + } + + if (options.entityType === ScheduledEventEntityType.Voice) { + if (!options.channelId) { + throw new Error( + "A channel id is required for creating a voice scheduled event.", + ); + } + + requireBotChannelPermissions(bot, options.channelId, [ + "VIEW_CHANNEL", + "CONNECT", + ]); + + // MANAGE_EVENTS at the guild level or at least MANAGE_EVENTS for the channel_id associated with the event + try { + requireBotGuildPermissions(bot, guildId, [ + "MANAGE_EVENTS", + ]); + } catch { + requireBotChannelPermissions(bot, options.channelId, [ + "MANAGE_EVENTS", + ]); + } + + return createScheduledEventOld(guildId, options); + } + + // EXTERNAL EVENTS + + requireBotGuildPermissions(bot, guildId, [ + "MANAGE_EVENTS", + ]); + + return createScheduledEventOld(guildId, options); + }; +} + +export function editScheduledEvent(bot: BotWithCache) { + const editScheduledEventOld = bot.helpers.editScheduledEvent; + + bot.helpers.editScheduledEvent = function (guildId, eventId, options) { + if (options.entityType === ScheduledEventEntityType.StageInstance) { + if (!options.channelId) { + throw new Error( + "A channel id is required for creating a stage scheduled event.", + ); + } + + requireBotChannelPermissions(bot, options.channelId, [ + "MANAGE_CHANNELS", + "MUTE_MEMBERS", + "MOVE_MEMBERS", + ]); + + // MANAGE_EVENTS at the guild level or at least MANAGE_EVENTS for the channel_id associated with the event + try { + requireBotGuildPermissions(bot, guildId, [ + "MANAGE_EVENTS", + ]); + } catch { + requireBotChannelPermissions(bot, options.channelId, [ + "MANAGE_EVENTS", + ]); + } + + return editScheduledEventOld(guildId, eventId, options); + } + + if (options.entityType === ScheduledEventEntityType.Voice) { + if (!options.channelId) { + throw new Error( + "A channel id is required for creating a voice scheduled event.", + ); + } + + requireBotChannelPermissions(bot, options.channelId, [ + "VIEW_CHANNEL", + "CONNECT", + ]); + + // MANAGE_EVENTS at the guild level or at least MANAGE_EVENTS for the channel_id associated with the event + try { + requireBotGuildPermissions(bot, guildId, [ + "MANAGE_EVENTS", + ]); + } catch { + requireBotChannelPermissions(bot, options.channelId, [ + "MANAGE_EVENTS", + ]); + } + + return editScheduledEventOld(guildId, eventId, options); + } + + // EXTERNAL EVENTS + + requireBotGuildPermissions(bot, guildId, [ + "MANAGE_EVENTS", + ]); + + return editScheduledEventOld(guildId, eventId, options); + }; +} + +export default function setupEventsPermChecks(bot: BotWithCache) { + createScheduledEvent(bot); + editScheduledEvent(bot); +} diff --git a/plugins/permissions/src/guilds/getAuditLogs.ts b/plugins/permissions/src/guilds/getAuditLogs.ts new file mode 100644 index 000000000..528080520 --- /dev/null +++ b/plugins/permissions/src/guilds/getAuditLogs.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function getAuditLogs(bot: BotWithCache) { + const getAuditLogsOld = bot.helpers.getAuditLogs; + + bot.helpers.getAuditLogs = function (guildId, options) { + requireBotGuildPermissions(bot, guildId, ["VIEW_AUDIT_LOG"]); + + return getAuditLogsOld(guildId, options); + }; +} diff --git a/plugins/permissions/src/guilds/getBan.ts b/plugins/permissions/src/guilds/getBan.ts new file mode 100644 index 000000000..3e745d165 --- /dev/null +++ b/plugins/permissions/src/guilds/getBan.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function getBan(bot: BotWithCache) { + const getBanOld = bot.helpers.getBan; + + bot.helpers.getBan = function (guildId, memberId) { + requireBotGuildPermissions(bot, guildId, ["BAN_MEMBERS"]); + + return getBanOld(guildId, memberId); + }; +} diff --git a/plugins/permissions/src/guilds/getBans.ts b/plugins/permissions/src/guilds/getBans.ts new file mode 100644 index 000000000..b5a9ceec6 --- /dev/null +++ b/plugins/permissions/src/guilds/getBans.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function getBans(bot: BotWithCache) { + const getBansOld = bot.helpers.getBans; + + bot.helpers.getBans = function (guildId) { + requireBotGuildPermissions(bot, guildId, ["BAN_MEMBERS"]); + + return getBansOld(guildId); + }; +} diff --git a/plugins/permissions/src/guilds/getPruneCount.ts b/plugins/permissions/src/guilds/getPruneCount.ts new file mode 100644 index 000000000..521d43f75 --- /dev/null +++ b/plugins/permissions/src/guilds/getPruneCount.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function getPruneCount(bot: BotWithCache) { + const getPruneCountOld = bot.helpers.getPruneCount; + + bot.helpers.getPruneCount = function (guildId, options) { + requireBotGuildPermissions(bot, guildId, ["KICK_MEMBERS"]); + + return getPruneCountOld(guildId, options); + }; +} diff --git a/plugins/permissions/src/guilds/getVanityUrl.ts b/plugins/permissions/src/guilds/getVanityUrl.ts new file mode 100644 index 000000000..acc31ce9e --- /dev/null +++ b/plugins/permissions/src/guilds/getVanityUrl.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function getVanityUrl(bot: BotWithCache) { + const getVanityUrlOld = bot.helpers.getVanityUrl; + + bot.helpers.getVanityUrl = function (guildId) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return getVanityUrlOld(guildId); + }; +} diff --git a/plugins/permissions/src/guilds/mod.ts b/plugins/permissions/src/guilds/mod.ts new file mode 100644 index 000000000..d87c4edcb --- /dev/null +++ b/plugins/permissions/src/guilds/mod.ts @@ -0,0 +1,26 @@ +import { BotWithCache } from "../../deps.ts"; +import setupEventsPermChecks from "./events.ts"; +import setupWelcomeScreenPermChecks from "./welcomeScreen.ts"; +import setupWidgetPermChecks from "./widget.ts"; +import createGuild from "./createGuild.ts"; +import deleteGuild from "./deleteGuild.ts"; +import editGuild from "./editGuild.ts"; +import getAuditLogs from "./getAuditLogs.ts"; +import getBan from "./getBan.ts"; +import getBans from "./getBans.ts"; +import getPruneCount from "./getPruneCount.ts"; +import getVanityUrl from "./getVanityUrl.ts"; + +export default function setupGuildPermChecks(bot: BotWithCache) { + setupEventsPermChecks(bot); + createGuild(bot); + deleteGuild(bot); + editGuild(bot); + setupWelcomeScreenPermChecks(bot); + setupWidgetPermChecks(bot); + getAuditLogs(bot); + getBan(bot); + getBans(bot); + getPruneCount(bot); + getVanityUrl(bot); +} diff --git a/plugins/permissions/src/guilds/welcomeScreen.ts b/plugins/permissions/src/guilds/welcomeScreen.ts new file mode 100644 index 000000000..f6515dd2d --- /dev/null +++ b/plugins/permissions/src/guilds/welcomeScreen.ts @@ -0,0 +1,16 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export function editWelcomeScreen(bot: BotWithCache) { + const editWelcomeScreenOld = bot.helpers.editWelcomeScreen; + + bot.helpers.editWelcomeScreen = function (guildId, options) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return editWelcomeScreenOld(guildId, options); + }; +} + +export default function setupWelcomeScreenPermChecks(bot: BotWithCache) { + editWelcomeScreen(bot); +} diff --git a/plugins/permissions/src/guilds/widget.ts b/plugins/permissions/src/guilds/widget.ts new file mode 100644 index 000000000..5b95c1666 --- /dev/null +++ b/plugins/permissions/src/guilds/widget.ts @@ -0,0 +1,16 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export function editWidget(bot: BotWithCache) { + const editWidgetOld = bot.helpers.editWidget; + + bot.helpers.editWidget = function (guildId, enabled, channelId) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return editWidgetOld(guildId, enabled, channelId); + }; +} + +export default function setupWidgetPermChecks(bot: BotWithCache) { + editWidget(bot); +} diff --git a/plugins/permissions/src/integrations.ts b/plugins/permissions/src/integrations.ts new file mode 100644 index 000000000..b340dbe18 --- /dev/null +++ b/plugins/permissions/src/integrations.ts @@ -0,0 +1,27 @@ +import { BotWithCache } from "../deps.ts"; +import { requireBotGuildPermissions } from "./permissions.ts"; + +export function deleteIntegration(bot: BotWithCache) { + const deleteIntegrationOld = bot.helpers.deleteIntegration; + + bot.helpers.deleteIntegration = function (guildId, id) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return deleteIntegrationOld(guildId, id); + }; +} + +export function getIntegrations(bot: BotWithCache) { + const getIntegrationsOld = bot.helpers.getIntegrations; + + bot.helpers.getIntegrations = function (guildId) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return getIntegrationsOld(guildId); + }; +} + +export default function setupIntegrationPermChecks(bot: BotWithCache) { + deleteIntegration(bot); + getIntegrations(bot); +} diff --git a/plugins/permissions/src/interactions/commands.ts b/plugins/permissions/src/interactions/commands.ts new file mode 100644 index 000000000..e8a2ff7fe --- /dev/null +++ b/plugins/permissions/src/interactions/commands.ts @@ -0,0 +1,201 @@ +import { + AllowedMentionsTypes, + ApplicationCommandOption, + ApplicationCommandOptionTypes, + ApplicationCommandTypes, + BotWithCache, + CONTEXT_MENU_COMMANDS_NAME_REGEX, + SLASH_COMMANDS_NAME_REGEX, +} from "../../deps.ts"; + +export function validateApplicationCommandOptions( + bot: BotWithCache, + options: ApplicationCommandOption[], +) { + const requiredOptions: ApplicationCommandOption[] = []; + const optionalOptions: ApplicationCommandOption[] = []; + + for (const option of options) { + option.name = option.name.toLowerCase(); + + if (option.choices?.length) { + if (option.choices.length > 25) { + throw new Error("Too many application command options provided."); + } + + if ( + option.type !== ApplicationCommandOptionTypes.String && + option.type !== ApplicationCommandOptionTypes.Integer + ) { + throw new Error("Only string or integer options can have choices."); + } + } + + if (!bot.utils.validateLength(option.name, { min: 1, max: 32 })) { + throw new Error("Invalid application command option name."); + } + + if (!bot.utils.validateLength(option.description, { min: 1, max: 100 })) { + throw new Error("Invalid application command description."); + } + + option.choices?.every((choice) => { + if (!bot.utils.validateLength(choice.name, { min: 1, max: 100 })) { + throw new Error( + "Invalid application command option choice name. Must be between 1-100 characters long.", + ); + } + + if ( + option.type === ApplicationCommandOptionTypes.String && + (typeof choice.value !== "string" || choice.value.length < 1 || + choice.value.length > 100) + ) { + throw new Error("Invalid slash options choice value type."); + } + + if ( + option.type === ApplicationCommandOptionTypes.Integer && + typeof choice.value !== "number" + ) { + throw new Error("A number must be set for Integer types."); + } + }); + + if (option.required) { + requiredOptions.push(option); + continue; + } + + optionalOptions.push(option); + } + + return [...requiredOptions, ...optionalOptions]; +} + +export function createApplicationCommand(bot: BotWithCache) { + const createApplicationCommandOld = bot.helpers.createApplicationCommand; + + bot.helpers.createApplicationCommand = function (options, guildId) { + const isChatInput = !options.type || + options.type === ApplicationCommandTypes.ChatInput; + + if (!options.name) { + throw new Error("A name is required to create a options."); + } + if (isChatInput) { + if (!SLASH_COMMANDS_NAME_REGEX.test(options.name)) { + throw new Error( + "The name of the slash command did not match the required regex.", + ); + } + + // Only slash need to be lowercase + options.name = options.name.toLowerCase(); + } else { + if (!CONTEXT_MENU_COMMANDS_NAME_REGEX.test(options.name)) { + throw new Error( + "The name of the context menu did not match the required regex.", + ); + } + } + + // Slash commands require description + if ( + !options.description && + (isChatInput) + ) { + throw new Error( + "Slash commands require some form of a description be provided.", + ); + } + + if ( + options.description && + ((options.type === ApplicationCommandTypes.User) || + (options.type === ApplicationCommandTypes.Message)) + ) { + throw new Error("Context menu commands do not allow a description."); + } + + if ( + options.description && + !bot.utils.validateLength(options.description, { min: 1, max: 100 }) + ) { + throw new Error( + "Application command descriptions must be between 1 and 100 characters.", + ); + } + + if (options.options?.length) { + if (options.options.length > 25) { + throw new Error("Only 25 options are allowed to be provided."); + } + + options.options = validateApplicationCommandOptions(bot, options.options); + } + + return createApplicationCommandOld(options, guildId); + }; +} + +export function editInteractionResponse(bot: BotWithCache) { + const editInteractionResponseOld = bot.helpers.editInteractionResponse; + + bot.helpers.editInteractionResponse = function (token, options) { + if (options.content && options.content.length > 2000) { + throw Error(bot.constants.Errors.MESSAGE_MAX_LENGTH); + } + + if (options.embeds && options.embeds.length > 10) { + options.embeds.splice(10); + } + + if (options.allowedMentions) { + if (options.allowedMentions.users?.length) { + if ( + options.allowedMentions.parse?.includes( + AllowedMentionsTypes.UserMentions, + ) + ) { + options.allowedMentions.parse = options.allowedMentions.parse.filter(( + p, + ) => p !== "users"); + } + + if (options.allowedMentions.users.length > 100) { + options.allowedMentions.users = options.allowedMentions.users.slice( + 0, + 100, + ); + } + } + + if (options.allowedMentions.roles?.length) { + if ( + options.allowedMentions.parse?.includes( + AllowedMentionsTypes.RoleMentions, + ) + ) { + options.allowedMentions.parse = options.allowedMentions.parse.filter(( + p, + ) => p !== "roles"); + } + + if (options.allowedMentions.roles.length > 100) { + options.allowedMentions.roles = options.allowedMentions.roles.slice( + 0, + 100, + ); + } + } + } + + return editInteractionResponseOld(token, options); + }; +} + +export default function setupInteractionCommandPermChecks(bot: BotWithCache) { + createApplicationCommand(bot); + editInteractionResponse(bot); +} diff --git a/plugins/permissions/src/interactions/editFollowupMessage.ts b/plugins/permissions/src/interactions/editFollowupMessage.ts new file mode 100644 index 000000000..d9abb9a94 --- /dev/null +++ b/plugins/permissions/src/interactions/editFollowupMessage.ts @@ -0,0 +1,61 @@ +import { AllowedMentionsTypes, BotWithCache } from "../../deps.ts"; + +export default function editFollowupMessage(bot: BotWithCache) { + const editFollowupMessageOld = bot.helpers.editFollowupMessage; + + bot.helpers.editFollowupMessage = function ( + token, + messageId, + options, + ) { + if (options.content && options.content.length > 2000) { + throw Error("MESSAGE_MAX_LENGTH"); + } + + if (options.embeds && options.embeds.length > 10) { + options.embeds.splice(10); + } + + if (options.allowedMentions) { + if (options.allowedMentions.users?.length) { + if ( + options.allowedMentions.parse?.includes( + AllowedMentionsTypes.UserMentions, + ) + ) { + options.allowedMentions.parse = options.allowedMentions.parse.filter(( + p, + ) => p !== "users"); + } + + if (options.allowedMentions.users.length > 100) { + options.allowedMentions.users = options.allowedMentions.users.slice( + 0, + 100, + ); + } + } + + if (options.allowedMentions.roles?.length) { + if ( + options.allowedMentions.parse?.includes( + AllowedMentionsTypes.RoleMentions, + ) + ) { + options.allowedMentions.parse = options.allowedMentions.parse.filter(( + p, + ) => p !== "roles"); + } + + if (options.allowedMentions.roles.length > 100) { + options.allowedMentions.roles = options.allowedMentions.roles.slice( + 0, + 100, + ); + } + } + } + + return editFollowupMessageOld(token, messageId, options); + }; +} diff --git a/plugins/permissions/src/interactions/mod.ts b/plugins/permissions/src/interactions/mod.ts new file mode 100644 index 000000000..afc56bde6 --- /dev/null +++ b/plugins/permissions/src/interactions/mod.ts @@ -0,0 +1,31 @@ +import { BotWithCache } from "../../deps.ts"; +import setupInteractionCommandPermChecks from "./commands.ts"; +import editFollowupMessage from "./editFollowupMessage.ts"; + +export function sendInteractionResponse(bot: BotWithCache) { + const sendInteractionResponseOld = bot.helpers.sendInteractionResponse; + + bot.helpers.sendInteractionResponse = function (id, token, options) { + options.data?.choices?.every((choice) => { + if (!bot.utils.validateLength(choice.name, { min: 1, max: 100 })) { + throw new Error( + "Invalid application command option choice name. Must be between 1-100 characters long.", + ); + } + + if ( + typeof choice.value === "string" && (choice.value.length < 1 || + choice.value.length > 100) + ) { + throw new Error("Invalid slash options choice value type."); + } + }); + + return sendInteractionResponseOld(id, token, options); + }; +} + +export default function setupInteractionPermChecks(bot: BotWithCache) { + setupInteractionCommandPermChecks(bot); + editFollowupMessage(bot); +} diff --git a/plugins/permissions/src/invites.ts b/plugins/permissions/src/invites.ts new file mode 100644 index 000000000..32a1dd47b --- /dev/null +++ b/plugins/permissions/src/invites.ts @@ -0,0 +1,47 @@ +import { BotWithCache } from "../deps.ts"; +import { requireBotChannelPermissions } from "./permissions.ts"; + +export function createInvite(bot: BotWithCache) { + const createInviteOld = bot.helpers.createInvite; + + bot.helpers.createInvite = function (channelId, options = {}) { + if (options.maxAge && (options.maxAge < 0 || options.maxAge > 604800)) { + throw new Error( + "The max age for an invite must be between 0 and 604800.", + ); + } + if (options.maxUses && (options.maxUses < 0 || options.maxUses > 100)) { + throw new Error("The max uses for an invite must be between 0 and 100."); + } + + requireBotChannelPermissions(bot, channelId, ["CREATE_INSTANT_INVITE"]); + + return createInviteOld(channelId, options); + }; +} + +export function getChannelInvites(bot: BotWithCache) { + const getChannelInvitesOld = bot.helpers.getChannelInvites; + + bot.helpers.getChannelInvites = function (channelId) { + requireBotChannelPermissions(bot, channelId, ["MANAGE_CHANNELS"]); + + return getChannelInvitesOld(channelId); + }; +} + +export function getInvites(bot: BotWithCache) { + const getInvitesOld = bot.helpers.getInvites; + + bot.helpers.getInvites = function (guildId) { + requireBotChannelPermissions(bot, guildId, ["MANAGE_GUILD"]); + + return getInvitesOld(guildId); + }; +} + +export default function setupInvitesPermChecks(bot: BotWithCache) { + createInvite(bot); + getChannelInvites(bot); + getInvites(bot); +} diff --git a/plugins/permissions/src/members/ban.ts b/plugins/permissions/src/members/ban.ts new file mode 100644 index 000000000..b1c6d729e --- /dev/null +++ b/plugins/permissions/src/members/ban.ts @@ -0,0 +1,27 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export function banMember(bot: BotWithCache) { + const banMemberOld = bot.helpers.banMember; + + bot.helpers.banMember = function (guildId, id, options) { + requireBotGuildPermissions(bot, guildId, ["BAN_MEMBERS"]); + + return banMemberOld(guildId, id, options); + }; +} + +export function unbanMember(bot: BotWithCache) { + const unbanMemberOld = bot.helpers.unbanMember; + + bot.helpers.unbanMember = function (guildId, id) { + requireBotGuildPermissions(bot, guildId, ["BAN_MEMBERS"]); + + return unbanMemberOld(guildId, id); + }; +} + +export default function setupBanPermChecks(bot: BotWithCache) { + banMember(bot); + unbanMember(bot); +} diff --git a/plugins/permissions/src/members/editBot.ts b/plugins/permissions/src/members/editBot.ts new file mode 100644 index 000000000..0d246add5 --- /dev/null +++ b/plugins/permissions/src/members/editBot.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function editBotNickname(bot: BotWithCache) { + const editBotNicknameOld = bot.helpers.editBotNickname; + + bot.helpers.editBotNickname = function (guildId, options) { + requireBotGuildPermissions(bot, guildId, ["CHANGE_NICKNAME"]); + + return editBotNicknameOld(guildId, options); + }; +} diff --git a/plugins/permissions/src/members/editMember.ts b/plugins/permissions/src/members/editMember.ts new file mode 100644 index 000000000..8c4f069bc --- /dev/null +++ b/plugins/permissions/src/members/editMember.ts @@ -0,0 +1,22 @@ +import { BotWithCache, PermissionStrings } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function editMember(bot: BotWithCache) { + const editMemberOld = bot.helpers.editMember; + + bot.helpers.editMember = function (guildId, memberId, options) { + const requiredPerms: PermissionStrings[] = []; + if (options.roles) requiredPerms.push("MANAGE_ROLES"); + // NULL IS ALLOWED + if (options.nick !== undefined) requiredPerms.push("MANAGE_NICKNAMES"); + if (options.channelId !== undefined) requiredPerms.push("MOVE_MEMBERS"); + if (options.mute !== undefined) requiredPerms.push("MUTE_MEMBERS"); + if (options.deaf !== undefined) requiredPerms.push("DEAFEN_MEMBERS"); + + if (requiredPerms.length) { + requireBotGuildPermissions(bot, guildId, requiredPerms); + } + + return editMemberOld(guildId, memberId, options); + }; +} diff --git a/plugins/permissions/src/members/kickMember.ts b/plugins/permissions/src/members/kickMember.ts new file mode 100644 index 000000000..71a3fd8a5 --- /dev/null +++ b/plugins/permissions/src/members/kickMember.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function kickMember(bot: BotWithCache) { + const editMemberOld = bot.helpers.kickMember; + + bot.helpers.kickMember = function (guildId, memberId, reason) { + requireBotGuildPermissions(bot, guildId, ["KICK_MEMBERS"]); + + return editMemberOld(guildId, memberId, reason); + }; +} diff --git a/plugins/permissions/src/members/mod.ts b/plugins/permissions/src/members/mod.ts new file mode 100644 index 000000000..5f183ffae --- /dev/null +++ b/plugins/permissions/src/members/mod.ts @@ -0,0 +1,14 @@ +import { BotWithCache } from "../../deps.ts"; +import setupBanPermChecks from "./ban.ts"; +import editBotNickname from "./editBot.ts"; +import editMember from "./editMember.ts"; +import kickMember from "./kickMember.ts"; +import pruneMembers from "./pruneMembers.ts"; + +export default function setupMemberPermChecks(bot: BotWithCache) { + setupBanPermChecks(bot); + editBotNickname(bot); + editMember(bot); + kickMember(bot); + pruneMembers(bot); +} diff --git a/plugins/permissions/src/members/pruneMembers.ts b/plugins/permissions/src/members/pruneMembers.ts new file mode 100644 index 000000000..55aad7138 --- /dev/null +++ b/plugins/permissions/src/members/pruneMembers.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function pruneMembers(bot: BotWithCache) { + const pruneMembersOld = bot.helpers.pruneMembers; + + bot.helpers.pruneMembers = function (guildId, options) { + requireBotGuildPermissions(bot, guildId, ["KICK_MEMBERS"]); + + return pruneMembersOld(guildId, options); + }; +} diff --git a/plugins/permissions/src/messages/create.ts b/plugins/permissions/src/messages/create.ts new file mode 100644 index 000000000..5891bf155 --- /dev/null +++ b/plugins/permissions/src/messages/create.ts @@ -0,0 +1,200 @@ +import { + AllowedMentionsTypes, + BotWithCache, + ChannelTypes, + PermissionStrings, +} from "../../deps.ts"; +import { validateComponents } from "../components.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export function sendMessage(bot: BotWithCache) { + const sendMessageOld = bot.helpers.sendMessage; + + bot.helpers.sendMessage = function ( + channelId, + content, + ) { + if (typeof content === "string") { + throw new Error("TODO"); + } + + const channel = bot.channels.get(channelId); + if ( + channel && + [ + ChannelTypes.GuildCategory, + ChannelTypes.GuildStore, + ChannelTypes.GuildStageVoice, + ].includes(channel.type) + ) { + throw new Error( + `Can not send message to a channel of this type. Channel ID: ${channelId}`, + ); + } + + if ( + content.content && + !bot.utils.validateLength(content.content, { max: 2000 }) + ) { + throw new Error("The content should not exceed 2000 characters."); + } + + if (content.allowedMentions) { + if (content.allowedMentions.users?.length) { + if ( + content.allowedMentions.parse?.includes( + AllowedMentionsTypes.UserMentions, + ) + ) { + content.allowedMentions.parse = content.allowedMentions.parse.filter(( + p, + ) => p !== "users"); + } + + if (content.allowedMentions.users.length > 100) { + content.allowedMentions.users = content.allowedMentions.users.slice( + 0, + 100, + ); + } + } + + if (content.allowedMentions.roles?.length) { + if ( + content.allowedMentions.parse?.includes( + AllowedMentionsTypes.RoleMentions, + ) + ) { + content.allowedMentions.parse = content.allowedMentions.parse.filter(( + p, + ) => p !== "roles"); + } + + if (content.allowedMentions.roles.length > 100) { + content.allowedMentions.roles = content.allowedMentions.roles.slice( + 0, + 100, + ); + } + } + } + + if (content.components) { + validateComponents(bot, content.components); + } + + if (channel) { + const requiredPerms: PermissionStrings[] = []; + if (channel.guildId) { + requiredPerms.push("SEND_MESSAGES"); + } + if (content.tts) requiredPerms.push("SEND_TTS_MESSAGES"); + if (content.messageReference) requiredPerms.push("READ_MESSAGE_HISTORY"); + if (requiredPerms.length) { + requireBotChannelPermissions(bot, channel, requiredPerms); + } + } + + return sendMessageOld(channelId, content); + }; +} + +export function editMessage(bot: BotWithCache) { + const editMessageOld = bot.helpers.editMessage; + + bot.helpers.editMessage = function ( + channelId, + messageId, + content, + ) { + if (typeof content === "string") { + throw new Error("TODO"); + } + + const message = bot.messages.get(messageId); + if (message) { + if (message.authorId !== bot.id) { + content = { flags: content.flags }; + requireBotChannelPermissions(bot, channelId, ["MANAGE_MESSAGES"]); + } + } + + if (content.allowedMentions) { + if (content.allowedMentions.users?.length) { + if ( + content.allowedMentions.parse?.includes( + AllowedMentionsTypes.UserMentions, + ) + ) { + content.allowedMentions.parse = content.allowedMentions.parse.filter(( + p, + ) => p !== "users"); + } + + if (content.allowedMentions.users.length > 100) { + content.allowedMentions.users = content.allowedMentions.users.slice( + 0, + 100, + ); + } + } + + if (content.allowedMentions.roles?.length) { + if ( + content.allowedMentions.parse?.includes( + AllowedMentionsTypes.RoleMentions, + ) + ) { + content.allowedMentions.parse = content.allowedMentions.parse.filter(( + p, + ) => p !== "roles"); + } + + if (content.allowedMentions.roles.length > 100) { + content.allowedMentions.roles = content.allowedMentions.roles.slice( + 0, + 100, + ); + } + } + } + + content.embeds?.splice(10); + + if ( + content.content && + bot.utils.validateLength(content.content, { max: 2000 }) + ) { + throw new Error( + "A message content can not contain more than 2000 characters.", + ); + } + + return editMessageOld(channelId, messageId, content); + }; +} + +export function publishMessage(bot: BotWithCache) { + const publishMessageOld = bot.helpers.publishMessage; + + bot.helpers.publishMessage = function ( + channelId, + messageId, + ) { + const message = bot.messages.get(messageId); + + requireBotChannelPermissions( + bot, + channelId, + message?.authorId === bot.id ? ["SEND_MESSAGES"] : ["MANAGE_MESSAGES"], + ); + + return publishMessageOld(channelId, messageId); + }; +} + +export default function setupCreateMessagePermChecks(bot: BotWithCache) { + sendMessage(bot); + editMessage(bot); + publishMessage(bot); +} diff --git a/plugins/permissions/src/messages/delete.ts b/plugins/permissions/src/messages/delete.ts new file mode 100644 index 000000000..cfe53a431 --- /dev/null +++ b/plugins/permissions/src/messages/delete.ts @@ -0,0 +1,80 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export function deleteMessage(bot: BotWithCache) { + const deleteMessageOld = bot.helpers.deleteMessage; + + bot.helpers.deleteMessage = function ( + channelId, + messageId, + reason, + milliseconds, + ) { + const message = bot.messages.get(messageId); + // DELETING SELF MESSAGES IS ALWAYS ALLOWED + if (message?.authorId === bot.id) { + return deleteMessageOld(channelId, messageId, reason, milliseconds); + } + + const channel = bot.channels.get(channelId); + if (channel?.guildId) { + requireBotChannelPermissions(bot, channel, [ + "MANAGE_MESSAGES", + ]); + } else { + throw new Error( + `You can only delete messages in a channel which has a guild id. Channel ID: ${channelId} Message Id: ${messageId}`, + ); + } + + return deleteMessageOld(channelId, messageId, reason, milliseconds); + }; +} + +export function deleteMessages(bot: BotWithCache) { + const deleteMessagesOld = bot.helpers.deleteMessages; + + bot.helpers.deleteMessages = function ( + channelId, + ids, + reason, + ) { + const channel = bot.channels.get(channelId); + if (!channel?.guildId) { + throw new Error( + `Bulk deleting messages is only allowed in channels which has a guild id. Channel ID: ${channelId} IDS: ${ + ids.join(" ") + }`, + ); + } + + // 2 WEEKS + const oldestAllowed = Date.now() - 1209600000; + + ids = ids.filter((id) => { + const createdAt = Number(id / 4194304n + 1420070400000n); + // IF MESSAGE IS OLDER THAN 2 WEEKS + if (createdAt > oldestAllowed) return true; + + console.log( + `[Permission Plugin] Skipping bulk message delete of ID ${id} because it is older than 2 weeks.`, + ); + return false; + }); + + if (ids.length < 2) { + throw new Error("Bulk message delete requires at least 2 messages."); + } + + requireBotChannelPermissions(bot, channel, [ + "MANAGE_MESSAGES", + ]); + + return deleteMessagesOld(channelId, ids, reason); + }; +} + +export default function setupDeleteMessagePermChecks(bot: BotWithCache) { + deleteMessage(bot); + deleteMessages(bot); +} diff --git a/plugins/permissions/src/messages/get.ts b/plugins/permissions/src/messages/get.ts new file mode 100644 index 000000000..6add29acc --- /dev/null +++ b/plugins/permissions/src/messages/get.ts @@ -0,0 +1,44 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export function getMessage(bot: BotWithCache) { + const getMessageOld = bot.helpers.getMessage; + + bot.helpers.getMessage = function ( + channelId, + messageId, + ) { + const channel = bot.channels.get(channelId); + if (channel?.guildId) { + requireBotChannelPermissions(bot, channel, [ + "READ_MESSAGE_HISTORY", + ]); + } + + return getMessageOld(channelId, messageId); + }; +} + +export function getMessages(bot: BotWithCache) { + const getMessagesOld = bot.helpers.getMessages; + + bot.helpers.getMessages = function ( + channelId, + options, + ) { + const channel = bot.channels.get(channelId); + if (channel?.guildId) { + requireBotChannelPermissions(bot, channel, [ + "READ_MESSAGE_HISTORY", + "VIEW_CHANNEL", + ]); + } + + return getMessagesOld(channelId, options); + }; +} + +export default function setupGetMessagePermChecks(bot: BotWithCache) { + getMessage(bot); + getMessages(bot); +} diff --git a/plugins/permissions/src/messages/mod.ts b/plugins/permissions/src/messages/mod.ts new file mode 100644 index 000000000..dff8cd30a --- /dev/null +++ b/plugins/permissions/src/messages/mod.ts @@ -0,0 +1,14 @@ +import { BotWithCache } from "../../deps.ts"; +import setupCreateMessagePermChecks from "./create.ts"; +import setupDeleteMessagePermChecks from "./delete.ts"; +import setupGetMessagePermChecks from "./get.ts"; +import setupPinMessagePermChecks from "./pin.ts"; +import setupReactionsPermChecks from "./reactions.ts"; + +export default function setupMessagesPermChecks(bot: BotWithCache) { + setupReactionsPermChecks(bot); + setupDeleteMessagePermChecks(bot); + setupGetMessagePermChecks(bot); + setupPinMessagePermChecks(bot); + setupCreateMessagePermChecks(bot); +} diff --git a/plugins/permissions/src/messages/pin.ts b/plugins/permissions/src/messages/pin.ts new file mode 100644 index 000000000..6b7d04402 --- /dev/null +++ b/plugins/permissions/src/messages/pin.ts @@ -0,0 +1,37 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export function pinMessage(bot: BotWithCache) { + const pinMessageOld = bot.helpers.pinMessage; + + bot.helpers.pinMessage = function ( + channelId, + messageId, + ) { + requireBotChannelPermissions(bot, channelId, [ + "MANAGE_MESSAGES", + ]); + + return pinMessageOld(channelId, messageId); + }; +} + +export function unpinMessage(bot: BotWithCache) { + const unpinMessageOld = bot.helpers.unpinMessage; + + bot.helpers.unpinMessage = function ( + channelId, + messageId, + ) { + requireBotChannelPermissions(bot, channelId, [ + "MANAGE_MESSAGES", + ]); + + return unpinMessageOld(channelId, messageId); + }; +} + +export default function setupPinMessagePermChecks(bot: BotWithCache) { + pinMessage(bot); + unpinMessage(bot); +} diff --git a/plugins/permissions/src/messages/reactions.ts b/plugins/permissions/src/messages/reactions.ts new file mode 100644 index 000000000..7ec78dd99 --- /dev/null +++ b/plugins/permissions/src/messages/reactions.ts @@ -0,0 +1,92 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export function addReaction(bot: BotWithCache) { + const addReactionOld = bot.helpers.addReaction; + + bot.helpers.addReaction = function (channelId, messageId, reaction) { + requireBotChannelPermissions(bot, channelId, [ + "READ_MESSAGE_HISTORY", + "ADD_REACTIONS", + ]); + + return addReactionOld(channelId, messageId, reaction); + }; +} + +export function addReactions(bot: BotWithCache) { + const addReactionsOld = bot.helpers.addReactions; + + bot.helpers.addReactions = function ( + channelId, + messageId, + reactions, + ordered, + ) { + requireBotChannelPermissions(bot, channelId, [ + "READ_MESSAGE_HISTORY", + "ADD_REACTIONS", + ]); + + return addReactionsOld(channelId, messageId, reactions, ordered); + }; +} + +export function removeReaction(bot: BotWithCache) { + const removeReactionOld = bot.helpers.removeReaction; + + bot.helpers.removeReaction = function ( + channelId, + messageId, + reactions, + options, + ) { + // IF REMOVING OTHER USER PERMS MANAGE MESSAGES IS REQUIRED + if (options?.userId) { + requireBotChannelPermissions(bot, channelId, [ + "MANAGE_MESSAGES", + ]); + } + + return removeReactionOld(channelId, messageId, reactions, options); + }; +} + +export function removeAllReactions(bot: BotWithCache) { + const removeAllReactionsOld = bot.helpers.removeAllReactions; + + bot.helpers.removeAllReactions = function ( + channelId, + messageId, + ) { + requireBotChannelPermissions(bot, channelId, [ + "MANAGE_MESSAGES", + ]); + + return removeAllReactionsOld(channelId, messageId); + }; +} + +export function removeReactionEmoji(bot: BotWithCache) { + const removeReactionEmojiOld = bot.helpers.removeReactionEmoji; + + bot.helpers.removeReactionEmoji = function ( + channelId, + messageId, + reaction, + ) { + requireBotChannelPermissions(bot, channelId, [ + "MANAGE_MESSAGES", + ]); + + return removeReactionEmojiOld(channelId, messageId, reaction); + }; +} + +export default function setupReactionsPermChecks(bot: BotWithCache) { + addReaction(bot); + addReactions(bot); + removeReaction(bot); + removeAllReactions(bot); + removeReactionEmoji(bot); +} diff --git a/plugins/permissions/src/misc/mod.ts b/plugins/permissions/src/misc/mod.ts new file mode 100644 index 000000000..68a31f3dc --- /dev/null +++ b/plugins/permissions/src/misc/mod.ts @@ -0,0 +1,45 @@ +import { BotWithCache } from "../../deps.ts"; + +export function editBotProfile(bot: BotWithCache) { + const editBotProfileOld = bot.helpers.editBotProfile; + + bot.helpers.editBotProfile = function ( + options, + ) { + // Nothing was edited + if (!options.username && options.botAvatarURL === undefined) { + throw new Error( + "There was no change to the username or avatar found in the request.", + ); + } + // Check username requirements if username was provided + if (options.username) { + if (options.username.length > 32) { + throw new Error( + "A username for the bot must be less than 32 characters.", + ); + } + if (options.username.length < 2) { + throw new Error( + "A username for the bot can not be less than 2 characters.", + ); + } + if ( + ["@", "#", ":", "```"].some((char) => options.username!.includes(char)) + ) { + throw new Error("A bot username can not include @ # : or ```"); + } + if (["discordtag", "everyone", "here"].includes(options.username)) { + throw new Error( + "A bot username can not be set to `discordtag` `everyone` and `here`", + ); + } + } + + return editBotProfileOld(options); + }; +} + +export default function setupMiscPermChecks(bot: BotWithCache) { + editBotProfile(bot); +} diff --git a/plugins/permissions/src/permissions.ts b/plugins/permissions/src/permissions.ts new file mode 100644 index 000000000..f5a165f5b --- /dev/null +++ b/plugins/permissions/src/permissions.ts @@ -0,0 +1,450 @@ +import { + BitwisePermissionFlags, + BotWithCache, + DiscordenoChannel, + DiscordenoGuild, + DiscordenoMember, + DiscordenoRole, + Errors, + Overwrite, + PermissionStrings, + separateOverwrites, +} from "../deps.ts"; + +/** Calculates the permissions this member has in the given guild */ +export function calculateBasePermissions( + bot: BotWithCache, + guildOrId: bigint | DiscordenoGuild, + memberOrId: bigint | DiscordenoMember, +) { + const guild = typeof guildOrId === "bigint" + ? bot.guilds.get(guildOrId) + : guildOrId; + const member = typeof memberOrId === "bigint" + ? bot.members.get(memberOrId) + : memberOrId; + + if (!guild || !member) return 8n; + + let permissions = 0n; + // Calculate the role permissions bits, @everyone role is not in memberRoleIds so we need to pass guildId manualy + permissions |= [...member.roles, guild.id] + .map((id) => guild.roles.get(id)?.permissions) + // Removes any edge case undefined + .filter((perm) => perm) + .reduce((bits, perms) => { + bits! |= perms!; + return bits; + }, 0n) || 0n; + + // If the memberId is equal to the guild ownerId he automatically has every permission so we add ADMINISTRATOR permission + if (guild.ownerId === member.id) permissions |= 8n; + // Return the members permission bits as a string + return permissions; +} + +/** Calculates the permissions this member has for the given Channel */ +export function calculateChannelOverwrites( + bot: BotWithCache, + channelOrId: bigint | DiscordenoChannel, + memberOrId: bigint | DiscordenoMember, +) { + const channel = typeof channelOrId === "bigint" + ? bot.channels.get(channelOrId) + : channelOrId; + + // This is a DM channel so return ADMINISTRATOR permission + if (!channel?.guildId) return 8n; + + const member = typeof memberOrId === "bigint" + ? bot.members.get(memberOrId) + : memberOrId; + + if (!channel || !member) return 8n; + + // Get all the role permissions this member already has + let permissions = calculateBasePermissions( + bot, + channel.guildId, + member, + ); + + // First calculate @everyone overwrites since these have the lowest priority + const overwriteEveryone = channel.permissionOverwrites?.find((overwrite) => { + const [_, id] = separateOverwrites(overwrite); + return id === channel.guildId; + }); + if (overwriteEveryone) { + const [_type, _id, allow, deny] = separateOverwrites(overwriteEveryone); + // First remove denied permissions since denied < allowed + permissions &= ~deny; + permissions |= allow; + } + + const overwrites = channel.permissionOverwrites; + + // In order to calculate the role permissions correctly we need to temporarily save the allowed and denied permissions + let allow = 0n; + let deny = 0n; + const memberRoles = member.roles || []; + // Second calculate members role overwrites since these have middle priority + for (const overwrite of overwrites || []) { + const [_type, id, allowBits, denyBits] = separateOverwrites(overwrite); + + if (!memberRoles.includes(id)) continue; + + deny |= denyBits; + allow |= allowBits; + } + // After role overwrite calculate save allowed permissions first we remove denied permissions since "denied < allowed" + permissions &= ~deny; + permissions |= allow; + + // Third calculate member specific overwrites since these have the highest priority + const overwriteMember = overwrites?.find((overwrite) => { + const [_, id] = separateOverwrites(overwrite); + return id === member.id; + }); + if (overwriteMember) { + const [_type, _id, allowBits, denyBits] = separateOverwrites( + overwriteMember, + ); + + permissions &= ~denyBits; + permissions |= allowBits; + } + + return permissions; +} + +/** Checks if the given permission bits are matching the given permissions. `ADMINISTRATOR` always returns `true` */ +export function validatePermissions( + permissionBits: bigint, + permissions: PermissionStrings[], +) { + if (permissionBits & 8n) return true; + + return permissions.every( + (permission) => + // Check if permission is in permissionBits + permissionBits & BigInt(BitwisePermissionFlags[permission]), + ); +} + +/** Checks if the given member has these permissions in the given guild */ +export function hasGuildPermissions( + bot: BotWithCache, + guild: bigint | DiscordenoGuild, + member: bigint | DiscordenoMember, + permissions: PermissionStrings[], +) { + // First we need the role permission bits this member has + const basePermissions = calculateBasePermissions( + bot, + guild, + member, + ); + // Second use the validatePermissions function to check if the member has every permission + return validatePermissions(basePermissions, permissions); +} + +/** Checks if the bot has these permissions in the given guild */ +export function botHasGuildPermissions( + bot: BotWithCache, + guild: bigint | DiscordenoGuild, + permissions: PermissionStrings[], +) { + // Since Bot is a normal member we can use the hasRolePermissions() function + return hasGuildPermissions(bot, guild, bot.id, permissions); +} + +/** Checks if the given member has these permissions for the given channel */ +export function hasChannelPermissions( + bot: BotWithCache, + channel: bigint | DiscordenoChannel, + member: bigint | DiscordenoMember, + permissions: PermissionStrings[], +) { + // First we need the overwrite bits this member has + const channelOverwrites = calculateChannelOverwrites( + bot, + channel, + member, + ); + // Second use the validatePermissions function to check if the member has every permission + return validatePermissions(channelOverwrites, permissions); +} + +/** Checks if the bot has these permissions f0r the given channel */ +export function botHasChannelPermissions( + bot: BotWithCache, + channel: bigint | DiscordenoChannel, + permissions: PermissionStrings[], +) { + // Since Bot is a normal member we can use the hasRolePermissions() function + return hasChannelPermissions(bot, channel, bot.id, permissions); +} + +/** Returns the permissions that are not in the given permissionBits */ +export function missingPermissions( + permissionBits: bigint, + permissions: PermissionStrings[], +) { + if (permissionBits & 8n) return []; + + return permissions.filter((permission) => + !(permissionBits & BigInt(BitwisePermissionFlags[permission])) + ); +} + +/** Get the missing Guild permissions this member has */ +export function getMissingGuildPermissions( + bot: BotWithCache, + guild: bigint | DiscordenoGuild, + member: bigint | DiscordenoMember, + permissions: PermissionStrings[], +) { + // First we need the role permission bits this member has + const permissionBits = calculateBasePermissions( + bot, + guild, + member, + ); + // Second return the members missing permissions + return missingPermissions(permissionBits, permissions); +} + +/** Get the missing Channel permissions this member has */ +export function getMissingChannelPermissions( + bot: BotWithCache, + channel: bigint | DiscordenoChannel, + member: bigint | DiscordenoMember, + permissions: PermissionStrings[], +) { + // First we need the role permissino bits this member has + const permissionBits = calculateChannelOverwrites( + bot, + channel, + member, + ); + // Second returnn the members missing permissions + return missingPermissions(permissionBits, permissions); +} + +/** Throws an error if this member has not all of the given permissions */ +export function requireGuildPermissions( + bot: BotWithCache, + guild: bigint | DiscordenoGuild, + member: bigint | DiscordenoMember, + permissions: PermissionStrings[], +) { + const missing = getMissingGuildPermissions( + bot, + guild, + member, + permissions, + ); + if (missing.length) { + // If the member is missing a permission throw an Error + throw new Error(`Missing Permissions: ${missing.join(" & ")}`); + } +} + +/** Throws an error if the bot does not have all permissions */ +export function requireBotGuildPermissions( + bot: BotWithCache, + guild: bigint | DiscordenoGuild, + permissions: PermissionStrings[], +) { + // Since Bot is a normal member we can use the throwOnMissingGuildPermission() function + return requireGuildPermissions(bot, guild, bot.id, permissions); +} + +/** Throws an error if this member has not all of the given permissions */ +export function requireChannelPermissions( + bot: BotWithCache, + channel: bigint | DiscordenoChannel, + member: bigint | DiscordenoMember, + permissions: PermissionStrings[], +) { + const missing = getMissingChannelPermissions( + bot, + channel, + member, + permissions, + ); + if (missing.length) { + // If the member is missing a permission throw an Error + throw new Error(`Missing Permissions: ${missing.join(" & ")}`); + } +} + +/** Throws an error if the bot has not all of the given channel permissions */ +export function requireBotChannelPermissions( + bot: BotWithCache, + channel: bigint | DiscordenoChannel, + permissions: PermissionStrings[], +) { + // Since Bot is a normal member we can use the throwOnMissingChannelPermission() function + return requireChannelPermissions(bot, channel, bot.id, permissions); +} + +/** This function converts a bitwise string to permission strings */ +export function calculatePermissions(permissionBits: bigint) { + return Object.keys(BitwisePermissionFlags).filter((permission) => { + // Since Object.keys() not only returns the permission names but also the bit values we need to return false if it is a Number + if (Number(permission)) return false; + // Check if permissionBits has this permission + return permissionBits & + BigInt(BitwisePermissionFlags[permission as PermissionStrings]); + }) as PermissionStrings[]; +} + +/** This function converts an array of permissions into the bitwise string. */ +export function calculateBits(permissions: PermissionStrings[]) { + return permissions + .reduce((bits, perm) => { + bits |= BigInt(BitwisePermissionFlags[perm]); + return bits; + }, 0n) + .toString(); +} + +/** Internal function to check if the bot has the permissions to set these overwrites */ +export function requireOverwritePermissions( + bot: BotWithCache, + guildOrId: bigint | DiscordenoGuild, + overwrites: Overwrite[], +) { + let requiredPerms: Set = new Set(["MANAGE_CHANNELS"]); + + overwrites?.forEach((overwrite) => { + if (overwrite.allow) { + overwrite.allow.forEach(requiredPerms.add, requiredPerms); + } + if (overwrite.deny) { + overwrite.deny.forEach(requiredPerms.add, requiredPerms); + } + }); + + // MANAGE_ROLES permission can only be set by administrators + if (requiredPerms.has("MANAGE_ROLES")) { + requiredPerms = new Set(["ADMINISTRATOR"]); + } + + requireGuildPermissions(bot, guildOrId, bot.id, [ + ...requiredPerms, + ]); +} + +/** Gets the highest role from the member in this guild */ +export function highestRole( + bot: BotWithCache, + guildOrId: bigint | DiscordenoGuild, + memberOrId: bigint | DiscordenoMember, +) { + const guild = typeof guildOrId === "bigint" + ? bot.guilds.get(guildOrId) + : guildOrId; + if (!guild) throw new Error(Errors.GUILD_NOT_FOUND); + + // Get the roles from the member + const memberRoles = + (typeof memberOrId === "bigint" ? bot.members.get(memberOrId) : memberOrId) + ?.roles; + // This member has no roles so the highest one is the @everyone role + if (!memberRoles) return guild.roles.get(guild.id)!; + + let memberHighestRole: DiscordenoRole | undefined; + + for (const roleId of memberRoles) { + const role = guild.roles.get(roleId); + // Rare edge case handling if undefined + if (!role) continue; + + // If memberHighestRole is still undefined we want to assign the role, + // else we want to check if the current role position is higher than the current memberHighestRole + if ( + !memberHighestRole || + memberHighestRole.position < role.position || + memberHighestRole.position === role.position + ) { + memberHighestRole = role; + } + } + + // The member has at least one role so memberHighestRole must exist + return memberHighestRole!; +} + +/** Checks if the first role is higher than the second role */ +export function higherRolePosition( + bot: BotWithCache, + guildOrId: bigint | DiscordenoGuild, + roleId: bigint, + otherRoleId: bigint, +) { + const guild = typeof guildOrId === "bigint" ? bot.guilds.get(guildOrId) : guildOrId; + if (!guild) return true; + + const role = guild.roles.get(roleId); + const otherRole = guild.roles.get(otherRoleId); + if (!role || !otherRole) throw new Error(Errors.ROLE_NOT_FOUND); + + // Rare edge case handling + if (role.position === otherRole.position) { + return role.id < otherRole.id; + } + + return role.position > otherRole.position; +} + +/** Checks if the member has a higher position than the given role */ +export function isHigherPosition( + bot: BotWithCache, + guildOrId: bigint | DiscordenoGuild, + memberId: bigint, + compareRoleId: bigint, +) { + const guild = typeof guildOrId === "bigint" ? bot.guilds.get(guildOrId) : guildOrId; + + if (!guild || guild.ownerId === memberId) return true; + + const memberHighestRole = highestRole(bot, guild, memberId); + return higherRolePosition( + bot, + guild.id, + memberHighestRole.id, + compareRoleId, + ); +} + +/** Checks if a channel overwrite for a user id or a role id has permission in this channel */ +export function channelOverwriteHasPermission( + guildId: bigint, + id: bigint, + overwrites: bigint[], + permissions: PermissionStrings[] +) { + const overwrite = + overwrites.find((perm) => { + const [_, bitID] = separateOverwrites(perm); + return id === bitID; + }) || + overwrites.find((perm) => { + const [_, bitID] = separateOverwrites(perm); + return bitID === guildId; + }); + + if (!overwrite) return false; + + return permissions.every((perm) => { + const [_type, _id, allowBits, denyBits] = separateOverwrites(overwrite); + if (BigInt(denyBits) & BigInt(BitwisePermissionFlags[perm])) { + return false; + } + if (BigInt(allowBits) & BigInt(BitwisePermissionFlags[perm])) { + return true; + } + }); +} diff --git a/plugins/permissions/src/roles/add.ts b/plugins/permissions/src/roles/add.ts new file mode 100644 index 000000000..e0f16572e --- /dev/null +++ b/plugins/permissions/src/roles/add.ts @@ -0,0 +1,32 @@ +import { BotWithCache } from "../../deps.ts"; +import { higherRolePosition } from "../permissions.ts"; +import { highestRole, requireBotGuildPermissions } from "../permissions.ts"; + +export default function addRole(bot: BotWithCache) { + const addRoleOld = bot.helpers.addRole; + + bot.helpers.addRole = function ( + guildId, + memberId, + roleId, + reason, + ) { + const guild = bot.guilds.get(guildId); + if (guild) { + const role = guild.roles.get(roleId); + if (role) { + const botRole = highestRole(bot, guild, bot.id); + + if (!higherRolePosition(bot, guild, botRole.id, role.id)) { + throw new Error( + `The bot can not add this role to the member because it does not have a role higher than the role ID: ${role.id}.`, + ); + } + } + + requireBotGuildPermissions(bot, guild, ["MANAGE_ROLES"]); + } + + return addRoleOld(guildId, memberId, roleId, reason); + }; +} diff --git a/plugins/permissions/src/roles/create.ts b/plugins/permissions/src/roles/create.ts new file mode 100644 index 000000000..e66fcf257 --- /dev/null +++ b/plugins/permissions/src/roles/create.ts @@ -0,0 +1,16 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function createRole(bot: BotWithCache) { + const createRoleOld = bot.helpers.createRole; + + bot.helpers.createRole = function ( + guildId, + options, + reason, + ) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_ROLES"]); + + return createRoleOld(guildId, options, reason); + }; +} diff --git a/plugins/permissions/src/roles/delete.ts b/plugins/permissions/src/roles/delete.ts new file mode 100644 index 000000000..896d33bef --- /dev/null +++ b/plugins/permissions/src/roles/delete.ts @@ -0,0 +1,15 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotGuildPermissions } from "../permissions.ts"; + +export default function deleteRole(bot: BotWithCache) { + const deleteRoleOld = bot.helpers.deleteRole; + + bot.helpers.deleteRole = function ( + guildId, + id, + ) { + requireBotGuildPermissions(bot, guildId, ["MANAGE_ROLES"]); + + return deleteRoleOld(guildId, id); + }; +} diff --git a/plugins/permissions/src/roles/edit.ts b/plugins/permissions/src/roles/edit.ts new file mode 100644 index 000000000..ab4034b08 --- /dev/null +++ b/plugins/permissions/src/roles/edit.ts @@ -0,0 +1,31 @@ +import { BotWithCache } from "../../deps.ts"; +import { higherRolePosition } from "../permissions.ts"; +import { highestRole, requireBotGuildPermissions } from "../permissions.ts"; + +export default function editRole(bot: BotWithCache) { + const editRoleOld = bot.helpers.editRole; + + bot.helpers.editRole = function ( + guildId, + id, + options, + ) { + const guild = bot.guilds.get(guildId); + if (guild) { + const role = guild.roles.get(id); + if (role) { + const botRole = highestRole(bot, guild, bot.id); + + if (!higherRolePosition(bot, guild, botRole.id, role.id)) { + throw new Error( + `The bot can not add this role to the member because it does not have a role higher than the role ID: ${role.id}.`, + ); + } + } + + requireBotGuildPermissions(bot, guild, ["MANAGE_ROLES"]); + } + + return editRoleOld(guildId, id, options); + }; +} diff --git a/plugins/permissions/src/roles/mod.ts b/plugins/permissions/src/roles/mod.ts new file mode 100644 index 000000000..a279d5e48 --- /dev/null +++ b/plugins/permissions/src/roles/mod.ts @@ -0,0 +1,14 @@ +import { BotWithCache } from "../../deps.ts"; +import addRole from "./add.ts"; +import createRole from "./create.ts"; +import deleteRole from "./delete.ts"; +import editRole from "./edit.ts"; +import removeRole from "./remove.ts"; + +export default function setupRolePermChecks(bot: BotWithCache) { + addRole(bot); + createRole(bot); + deleteRole(bot); + editRole(bot); + removeRole(bot); +} diff --git a/plugins/permissions/src/roles/remove.ts b/plugins/permissions/src/roles/remove.ts new file mode 100644 index 000000000..d85a6af80 --- /dev/null +++ b/plugins/permissions/src/roles/remove.ts @@ -0,0 +1,32 @@ +import { BotWithCache } from "../../deps.ts"; +import { higherRolePosition } from "../permissions.ts"; +import { highestRole, requireBotGuildPermissions } from "../permissions.ts"; + +export default function removeRole(bot: BotWithCache) { + const removeRoleOld = bot.helpers.removeRole; + + bot.helpers.removeRole = function ( + guildId, + memberId, + roleId, + reason, + ) { + const guild = bot.guilds.get(guildId); + if (guild) { + const role = guild.roles.get(roleId); + if (role) { + const botRole = highestRole(bot, guild, bot.id); + + if (!higherRolePosition(bot, guild, botRole.id, role.id)) { + throw new Error( + `The bot can not add this role to the member because it does not have a role higher than the role ID: ${role.id}.`, + ); + } + } + + requireBotGuildPermissions(bot, guild, ["MANAGE_ROLES"]); + } + + return removeRoleOld(guildId, memberId, roleId, reason); + }; +} diff --git a/plugins/permissions/src/webhooks/createWebhook.ts b/plugins/permissions/src/webhooks/createWebhook.ts new file mode 100644 index 000000000..95a107791 --- /dev/null +++ b/plugins/permissions/src/webhooks/createWebhook.ts @@ -0,0 +1,22 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export default function createWebhook(bot: BotWithCache) { + const createWebhookOld = bot.helpers.createWebhook; + + bot.helpers.createWebhook = function (channelId, options) { + requireBotChannelPermissions(bot, channelId, ["MANAGE_WEBHOOKS"]); + + if ( + // Specific usernames that discord does not allow + options.name === "clyde" || + !bot.utils.validateLength(options.name, { min: 2, max: 32 }) + ) { + throw new Error( + "The webhook name can not be clyde and it must be between 2 and 32 characters long.", + ); + } + + return createWebhookOld(channelId, options); + }; +} diff --git a/plugins/permissions/src/webhooks/deleteWebhook.ts b/plugins/permissions/src/webhooks/deleteWebhook.ts new file mode 100644 index 000000000..1ba0cf19b --- /dev/null +++ b/plugins/permissions/src/webhooks/deleteWebhook.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export default function deleteWebhook(bot: BotWithCache) { + const deleteWebhookOld = bot.helpers.deleteWebhook; + + bot.helpers.deleteWebhook = function (channelId, options) { + requireBotChannelPermissions(bot, channelId, ["MANAGE_WEBHOOKS"]); + + return deleteWebhookOld(channelId, options); + }; +} diff --git a/plugins/permissions/src/webhooks/editWebhook.ts b/plugins/permissions/src/webhooks/editWebhook.ts new file mode 100644 index 000000000..75c055aee --- /dev/null +++ b/plugins/permissions/src/webhooks/editWebhook.ts @@ -0,0 +1,23 @@ +import { BotWithCache } from "../../deps.ts"; +import { requireBotChannelPermissions } from "../permissions.ts"; + +export default function editWebhook(bot: BotWithCache) { + const editWebhookOld = bot.helpers.editWebhook; + + bot.helpers.editWebhook = function (channelId, webhookId, options) { + requireBotChannelPermissions(bot, channelId, ["MANAGE_WEBHOOKS"]); + if (options.name) { + if ( + // Specific usernames that discord does not allow + options.name === "clyde" || + !bot.utils.validateLength(options.name, { min: 2, max: 32 }) + ) { + throw new Error( + "The webhook name can not be clyde and it must be between 2 and 32 characters long.", + ); + } + } + + return editWebhookOld(channelId, webhookId, options); + }; +} diff --git a/plugins/permissions/src/webhooks/message.ts b/plugins/permissions/src/webhooks/message.ts new file mode 100644 index 000000000..0eb975942 --- /dev/null +++ b/plugins/permissions/src/webhooks/message.ts @@ -0,0 +1,71 @@ +import { AllowedMentionsTypes, BotWithCache } from "../../deps.ts"; +import { validateComponents } from "../components.ts"; + +export function editWebhookMessage(bot: BotWithCache) { + const editWebhookMessageOld = bot.helpers.editWebhookMessage; + + bot.helpers.editWebhookMessage = function ( + webhookId, + webhookToken, + options, + ) { + if ( + options.content && + !bot.utils.validateLength(options.content, { max: 2000 }) + ) { + throw Error("The content can not exceed 2000 characters."); + } + + if (options.embeds && options.embeds.length > 10) { + options.embeds.splice(10); + } + + if (options.allowedMentions) { + if (options.allowedMentions.users?.length) { + if ( + options.allowedMentions.parse?.includes( + AllowedMentionsTypes.UserMentions, + ) + ) { + options.allowedMentions.parse = options.allowedMentions.parse.filter(( + p, + ) => p !== "users"); + } + + if (options.allowedMentions.users.length > 100) { + options.allowedMentions.users = options.allowedMentions.users.slice( + 0, + 100, + ); + } + } + + if (options.allowedMentions.roles?.length) { + if ( + options.allowedMentions.parse?.includes( + AllowedMentionsTypes.RoleMentions, + ) + ) { + options.allowedMentions.parse = options.allowedMentions.parse.filter(( + p, + ) => p !== "roles"); + } + + if (options.allowedMentions.roles.length > 100) { + options.allowedMentions.roles = options.allowedMentions.roles.slice( + 0, + 100, + ); + } + } + } + + if (options.components) validateComponents(bot, options.components); + + return editWebhookMessageOld(webhookId, webhookToken, options); + }; +} + +export default function setupMessageWebhookPermChecks(bot: BotWithCache) { + editWebhookMessage(bot); +} diff --git a/plugins/permissions/src/webhooks/mod.ts b/plugins/permissions/src/webhooks/mod.ts new file mode 100644 index 000000000..35ba01bc0 --- /dev/null +++ b/plugins/permissions/src/webhooks/mod.ts @@ -0,0 +1,12 @@ +import { BotWithCache } from "../../deps.ts"; +import createWebhook from "./createWebhook.ts"; +import deleteWebhook from "./deleteWebhook.ts"; +import editWebhook from "./editWebhook.ts"; +import setupMessageWebhookPermChecks from "./message.ts"; + +export default function setupWebhooksPermChecks(bot: BotWithCache) { + createWebhook(bot); + deleteWebhook(bot); + editWebhook(bot); + setupMessageWebhookPermChecks(bot); +} diff --git a/plugins/permissions/src/webhooks/sendWebhook.ts b/plugins/permissions/src/webhooks/sendWebhook.ts new file mode 100644 index 000000000..4078f521c --- /dev/null +++ b/plugins/permissions/src/webhooks/sendWebhook.ts @@ -0,0 +1,67 @@ +import { AllowedMentionsTypes, BotWithCache } from "../../deps.ts"; +import { validateComponents } from "../components.ts"; + +export default function sendWebhook(bot: BotWithCache) { + const sendWebhookOld = bot.helpers.sendWebhook; + + bot.helpers.sendWebhook = function (webhookId, webhookToken, options) { + if ( + options.content && + !bot.utils.validateLength(options.content, { max: 2000 }) + ) { + throw new Error("The content should not exceed 2000 characters."); + } + + if (options.allowedMentions) { + if (options.allowedMentions.users?.length) { + if ( + options.allowedMentions.parse?.includes( + AllowedMentionsTypes.UserMentions, + ) + ) { + options.allowedMentions.parse = options.allowedMentions.parse.filter(( + p, + ) => p !== "users"); + } + + if (options.allowedMentions.users.length > 100) { + options.allowedMentions.users = options.allowedMentions.users.slice( + 0, + 100, + ); + } + } + + if (options.allowedMentions.roles?.length) { + if ( + options.allowedMentions.parse?.includes( + AllowedMentionsTypes.RoleMentions, + ) + ) { + options.allowedMentions.parse = options.allowedMentions.parse.filter(( + p, + ) => p !== "roles"); + } + + if (options.allowedMentions.roles.length > 100) { + options.allowedMentions.roles = options.allowedMentions.roles.slice( + 0, + 100, + ); + } + } + } + + if (options.components) { + validateComponents(bot, options.components); + } + + if (!options.content && !options.file && !options.embeds) { + throw new Error( + "You must provide a value for at least one of content, embeds, or file.", + ); + } + + return sendWebhookOld(webhookId, webhookToken, options); + }; +} From 886021e751122529bd407a4ef0ae96ca193f91ad Mon Sep 17 00:00:00 2001 From: ITOH Date: Wed, 26 Jan 2022 19:04:03 +0100 Subject: [PATCH 2/2] remove the sublicensing --- plugins/cache/LICENSE | 201 ------------------------------------ plugins/fileloader/LICENSE | 201 ------------------------------------ plugins/helpers/LICENSE | 201 ------------------------------------ plugins/permissions/LICENSE | 201 ------------------------------------ 4 files changed, 804 deletions(-) delete mode 100644 plugins/cache/LICENSE delete mode 100644 plugins/fileloader/LICENSE delete mode 100644 plugins/helpers/LICENSE delete mode 100644 plugins/permissions/LICENSE diff --git a/plugins/cache/LICENSE b/plugins/cache/LICENSE deleted file mode 100644 index 80a84a261..000000000 --- a/plugins/cache/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 - 2022 Discordeno - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/fileloader/LICENSE b/plugins/fileloader/LICENSE deleted file mode 100644 index 80a84a261..000000000 --- a/plugins/fileloader/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 - 2022 Discordeno - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/helpers/LICENSE b/plugins/helpers/LICENSE deleted file mode 100644 index 049d614f1..000000000 --- a/plugins/helpers/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright Copyright 2021 - 2022 Discordeno - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/plugins/permissions/LICENSE b/plugins/permissions/LICENSE deleted file mode 100644 index 80a84a261..000000000 --- a/plugins/permissions/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2021 - 2022 Discordeno - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.