mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-05-25 02:25:01 +00:00
Merge commit '660f8e3888
' as 'patches'
This commit is contained in:
commit
07e03ffc96
9 changed files with 1841 additions and 0 deletions
104
patches/.gitignore
vendored
Normal file
104
patches/.gitignore
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
3
patches/.gitmodules
vendored
Normal file
3
patches/.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "Zeppelin"]
|
||||
path = Zeppelin
|
||||
url = https://github.com/ZeppelinBot/Zeppelin
|
21
patches/LICENSE
Normal file
21
patches/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Zeppelin Hangar
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
5
patches/README.md
Normal file
5
patches/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# community-patch
|
||||
|
||||
List of community-made patches for self-hosting.
|
||||
|
||||
Check out our [guide page](https://zepdocs.i0.tf/guides/patch/intro) for more information.
|
1
patches/Zeppelin
Submodule
1
patches/Zeppelin
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 8445c37f6410683b7b1b53811cf49eee61b9068a
|
792
patches/patches/0001-creating-select-menu-role.patch
Normal file
792
patches/patches/0001-creating-select-menu-role.patch
Normal file
|
@ -0,0 +1,792 @@
|
|||
From 1f18cfe51543c8990a0921aa31a4f548959fa032 Mon Sep 17 00:00:00 2001
|
||||
From: Jernik <info@jernik.me>
|
||||
Date: Fri, 8 Apr 2022 14:20:26 -0500
|
||||
Subject: [PATCH] creating select-menu role patch
|
||||
|
||||
---
|
||||
backend/src/data/GuildSelectRoles.ts | 58 +++++++++
|
||||
backend/src/data/entities/SelectRole.ts | 24 ++++
|
||||
.../1633452231495-CreateSelectRolesTable.ts | 49 ++++++++
|
||||
.../events/ButtonInteractionEvt.ts | 2 +-
|
||||
.../SelectMenuRoles/SelectMenuRolesPlugin.ts | 115 ++++++++++++++++++
|
||||
.../commands/PostSelectRolesCmd.ts | 71 +++++++++++
|
||||
.../events/ButtonInteractionEvt.ts | 76 ++++++++++++
|
||||
.../events/MessageDeletedEvt.ts | 13 ++
|
||||
backend/src/plugins/SelectMenuRoles/types.ts | 65 ++++++++++
|
||||
.../util/addMemberPendingRoleChange.ts | 58 +++++++++
|
||||
.../util/buttonActionHandlers.ts | 54 ++++++++
|
||||
.../util/buttonCustomIdFunctions.ts | 37 ++++++
|
||||
.../SelectMenuRoles/util/buttonMenuActions.ts | 3 +
|
||||
.../util/splitMenusIntoRows.ts | 19 +++
|
||||
backend/src/plugins/availablePlugins.ts | 2 +
|
||||
15 files changed, 645 insertions(+), 1 deletion(-)
|
||||
create mode 100644 backend/src/data/GuildSelectRoles.ts
|
||||
create mode 100644 backend/src/data/entities/SelectRole.ts
|
||||
create mode 100644 backend/src/migrations/1633452231495-CreateSelectRolesTable.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/SelectMenuRolesPlugin.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/commands/PostSelectRolesCmd.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/events/ButtonInteractionEvt.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/events/MessageDeletedEvt.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/types.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/util/addMemberPendingRoleChange.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/util/buttonActionHandlers.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/util/buttonCustomIdFunctions.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/util/buttonMenuActions.ts
|
||||
create mode 100644 backend/src/plugins/SelectMenuRoles/util/splitMenusIntoRows.ts
|
||||
|
||||
diff --git a/backend/src/data/GuildSelectRoles.ts b/backend/src/data/GuildSelectRoles.ts
|
||||
new file mode 100644
|
||||
index 00000000..c0cbff1f
|
||||
--- /dev/null
|
||||
+++ b/backend/src/data/GuildSelectRoles.ts
|
||||
@@ -0,0 +1,58 @@
|
||||
+import { getRepository, Repository } from "typeorm";
|
||||
+import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
+import { SelectRole } from "./entities/SelectRole";
|
||||
+
|
||||
+export class GuildSelectRoles extends BaseGuildRepository {
|
||||
+ private selectRoles: Repository<SelectRole>;
|
||||
+
|
||||
+ constructor(guildId) {
|
||||
+ super(guildId);
|
||||
+ this.selectRoles = getRepository(SelectRole);
|
||||
+ }
|
||||
+
|
||||
+ async getForSelectId(selectId: string) {
|
||||
+ return this.selectRoles.findOne({
|
||||
+ guild_id: this.guildId,
|
||||
+ menu_id: selectId,
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ async getAllForMessageId(messageId: string) {
|
||||
+ return this.selectRoles.find({
|
||||
+ guild_id: this.guildId,
|
||||
+ message_id: messageId,
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ async removeForButtonId(selectId: string) {
|
||||
+ return this.selectRoles.delete({
|
||||
+ guild_id: this.guildId,
|
||||
+ menu_id: selectId,
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ async removeAllForMessageId(messageId: string) {
|
||||
+ return this.selectRoles.delete({
|
||||
+ guild_id: this.guildId,
|
||||
+ message_id: messageId,
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ async getForButtonGroup(selectGroup: string) {
|
||||
+ return this.selectRoles.find({
|
||||
+ guild_id: this.guildId,
|
||||
+ menu_group: selectGroup,
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ async add(channelId: string, messageId: string, menuId: string, menuGroup: string, menuName: string) {
|
||||
+ await this.selectRoles.insert({
|
||||
+ guild_id: this.guildId,
|
||||
+ channel_id: channelId,
|
||||
+ message_id: messageId,
|
||||
+ menu_id: menuId,
|
||||
+ menu_group: menuGroup,
|
||||
+ menu_name: menuName,
|
||||
+ });
|
||||
+ }
|
||||
+}
|
||||
diff --git a/backend/src/data/entities/SelectRole.ts b/backend/src/data/entities/SelectRole.ts
|
||||
new file mode 100644
|
||||
index 00000000..0a72a4b5
|
||||
--- /dev/null
|
||||
+++ b/backend/src/data/entities/SelectRole.ts
|
||||
@@ -0,0 +1,24 @@
|
||||
+import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
+
|
||||
+@Entity("select_roles")
|
||||
+export class SelectRole {
|
||||
+ @Column()
|
||||
+ @PrimaryColumn()
|
||||
+ guild_id: string;
|
||||
+
|
||||
+ @Column()
|
||||
+ @PrimaryColumn()
|
||||
+ channel_id: string;
|
||||
+
|
||||
+ @Column()
|
||||
+ @PrimaryColumn()
|
||||
+ message_id: string;
|
||||
+
|
||||
+ @Column()
|
||||
+ @PrimaryColumn()
|
||||
+ menu_id: string;
|
||||
+
|
||||
+ @Column() menu_group: string;
|
||||
+
|
||||
+ @Column() menu_name: string;
|
||||
+}
|
||||
diff --git a/backend/src/migrations/1633452231495-CreateSelectRolesTable.ts b/backend/src/migrations/1633452231495-CreateSelectRolesTable.ts
|
||||
new file mode 100644
|
||||
index 00000000..9c6b341f
|
||||
--- /dev/null
|
||||
+++ b/backend/src/migrations/1633452231495-CreateSelectRolesTable.ts
|
||||
@@ -0,0 +1,49 @@
|
||||
+import { MigrationInterface, QueryRunner, Table } from "typeorm";
|
||||
+
|
||||
+export class CreateSelectRolesTable1633452231495 implements MigrationInterface {
|
||||
+ public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
+ await queryRunner.createTable(
|
||||
+ new Table({
|
||||
+ name: "select_roles",
|
||||
+ columns: [
|
||||
+ {
|
||||
+ name: "guild_id",
|
||||
+ type: "bigint",
|
||||
+ isPrimary: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "channel_id",
|
||||
+ type: "bigint",
|
||||
+ isPrimary: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "message_id",
|
||||
+ type: "bigint",
|
||||
+ isPrimary: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "menu_id",
|
||||
+ type: "varchar",
|
||||
+ length: "100",
|
||||
+ isPrimary: true,
|
||||
+ isUnique: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "menu_group",
|
||||
+ type: "varchar",
|
||||
+ length: "100",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "menu_name",
|
||||
+ type: "varchar",
|
||||
+ length: "100",
|
||||
+ },
|
||||
+ ],
|
||||
+ }),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
+ await queryRunner.dropTable("select_roles");
|
||||
+ }
|
||||
+}
|
||||
diff --git a/backend/src/plugins/ReactionRoles/events/ButtonInteractionEvt.ts b/backend/src/plugins/ReactionRoles/events/ButtonInteractionEvt.ts
|
||||
index 65ace443..8ff382ca 100644
|
||||
--- a/backend/src/plugins/ReactionRoles/events/ButtonInteractionEvt.ts
|
||||
+++ b/backend/src/plugins/ReactionRoles/events/ButtonInteractionEvt.ts
|
||||
@@ -18,7 +18,7 @@ export const ButtonInteractionEvt = reactionRolesEvt({
|
||||
|
||||
async listener(meta) {
|
||||
const int = meta.args.interaction;
|
||||
- if (!int.isMessageComponent()) return;
|
||||
+ if (!int.isMessageComponent() || !int.isButton()) return;
|
||||
|
||||
const cfg = meta.pluginData.config.get();
|
||||
const split = int.customId.split(BUTTON_CONTEXT_SEPARATOR);
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/SelectMenuRolesPlugin.ts b/backend/src/plugins/SelectMenuRoles/SelectMenuRolesPlugin.ts
|
||||
new file mode 100644
|
||||
index 00000000..1aa6a69b
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/SelectMenuRolesPlugin.ts
|
||||
@@ -0,0 +1,115 @@
|
||||
+import { PluginOptions } from "knub";
|
||||
+import { ConfigPreprocessorFn } from "knub/dist/config/configTypes";
|
||||
+import { GuildSelectRoles } from "src/data/GuildSelectRoles";
|
||||
+import { Queue } from "src/Queue";
|
||||
+import { isValidSnowflake } from "src/utils";
|
||||
+import { StrictValidationError } from "src/validatorUtils";
|
||||
+import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||
+import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||
+import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
+import { PostSelectRolesCmd } from "./commands/PostSelectRolesCmd";
|
||||
+import { InteractionEvt } from "./events/ButtonInteractionEvt";
|
||||
+import { ConfigSchema, SelectMenuRolesPluginType } from "./types";
|
||||
+
|
||||
+const defaultOptions: PluginOptions<SelectMenuRolesPluginType> = {
|
||||
+ config: {
|
||||
+ select_groups: {},
|
||||
+
|
||||
+ can_manage: false,
|
||||
+ },
|
||||
+
|
||||
+ overrides: [
|
||||
+ {
|
||||
+ level: ">=100",
|
||||
+ config: {
|
||||
+ can_manage: true,
|
||||
+ },
|
||||
+ },
|
||||
+ ],
|
||||
+};
|
||||
+
|
||||
+const MAXIMUM_COMPONENT_ROWS = 5;
|
||||
+
|
||||
+const configPreprocessor: ConfigPreprocessorFn<SelectMenuRolesPluginType> = (options) => {
|
||||
+ if (options.config.select_groups) {
|
||||
+ for (const [groupName, group] of Object.entries(options.config.select_groups)) {
|
||||
+ const defaultSelectMenuNames = Object.keys(group.menus);
|
||||
+ const defaultMenus = Object.values(group.menus);
|
||||
+ const menuNames = Object.keys(group.menus ?? []);
|
||||
+
|
||||
+ const defaultMenuRowCount = defaultMenus.length;
|
||||
+ if (defaultMenuRowCount > MAXIMUM_COMPONENT_ROWS || defaultMenuRowCount === 0) {
|
||||
+ throw new StrictValidationError([
|
||||
+ `Invalid row count for menus: You currently have ${defaultMenuRowCount}, the maximum is 5. A new row is started automatically after each menu.`,
|
||||
+ ]);
|
||||
+ }
|
||||
+
|
||||
+ for (let i = 0; i < defaultMenus.length; i++) {
|
||||
+ const defMenu = defaultMenus[i];
|
||||
+ if (defMenu.maxValues && (defMenu.maxValues > 25 || defMenu.maxValues < 1)) {
|
||||
+ throw new StrictValidationError([
|
||||
+ `Invalid value for menus/${defaultSelectMenuNames[i]}/maxValues: Maximum Values Must be between 1 and 25`,
|
||||
+ ]);
|
||||
+ }
|
||||
+ if (defMenu.minValues && (defMenu.minValues > 25 || defMenu.minValues < 0)) {
|
||||
+ throw new StrictValidationError([
|
||||
+ `Invalid value for menus/${defaultSelectMenuNames[i]}/maxValues: Minimum Values Must be between 0 and 25`,
|
||||
+ ]);
|
||||
+ }
|
||||
+ if (defMenu.items.length > 25 || defMenu.items.length === 0) {
|
||||
+ throw new StrictValidationError([
|
||||
+ `Invalid values for menus/${defaultSelectMenuNames[i]}/items: Must have between 1 and 25 items configured`,
|
||||
+ ]);
|
||||
+ }
|
||||
+ for (let i2 = 0; i2 < defMenu.items.length; i2++) {
|
||||
+ const item = defMenu.items[i2];
|
||||
+
|
||||
+ if (!isValidSnowflake(item.role)) {
|
||||
+ throw new StrictValidationError([
|
||||
+ `Invalid value for menus/${defaultSelectMenuNames[i]}/items/${i2}/role: ${item.role} is not a valid snowflake.`,
|
||||
+ ]);
|
||||
+ }
|
||||
+ if (!item.label && !item.emoji) {
|
||||
+ throw new StrictValidationError([
|
||||
+ `Invalid values for menus/${defaultSelectMenuNames[i]}/items/${i2}/(label|emoji): Must have label, emoji or both set for the select menu to be valid.`,
|
||||
+ ]);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return options;
|
||||
+};
|
||||
+
|
||||
+export const SelectMenuRolesPlugin = zeppelinGuildPlugin<SelectMenuRolesPluginType>()({
|
||||
+ name: "select_menu_roles",
|
||||
+ showInDocs: true,
|
||||
+ info: {
|
||||
+ prettyName: "Select menu roles",
|
||||
+ },
|
||||
+
|
||||
+ dependencies: () => [LogsPlugin],
|
||||
+ configSchema: ConfigSchema,
|
||||
+ defaultOptions,
|
||||
+
|
||||
+ // prettier-ignore
|
||||
+ commands: [
|
||||
+ PostSelectRolesCmd,
|
||||
+ ],
|
||||
+
|
||||
+ // prettier-ignore
|
||||
+ events: [
|
||||
+ InteractionEvt,
|
||||
+ ],
|
||||
+ configPreprocessor,
|
||||
+
|
||||
+ beforeLoad(pluginData) {
|
||||
+ const { state, guild } = pluginData;
|
||||
+
|
||||
+ state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id);
|
||||
+ state.selectMenus = GuildSelectRoles.getGuildInstance(guild.id);
|
||||
+ state.roleChangeQueue = new Queue();
|
||||
+ state.pendingRoleChanges = new Map();
|
||||
+ },
|
||||
+});
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/commands/PostSelectRolesCmd.ts b/backend/src/plugins/SelectMenuRoles/commands/PostSelectRolesCmd.ts
|
||||
new file mode 100644
|
||||
index 00000000..7f017436
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/commands/PostSelectRolesCmd.ts
|
||||
@@ -0,0 +1,71 @@
|
||||
+import { createHash } from "crypto";
|
||||
+import { MessageButton, MessageSelectMenu, Snowflake, MessageSelectOptionData } from "discord.js";
|
||||
+import moment from "moment";
|
||||
+import { sendErrorMessage, sendSuccessMessage } from "src/pluginUtils";
|
||||
+import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
+import { selectRolesCmd } from "../types";
|
||||
+import { splitButtonsIntoRows } from "../util/splitMenusIntoRows";
|
||||
+
|
||||
+export const PostSelectRolesCmd = selectRolesCmd({
|
||||
+ trigger: "select_roles post",
|
||||
+ permission: "can_manage",
|
||||
+
|
||||
+ signature: {
|
||||
+ channel: ct.textChannel(),
|
||||
+ selectGroup: ct.string(),
|
||||
+ },
|
||||
+
|
||||
+ async run({ message: msg, args, pluginData }) {
|
||||
+ const cfg = pluginData.config.get();
|
||||
+ if (!cfg.select_groups) {
|
||||
+ sendErrorMessage(pluginData, msg.channel, "No select menu groups defined in config");
|
||||
+ return;
|
||||
+ }
|
||||
+ const group = cfg.select_groups[args.selectGroup];
|
||||
+
|
||||
+ if (!group) {
|
||||
+ sendErrorMessage(pluginData, msg.channel, `No select menu group matches the name **${args.selectGroup}**`);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const selectMenus: MessageSelectMenu[] = [];
|
||||
+ const toInsert: Array<{ customId; selectGroup; menuName }> = [];
|
||||
+ for (const [menuName, menu] of Object.entries(group.menus)) {
|
||||
+ const customId = createHash("md5").update(`${menuName}${moment.utc().valueOf()}`).digest("hex");
|
||||
+ const opts: MessageSelectOptionData[] = [];
|
||||
+ for (const [k, item] of Object.entries(menu.items)) {
|
||||
+ opts.push({
|
||||
+ label: item.label,
|
||||
+ description: item.description ?? "",
|
||||
+ default: item.default ?? false,
|
||||
+ value: item.role,
|
||||
+ emoji: item.emoji ? pluginData.client.emojis.resolve(item.emoji as Snowflake) ?? item.emoji : undefined,
|
||||
+ });
|
||||
+ }
|
||||
+ const slm = new MessageSelectMenu()
|
||||
+ .setMinValues(menu.minValues ?? 1)
|
||||
+ .setMaxValues(menu.maxValues ?? opts.length)
|
||||
+ .setCustomId(customId)
|
||||
+ .setDisabled(menu.disabled ?? false);
|
||||
+ if (menu.placeholder) slm.setPlaceholder(menu.placeholder);
|
||||
+ slm.addOptions(...opts);
|
||||
+ selectMenus.push(slm);
|
||||
+
|
||||
+ toInsert.push({ customId, selectGroup: args.selectGroup, menuName });
|
||||
+ }
|
||||
+ const rows = splitButtonsIntoRows(selectMenus, Object.values(group.menus)); // new MessageActionRow().addComponents(buttons);
|
||||
+
|
||||
+ try {
|
||||
+ const newMsg = await args.channel.send({ content: group.message, components: rows });
|
||||
+
|
||||
+ for (const btn of toInsert) {
|
||||
+ await pluginData.state.selectMenus.add(args.channel.id, newMsg.id, btn.customId, btn.selectGroup, btn.menuName);
|
||||
+ }
|
||||
+ } catch (e) {
|
||||
+ sendErrorMessage(pluginData, msg.channel, `Error trying to post message: ${e}`);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ await sendSuccessMessage(pluginData, msg.channel, `Successfully posted message in <#${args.channel.id}>`);
|
||||
+ },
|
||||
+});
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/events/ButtonInteractionEvt.ts b/backend/src/plugins/SelectMenuRoles/events/ButtonInteractionEvt.ts
|
||||
new file mode 100644
|
||||
index 00000000..caf30c42
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/events/ButtonInteractionEvt.ts
|
||||
@@ -0,0 +1,76 @@
|
||||
+import { MessageComponentInteraction } from "discord.js";
|
||||
+import humanizeDuration from "humanize-duration";
|
||||
+import moment from "moment";
|
||||
+import { logger } from "src/logger";
|
||||
+import { LogsPlugin } from "src/plugins/Logs/LogsPlugin";
|
||||
+import { MINUTES } from "src/utils";
|
||||
+import { idToTimestamp } from "src/utils/idToTimestamp";
|
||||
+import { interactionEvt } from "../types";
|
||||
+import { handleModifyRole } from "../util/buttonActionHandlers";
|
||||
+import { MENU_CONTEXT_SEPARATOR, resolveStatefulCustomId } from "../util/buttonCustomIdFunctions";
|
||||
+import { SelectMenuActions } from "../util/buttonMenuActions";
|
||||
+
|
||||
+const INVALIDATION_TIME = 15 * MINUTES;
|
||||
+
|
||||
+export const InteractionEvt = interactionEvt({
|
||||
+ event: "interactionCreate",
|
||||
+
|
||||
+ async listener(meta) {
|
||||
+ const int = meta.args.interaction;
|
||||
+ if (!int.isMessageComponent() || !int.isSelectMenu()) return;
|
||||
+
|
||||
+ const cfg = meta.pluginData.config.get();
|
||||
+ const split = int.customId.split(MENU_CONTEXT_SEPARATOR);
|
||||
+ const context = (await resolveStatefulCustomId(meta.pluginData, int.customId)) ?? {
|
||||
+ groupName: split[0],
|
||||
+ menuName: split[1],
|
||||
+ action: split[2],
|
||||
+ stateless: true,
|
||||
+ };
|
||||
+
|
||||
+ if (context.stateless) {
|
||||
+ const timeSinceCreation = moment.utc().valueOf() - idToTimestamp(int.message.id)!;
|
||||
+ if (timeSinceCreation >= INVALIDATION_TIME) {
|
||||
+ sendEphemeralReply(
|
||||
+ int,
|
||||
+ `Sorry, but these select menus are invalid because they are older than ${humanizeDuration(
|
||||
+ INVALIDATION_TIME,
|
||||
+ )}.\nIf the menu is still available, open it again to assign yourself roles!`,
|
||||
+ );
|
||||
+ return;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ const group = cfg.select_groups[context.groupName];
|
||||
+ if (!group) {
|
||||
+ await sendEphemeralReply(int, `A configuration error was encountered, please contact the Administrators!`);
|
||||
+ meta.pluginData.getPlugin(LogsPlugin).logBotAlert({
|
||||
+ body: `**A configuration error occurred** on select menus for message ${int.message.id}, group **${context.groupName}** not found in config`,
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // Verify that detected action is known by us
|
||||
+ if (!(<any>Object).values(SelectMenuActions).includes(context.action)) {
|
||||
+ await sendEphemeralReply(int, `A internal error was encountered, please contact the Administrators!`);
|
||||
+ meta.pluginData.getPlugin(LogsPlugin).logBotAlert({
|
||||
+ body: `**A internal error occurred** on select menus for message ${int.message.id}, action **${context.action}** is not known`,
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (context.action === SelectMenuActions.MODIFY_ROLE) {
|
||||
+ await handleModifyRole(meta.pluginData, int, group, context);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ logger.warn(
|
||||
+ `Action ${context.action} on select menu ${int.customId} (Guild: ${int.guildId}, Channel: ${int.channelId}) is unknown!`,
|
||||
+ );
|
||||
+ await sendEphemeralReply(int, `A internal error was encountered, please contact the Administrators!`);
|
||||
+ },
|
||||
+});
|
||||
+
|
||||
+async function sendEphemeralReply(interaction: MessageComponentInteraction, message: string) {
|
||||
+ await interaction.reply({ content: message, ephemeral: true });
|
||||
+}
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/events/MessageDeletedEvt.ts b/backend/src/plugins/SelectMenuRoles/events/MessageDeletedEvt.ts
|
||||
new file mode 100644
|
||||
index 00000000..2acbbb23
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/events/MessageDeletedEvt.ts
|
||||
@@ -0,0 +1,13 @@
|
||||
+import { interactionEvt } from "../types";
|
||||
+
|
||||
+export const MessageDeletedEvt = interactionEvt({
|
||||
+ event: "messageDelete",
|
||||
+ allowBots: true,
|
||||
+ allowSelf: true,
|
||||
+
|
||||
+ async listener(meta) {
|
||||
+ const pluginData = meta.pluginData;
|
||||
+
|
||||
+ await pluginData.state.selectMenus.removeAllForMessageId(meta.args.message.id);
|
||||
+ },
|
||||
+});
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/types.ts b/backend/src/plugins/SelectMenuRoles/types.ts
|
||||
new file mode 100644
|
||||
index 00000000..045fe47b
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/types.ts
|
||||
@@ -0,0 +1,65 @@
|
||||
+import * as t from "io-ts";
|
||||
+import { BasePluginType, typedGuildCommand, typedGuildEventListener } from "knub";
|
||||
+import { GuildSelectRoles } from "src/data/GuildSelectRoles";
|
||||
+import { GuildSavedMessages } from "../../data/GuildSavedMessages";
|
||||
+import { Queue } from "../../Queue";
|
||||
+import { tNullable } from "../../utils";
|
||||
+
|
||||
+const SelectMenuItem = t.type({
|
||||
+ label: t.string,
|
||||
+ role: t.string,
|
||||
+ description: tNullable(t.string),
|
||||
+ emoji: tNullable(t.string),
|
||||
+ default: tNullable(t.boolean),
|
||||
+});
|
||||
+export type TSelectMenuItemOpts = t.TypeOf<typeof SelectMenuItem>;
|
||||
+
|
||||
+const SelectMenuOpts = t.type({
|
||||
+ items: t.array(SelectMenuItem),
|
||||
+ placeholder: tNullable(t.string),
|
||||
+ minValues: tNullable(t.number),
|
||||
+ maxValues: tNullable(t.number),
|
||||
+ disabled: tNullable(t.boolean),
|
||||
+});
|
||||
+export type TSelectMenuOpts = t.TypeOf<typeof SelectMenuOpts>;
|
||||
+
|
||||
+const SelectMenuPairOpts = t.type({
|
||||
+ message: t.string,
|
||||
+ menus: t.record(t.string, SelectMenuOpts),
|
||||
+});
|
||||
+export type TSelectMenuPairOpts = t.TypeOf<typeof SelectMenuPairOpts>;
|
||||
+
|
||||
+export const ConfigSchema = t.type({
|
||||
+ select_groups: t.record(t.string, SelectMenuPairOpts),
|
||||
+ can_manage: t.boolean,
|
||||
+});
|
||||
+export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
+
|
||||
+export type RoleChangeMode = "+" | "-";
|
||||
+
|
||||
+export type PendingMemberRoleChanges = {
|
||||
+ timeout: NodeJS.Timeout | null;
|
||||
+ applyFn: () => void;
|
||||
+ changes: Array<{
|
||||
+ mode: RoleChangeMode;
|
||||
+ roleId: string;
|
||||
+ }>;
|
||||
+};
|
||||
+
|
||||
+export interface SelectMenuRolesPluginType extends BasePluginType {
|
||||
+ config: TConfigSchema;
|
||||
+ state: {
|
||||
+ savedMessages: GuildSavedMessages;
|
||||
+ selectMenus: GuildSelectRoles;
|
||||
+
|
||||
+ reactionRemoveQueue: Queue;
|
||||
+ roleChangeQueue: Queue;
|
||||
+ pendingRoleChanges: Map<string, PendingMemberRoleChanges>;
|
||||
+ pendingRefreshes: Set<string>;
|
||||
+
|
||||
+ autoRefreshTimeout: NodeJS.Timeout;
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+export const interactionEvt = typedGuildEventListener<SelectMenuRolesPluginType>();
|
||||
+export const selectRolesCmd = typedGuildCommand<SelectMenuRolesPluginType>();
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/util/addMemberPendingRoleChange.ts b/backend/src/plugins/SelectMenuRoles/util/addMemberPendingRoleChange.ts
|
||||
new file mode 100644
|
||||
index 00000000..01dcb454
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/util/addMemberPendingRoleChange.ts
|
||||
@@ -0,0 +1,58 @@
|
||||
+import { Snowflake } from "discord.js";
|
||||
+import { GuildPluginData } from "knub";
|
||||
+import { logger } from "../../../logger";
|
||||
+import { resolveMember } from "../../../utils";
|
||||
+import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
+import { PendingMemberRoleChanges, SelectMenuRolesPluginType, RoleChangeMode } from "../types";
|
||||
+
|
||||
+const ROLE_CHANGE_BATCH_DEBOUNCE_TIME = 3000;
|
||||
+
|
||||
+export async function addMemberPendingRoleChange(
|
||||
+ pluginData: GuildPluginData<SelectMenuRolesPluginType>,
|
||||
+ memberId: string,
|
||||
+ mode: RoleChangeMode,
|
||||
+ roleId: string,
|
||||
+) {
|
||||
+ if (!pluginData.state.pendingRoleChanges.has(memberId)) {
|
||||
+ const newPendingRoleChangeObj: PendingMemberRoleChanges = {
|
||||
+ timeout: null,
|
||||
+ changes: [],
|
||||
+ applyFn: async () => {
|
||||
+ pluginData.state.pendingRoleChanges.delete(memberId);
|
||||
+
|
||||
+ const lock = await pluginData.locks.acquire(memberRolesLock({ id: memberId }));
|
||||
+
|
||||
+ const member = await resolveMember(pluginData.client, pluginData.guild, memberId);
|
||||
+ if (member) {
|
||||
+ const newRoleIds = new Set(member.roles.cache.keys());
|
||||
+ for (const change of newPendingRoleChangeObj.changes) {
|
||||
+ if (change.mode === "+") newRoleIds.add(change.roleId as Snowflake);
|
||||
+ else newRoleIds.delete(change.roleId as Snowflake);
|
||||
+ }
|
||||
+
|
||||
+ try {
|
||||
+ await member.edit(
|
||||
+ {
|
||||
+ roles: Array.from(newRoleIds.values()),
|
||||
+ },
|
||||
+ "Select menu roles",
|
||||
+ );
|
||||
+ } catch (e) {
|
||||
+ logger.warn(`Failed to apply role changes to ${member.user.tag} (${member.id}): ${e.message}`);
|
||||
+ }
|
||||
+ }
|
||||
+ lock.unlock();
|
||||
+ },
|
||||
+ };
|
||||
+
|
||||
+ pluginData.state.pendingRoleChanges.set(memberId, newPendingRoleChangeObj);
|
||||
+ }
|
||||
+
|
||||
+ const pendingRoleChangeObj = pluginData.state.pendingRoleChanges.get(memberId)!;
|
||||
+ pendingRoleChangeObj.changes.push({ mode, roleId });
|
||||
+ if (pendingRoleChangeObj.timeout) clearTimeout(pendingRoleChangeObj.timeout);
|
||||
+ pendingRoleChangeObj.timeout = setTimeout(
|
||||
+ () => pluginData.state.roleChangeQueue.add(pendingRoleChangeObj.applyFn),
|
||||
+ ROLE_CHANGE_BATCH_DEBOUNCE_TIME,
|
||||
+ );
|
||||
+}
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/util/buttonActionHandlers.ts b/backend/src/plugins/SelectMenuRoles/util/buttonActionHandlers.ts
|
||||
new file mode 100644
|
||||
index 00000000..4aa948a5
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/util/buttonActionHandlers.ts
|
||||
@@ -0,0 +1,54 @@
|
||||
+import { SelectMenuInteraction } from "discord.js";
|
||||
+import { GuildPluginData } from "knub";
|
||||
+import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
+import { SelectMenuRolesPluginType, TSelectMenuPairOpts } from "../types";
|
||||
+import { addMemberPendingRoleChange } from "./addMemberPendingRoleChange";
|
||||
+
|
||||
+export async function handleModifyRole(
|
||||
+ pluginData: GuildPluginData<SelectMenuRolesPluginType>,
|
||||
+ int: SelectMenuInteraction,
|
||||
+ group: TSelectMenuPairOpts,
|
||||
+ context,
|
||||
+) {
|
||||
+ const values = int.values;
|
||||
+ const menuName = context.menuName;
|
||||
+ if (!values) return;
|
||||
+ const guildRoles = await pluginData.guild.roles.fetch();
|
||||
+ for (const i in values) {
|
||||
+ if (!guildRoles.find((rl) => rl.id === values[i])) {
|
||||
+ await int.reply({
|
||||
+ content: `A configuration error was encountered, please contact the Administrators!`,
|
||||
+ ephemeral: true,
|
||||
+ });
|
||||
+ pluginData.getPlugin(LogsPlugin).logBotAlert({
|
||||
+ body: `**A configuration error occurred** on select menus for message ${int.message.id}, role **${values[i]}** not found on server`,
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ try {
|
||||
+ const member = await pluginData.guild.members.fetch(int.user.id);
|
||||
+ const configuredRoles: string[] = [];
|
||||
+ const menuGroup = group.menus[menuName];
|
||||
+ for (const key in menuGroup.items) {
|
||||
+ configuredRoles.push(menuGroup.items[key].role);
|
||||
+ }
|
||||
+ const memberRoles = Array.from(member.roles.cache.keys());
|
||||
+ const toAdd = configuredRoles.filter((rl) => values.includes(rl) && !memberRoles.includes(rl));
|
||||
+ const toRemove = configuredRoles.filter(
|
||||
+ (rl) => !toAdd.includes(rl) && memberRoles.includes(rl) && !values.includes(rl),
|
||||
+ );
|
||||
+ toAdd.forEach((r) => addMemberPendingRoleChange(pluginData, member.id, "+", r));
|
||||
+ toRemove.forEach((r) => addMemberPendingRoleChange(pluginData, member.id, "-", r));
|
||||
+ await int.deferUpdate();
|
||||
+ } catch (e) {
|
||||
+ await int.reply({
|
||||
+ content: "A configuration error was encountered, please contact the Administrators!",
|
||||
+ ephemeral: true,
|
||||
+ });
|
||||
+ pluginData.getPlugin(LogsPlugin).logBotAlert({
|
||||
+ body: `**A configuration error occurred** on select menus for message ${int.message.id}, error: ${e}. We might be missing permissions!`,
|
||||
+ });
|
||||
+ }
|
||||
+}
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/util/buttonCustomIdFunctions.ts b/backend/src/plugins/SelectMenuRoles/util/buttonCustomIdFunctions.ts
|
||||
new file mode 100644
|
||||
index 00000000..c0a4a29a
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/util/buttonCustomIdFunctions.ts
|
||||
@@ -0,0 +1,37 @@
|
||||
+import { Snowflake } from "discord.js";
|
||||
+import { GuildPluginData } from "knub";
|
||||
+import { SelectMenuRolesPluginType } from "../types";
|
||||
+import { SelectMenuActions } from "./buttonMenuActions";
|
||||
+
|
||||
+export const MENU_CONTEXT_SEPARATOR = ":rm:";
|
||||
+
|
||||
+export async function generateStatelessCustomId(
|
||||
+ pluginData: GuildPluginData<SelectMenuRolesPluginType>,
|
||||
+ groupName: string,
|
||||
+ menuName: string,
|
||||
+) {
|
||||
+ let id = groupName + MENU_CONTEXT_SEPARATOR + menuName + MENU_CONTEXT_SEPARATOR;
|
||||
+
|
||||
+ id += `${SelectMenuActions.MODIFY_ROLE}`;
|
||||
+
|
||||
+ return id;
|
||||
+}
|
||||
+
|
||||
+export async function resolveStatefulCustomId(pluginData: GuildPluginData<SelectMenuRolesPluginType>, id: string) {
|
||||
+ const menu = await pluginData.state.selectMenus.getForSelectId(id);
|
||||
+
|
||||
+ if (menu) {
|
||||
+ const group = pluginData.config.get().select_groups[menu.menu_group];
|
||||
+ if (!group) return null;
|
||||
+ const cfgButton = group.menus[menu.menu_name];
|
||||
+
|
||||
+ return {
|
||||
+ groupName: menu.menu_group,
|
||||
+ menuName: menu.menu_name,
|
||||
+ action: SelectMenuActions.MODIFY_ROLE,
|
||||
+ stateless: false,
|
||||
+ };
|
||||
+ } else {
|
||||
+ return null;
|
||||
+ }
|
||||
+}
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/util/buttonMenuActions.ts b/backend/src/plugins/SelectMenuRoles/util/buttonMenuActions.ts
|
||||
new file mode 100644
|
||||
index 00000000..d3fd4c1e
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/util/buttonMenuActions.ts
|
||||
@@ -0,0 +1,3 @@
|
||||
+export enum SelectMenuActions {
|
||||
+ MODIFY_ROLE = "grant",
|
||||
+}
|
||||
diff --git a/backend/src/plugins/SelectMenuRoles/util/splitMenusIntoRows.ts b/backend/src/plugins/SelectMenuRoles/util/splitMenusIntoRows.ts
|
||||
new file mode 100644
|
||||
index 00000000..7c0e6d71
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SelectMenuRoles/util/splitMenusIntoRows.ts
|
||||
@@ -0,0 +1,19 @@
|
||||
+import { MessageActionRow, MessageSelectMenu } from "discord.js";
|
||||
+import { TSelectMenuOpts } from "../types";
|
||||
+
|
||||
+export function splitButtonsIntoRows(
|
||||
+ actualMenus: MessageSelectMenu[],
|
||||
+ configButtons: TSelectMenuOpts[],
|
||||
+): MessageActionRow[] {
|
||||
+ const rows: MessageActionRow[] = [];
|
||||
+ let curRow = new MessageActionRow();
|
||||
+
|
||||
+ for (const item of actualMenus) {
|
||||
+ curRow.addComponents(item);
|
||||
+ rows.push(curRow);
|
||||
+ curRow = new MessageActionRow();
|
||||
+ }
|
||||
+
|
||||
+ if (curRow.components.length >= 1) rows.push(curRow);
|
||||
+ return rows;
|
||||
+}
|
||||
diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts
|
||||
index dc8adf25..f98fc40c 100644
|
||||
--- a/backend/src/plugins/availablePlugins.ts
|
||||
+++ b/backend/src/plugins/availablePlugins.ts
|
||||
@@ -24,6 +24,7 @@ import { PostPlugin } from "./Post/PostPlugin";
|
||||
import { ReactionRolesPlugin } from "./ReactionRoles/ReactionRolesPlugin";
|
||||
import { RemindersPlugin } from "./Reminders/RemindersPlugin";
|
||||
import { RolesPlugin } from "./Roles/RolesPlugin";
|
||||
+import { SelectMenuRolesPlugin } from "./SelectMenuRoles/SelectMenuRolesPlugin";
|
||||
import { SelfGrantableRolesPlugin } from "./SelfGrantableRoles/SelfGrantableRolesPlugin";
|
||||
import { SlowmodePlugin } from "./Slowmode/SlowmodePlugin";
|
||||
import { SpamPlugin } from "./Spam/SpamPlugin";
|
||||
@@ -73,6 +74,7 @@ export const guildPlugins: Array<ZeppelinGuildPluginBlueprint<any>> = [
|
||||
ContextMenuPlugin,
|
||||
PhishermanPlugin,
|
||||
InternalPosterPlugin,
|
||||
+ SelectMenuRolesPlugin
|
||||
];
|
||||
|
||||
// prettier-ignore
|
||||
--
|
||||
2.33.1.windows.1
|
||||
|
238
patches/patches/0002-timeouts-support.patch
Normal file
238
patches/patches/0002-timeouts-support.patch
Normal file
|
@ -0,0 +1,238 @@
|
|||
From 60b88cc8fab83fe79574525d987d45392581a6cb Mon Sep 17 00:00:00 2001
|
||||
From: metal <metal@i0.tf>
|
||||
Date: Tue, 13 Dec 2022 10:40:53 +0000
|
||||
Subject: [PATCH 1/4] ghetto timeouts support
|
||||
|
||||
Signed-off-by: GitHub <noreply@github.com>
|
||||
---
|
||||
.../src/plugins/Mutes/functions/clearMute.ts | 5 ++
|
||||
.../Mutes/functions/memberHasMutedRole.ts | 5 ++
|
||||
.../src/plugins/Mutes/functions/muteUser.ts | 75 +++++++++++--------
|
||||
3 files changed, 55 insertions(+), 30 deletions(-)
|
||||
|
||||
diff --git a/backend/src/plugins/Mutes/functions/clearMute.ts b/backend/src/plugins/Mutes/functions/clearMute.ts
|
||||
index e7167474..a4ca0ce1 100644
|
||||
--- a/backend/src/plugins/Mutes/functions/clearMute.ts
|
||||
+++ b/backend/src/plugins/Mutes/functions/clearMute.ts
|
||||
@@ -6,6 +6,7 @@ import { GuildPluginData } from "knub";
|
||||
import { MutesPluginType } from "../types";
|
||||
import { clearExpiringMute } from "../../../data/loops/expiringMutesLoop";
|
||||
import { GuildMember } from "discord.js";
|
||||
+import { memberHasMutedRole } from "./memberHasMutedRole";
|
||||
|
||||
export async function clearMute(
|
||||
pluginData: GuildPluginData<MutesPluginType>,
|
||||
@@ -27,6 +28,10 @@ export async function clearMute(
|
||||
const muteRole = pluginData.config.get().mute_role;
|
||||
if (muteRole) {
|
||||
await member.roles.remove(muteRole);
|
||||
+ } else {
|
||||
+ if (member.communicationDisabledUntil !== null && memberHasMutedRole(pluginData, member)) {
|
||||
+ await member.timeout(null);
|
||||
+ }
|
||||
}
|
||||
if (mute?.roles_to_restore) {
|
||||
const guildRoles = pluginData.guild.roles.cache;
|
||||
diff --git a/backend/src/plugins/Mutes/functions/memberHasMutedRole.ts b/backend/src/plugins/Mutes/functions/memberHasMutedRole.ts
|
||||
index 2790285a..a6f91763 100644
|
||||
--- a/backend/src/plugins/Mutes/functions/memberHasMutedRole.ts
|
||||
+++ b/backend/src/plugins/Mutes/functions/memberHasMutedRole.ts
|
||||
@@ -4,5 +4,10 @@ import { MutesPluginType } from "../types";
|
||||
|
||||
export function memberHasMutedRole(pluginData: GuildPluginData<MutesPluginType>, member: GuildMember): boolean {
|
||||
const muteRole = pluginData.config.get().mute_role;
|
||||
+ if (!muteRole) {
|
||||
+ if (!member.communicationDisabledUntil) return false;
|
||||
+ const diff = new Date(member.communicationDisabledUntil).getTime() - Date.now();
|
||||
+ return diff > 10 * 1000;
|
||||
+ }
|
||||
return muteRole ? member.roles.cache.has(muteRole as Snowflake) : false;
|
||||
}
|
||||
diff --git a/backend/src/plugins/Mutes/functions/muteUser.ts b/backend/src/plugins/Mutes/functions/muteUser.ts
|
||||
index 3956be8a..b622dd41 100644
|
||||
--- a/backend/src/plugins/Mutes/functions/muteUser.ts
|
||||
+++ b/backend/src/plugins/Mutes/functions/muteUser.ts
|
||||
@@ -21,6 +21,8 @@ import { MuteOptions, MutesPluginType } from "../types";
|
||||
import { Mute } from "../../../data/entities/Mute";
|
||||
import { registerExpiringMute } from "../../../data/loops/expiringMutesLoop";
|
||||
|
||||
+const TIMEOUT_MAX = 27 * 24 * 60 * 60 * 1000;
|
||||
+
|
||||
/**
|
||||
* TODO: Clean up this function
|
||||
*/
|
||||
@@ -36,11 +38,13 @@ export async function muteUser(
|
||||
const lock = await pluginData.locks.acquire(muteLock({ id: userId }));
|
||||
|
||||
const muteRole = pluginData.config.get().mute_role;
|
||||
- if (!muteRole) {
|
||||
+ /*if (!muteRole) {
|
||||
lock.unlock();
|
||||
throw new RecoverablePluginError(ERRORS.NO_MUTE_ROLE_IN_CONFIG);
|
||||
- }
|
||||
+ }*/
|
||||
|
||||
+ if (!muteRole && muteTime && muteTime > TIMEOUT_MAX) muteTime = TIMEOUT_MAX;
|
||||
+ if (!muteRole && !muteTime) muteTime = TIMEOUT_MAX;
|
||||
const timeUntilUnmute = muteTime ? humanizeDuration(muteTime) : "indefinite";
|
||||
|
||||
// No mod specified -> mark Zeppelin as the mod
|
||||
@@ -90,37 +94,48 @@ export async function muteUser(
|
||||
}
|
||||
|
||||
// Apply mute role if it's missing
|
||||
- if (!currentUserRoles.includes(muteRole as Snowflake)) {
|
||||
- try {
|
||||
- await member.roles.add(muteRole as Snowflake);
|
||||
- } catch (e) {
|
||||
- const actualMuteRole = pluginData.guild.roles.cache.get(muteRole as Snowflake);
|
||||
- if (!actualMuteRole) {
|
||||
- lock.unlock();
|
||||
- logs.logBotAlert({
|
||||
- body: `Cannot mute users, specified mute role Id is invalid`,
|
||||
- });
|
||||
- throw new RecoverablePluginError(ERRORS.INVALID_MUTE_ROLE_ID);
|
||||
- }
|
||||
+ if (muteRole) {
|
||||
+ if (!currentUserRoles.includes(muteRole as Snowflake)) {
|
||||
+ try {
|
||||
+ await member.roles.add(muteRole as Snowflake);
|
||||
+ } catch (e) {
|
||||
+ const actualMuteRole = pluginData.guild.roles.cache.get(muteRole as Snowflake);
|
||||
+ if (!actualMuteRole) {
|
||||
+ lock.unlock();
|
||||
+ logs.logBotAlert({
|
||||
+ body: `Cannot mute users, specified mute role Id is invalid`,
|
||||
+ });
|
||||
+ throw new RecoverablePluginError(ERRORS.INVALID_MUTE_ROLE_ID);
|
||||
+ }
|
||||
|
||||
- const zep = await resolveMember(pluginData.client, pluginData.guild, pluginData.client.user!.id);
|
||||
- const zepRoles = pluginData.guild.roles.cache.filter((x) => zep!.roles.cache.has(x.id));
|
||||
- // If we have roles and one of them is above the muted role, throw generic error
|
||||
- if (zepRoles.size >= 0 && zepRoles.some((zepRole) => zepRole.position > actualMuteRole.position)) {
|
||||
- lock.unlock();
|
||||
- logs.logBotAlert({
|
||||
- body: `Cannot mute user ${member.id}: ${e}`,
|
||||
- });
|
||||
- throw e;
|
||||
- } else {
|
||||
- // Otherwise, throw error that mute role is above zeps roles
|
||||
- lock.unlock();
|
||||
- logs.logBotAlert({
|
||||
- body: `Cannot mute users, specified mute role is above Zeppelin in the role hierarchy`,
|
||||
- });
|
||||
- throw new RecoverablePluginError(ERRORS.MUTE_ROLE_ABOVE_ZEP, pluginData.guild);
|
||||
+ const zep = await resolveMember(pluginData.client, pluginData.guild, pluginData.client.user!.id);
|
||||
+ const zepRoles = pluginData.guild.roles.cache.filter((x) => zep!.roles.cache.has(x.id));
|
||||
+ // If we have roles and one of them is above the muted role, throw generic error
|
||||
+ if (zepRoles.size >= 0 && zepRoles.some((zepRole) => zepRole.position > actualMuteRole.position)) {
|
||||
+ lock.unlock();
|
||||
+ logs.logBotAlert({
|
||||
+ body: `Cannot mute user ${member.id}: ${e}`,
|
||||
+ });
|
||||
+ throw e;
|
||||
+ } else {
|
||||
+ // Otherwise, throw error that mute role is above zeps roles
|
||||
+ lock.unlock();
|
||||
+ logs.logBotAlert({
|
||||
+ body: `Cannot mute users, specified mute role is above Zeppelin in the role hierarchy`,
|
||||
+ });
|
||||
+ throw new RecoverablePluginError(ERRORS.MUTE_ROLE_ABOVE_ZEP, pluginData.guild);
|
||||
+ }
|
||||
}
|
||||
}
|
||||
+ } else {
|
||||
+ try {
|
||||
+ await member.timeout(muteTime ?? TIMEOUT_MAX, reason);
|
||||
+ } catch (e) {
|
||||
+ lock.unlock();
|
||||
+ logs.logBotAlert({
|
||||
+ body: `Cannot mute users, timeout failed`,
|
||||
+ });
|
||||
+ }
|
||||
}
|
||||
|
||||
// If enabled, move the user to the mute voice channel (e.g. afk - just to apply the voice perms from the mute role)
|
||||
--
|
||||
2.38.1
|
||||
|
||||
|
||||
From 143753ee9688781cc54bb37aa1a9301cdb83b2b0 Mon Sep 17 00:00:00 2001
|
||||
From: metal <metal@i0.tf>
|
||||
Date: Sat, 16 Jul 2022 19:08:42 +0000
|
||||
Subject: [PATCH 2/4] better fix
|
||||
|
||||
Signed-off-by: GitHub <noreply@github.com>
|
||||
---
|
||||
backend/src/plugins/Mutes/MutesPlugin.ts | 1 +
|
||||
1 file changed, 1 insertion(+)
|
||||
|
||||
diff --git a/backend/src/plugins/Mutes/MutesPlugin.ts b/backend/src/plugins/Mutes/MutesPlugin.ts
|
||||
index eba6b40f..922d5e14 100644
|
||||
--- a/backend/src/plugins/Mutes/MutesPlugin.ts
|
||||
+++ b/backend/src/plugins/Mutes/MutesPlugin.ts
|
||||
@@ -92,6 +92,7 @@ export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()({
|
||||
unmuteUser: mapToPublicFn(unmuteUser),
|
||||
hasMutedRole(pluginData) {
|
||||
return (member: GuildMember) => {
|
||||
+ if (member.isCommunicationDisabled()) return true;
|
||||
const muteRole = pluginData.config.get().mute_role;
|
||||
return muteRole ? member.roles.cache.has(muteRole as Snowflake) : false;
|
||||
};
|
||||
--
|
||||
2.38.1
|
||||
|
||||
|
||||
From 23031caab6ee3d5d4da316b0a4d9fc6f43fcc359 Mon Sep 17 00:00:00 2001
|
||||
From: metal <metal@i0.tf>
|
||||
Date: Sat, 16 Jul 2022 19:04:17 +0000
|
||||
Subject: [PATCH 3/4] fix timeout unmute detection
|
||||
|
||||
Signed-off-by: GitHub <noreply@github.com>
|
||||
---
|
||||
backend/src/plugins/Mutes/MutesPlugin.ts | 1 +
|
||||
1 file changed, 1 insertion(+)
|
||||
|
||||
diff --git a/backend/src/plugins/Mutes/MutesPlugin.ts b/backend/src/plugins/Mutes/MutesPlugin.ts
|
||||
index 922d5e14..fffb4aab 100644
|
||||
--- a/backend/src/plugins/Mutes/MutesPlugin.ts
|
||||
+++ b/backend/src/plugins/Mutes/MutesPlugin.ts
|
||||
@@ -94,6 +94,7 @@ export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()({
|
||||
return (member: GuildMember) => {
|
||||
if (member.isCommunicationDisabled()) return true;
|
||||
const muteRole = pluginData.config.get().mute_role;
|
||||
+ if (member.communicationDisabledUntilTimestamp) return true;
|
||||
return muteRole ? member.roles.cache.has(muteRole as Snowflake) : false;
|
||||
};
|
||||
},
|
||||
--
|
||||
2.38.1
|
||||
|
||||
|
||||
From 5463c89400e58f64f3e3f822f10ec656f18ae8fe Mon Sep 17 00:00:00 2001
|
||||
From: metal <metal@i0.tf>
|
||||
Date: Sat, 16 Jul 2022 19:11:04 +0000
|
||||
Subject: [PATCH 4/4] attempted fix at isMuted #2
|
||||
|
||||
Signed-off-by: GitHub <noreply@github.com>
|
||||
---
|
||||
backend/src/plugins/ModActions/commands/UnmuteCmd.ts | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/backend/src/plugins/ModActions/commands/UnmuteCmd.ts b/backend/src/plugins/ModActions/commands/UnmuteCmd.ts
|
||||
index f09c2e83..e15b6b42 100644
|
||||
--- a/backend/src/plugins/ModActions/commands/UnmuteCmd.ts
|
||||
+++ b/backend/src/plugins/ModActions/commands/UnmuteCmd.ts
|
||||
@@ -44,7 +44,7 @@ export const UnmuteCmd = modActionsCmd({
|
||||
const hasMuteRole = memberToUnmute && mutesPlugin.hasMutedRole(memberToUnmute);
|
||||
|
||||
// Check if they're muted in the first place
|
||||
- if (!(await pluginData.state.mutes.isMuted(args.user)) && !hasMuteRole) {
|
||||
+ if (!(await pluginData.state.mutes.isMuted(user.id)) && !hasMuteRole) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Cannot unmute: member is not muted");
|
||||
return;
|
||||
}
|
||||
--
|
||||
2.38.1
|
||||
|
480
patches/patches/0003-Created-Social-Media-Poster-Plugin.patch
Normal file
480
patches/patches/0003-Created-Social-Media-Poster-Plugin.patch
Normal file
|
@ -0,0 +1,480 @@
|
|||
From 62ee0291b640269f227698f0aa0114b426965c6e Mon Sep 17 00:00:00 2001
|
||||
From: KK
|
||||
Date: Wed, 1 Jun 2022 07:38:29 -0500
|
||||
Subject: [PATCH] Created Social Media Poster Plugin
|
||||
|
||||
---
|
||||
backend/src/data/GuildSocialPosts.ts | 42 +++++++
|
||||
backend/src/data/entities/SocialPosts.ts | 18 +++
|
||||
.../1654041201056-CreateSocialPostsTable.ts | 40 +++++++
|
||||
.../SocialMediaPosterPlugin.ts | 77 ++++++++++++
|
||||
.../functions/pollSubreddit.ts | 110 ++++++++++++++++++
|
||||
backend/src/plugins/SocialMediaPoster/info.ts | 65 +++++++++++
|
||||
.../src/plugins/SocialMediaPoster/types.ts | 37 ++++++
|
||||
backend/src/plugins/availablePlugins.ts | 2 +
|
||||
8 files changed, 391 insertions(+)
|
||||
create mode 100644 backend/src/data/GuildSocialPosts.ts
|
||||
create mode 100644 backend/src/data/entities/SocialPosts.ts
|
||||
create mode 100644 backend/src/migrations/1654041201056-CreateSocialPostsTable.ts
|
||||
create mode 100644 backend/src/plugins/SocialMediaPoster/SocialMediaPosterPlugin.ts
|
||||
create mode 100644 backend/src/plugins/SocialMediaPoster/functions/pollSubreddit.ts
|
||||
create mode 100644 backend/src/plugins/SocialMediaPoster/info.ts
|
||||
create mode 100644 backend/src/plugins/SocialMediaPoster/types.ts
|
||||
|
||||
diff --git a/backend/src/data/GuildSocialPosts.ts b/backend/src/data/GuildSocialPosts.ts
|
||||
new file mode 100644
|
||||
index 00000000..31f7ad68
|
||||
--- /dev/null
|
||||
+++ b/backend/src/data/GuildSocialPosts.ts
|
||||
@@ -0,0 +1,42 @@
|
||||
+import { getRepository, Repository } from "typeorm";
|
||||
+import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
+import { SocialPosts } from "./entities/SocialPosts";
|
||||
+
|
||||
+export class GuildSocialPosts extends BaseGuildRepository {
|
||||
+ socialPosts: Repository<SocialPosts>;
|
||||
+
|
||||
+ constructor(guildId) {
|
||||
+ super(guildId);
|
||||
+ this.socialPosts = getRepository(SocialPosts);
|
||||
+ }
|
||||
+
|
||||
+ async getLastPost(platform: string, path: string) {
|
||||
+ const post = await this.socialPosts.findOne({
|
||||
+ guild_id: this.guildId,
|
||||
+ platform,
|
||||
+ path
|
||||
+ });
|
||||
+ return BigInt(post?.last_post ?? 0);
|
||||
+ }
|
||||
+
|
||||
+ async setLastPost(platform: string, path: string, lastPost: string) {
|
||||
+ if (await this.getLastPost(platform, path) == BigInt(0))
|
||||
+ return await this.addLastPost(platform, path, lastPost);
|
||||
+ await this.socialPosts.update({
|
||||
+ guild_id: this.guildId,
|
||||
+ platform,
|
||||
+ path
|
||||
+ }, {
|
||||
+ last_post: lastPost
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ async addLastPost(platform: string, path: string, lastPost: string) {
|
||||
+ await this.socialPosts.insert({
|
||||
+ guild_id: this.guildId,
|
||||
+ platform,
|
||||
+ path,
|
||||
+ last_post: lastPost
|
||||
+ });
|
||||
+ }
|
||||
+}
|
||||
\ No newline at end of file
|
||||
diff --git a/backend/src/data/entities/SocialPosts.ts b/backend/src/data/entities/SocialPosts.ts
|
||||
new file mode 100644
|
||||
index 00000000..5497856c
|
||||
--- /dev/null
|
||||
+++ b/backend/src/data/entities/SocialPosts.ts
|
||||
@@ -0,0 +1,18 @@
|
||||
+import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
+
|
||||
+@Entity("social_posts")
|
||||
+export class SocialPosts {
|
||||
+ @Column()
|
||||
+ @PrimaryColumn()
|
||||
+ guild_id: string;
|
||||
+
|
||||
+ @Column()
|
||||
+ platform: string;
|
||||
+
|
||||
+ @Column()
|
||||
+ path: string;
|
||||
+
|
||||
+ @Column() last_post: string;
|
||||
+
|
||||
+ // Guild, reddit, subreddit, last_post (ms)
|
||||
+}
|
||||
diff --git a/backend/src/migrations/1654041201056-CreateSocialPostsTable.ts b/backend/src/migrations/1654041201056-CreateSocialPostsTable.ts
|
||||
new file mode 100644
|
||||
index 00000000..e72b2874
|
||||
--- /dev/null
|
||||
+++ b/backend/src/migrations/1654041201056-CreateSocialPostsTable.ts
|
||||
@@ -0,0 +1,40 @@
|
||||
+import {MigrationInterface, QueryRunner, Table} from "typeorm";
|
||||
+
|
||||
+export class CreateSocialPostsTable1654041201056 implements MigrationInterface {
|
||||
+
|
||||
+ public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
+ await queryRunner.createTable(
|
||||
+ new Table({
|
||||
+ name: "social_posts",
|
||||
+ columns: [
|
||||
+ {
|
||||
+ name: "guild_id",
|
||||
+ type: "bigint",
|
||||
+ isPrimary: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "platform",
|
||||
+ type: "varchar",
|
||||
+ length: "100",
|
||||
+ isPrimary: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "path",
|
||||
+ type: "varchar",
|
||||
+ length: "100",
|
||||
+ isPrimary: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "last_post",
|
||||
+ type: "bigint",
|
||||
+ },
|
||||
+ ],
|
||||
+ }
|
||||
+ ),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
+ await queryRunner.dropTable("social_posts");
|
||||
+ }
|
||||
+}
|
||||
diff --git a/backend/src/plugins/SocialMediaPoster/SocialMediaPosterPlugin.ts b/backend/src/plugins/SocialMediaPoster/SocialMediaPosterPlugin.ts
|
||||
new file mode 100644
|
||||
index 00000000..c597116c
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SocialMediaPoster/SocialMediaPosterPlugin.ts
|
||||
@@ -0,0 +1,77 @@
|
||||
+import { GuildPluginData, PluginOptions } from "knub";
|
||||
+import { GuildLogs } from "../../data/GuildLogs";
|
||||
+import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
+import { ConfigSchema, SocialMediaPosterPluginType, TPlatformPath } from "./types";
|
||||
+import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||
+import { convertDelayStringToMS } from "src/utils";
|
||||
+import { pollSubreddit } from "./functions/pollSubreddit";
|
||||
+import { GuildSocialPosts } from "src/data/GuildSocialPosts";
|
||||
+
|
||||
+import { pluginInfo } from "./info";
|
||||
+
|
||||
+const defaultOptions: PluginOptions<SocialMediaPosterPluginType> = {
|
||||
+ config: {
|
||||
+ platforms: {
|
||||
+ reddit: {}
|
||||
+ },
|
||||
+ },
|
||||
+};
|
||||
+
|
||||
+const platforms = {
|
||||
+ reddit: pollSubreddit
|
||||
+}
|
||||
+
|
||||
+export const SocialMediaPosterPlugin = zeppelinGuildPlugin<SocialMediaPosterPluginType>()({
|
||||
+ name: "social_media_poster",
|
||||
+ showInDocs: true,
|
||||
+
|
||||
+ info: pluginInfo,
|
||||
+
|
||||
+ configSchema: ConfigSchema,
|
||||
+ dependencies: () => [LogsPlugin],
|
||||
+ defaultOptions,
|
||||
+
|
||||
+ beforeLoad(pluginData) {
|
||||
+ const { state, guild } = pluginData;
|
||||
+
|
||||
+ state.logs = new GuildLogs(guild.id);
|
||||
+ state.sentPosts = new Map();
|
||||
+ state.socialPosts = new GuildSocialPosts(guild.id);
|
||||
+ },
|
||||
+
|
||||
+ async afterLoad(pluginData) {
|
||||
+ const config = pluginData.config.get();
|
||||
+
|
||||
+ // Start poll timers
|
||||
+ pluginData.state.pollTimers = [];
|
||||
+ for (const platform of Object.keys(config.platforms)) {
|
||||
+ if (!Object.keys(platforms).includes(platform)) continue;
|
||||
+ if (Object.values(config.platforms[platform]).length == 0) continue;
|
||||
+ pluginData.state.sentPosts.set(platform, new Map());
|
||||
+ for (const [path, pathSettings] of Object.entries(config.platforms[platform])) {
|
||||
+ await this.addPathTimer(pluginData, 'reddit', path, pathSettings as TPlatformPath);
|
||||
+ }
|
||||
+ }
|
||||
+ },
|
||||
+
|
||||
+ async addPathTimer(pluginData: GuildPluginData<SocialMediaPosterPluginType>, platform: string, path: string, pathSettings: TPlatformPath) {
|
||||
+ const pathLastPost = await pluginData.state.socialPosts.getLastPost(platform, path);
|
||||
+ pluginData.state.sentPosts.get(platform)!.set(path, pathLastPost);
|
||||
+
|
||||
+ const poll = pathSettings.poll_interval ?? '60s';
|
||||
+ const pollPeriodMs = convertDelayStringToMS(poll)!;
|
||||
+ pluginData.state.pollTimers.push(
|
||||
+ setInterval(() => {
|
||||
+ platforms[platform](pluginData, path);
|
||||
+ }, pollPeriodMs),
|
||||
+ );
|
||||
+ },
|
||||
+
|
||||
+ beforeUnload(pluginData) {
|
||||
+ if (pluginData.state.pollTimers) {
|
||||
+ for (const interval of pluginData.state.pollTimers) {
|
||||
+ clearInterval(interval);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+});
|
||||
diff --git a/backend/src/plugins/SocialMediaPoster/functions/pollSubreddit.ts b/backend/src/plugins/SocialMediaPoster/functions/pollSubreddit.ts
|
||||
new file mode 100644
|
||||
index 00000000..cbcfa5ee
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SocialMediaPoster/functions/pollSubreddit.ts
|
||||
@@ -0,0 +1,110 @@
|
||||
+import fetch from "node-fetch";
|
||||
+import { GuildPluginData } from "knub";
|
||||
+import { SocialMediaPosterPluginType } from "../types";
|
||||
+import { LogsPlugin } from "src/plugins/Logs/LogsPlugin";
|
||||
+import { convertDelayStringToMS, renderRecursively, validateAndParseMessageContent } from "src/utils";
|
||||
+import { renderTemplate, TemplateSafeValueContainer } from "src/templateFormatter";
|
||||
+import { userToTemplateSafeUser } from "src/utils/templateSafeObjects";
|
||||
+import { MessageOptions, Snowflake, TextChannel } from "discord.js";
|
||||
+import { messageIsEmpty } from "src/utils/messageIsEmpty";
|
||||
+
|
||||
+const REDDIT_URL = 'http://www.reddit.com';
|
||||
+const REDDIT_ICON = 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-32x32.png'
|
||||
+const REDDIT_SPOILER_IMG = 'https://cdn.discordapp.com/attachments/980238012384444446/981535288289525830/SpoilerImg.jpg'
|
||||
+export const DEFAULT_POST_POLL_COUNT = 10;
|
||||
+
|
||||
+type TRedditPost = {
|
||||
+ title: string,
|
||||
+ thumbnail: string,
|
||||
+ created: number,
|
||||
+ author: string,
|
||||
+ permalink: string,
|
||||
+ url: string,
|
||||
+ id: string
|
||||
+};
|
||||
+
|
||||
+export async function pollSubreddit(
|
||||
+ pluginData: GuildPluginData<SocialMediaPosterPluginType>,
|
||||
+ subredditName: string,
|
||||
+) {
|
||||
+ const config = pluginData.config.get();
|
||||
+ const subreddit = config.platforms.reddit![subredditName];
|
||||
+ if (!subreddit) return;
|
||||
+ if (!subreddit.enabled) return;
|
||||
+ if (subreddit.channels.length == 0) return;
|
||||
+ const limit = subreddit.post_poll_count ?? DEFAULT_POST_POLL_COUNT;
|
||||
+ let redditJson;
|
||||
+ try {
|
||||
+ const redditResponse = await fetch(getRedditUrl(subredditName, limit));
|
||||
+ if (!redditResponse) return;
|
||||
+ redditJson = await redditResponse.json();
|
||||
+ if (!redditJson) return;
|
||||
+ } catch (err) {
|
||||
+ subreddit.enabled = false;
|
||||
+ const logs = pluginData.getPlugin(LogsPlugin);
|
||||
+ logs.logBotAlert({
|
||||
+ body: `Unable to poll from r/**${subredditName}**.\nPolling will be disabled for this subreddit until a config change is made.`,
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const previousPost = pluginData.state.sentPosts.get('reddit')!.get(subredditName)!;
|
||||
+
|
||||
+ let posts: TRedditPost[] = redditJson.data.children.map((child) => child.data as TRedditPost);
|
||||
+ posts = posts.filter((post) => {
|
||||
+ return (BigInt(post.created) > previousPost);
|
||||
+ });
|
||||
+ if (posts.length === 0) return;
|
||||
+
|
||||
+ posts = posts.sort((a,b) => a.created - b.created);
|
||||
+
|
||||
+ pluginData.state.sentPosts.get('reddit')!.set(subredditName, BigInt(posts[posts.length - 1].created));
|
||||
+ await pluginData.state.socialPosts.setLastPost('reddit', subredditName, posts[posts.length - 1].created.toString());
|
||||
+
|
||||
+ for (const post of posts) {
|
||||
+ const renderReplyText = async (str: string) =>
|
||||
+ {
|
||||
+ let thumbnail = post.thumbnail;
|
||||
+ if (!thumbnail.startsWith('http')) thumbnail = REDDIT_SPOILER_IMG;
|
||||
+ return renderTemplate(
|
||||
+ str,
|
||||
+ new TemplateSafeValueContainer({
|
||||
+ title: post.title,
|
||||
+ thumbnail,
|
||||
+ created: post.created,
|
||||
+ author: post.author,
|
||||
+ permalink: REDDIT_URL + post.permalink,
|
||||
+ url: post.url,
|
||||
+ id: post.id,
|
||||
+ reddit_icon: REDDIT_ICON
|
||||
+ }),
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ const formatted =
|
||||
+ typeof subreddit.message === "string"
|
||||
+ ? await renderReplyText(subreddit.message)
|
||||
+ : ((await renderRecursively(subreddit.message, renderReplyText)) as MessageOptions);
|
||||
+
|
||||
+ if (!formatted) continue;
|
||||
+
|
||||
+ const messageContent = validateAndParseMessageContent(formatted);
|
||||
+ const messageOpts: MessageOptions = {
|
||||
+ ...messageContent,
|
||||
+ };
|
||||
+
|
||||
+ if (messageIsEmpty(messageOpts)) continue;
|
||||
+
|
||||
+ for (const channelId of subreddit.channels) {
|
||||
+ const channel = pluginData.guild.channels.cache.get(channelId as Snowflake);
|
||||
+ if (!channel) continue;
|
||||
+ if (!channel.isText) continue;
|
||||
+ const txtChannel = channel as TextChannel;
|
||||
+ await txtChannel.send(messageOpts);
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+function getRedditUrl(subreddit, limit) {
|
||||
+ return `${REDDIT_URL}/r/${subreddit}/new.json?limit=${limit}`
|
||||
+}
|
||||
\ No newline at end of file
|
||||
diff --git a/backend/src/plugins/SocialMediaPoster/info.ts b/backend/src/plugins/SocialMediaPoster/info.ts
|
||||
new file mode 100644
|
||||
index 00000000..263c83cd
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SocialMediaPoster/info.ts
|
||||
@@ -0,0 +1,65 @@
|
||||
+import { trimPluginDescription } from "../../utils";
|
||||
+import { ZeppelinGuildPluginBlueprint } from "../ZeppelinPluginBlueprint";
|
||||
+
|
||||
+export const pluginInfo: ZeppelinGuildPluginBlueprint["info"] = {
|
||||
+ prettyName: "Social Media Auto Poster",
|
||||
+ description: trimPluginDescription(`
|
||||
+ Allows posting new social media posts automatically.
|
||||
+ `),
|
||||
+ configurationGuide: trimPluginDescription(`
|
||||
+ The Social Media Auto Poster plugin is very customizable. For a full list of available platforms, and their options, see Config schema at the bottom of this page.
|
||||
+
|
||||
+ ### Simple reddit poster
|
||||
+ Automatically sends reddit posts to a channel
|
||||
+
|
||||
+ ~~~yml
|
||||
+ social_media_poster:
|
||||
+ config:
|
||||
+ platforms:
|
||||
+ reddit:
|
||||
+ tbhCreature: # Name of the subreddit
|
||||
+ enabled: true
|
||||
+ channels: ["473087035574321152"]
|
||||
+ message: |-
|
||||
+ **{author}** posted **{title}** in **tbhCreature**
|
||||
+ ~~~
|
||||
+
|
||||
+ ### Embed reddit poster
|
||||
+ This example posts the post as an embed
|
||||
+
|
||||
+ ~~~yml
|
||||
+ social_media_poster:
|
||||
+ config:
|
||||
+ platforms:
|
||||
+ reddit:
|
||||
+ tbhCreature: # Name of the subreddit
|
||||
+ enabled: true
|
||||
+ channels: ["473087035574321152"]
|
||||
+ message:
|
||||
+ embed:
|
||||
+ title: "{title}"
|
||||
+ color: 0xff4500
|
||||
+ description: "{url}"
|
||||
+ url: "{permalink}"
|
||||
+ footer:
|
||||
+ url: "{permalink}"
|
||||
+ icon_url: "{reddit_icon}"
|
||||
+ text: "{author}"
|
||||
+ thumbnail:
|
||||
+ url: "{thumbnail}"
|
||||
+ ~~~
|
||||
+
|
||||
+ ### List of variables
|
||||
+
|
||||
+ \`\`\`
|
||||
+ - {title} : Title of the post
|
||||
+ - {thumbnail} : Img URL for the post
|
||||
+ - {created} : The epoch time of when posted
|
||||
+ - {author} : The posts author
|
||||
+ - {permalink} : The posts permalink url
|
||||
+ - {url} : The posts URL
|
||||
+ - {id}: The ID of the post
|
||||
+ - {reddit_icon}: A 32x32 image of the reddit logo
|
||||
+ \`\`\`
|
||||
+ `),
|
||||
+};
|
||||
diff --git a/backend/src/plugins/SocialMediaPoster/types.ts b/backend/src/plugins/SocialMediaPoster/types.ts
|
||||
new file mode 100644
|
||||
index 00000000..710f4ef7
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/SocialMediaPoster/types.ts
|
||||
@@ -0,0 +1,37 @@
|
||||
+import * as t from "io-ts";
|
||||
+import { BasePluginType, typedGuildCommand } from "knub";
|
||||
+import { GuildSocialPosts } from "src/data/GuildSocialPosts";
|
||||
+import { GuildLogs } from "../../data/GuildLogs";
|
||||
+import { tMessageContent, tNullable } from "../../utils";
|
||||
+import Timeout = NodeJS.Timeout;
|
||||
+
|
||||
+export const PlatformPath = t.type({
|
||||
+ enabled: t.boolean,
|
||||
+ channels: t.array(t.string),
|
||||
+ message: tMessageContent,
|
||||
+ poll_interval: tNullable(t.string),
|
||||
+ post_poll_count: tNullable(t.number)
|
||||
+});
|
||||
+export type TPlatformPath = t.TypeOf<typeof PlatformPath>;
|
||||
+
|
||||
+export const PlatformTypes = t.type({
|
||||
+ reddit: t.record(t.string, PlatformPath),
|
||||
+});
|
||||
+
|
||||
+export type TPlatformTypes = t.TypeOf<typeof PlatformTypes>;
|
||||
+
|
||||
+export const ConfigSchema = t.type({
|
||||
+ platforms: PlatformTypes,
|
||||
+});
|
||||
+export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
+
|
||||
+export interface SocialMediaPosterPluginType extends BasePluginType {
|
||||
+ config: TConfigSchema;
|
||||
+ state: {
|
||||
+ logs: GuildLogs;
|
||||
+ pollTimers: Timeout[];
|
||||
+ // Platform: { path: lastPost }
|
||||
+ sentPosts: Map<string, Map<string, BigInt>>;
|
||||
+ socialPosts: GuildSocialPosts;
|
||||
+ };
|
||||
+}
|
||||
\ No newline at end of file
|
||||
diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts
|
||||
index d47df12f..f337e2e2 100644
|
||||
--- a/backend/src/plugins/availablePlugins.ts
|
||||
+++ b/backend/src/plugins/availablePlugins.ts
|
||||
@@ -38,6 +38,7 @@ import { PhishermanPlugin } from "./Phisherman/PhishermanPlugin";
|
||||
import { InternalPosterPlugin } from "./InternalPoster/InternalPosterPlugin";
|
||||
import { RoleManagerPlugin } from "./RoleManager/RoleManagerPlugin";
|
||||
import { RoleButtonsPlugin } from "./RoleButtons/RoleButtonsPlugin";
|
||||
+import { SocialMediaPosterPlugin } from "./SocialMediaPoster/SocialMediaPosterPlugin";
|
||||
|
||||
// prettier-ignore
|
||||
export const guildPlugins: Array<ZeppelinGuildPluginBlueprint<any>> = [
|
||||
@@ -77,6 +78,7 @@ export const guildPlugins: Array<ZeppelinGuildPluginBlueprint<any>> = [
|
||||
InternalPosterPlugin,
|
||||
RoleManagerPlugin,
|
||||
RoleButtonsPlugin,
|
||||
+ SocialMediaPosterPlugin,
|
||||
];
|
||||
|
||||
// prettier-ignore
|
||||
--
|
||||
2.35.1.windows.2
|
||||
|
197
patches/patches/0005-created-activities-plugin.patch
Normal file
197
patches/patches/0005-created-activities-plugin.patch
Normal file
|
@ -0,0 +1,197 @@
|
|||
From 140a4ad8070a442b5008000f551260b7b8053695 Mon Sep 17 00:00:00 2001
|
||||
From: KK <morrkade05@gmail.com>
|
||||
Date: Wed, 1 Jun 2022 16:53:20 -0500
|
||||
Subject: [PATCH] Created Activities Plugin
|
||||
|
||||
---
|
||||
.../plugins/Activities/ActivitiesPlugin.ts | 32 +++++++++
|
||||
.../Activities/commands/ActivitiesCmd.ts | 72 +++++++++++++++++++
|
||||
.../Activities/commands/ActivitiesList.ts | 13 ++++
|
||||
backend/src/plugins/Activities/types.ts | 14 ++++
|
||||
backend/src/plugins/availablePlugins.ts | 2 +
|
||||
5 files changed, 133 insertions(+)
|
||||
create mode 100644 backend/src/plugins/Activities/ActivitiesPlugin.ts
|
||||
create mode 100644 backend/src/plugins/Activities/commands/ActivitiesCmd.ts
|
||||
create mode 100644 backend/src/plugins/Activities/commands/ActivitiesList.ts
|
||||
create mode 100644 backend/src/plugins/Activities/types.ts
|
||||
|
||||
diff --git a/backend/src/plugins/Activities/ActivitiesPlugin.ts b/backend/src/plugins/Activities/ActivitiesPlugin.ts
|
||||
new file mode 100644
|
||||
index 00000000..6f11a1fb
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/Activities/ActivitiesPlugin.ts
|
||||
@@ -0,0 +1,32 @@
|
||||
+import { PluginOptions } from "knub";
|
||||
+import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
+import { ActivitiesCmd } from "./commands/ActivitiesCmd";
|
||||
+import { ActivitiesListCmd } from "./commands/ActivitiesList";
|
||||
+import { ActivitiesPluginType, ConfigSchema } from "./types";
|
||||
+
|
||||
+const defaultOptions: PluginOptions<ActivitiesPluginType> = {
|
||||
+ config: {
|
||||
+ start_activities: false
|
||||
+ },
|
||||
+ overrides: [
|
||||
+ {
|
||||
+ level: ">=50",
|
||||
+ config: {
|
||||
+ start_activities: true
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+};
|
||||
+
|
||||
+export const ActivitiesPlugin = zeppelinGuildPlugin<ActivitiesPluginType>()({
|
||||
+ name: "activities",
|
||||
+ showInDocs: true,
|
||||
+
|
||||
+ commands: [
|
||||
+ ActivitiesCmd,
|
||||
+ ActivitiesListCmd
|
||||
+ ],
|
||||
+
|
||||
+ configSchema: ConfigSchema,
|
||||
+ defaultOptions,
|
||||
+});
|
||||
diff --git a/backend/src/plugins/Activities/commands/ActivitiesCmd.ts b/backend/src/plugins/Activities/commands/ActivitiesCmd.ts
|
||||
new file mode 100644
|
||||
index 00000000..9507f917
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/Activities/commands/ActivitiesCmd.ts
|
||||
@@ -0,0 +1,72 @@
|
||||
+import { activitiesCmd } from "../types";
|
||||
+import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
+import { sendErrorMessage } from "src/pluginUtils";
|
||||
+
|
||||
+import { RESTPostAPIChannelInviteJSONBody, APIInvite, InviteTargetType, RouteBases, Routes } from 'discord-api-types/v9';
|
||||
+import fetch from "node-fetch";
|
||||
+
|
||||
+export const activities = {
|
||||
+ poker: '755827207812677713',
|
||||
+ betrayal: '773336526917861400',
|
||||
+ youtube: '880218394199220334',
|
||||
+ fishington: '814288819477020702',
|
||||
+ chess: '832012774040141894',
|
||||
+ checkers: '832013003968348200',
|
||||
+ letter: '879863686565621790',
|
||||
+ word: '879863976006127627',
|
||||
+ sketchheads: '902271654783242291',
|
||||
+ spellcast: '852509694341283871',
|
||||
+ ocho: '832025144389533716',
|
||||
+}
|
||||
+
|
||||
+export const ActivitiesCmd = activitiesCmd({
|
||||
+ trigger: ['activities', 'activity'],
|
||||
+ permission: 'start_activities',
|
||||
+
|
||||
+ signature: {
|
||||
+ activity: ct.string(),
|
||||
+ channel: ct.voiceChannel(),
|
||||
+ },
|
||||
+
|
||||
+ async run({ message, args, pluginData}) {
|
||||
+ if (!Object.keys(activities).includes(args.activity.toLowerCase())) {
|
||||
+ sendErrorMessage(pluginData, message.channel, `Unknown activity \`${args.activity}\`.\nUse one from this list: \`${Object.keys(activities).join(', ')}\``);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const channel = args.channel;
|
||||
+ if (!channel) {
|
||||
+ sendErrorMessage(pluginData, message.channel, `Unknown channel: ${args.channel}`);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (!channel.isVoice) {
|
||||
+ sendErrorMessage(pluginData, message.channel, `Supplied channel is not a voice channel`);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const r = await fetch(`${RouteBases.api}${Routes.channelInvites(channel.id)}`, {
|
||||
+ method: 'POST',
|
||||
+ headers: {
|
||||
+ authorization: `Bot ${process.env.TOKEN}`,
|
||||
+ 'content-type': 'application/json'
|
||||
+ },
|
||||
+ body: JSON.stringify({
|
||||
+ max_age: 0,
|
||||
+ target_type: InviteTargetType.EmbeddedApplication,
|
||||
+ target_application_id: activities[args.activity.toLowerCase()],
|
||||
+ } as RESTPostAPIChannelInviteJSONBody)
|
||||
+ });
|
||||
+
|
||||
+ const invite = await r.json() as APIInvite;
|
||||
+
|
||||
+ if (r.status !== 200) {
|
||||
+ sendErrorMessage(pluginData, message.channel, `An error occurred: ${(invite as any).message}\nUnable to create an activities invite!\nMake sure I have the "Create Invite" permission in ${channel}.`);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ message.channel.send({
|
||||
+ content: `Click here to join ${args.activity} in ${channel}: https://discord.gg/${invite.code}`,
|
||||
+ });
|
||||
+ },
|
||||
+});
|
||||
\ No newline at end of file
|
||||
diff --git a/backend/src/plugins/Activities/commands/ActivitiesList.ts b/backend/src/plugins/Activities/commands/ActivitiesList.ts
|
||||
new file mode 100644
|
||||
index 00000000..84c40f3e
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/Activities/commands/ActivitiesList.ts
|
||||
@@ -0,0 +1,13 @@
|
||||
+import { activitiesCmd } from "../types";
|
||||
+import { activities } from "./ActivitiesCmd";
|
||||
+
|
||||
+export const ActivitiesListCmd = activitiesCmd({
|
||||
+ trigger: ['activities', 'activity', 'activities_list'],
|
||||
+ permission: 'start_activities',
|
||||
+
|
||||
+ async run({ message, args, pluginData}) {
|
||||
+ message.channel.send({
|
||||
+ content: `The activities available are: \`${Object.keys(activities).join(', ')}\``,
|
||||
+ });
|
||||
+ },
|
||||
+});
|
||||
\ No newline at end of file
|
||||
diff --git a/backend/src/plugins/Activities/types.ts b/backend/src/plugins/Activities/types.ts
|
||||
new file mode 100644
|
||||
index 00000000..901058ad
|
||||
--- /dev/null
|
||||
+++ b/backend/src/plugins/Activities/types.ts
|
||||
@@ -0,0 +1,14 @@
|
||||
+import * as t from "io-ts";
|
||||
+import { BasePluginType, typedGuildCommand } from "knub";
|
||||
+
|
||||
+export const ConfigSchema = t.type({
|
||||
+ start_activities: t.boolean,
|
||||
+});
|
||||
+export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
+
|
||||
+export interface ActivitiesPluginType extends BasePluginType {
|
||||
+ config: TConfigSchema;
|
||||
+ state: {};
|
||||
+}
|
||||
+
|
||||
+export const activitiesCmd = typedGuildCommand<ActivitiesPluginType>();
|
||||
diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts
|
||||
index f337e2e2..59cdfd7b 100644
|
||||
--- a/backend/src/plugins/availablePlugins.ts
|
||||
+++ b/backend/src/plugins/availablePlugins.ts
|
||||
@@ -39,6 +39,7 @@ import { InternalPosterPlugin } from "./InternalPoster/InternalPosterPlugin";
|
||||
import { RoleManagerPlugin } from "./RoleManager/RoleManagerPlugin";
|
||||
import { RoleButtonsPlugin } from "./RoleButtons/RoleButtonsPlugin";
|
||||
import { SocialMediaPosterPlugin } from "./SocialMediaPoster/SocialMediaPosterPlugin";
|
||||
+import { ActivitiesPlugin } from "./Activities/ActivitiesPlugin";
|
||||
|
||||
// prettier-ignore
|
||||
export const guildPlugins: Array<ZeppelinGuildPluginBlueprint<any>> = [
|
||||
@@ -79,6 +80,7 @@ export const guildPlugins: Array<ZeppelinGuildPluginBlueprint<any>> = [
|
||||
RoleManagerPlugin,
|
||||
RoleButtonsPlugin,
|
||||
SocialMediaPosterPlugin,
|
||||
+ ActivitiesPlugin,
|
||||
];
|
||||
|
||||
// prettier-ignore
|
||||
--
|
||||
2.35.1.windows.2
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue