mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-03-15 05:41:51 +00:00
feat: add member cache; handle all role changes with RoleManagerPlugin; exit gracefully
This commit is contained in:
parent
fd60a09947
commit
fa50110766
48 changed files with 755 additions and 264 deletions
|
@ -1,6 +1,5 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const pkgUp = require("pkg-up");
|
||||
const { backendDir } = require("./dist/backend/src/paths");
|
||||
const { env } = require("./dist/backend/src/env");
|
||||
|
||||
|
|
40
backend/src/Blocker.ts
Normal file
40
backend/src/Blocker.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
export type Block = {
|
||||
count: number;
|
||||
unblock: () => void;
|
||||
getPromise: () => Promise<void>;
|
||||
};
|
||||
|
||||
export class Blocker {
|
||||
#blocks: Map<string, Block> = new Map();
|
||||
|
||||
block(key: string): void {
|
||||
if (!this.#blocks.has(key)) {
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
this.#blocks.set(key, {
|
||||
count: 0, // Incremented to 1 further below
|
||||
unblock() {
|
||||
this.count--;
|
||||
if (this.count === 0) {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
getPromise: () => promise, // :d
|
||||
});
|
||||
});
|
||||
}
|
||||
this.#blocks.get(key)!.count++;
|
||||
}
|
||||
|
||||
unblock(key: string): void {
|
||||
if (this.#blocks.has(key)) {
|
||||
this.#blocks.get(key)!.unblock();
|
||||
}
|
||||
}
|
||||
|
||||
async waitToBeUnblocked(key: string): Promise<void> {
|
||||
if (!this.#blocks.has(key)) {
|
||||
return;
|
||||
}
|
||||
await this.#blocks.get(key)!.getPromise();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
// KEEP THIS AS FIRST IMPORT
|
||||
// See comment in module for details
|
||||
import "../threadsSignalFix";
|
||||
|
||||
import { connect } from "../data/db";
|
||||
import { env } from "../env";
|
||||
import { setIsAPI } from "../globals";
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
"MEMBER_SOFTBAN": "{timestamp} 🔨 {userMention(member)} was softbanned by {userMention(mod)}",
|
||||
"MEMBER_JOIN": "{timestamp} 📥 {new} {userMention(member)} joined (created {account_age} ago)",
|
||||
"MEMBER_LEAVE": "{timestamp} 📤 {userMention(member)} left the server",
|
||||
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(member)} received roles: **{roles}**",
|
||||
"MEMBER_ROLE_REMOVE": "{timestamp} 🔑 {userMention(member)} lost roles: **{roles}**",
|
||||
"MEMBER_ROLE_ADD": "{timestamp} 🔑 {userMention(mod)} added roles for {userMention(member)}: **{roles}**",
|
||||
"MEMBER_ROLE_REMOVE": "{timestamp} 🔑 {userMention(mod)} removed roles from {userMention(member)}: **{roles}**",
|
||||
"MEMBER_ROLE_CHANGES": "{timestamp} 🔑 {userMention(member)} had role changes: received **{addedRoles}**, lost **{removedRoles}**",
|
||||
"MEMBER_NICK_CHANGE": "{timestamp} ✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
|
||||
"MEMBER_USERNAME_CHANGE": "{timestamp} ✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**",
|
||||
|
|
101
backend/src/data/GuildMemberCache.ts
Normal file
101
backend/src/data/GuildMemberCache.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { Blocker } from "../Blocker";
|
||||
import { DBDateFormat, MINUTES } from "../utils";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { MemberCacheItem } from "./entities/MemberCacheItem";
|
||||
|
||||
const SAVE_PENDING_BLOCKER_KEY = "save-pending" as const;
|
||||
|
||||
const DELETION_DELAY = 5 * MINUTES;
|
||||
|
||||
type UpdateData = Pick<MemberCacheItem, "username" | "nickname" | "roles">;
|
||||
|
||||
export class GuildMemberCache extends BaseGuildRepository {
|
||||
#memberCache: Repository<MemberCacheItem>;
|
||||
|
||||
#pendingUpdates: Map<string, Partial<MemberCacheItem>>;
|
||||
|
||||
#blocker: Blocker;
|
||||
|
||||
constructor(guildId: string) {
|
||||
super(guildId);
|
||||
this.#memberCache = getRepository(MemberCacheItem);
|
||||
this.#pendingUpdates = new Map();
|
||||
this.#blocker = new Blocker();
|
||||
}
|
||||
|
||||
async savePendingUpdates(): Promise<void> {
|
||||
await this.#blocker.waitToBeUnblocked(SAVE_PENDING_BLOCKER_KEY);
|
||||
|
||||
if (this.#pendingUpdates.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#blocker.block(SAVE_PENDING_BLOCKER_KEY);
|
||||
|
||||
const entitiesToSave = Array.from(this.#pendingUpdates.values());
|
||||
this.#pendingUpdates.clear();
|
||||
|
||||
await this.#memberCache.upsert(entitiesToSave, ["guild_id", "user_id"]).finally(() => {
|
||||
this.#blocker.unblock(SAVE_PENDING_BLOCKER_KEY);
|
||||
});
|
||||
}
|
||||
|
||||
async getCachedMemberData(userId: string): Promise<MemberCacheItem | null> {
|
||||
await this.#blocker.waitToBeUnblocked(SAVE_PENDING_BLOCKER_KEY);
|
||||
|
||||
const dbItem = await this.#memberCache.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
const pendingItem = this.#pendingUpdates.get(userId);
|
||||
if (!dbItem && !pendingItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = new MemberCacheItem();
|
||||
Object.assign(item, dbItem ?? {});
|
||||
Object.assign(item, pendingItem ?? {});
|
||||
return item;
|
||||
}
|
||||
|
||||
async setCachedMemberData(userId: string, data: UpdateData): Promise<void> {
|
||||
await this.#blocker.waitToBeUnblocked(SAVE_PENDING_BLOCKER_KEY);
|
||||
|
||||
if (!this.#pendingUpdates.has(userId)) {
|
||||
const newItem = new MemberCacheItem();
|
||||
newItem.guild_id = this.guildId;
|
||||
newItem.user_id = userId;
|
||||
this.#pendingUpdates.set(userId, newItem);
|
||||
}
|
||||
Object.assign(this.#pendingUpdates.get(userId)!, data);
|
||||
this.#pendingUpdates.get(userId)!.last_seen = moment().format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
async markMemberForDeletion(userId: string): Promise<void> {
|
||||
await this.#memberCache.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
delete_at: moment().add(DELETION_DELAY, "ms").format(DBDateFormat),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async unmarkMemberForDeletion(userId: string): Promise<void> {
|
||||
await this.#memberCache.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
delete_at: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,11 +2,6 @@ import { getRepository, Repository } from "typeorm";
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { PersistedData } from "./entities/PersistedData";
|
||||
|
||||
export interface IPartialPersistData {
|
||||
roles?: string[];
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
export class GuildPersistedData extends BaseGuildRepository {
|
||||
private persistedData: Repository<PersistedData>;
|
||||
|
||||
|
@ -24,11 +19,7 @@ export class GuildPersistedData extends BaseGuildRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async set(userId: string, data: IPartialPersistData = {}) {
|
||||
const finalData: any = {};
|
||||
if (data.roles) finalData.roles = data.roles.join(",");
|
||||
if (data.nickname) finalData.nickname = data.nickname;
|
||||
|
||||
async set(userId: string, data: Partial<PersistedData> = {}) {
|
||||
const existing = await this.find(userId);
|
||||
if (existing) {
|
||||
await this.persistedData.update(
|
||||
|
@ -36,11 +27,11 @@ export class GuildPersistedData extends BaseGuildRepository {
|
|||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
finalData,
|
||||
data,
|
||||
);
|
||||
} else {
|
||||
await this.persistedData.insert({
|
||||
...finalData,
|
||||
...data,
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
});
|
||||
|
|
29
backend/src/data/MemberCache.ts
Normal file
29
backend/src/data/MemberCache.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import moment from "moment-timezone";
|
||||
import { getRepository, Repository } from "typeorm";
|
||||
import { DAYS } from "../utils";
|
||||
import { BaseRepository } from "./BaseRepository";
|
||||
import { MemberCacheItem } from "./entities/MemberCacheItem";
|
||||
|
||||
const STALE_PERIOD = 90 * DAYS;
|
||||
|
||||
export class MemberCache extends BaseRepository {
|
||||
#memberCache: Repository<MemberCacheItem>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#memberCache = getRepository(MemberCacheItem);
|
||||
}
|
||||
|
||||
async deleteStaleData(): Promise<void> {
|
||||
const cutoff = moment().subtract(STALE_PERIOD, "ms").format("YYYY-MM-DD");
|
||||
await this.#memberCache.createQueryBuilder().where("last_seen < :cutoff", { cutoff }).delete().execute();
|
||||
}
|
||||
|
||||
async deleteMarkedToBeDeletedEntries(): Promise<void> {
|
||||
await this.#memberCache
|
||||
.createQueryBuilder()
|
||||
.where("delete_at IS NOT NULL AND delete_at <= NOW()")
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
}
|
20
backend/src/data/entities/MemberCacheItem.ts
Normal file
20
backend/src/data/entities/MemberCacheItem.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity("member_cache")
|
||||
export class MemberCacheItem {
|
||||
@PrimaryGeneratedColumn() id: number;
|
||||
|
||||
@Column() guild_id: string;
|
||||
|
||||
@Column() user_id: string;
|
||||
|
||||
@Column() username: string;
|
||||
|
||||
@Column({ type: String, nullable: true }) nickname: string | null;
|
||||
|
||||
@Column("simple-json") roles: string[];
|
||||
|
||||
@Column() last_seen: string;
|
||||
|
||||
@Column({ type: String, nullable: true }) delete_at: string | null;
|
||||
}
|
|
@ -14,5 +14,5 @@ export class PersistedData {
|
|||
|
||||
@Column() nickname: string;
|
||||
|
||||
@Column() is_voice_muted: number;
|
||||
@Column({ type: "boolean" }) is_voice_muted: boolean;
|
||||
}
|
||||
|
|
13
backend/src/data/loops/expiredMemberCacheDeletionLoop.ts
Normal file
13
backend/src/data/loops/expiredMemberCacheDeletionLoop.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
// tslint:disable:no-console
|
||||
|
||||
import { HOURS, lazyMemoize } from "../../utils";
|
||||
import { MemberCache } from "../MemberCache";
|
||||
|
||||
const LOOP_INTERVAL = 6 * HOURS;
|
||||
const getMemberCacheRepository = lazyMemoize(() => new MemberCache());
|
||||
|
||||
export async function runExpiredMemberCacheDeletionLoop() {
|
||||
console.log("[EXPIRED MEMBER CACHE DELETION LOOP] Deleting stale member cache entries");
|
||||
await getMemberCacheRepository().deleteStaleData();
|
||||
setTimeout(() => runExpiredMemberCacheDeletionLoop(), LOOP_INTERVAL);
|
||||
}
|
13
backend/src/data/loops/memberCacheDeletionLoop.ts
Normal file
13
backend/src/data/loops/memberCacheDeletionLoop.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
// tslint:disable:no-console
|
||||
|
||||
import { lazyMemoize, MINUTES } from "../../utils";
|
||||
import { MemberCache } from "../MemberCache";
|
||||
|
||||
const LOOP_INTERVAL = 5 * MINUTES;
|
||||
const getMemberCacheRepository = lazyMemoize(() => new MemberCache());
|
||||
|
||||
export async function runMemberCacheDeletionLoop() {
|
||||
console.log("[MEMBER CACHE DELETION LOOP] Deleting entries marked to be deleted");
|
||||
await getMemberCacheRepository().deleteMarkedToBeDeletedEntries();
|
||||
setTimeout(() => runMemberCacheDeletionLoop(), LOOP_INTERVAL);
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
// KEEP THIS AS FIRST IMPORT
|
||||
// See comment in module for details
|
||||
import "./threadsSignalFix";
|
||||
|
||||
import {
|
||||
Client,
|
||||
Events,
|
||||
|
@ -12,31 +16,34 @@ import { EventEmitter } from "events";
|
|||
import { Knub, PluginError, PluginLoadError, PluginNotLoadedError } from "knub";
|
||||
import moment from "moment-timezone";
|
||||
import { performance } from "perf_hooks";
|
||||
import process from "process";
|
||||
import { DiscordJSError } from "./DiscordJSError";
|
||||
import { RecoverablePluginError } from "./RecoverablePluginError";
|
||||
import { SimpleError } from "./SimpleError";
|
||||
import { AllowedGuilds } from "./data/AllowedGuilds";
|
||||
import { Configs } from "./data/Configs";
|
||||
import { connect } from "./data/db";
|
||||
import { GuildLogs } from "./data/GuildLogs";
|
||||
import { LogType } from "./data/LogType";
|
||||
import { hasPhishermanMasterAPIKey } from "./data/Phisherman";
|
||||
import { connect } from "./data/db";
|
||||
import { runExpiredArchiveDeletionLoop } from "./data/loops/expiredArchiveDeletionLoop";
|
||||
import { runExpiredMemberCacheDeletionLoop } from "./data/loops/expiredMemberCacheDeletionLoop";
|
||||
import { runExpiringMutesLoop } from "./data/loops/expiringMutesLoop";
|
||||
import { runExpiringTempbansLoop } from "./data/loops/expiringTempbansLoop";
|
||||
import { runExpiringVCAlertsLoop } from "./data/loops/expiringVCAlertsLoop";
|
||||
import { runMemberCacheDeletionLoop } from "./data/loops/memberCacheDeletionLoop";
|
||||
import { runPhishermanCacheCleanupLoop, runPhishermanReportingLoop } from "./data/loops/phishermanLoops";
|
||||
import { runSavedMessageCleanupLoop } from "./data/loops/savedMessageCleanupLoop";
|
||||
import { runUpcomingRemindersLoop } from "./data/loops/upcomingRemindersLoop";
|
||||
import { runUpcomingScheduledPostsLoop } from "./data/loops/upcomingScheduledPostsLoop";
|
||||
import { hasPhishermanMasterAPIKey } from "./data/Phisherman";
|
||||
import { consumeQueryStats } from "./data/queryLogger";
|
||||
import { DiscordJSError } from "./DiscordJSError";
|
||||
import { env } from "./env";
|
||||
import { logger } from "./logger";
|
||||
import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availablePlugins";
|
||||
import { setProfiler } from "./profiler";
|
||||
import { logRateLimit } from "./rateLimitStats";
|
||||
import { RecoverablePluginError } from "./RecoverablePluginError";
|
||||
import { SimpleError } from "./SimpleError";
|
||||
import { startUptimeCounter } from "./uptime";
|
||||
import { errorMessage, isDiscordAPIError, isDiscordHTTPError, MINUTES, SECONDS, sleep, successMessage } from "./utils";
|
||||
import { MINUTES, SECONDS, errorMessage, isDiscordAPIError, isDiscordHTTPError, sleep, successMessage } from "./utils";
|
||||
import { DecayingCounter } from "./utils/DecayingCounter";
|
||||
import { enableProfiling } from "./utils/easyProfiler";
|
||||
import { loadYamlSafely } from "./utils/loadYamlSafely";
|
||||
|
@ -191,7 +198,7 @@ setInterval(() => {
|
|||
}, 5 * 60 * 1000);
|
||||
|
||||
logger.info("Connecting to database");
|
||||
connect().then(async () => {
|
||||
connect().then(async (connection) => {
|
||||
const client = new Client({
|
||||
partials: [Partials.User, Partials.Channel, Partials.GuildMember, Partials.Message, Partials.Reaction],
|
||||
|
||||
|
@ -365,6 +372,11 @@ connect().then(async () => {
|
|||
});
|
||||
|
||||
bot.on("loadingFinished", async () => {
|
||||
setProfiler(bot.profiler);
|
||||
if (process.env.PROFILING === "true") {
|
||||
enableProfiling();
|
||||
}
|
||||
|
||||
runExpiringMutesLoop();
|
||||
await sleep(10 * SECONDS);
|
||||
runExpiringTempbansLoop();
|
||||
|
@ -378,6 +390,10 @@ connect().then(async () => {
|
|||
runExpiredArchiveDeletionLoop();
|
||||
await sleep(10 * SECONDS);
|
||||
runSavedMessageCleanupLoop();
|
||||
await sleep(10 * SECONDS);
|
||||
runExpiredMemberCacheDeletionLoop();
|
||||
await sleep(10 * SECONDS);
|
||||
runMemberCacheDeletionLoop();
|
||||
|
||||
if (hasPhishermanMasterAPIKey()) {
|
||||
await sleep(10 * SECONDS);
|
||||
|
@ -387,11 +403,6 @@ connect().then(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
setProfiler(bot.profiler);
|
||||
if (process.env.PROFILING === "true") {
|
||||
enableProfiling();
|
||||
}
|
||||
|
||||
let lowestGlobalRemaining = Infinity;
|
||||
setInterval(() => {
|
||||
lowestGlobalRemaining = Math.min(lowestGlobalRemaining, (client as any).rest.globalRemaining);
|
||||
|
@ -422,4 +433,28 @@ connect().then(async () => {
|
|||
logger.info("Bot Initialized");
|
||||
logger.info("Logging in...");
|
||||
await client.login(env.BOT_TOKEN);
|
||||
|
||||
let stopping = false;
|
||||
const cleanupAndStop = async (code) => {
|
||||
if (stopping) {
|
||||
return;
|
||||
}
|
||||
stopping = true;
|
||||
logger.info("Cleaning up before exit...");
|
||||
// Force exit after 10sec
|
||||
setTimeout(() => process.exit(code), 10 * SECONDS);
|
||||
await bot.stop();
|
||||
await connection.close();
|
||||
logger.info("Done! Exiting now.");
|
||||
process.exit(code);
|
||||
};
|
||||
process.on("beforeExit", () => cleanupAndStop(0));
|
||||
process.on("SIGINT", () => {
|
||||
logger.info("Received SIGINT, exiting...");
|
||||
cleanupAndStop(0);
|
||||
});
|
||||
process.on("SIGTERM", (code) => {
|
||||
logger.info("Received SIGTERM, exiting...");
|
||||
cleanupAndStop(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { MigrationInterface, QueryRunner, Table } from "typeorm";
|
||||
|
||||
export class CreateMemberCacheTable1682788165866 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "member_cache",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "int",
|
||||
isPrimary: true,
|
||||
isGenerated: true,
|
||||
generationStrategy: "increment",
|
||||
},
|
||||
{
|
||||
name: "guild_id",
|
||||
type: "bigint",
|
||||
},
|
||||
{
|
||||
name: "user_id",
|
||||
type: "bigint",
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
type: "varchar",
|
||||
length: "255",
|
||||
},
|
||||
{
|
||||
name: "nickname",
|
||||
type: "varchar",
|
||||
length: "255",
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
name: "roles",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
name: "last_seen",
|
||||
type: "date",
|
||||
},
|
||||
{
|
||||
name: "delete_at",
|
||||
type: "datetime",
|
||||
isNullable: true,
|
||||
default: null,
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
columnNames: ["guild_id", "user_id"],
|
||||
isUnique: true,
|
||||
},
|
||||
{
|
||||
columnNames: ["last_seen"],
|
||||
},
|
||||
{
|
||||
columnNames: ["delete_at"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable("member_cache");
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import { LogsPlugin } from "../Logs/LogsPlugin";
|
|||
import { ModActionsPlugin } from "../ModActions/ModActionsPlugin";
|
||||
import { MutesPlugin } from "../Mutes/MutesPlugin";
|
||||
import { PhishermanPlugin } from "../Phisherman/PhishermanPlugin";
|
||||
import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin";
|
||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
import { availableActions } from "./actions/availableActions";
|
||||
import { AntiraidClearCmd } from "./commands/AntiraidClearCmd";
|
||||
|
@ -196,6 +197,7 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()({
|
|||
CountersPlugin,
|
||||
PhishermanPlugin,
|
||||
InternalPosterPlugin,
|
||||
RoleManagerPlugin,
|
||||
],
|
||||
|
||||
defaultOptions,
|
||||
|
|
|
@ -3,9 +3,9 @@ import * as t from "io-ts";
|
|||
import { nonNullish, unique } from "../../../utils";
|
||||
import { canAssignRole } from "../../../utils/canAssignRole";
|
||||
import { getMissingPermissions } from "../../../utils/getMissingPermissions";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
import { missingPermissionError } from "../../../utils/missingPermissionError";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { ignoreRoleChange } from "../functions/ignoredRoleChanges";
|
||||
import { automodAction } from "../helpers";
|
||||
|
||||
|
@ -52,25 +52,14 @@ export const AddRolesAction = automodAction({
|
|||
|
||||
await Promise.all(
|
||||
members.map(async (member) => {
|
||||
const memberRoles = new Set(member.roles.cache.keys());
|
||||
const currentMemberRoles = new Set(member.roles.cache.keys());
|
||||
for (const roleId of rolesToAssign) {
|
||||
memberRoles.add(roleId as Snowflake);
|
||||
if (!currentMemberRoles.has(roleId)) {
|
||||
pluginData.getPlugin(RoleManagerPlugin).addRole(member.id, roleId);
|
||||
// TODO: Remove this and just ignore bot changes in general?
|
||||
ignoreRoleChange(pluginData, member.id, roleId);
|
||||
}
|
||||
|
||||
if (memberRoles.size === member.roles.cache.size) {
|
||||
// No role changes
|
||||
return;
|
||||
}
|
||||
|
||||
const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
|
||||
|
||||
const rolesArr = Array.from(memberRoles.values());
|
||||
await member.edit({
|
||||
roles: rolesArr,
|
||||
});
|
||||
|
||||
memberRoleLock.unlock();
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import * as t from "io-ts";
|
||||
import { GuildMemberCache } from "../../data/GuildMemberCache";
|
||||
import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils";
|
||||
import { SECONDS } from "../../utils";
|
||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
import { cancelDeletionOnMemberJoin } from "./events/cancelDeletionOnMemberJoin";
|
||||
import { removeMemberCacheOnMemberLeave } from "./events/removeMemberCacheOnMemberLeave";
|
||||
import { updateMemberCacheOnMemberUpdate } from "./events/updateMemberCacheOnMemberUpdate";
|
||||
import { updateMemberCacheOnMessage } from "./events/updateMemberCacheOnMessage";
|
||||
import { updateMemberCacheOnRoleChange } from "./events/updateMemberCacheOnRoleChange";
|
||||
import { updateMemberCacheOnVoiceStateUpdate } from "./events/updateMemberCacheOnVoiceStateUpdate";
|
||||
import { getCachedMemberData } from "./functions/getCachedMemberData";
|
||||
import { GuildMemberCachePluginType } from "./types";
|
||||
|
||||
const PENDING_SAVE_INTERVAL = 30 * SECONDS;
|
||||
|
||||
export const GuildMemberCachePlugin = zeppelinGuildPlugin<GuildMemberCachePluginType>()({
|
||||
name: "guild_member_cache",
|
||||
showInDocs: false,
|
||||
|
||||
configParser: makeIoTsConfigParser(t.type({})),
|
||||
|
||||
events: [
|
||||
updateMemberCacheOnMemberUpdate,
|
||||
updateMemberCacheOnMessage,
|
||||
updateMemberCacheOnVoiceStateUpdate,
|
||||
updateMemberCacheOnRoleChange,
|
||||
removeMemberCacheOnMemberLeave,
|
||||
cancelDeletionOnMemberJoin,
|
||||
],
|
||||
|
||||
public: {
|
||||
getCachedMemberData: mapToPublicFn(getCachedMemberData),
|
||||
},
|
||||
|
||||
beforeLoad(pluginData) {
|
||||
pluginData.state.memberCache = GuildMemberCache.getGuildInstance(pluginData.guild.id);
|
||||
// This won't leak memory... too much #trust
|
||||
pluginData.state.initialUpdatedMembers = new Set();
|
||||
},
|
||||
|
||||
afterLoad(pluginData) {
|
||||
pluginData.state.saveInterval = setInterval(
|
||||
() => pluginData.state.memberCache.savePendingUpdates(),
|
||||
PENDING_SAVE_INTERVAL,
|
||||
);
|
||||
},
|
||||
|
||||
async beforeUnload(pluginData) {
|
||||
clearInterval(pluginData.state.saveInterval);
|
||||
await pluginData.state.memberCache.savePendingUpdates();
|
||||
},
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import { guildPluginEventListener } from "knub";
|
||||
import { GuildMemberCachePluginType } from "../types";
|
||||
|
||||
export const cancelDeletionOnMemberJoin = guildPluginEventListener<GuildMemberCachePluginType>()({
|
||||
event: "guildMemberAdd",
|
||||
async listener({ pluginData, args: { member } }) {
|
||||
pluginData.state.memberCache.unmarkMemberForDeletion(member.id);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import { guildPluginEventListener } from "knub";
|
||||
import { MINUTES } from "../../../utils";
|
||||
import { GuildMemberCachePluginType } from "../types";
|
||||
|
||||
const DELETION_DELAY = 2 * MINUTES;
|
||||
|
||||
export const removeMemberCacheOnMemberLeave = guildPluginEventListener<GuildMemberCachePluginType>()({
|
||||
event: "guildMemberRemove",
|
||||
async listener({ pluginData, args: { member } }) {
|
||||
pluginData.state.memberCache.markMemberForDeletion(member.id);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { AuditLogEvent } from "discord.js";
|
||||
import { guildPluginEventListener } from "knub";
|
||||
import { updateMemberCacheForMember } from "../functions/updateMemberCacheForMember";
|
||||
import { GuildMemberCachePluginType } from "../types";
|
||||
|
||||
export const updateMemberCacheOnMemberUpdate = guildPluginEventListener<GuildMemberCachePluginType>()({
|
||||
event: "guildAuditLogEntryCreate",
|
||||
async listener({ pluginData, args: { auditLogEntry } }) {
|
||||
if (auditLogEntry.action !== AuditLogEvent.MemberUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMemberCacheForMember(pluginData, auditLogEntry.targetId!);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { guildPluginEventListener } from "knub";
|
||||
import { updateMemberCacheForMember } from "../functions/updateMemberCacheForMember";
|
||||
import { GuildMemberCachePluginType } from "../types";
|
||||
|
||||
export const updateMemberCacheOnMessage = guildPluginEventListener<GuildMemberCachePluginType>()({
|
||||
event: "messageCreate",
|
||||
listener({ pluginData, args }) {
|
||||
// Update each member once per guild load when we see a message from them
|
||||
const key = `${pluginData.guild.id}-${args.message.author.id}`;
|
||||
if (pluginData.state.initialUpdatedMembers.has(key)) {
|
||||
return;
|
||||
}
|
||||
updateMemberCacheForMember(pluginData, args.message.author.id);
|
||||
pluginData.state.initialUpdatedMembers.add(key);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { AuditLogEvent } from "discord.js";
|
||||
import { guildPluginEventListener } from "knub";
|
||||
import { updateMemberCacheForMember } from "../functions/updateMemberCacheForMember";
|
||||
import { GuildMemberCachePluginType } from "../types";
|
||||
|
||||
export const updateMemberCacheOnRoleChange = guildPluginEventListener<GuildMemberCachePluginType>()({
|
||||
event: "guildAuditLogEntryCreate",
|
||||
async listener({ pluginData, args: { auditLogEntry } }) {
|
||||
if (auditLogEntry.action !== AuditLogEvent.MemberRoleUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMemberCacheForMember(pluginData, auditLogEntry.targetId!);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import { guildPluginEventListener } from "knub";
|
||||
import { updateMemberCacheForMember } from "../functions/updateMemberCacheForMember";
|
||||
import { GuildMemberCachePluginType } from "../types";
|
||||
|
||||
export const updateMemberCacheOnVoiceStateUpdate = guildPluginEventListener<GuildMemberCachePluginType>()({
|
||||
event: "voiceStateUpdate",
|
||||
listener({ pluginData, args }) {
|
||||
const memberId = args.newState.member?.id;
|
||||
if (!memberId) {
|
||||
return;
|
||||
}
|
||||
// Update each member once per guild load when we see a message from them
|
||||
const key = `${pluginData.guild.id}-${memberId}`;
|
||||
if (pluginData.state.initialUpdatedMembers.has(key)) {
|
||||
return;
|
||||
}
|
||||
updateMemberCacheForMember(pluginData, memberId);
|
||||
pluginData.state.initialUpdatedMembers.add(key);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { MemberCacheItem } from "../../../data/entities/MemberCacheItem";
|
||||
import { GuildMemberCachePluginType } from "../types";
|
||||
|
||||
export function getCachedMemberData(
|
||||
pluginData: GuildPluginData<GuildMemberCachePluginType>,
|
||||
userId: string,
|
||||
): Promise<MemberCacheItem | null> {
|
||||
return pluginData.state.memberCache.getCachedMemberData(userId);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { GuildMemberCachePluginType } from "../types";
|
||||
|
||||
export async function updateMemberCacheForMember(
|
||||
pluginData: GuildPluginData<GuildMemberCachePluginType>,
|
||||
userId: string,
|
||||
) {
|
||||
const upToDateMember = await pluginData.guild.members.fetch(userId);
|
||||
const roles = Array.from(upToDateMember.roles.cache.keys())
|
||||
// Filter out @everyone role
|
||||
.filter((roleId) => roleId !== pluginData.guild.id);
|
||||
pluginData.state.memberCache.setCachedMemberData(upToDateMember.id, {
|
||||
username: upToDateMember.user.username,
|
||||
nickname: upToDateMember.nickname,
|
||||
roles,
|
||||
});
|
||||
}
|
10
backend/src/plugins/GuildMemberCache/types.ts
Normal file
10
backend/src/plugins/GuildMemberCache/types.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { BasePluginType } from "knub";
|
||||
import { GuildMemberCache } from "../../data/GuildMemberCache";
|
||||
|
||||
export interface GuildMemberCachePluginType extends BasePluginType {
|
||||
state: {
|
||||
memberCache: GuildMemberCache;
|
||||
saveInterval: NodeJS.Timeout;
|
||||
initialUpdatedMembers: Set<string>;
|
||||
};
|
||||
}
|
|
@ -8,7 +8,7 @@ import { LogType } from "../../data/LogType";
|
|||
import { logger } from "../../logger";
|
||||
import { makeIoTsConfigParser, mapToPublicFn } from "../../pluginUtils";
|
||||
import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners";
|
||||
import { createTypedTemplateSafeValueContainer, TypedTemplateSafeValueContainer } from "../../templateFormatter";
|
||||
import { TypedTemplateSafeValueContainer, createTypedTemplateSafeValueContainer } from "../../templateFormatter";
|
||||
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
|
||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
import { LogsChannelCreateEvt, LogsChannelDeleteEvt, LogsChannelUpdateEvt } from "./events/LogsChannelModifyEvts";
|
||||
|
@ -40,6 +40,7 @@ import { onMessageUpdate } from "./util/onMessageUpdate";
|
|||
|
||||
import { escapeCodeBlock } from "discord.js";
|
||||
import { InternalPosterPlugin } from "../InternalPoster/InternalPosterPlugin";
|
||||
import { LogsGuildMemberRoleChangeEvt } from "./events/LogsGuildMemberRoleChangeEvt";
|
||||
import { logAutomodAction } from "./logFunctions/logAutomodAction";
|
||||
import { logBotAlert } from "./logFunctions/logBotAlert";
|
||||
import { logCaseCreate } from "./logFunctions/logCaseCreate";
|
||||
|
@ -173,6 +174,7 @@ export const LogsPlugin = zeppelinGuildPlugin<LogsPluginType>()({
|
|||
LogsStickerCreateEvt,
|
||||
LogsStickerDeleteEvt,
|
||||
LogsStickerUpdateEvt,
|
||||
LogsGuildMemberRoleChangeEvt,
|
||||
],
|
||||
|
||||
public: {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { APIRole, AuditLogChange, AuditLogEvent } from "discord.js";
|
||||
import { guildPluginEventListener } from "knub";
|
||||
import { resolveRole } from "../../../utils";
|
||||
import { logMemberRoleAdd } from "../logFunctions/logMemberRoleAdd";
|
||||
import { logMemberRoleRemove } from "../logFunctions/logMemberRoleRemove";
|
||||
import { LogsPluginType } from "../types";
|
||||
|
||||
type RoleAddChange = AuditLogChange & {
|
||||
key: "$add";
|
||||
new: Array<Pick<APIRole, "id" | "name">>;
|
||||
};
|
||||
|
||||
function isRoleAddChange(change: AuditLogChange): change is RoleAddChange {
|
||||
return change.key === "$add";
|
||||
}
|
||||
|
||||
type RoleRemoveChange = AuditLogChange & {
|
||||
key: "$remove";
|
||||
new: Array<Pick<APIRole, "id" | "name">>;
|
||||
};
|
||||
|
||||
function isRoleRemoveChange(change: AuditLogChange): change is RoleRemoveChange {
|
||||
return change.key === "$remove";
|
||||
}
|
||||
|
||||
export const LogsGuildMemberRoleChangeEvt = guildPluginEventListener<LogsPluginType>()({
|
||||
event: "guildAuditLogEntryCreate",
|
||||
async listener({ pluginData, args: { auditLogEntry } }) {
|
||||
// Ignore the bot's own audit log events
|
||||
if (auditLogEntry.executorId === pluginData.client.user?.id) {
|
||||
return;
|
||||
}
|
||||
if (auditLogEntry.action !== AuditLogEvent.MemberRoleUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const member = await pluginData.guild.members.fetch(auditLogEntry.targetId!);
|
||||
const mod = await pluginData.client.users.fetch(auditLogEntry.executorId!);
|
||||
for (const change of auditLogEntry.changes) {
|
||||
if (isRoleAddChange(change)) {
|
||||
const addedRoles = change.new.map((r) => resolveRole(pluginData.guild, r.id));
|
||||
logMemberRoleAdd(pluginData, {
|
||||
member,
|
||||
mod,
|
||||
roles: addedRoles,
|
||||
});
|
||||
}
|
||||
if (isRoleRemoveChange(change)) {
|
||||
const removedRoles = change.new.map((r) => resolveRole(pluginData.guild, r.id));
|
||||
logMemberRoleRemove(pluginData, {
|
||||
member,
|
||||
mod,
|
||||
roles: removedRoles,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
|
@ -1,10 +1,4 @@
|
|||
import diff from "lodash.difference";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { logMemberNickChange } from "../logFunctions/logMemberNickChange";
|
||||
import { logMemberRoleAdd } from "../logFunctions/logMemberRoleAdd";
|
||||
import { logMemberRoleChanges } from "../logFunctions/logMemberRoleChanges";
|
||||
import { logMemberRoleRemove } from "../logFunctions/logMemberRoleRemove";
|
||||
import { logsEvt } from "../types";
|
||||
|
||||
export const LogsGuildMemberUpdateEvt = logsEvt({
|
||||
|
@ -14,8 +8,6 @@ export const LogsGuildMemberUpdateEvt = logsEvt({
|
|||
const pluginData = meta.pluginData;
|
||||
const oldMember = meta.args.oldMember;
|
||||
const member = meta.args.newMember;
|
||||
const oldRoles = [...oldMember.roles.cache.keys()];
|
||||
const currentRoles = [...member.roles.cache.keys()];
|
||||
|
||||
if (!oldMember || oldMember.partial) {
|
||||
return;
|
||||
|
@ -28,62 +20,5 @@ export const LogsGuildMemberUpdateEvt = logsEvt({
|
|||
newNick: member.nickname != null ? member.nickname : "<none>",
|
||||
});
|
||||
}
|
||||
|
||||
if (!isEqual(oldRoles, currentRoles)) {
|
||||
const addedRoles = diff(currentRoles, oldRoles);
|
||||
const removedRoles = diff(oldRoles, currentRoles);
|
||||
let skip = false;
|
||||
|
||||
if (
|
||||
addedRoles.length &&
|
||||
removedRoles.length &&
|
||||
pluginData.state.guildLogs.isLogIgnored(LogType.MEMBER_ROLE_CHANGES, member.id)
|
||||
) {
|
||||
skip = true;
|
||||
} else if (addedRoles.length && pluginData.state.guildLogs.isLogIgnored(LogType.MEMBER_ROLE_ADD, member.id)) {
|
||||
skip = true;
|
||||
} else if (
|
||||
removedRoles.length &&
|
||||
pluginData.state.guildLogs.isLogIgnored(LogType.MEMBER_ROLE_REMOVE, member.id)
|
||||
) {
|
||||
skip = true;
|
||||
}
|
||||
|
||||
if (!skip) {
|
||||
if (addedRoles.length && removedRoles.length) {
|
||||
// Roles added *and* removed
|
||||
logMemberRoleChanges(pluginData, {
|
||||
member,
|
||||
addedRoles: addedRoles.map(
|
||||
(roleId) => pluginData.guild.roles.cache.get(roleId) ?? { id: roleId, name: `Unknown (${roleId})` },
|
||||
),
|
||||
removedRoles: removedRoles.map(
|
||||
(roleId) => pluginData.guild.roles.cache.get(roleId) ?? { id: roleId, name: `Unknown (${roleId})` },
|
||||
),
|
||||
mod: null,
|
||||
});
|
||||
} else if (addedRoles.length) {
|
||||
// Roles added
|
||||
logMemberRoleAdd(pluginData, {
|
||||
member,
|
||||
roles: addedRoles.map(
|
||||
(roleId) => pluginData.guild.roles.cache.get(roleId) ?? { id: roleId, name: `Unknown (${roleId})` },
|
||||
),
|
||||
mod: null,
|
||||
});
|
||||
} else if (removedRoles.length && !addedRoles.length) {
|
||||
// Roles removed
|
||||
logMemberRoleRemove(pluginData, {
|
||||
member,
|
||||
roles: removedRoles.map(
|
||||
(roleId) => pluginData.guild.roles.cache.get(roleId) ?? { id: roleId, name: `Unknown (${roleId})` },
|
||||
),
|
||||
mod: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Reimplement USERNAME_CHANGE
|
||||
|
|
|
@ -2,6 +2,7 @@ import { GuildMember, Role, User } from "discord.js";
|
|||
import { GuildPluginData } from "knub";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { createTypedTemplateSafeValueContainer } from "../../../templateFormatter";
|
||||
import { UnknownRole } from "../../../utils";
|
||||
import { memberToTemplateSafeMember, userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
|
||||
import { LogsPluginType } from "../types";
|
||||
import { log } from "../util/log";
|
||||
|
@ -9,7 +10,7 @@ import { log } from "../util/log";
|
|||
interface LogMemberRoleAddData {
|
||||
mod: User | null;
|
||||
member: GuildMember;
|
||||
roles: Role[];
|
||||
roles: Array<Role | UnknownRole>;
|
||||
}
|
||||
|
||||
export function logMemberRoleAdd(pluginData: GuildPluginData<LogsPluginType>, data: LogMemberRoleAddData) {
|
||||
|
|
|
@ -14,6 +14,9 @@ interface LogMemberRoleChangesData {
|
|||
removedRoles: Role[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use logMemberRoleAdd() and logMemberRoleRemove() instead
|
||||
*/
|
||||
export function logMemberRoleChanges(pluginData: GuildPluginData<LogsPluginType>, data: LogMemberRoleChangesData) {
|
||||
return log(
|
||||
pluginData,
|
||||
|
|
|
@ -2,6 +2,7 @@ import { GuildMember, Role, User } from "discord.js";
|
|||
import { GuildPluginData } from "knub";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { createTypedTemplateSafeValueContainer } from "../../../templateFormatter";
|
||||
import { UnknownRole } from "../../../utils";
|
||||
import { memberToTemplateSafeMember, userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
|
||||
import { LogsPluginType } from "../types";
|
||||
import { log } from "../util/log";
|
||||
|
@ -9,7 +10,7 @@ import { log } from "../util/log";
|
|||
interface LogMemberRoleRemoveData {
|
||||
mod: User | null;
|
||||
member: GuildMember;
|
||||
roles: Role[];
|
||||
roles: Array<Role | UnknownRole>;
|
||||
}
|
||||
|
||||
export function logMemberRoleRemove(pluginData: GuildPluginData<LogsPluginType>, data: LogMemberRoleRemoveData) {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { Snowflake } from "discord.js";
|
||||
import moment from "moment-timezone";
|
||||
import { MuteTypes } from "../../../data/MuteTypes";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { getTimeoutExpiryTime } from "../functions/getTimeoutExpiryTime";
|
||||
import { mutesEvt } from "../types";
|
||||
|
||||
|
@ -18,15 +17,9 @@ export const ReapplyActiveMuteOnJoinEvt = mutesEvt({
|
|||
}
|
||||
|
||||
if (mute.type === MuteTypes.Role) {
|
||||
const muteRole = pluginData.config.get().mute_role;
|
||||
|
||||
if (muteRole) {
|
||||
const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
|
||||
try {
|
||||
await member.roles.add(muteRole as Snowflake);
|
||||
} finally {
|
||||
memberRoleLock.unlock();
|
||||
}
|
||||
const muteRoleId = pluginData.config.get().mute_role;
|
||||
if (muteRoleId) {
|
||||
pluginData.getPlugin(RoleManagerPlugin).addPriorityRole(member.id, muteRoleId);
|
||||
}
|
||||
} else {
|
||||
if (!member.isCommunicationDisabled()) {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { GuildMember } from "discord.js";
|
||||
import { GuildPluginData } from "knub";
|
||||
import { MuteTypes } from "../../../data/MuteTypes";
|
||||
import { Mute } from "../../../data/entities/Mute";
|
||||
import { clearExpiringMute } from "../../../data/loops/expiringMutesLoop";
|
||||
import { MuteTypes } from "../../../data/MuteTypes";
|
||||
import { resolveMember, verboseUserMention } from "../../../utils";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { MutesPluginType } from "../types";
|
||||
|
||||
export async function clearMute(
|
||||
|
@ -23,15 +24,16 @@ export async function clearMute(
|
|||
|
||||
if (member) {
|
||||
const lock = await pluginData.locks.acquire(memberRolesLock(member));
|
||||
const roleManagerPlugin = pluginData.getPlugin(RoleManagerPlugin);
|
||||
|
||||
try {
|
||||
const defaultMuteRole = pluginData.config.get().mute_role;
|
||||
if (mute) {
|
||||
const muteRole = mute.mute_role || pluginData.config.get().mute_role;
|
||||
const muteRoleId = mute.mute_role || pluginData.config.get().mute_role;
|
||||
|
||||
if (mute.type === MuteTypes.Role) {
|
||||
if (muteRole) {
|
||||
await member.roles.remove(muteRole);
|
||||
if (muteRoleId) {
|
||||
roleManagerPlugin.removePriorityRole(member.id, muteRoleId);
|
||||
}
|
||||
} else {
|
||||
await member.timeout(null);
|
||||
|
@ -39,19 +41,18 @@ export async function clearMute(
|
|||
|
||||
if (mute.roles_to_restore) {
|
||||
const guildRoles = pluginData.guild.roles.cache;
|
||||
const newRoles = [...member.roles.cache.keys()].filter((roleId) => roleId !== muteRole);
|
||||
for (const toRestore of mute?.roles_to_restore) {
|
||||
if (guildRoles.has(toRestore) && toRestore !== muteRole && !newRoles.includes(toRestore)) {
|
||||
newRoles.push(toRestore);
|
||||
const newRoles = [...member.roles.cache.keys()].filter((roleId) => roleId !== muteRoleId);
|
||||
for (const roleIdToRestore of mute?.roles_to_restore) {
|
||||
if (guildRoles.has(roleIdToRestore) && roleIdToRestore !== muteRoleId) {
|
||||
roleManagerPlugin.addRole(member.id, roleIdToRestore);
|
||||
}
|
||||
}
|
||||
await member.roles.set(newRoles);
|
||||
}
|
||||
} else {
|
||||
// Unmuting someone without an active mute -> remove timeouts and/or mute role
|
||||
const muteRole = pluginData.config.get().mute_role;
|
||||
if (muteRole && member.roles.cache.has(muteRole)) {
|
||||
await member.roles.remove(muteRole);
|
||||
roleManagerPlugin.removePriorityRole(member.id, muteRole);
|
||||
}
|
||||
if (member.isCommunicationDisabled()) {
|
||||
await member.timeout(null);
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import { Snowflake } from "discord.js";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import { GuildPluginData } from "knub";
|
||||
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { AddMuteParams } from "../../../data/GuildMutes";
|
||||
import { MuteTypes } from "../../../data/MuteTypes";
|
||||
import { Case } from "../../../data/entities/Case";
|
||||
import { Mute } from "../../../data/entities/Mute";
|
||||
import { AddMuteParams } from "../../../data/GuildMutes";
|
||||
import { registerExpiringMute } from "../../../data/loops/expiringMutesLoop";
|
||||
import { MuteTypes } from "../../../data/MuteTypes";
|
||||
import { LogsPlugin } from "../../../plugins/Logs/LogsPlugin";
|
||||
import { ERRORS, RecoverablePluginError } from "../../../RecoverablePluginError";
|
||||
import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter";
|
||||
import { TemplateSafeValueContainer, renderTemplate } from "../../../templateFormatter";
|
||||
import {
|
||||
UserNotificationMethod,
|
||||
UserNotificationResult,
|
||||
notifyUser,
|
||||
resolveMember,
|
||||
resolveUser,
|
||||
ucfirst,
|
||||
UserNotificationMethod,
|
||||
UserNotificationResult,
|
||||
} from "../../../utils";
|
||||
import { muteLock } from "../../../utils/lockNameHelpers";
|
||||
import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { MuteOptions, MutesPluginType } from "../types";
|
||||
import { getDefaultMuteType } from "./getDefaultMuteType";
|
||||
import { getTimeoutExpiryTime } from "./getTimeoutExpiryTime";
|
||||
|
@ -91,11 +92,7 @@ export async function muteUser(
|
|||
}
|
||||
|
||||
if (muteType === MuteTypes.Role) {
|
||||
// Apply mute role if it's missing
|
||||
if (!currentUserRoles.includes(muteRole!)) {
|
||||
try {
|
||||
await member.roles.add(muteRole!);
|
||||
} catch (e) {
|
||||
// Verify the configured mute role is valid
|
||||
const actualMuteRole = pluginData.guild.roles.cache.get(muteRole!);
|
||||
if (!actualMuteRole) {
|
||||
lock.unlock();
|
||||
|
@ -105,24 +102,19 @@ export async function muteUser(
|
|||
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
|
||||
// Verify the mute role is not above Zep's roles
|
||||
const zep = await pluginData.guild.members.fetchMe();
|
||||
const zepRoles = pluginData.guild.roles.cache.filter((x) => zep.roles.cache.has(x.id));
|
||||
if (zepRoles.size === 0 || !zepRoles.some((zepRole) => zepRole.position > actualMuteRole.position)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentUserRoles.includes(muteRole!)) {
|
||||
pluginData.getPlugin(RoleManagerPlugin).addPriorityRole(member.id, muteRole!);
|
||||
}
|
||||
} else {
|
||||
await member.disableCommunicationUntil(timeoutUntil);
|
||||
|
|
|
@ -3,7 +3,9 @@ import { GuildLogs } from "../../data/GuildLogs";
|
|||
import { GuildPersistedData } from "../../data/GuildPersistedData";
|
||||
import { makeIoTsConfigParser } from "../../pluginUtils";
|
||||
import { trimPluginDescription } from "../../utils";
|
||||
import { GuildMemberCachePlugin } from "../GuildMemberCache/GuildMemberCachePlugin";
|
||||
import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin";
|
||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
import { LoadDataEvt } from "./events/LoadDataEvt";
|
||||
import { StoreDataEvt } from "./events/StoreDataEvt";
|
||||
|
@ -29,7 +31,7 @@ export const PersistPlugin = zeppelinGuildPlugin<PersistPluginType>()({
|
|||
configSchema: ConfigSchema,
|
||||
},
|
||||
|
||||
dependencies: () => [LogsPlugin],
|
||||
dependencies: () => [LogsPlugin, RoleManagerPlugin, GuildMemberCachePlugin],
|
||||
configParser: makeIoTsConfigParser(ConfigSchema),
|
||||
defaultOptions,
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ import { GuildMemberEditOptions, PermissionFlagsBits } from "discord.js";
|
|||
import intersection from "lodash.intersection";
|
||||
import { canAssignRole } from "../../../utils/canAssignRole";
|
||||
import { getMissingPermissions } from "../../../utils/getMissingPermissions";
|
||||
import { memberRolesLock } from "../../../utils/lockNameHelpers";
|
||||
import { missingPermissionError } from "../../../utils/missingPermissionError";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { persistEvt } from "../types";
|
||||
|
||||
const p = PermissionFlagsBits;
|
||||
|
@ -16,13 +16,11 @@ export const LoadDataEvt = persistEvt({
|
|||
const member = meta.args.member;
|
||||
const pluginData = meta.pluginData;
|
||||
|
||||
const memberRoleLock = await pluginData.locks.acquire(memberRolesLock(member));
|
||||
|
||||
const persistedData = await pluginData.state.persistedData.find(member.id);
|
||||
if (!persistedData) {
|
||||
memberRoleLock.unlock();
|
||||
return;
|
||||
}
|
||||
await pluginData.state.persistedData.clear(member.id);
|
||||
|
||||
const toRestore: GuildMemberEditOptions = {
|
||||
reason: "Restored upon rejoin",
|
||||
|
@ -59,29 +57,29 @@ export const LoadDataEvt = persistEvt({
|
|||
|
||||
const persistedRoles = config.persisted_roles;
|
||||
if (persistedRoles.length) {
|
||||
const roleManager = pluginData.getPlugin(RoleManagerPlugin);
|
||||
const rolesToRestore = intersection(persistedRoles, persistedData.roles, guildRoles);
|
||||
|
||||
if (rolesToRestore.length) {
|
||||
restoredData.push("roles");
|
||||
toRestore.roles = Array.from(new Set([...rolesToRestore, ...member.roles.cache.keys()]));
|
||||
for (const roleId of rolesToRestore) {
|
||||
roleManager.addRole(member.id, roleId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.persist_nicknames && persistedData.nickname) {
|
||||
restoredData.push("nickname");
|
||||
toRestore.nick = persistedData.nickname;
|
||||
await member.edit({
|
||||
nick: persistedData.nickname,
|
||||
});
|
||||
}
|
||||
|
||||
if (restoredData.length) {
|
||||
await member.edit(toRestore);
|
||||
await pluginData.state.persistedData.clear(member.id);
|
||||
|
||||
pluginData.getPlugin(LogsPlugin).logMemberRestore({
|
||||
member,
|
||||
restoredData: restoredData.join(", "),
|
||||
});
|
||||
}
|
||||
|
||||
memberRoleLock.unlock();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,34 +1,41 @@
|
|||
import { GuildMember } from "discord.js";
|
||||
import intersection from "lodash.intersection";
|
||||
import { IPartialPersistData } from "../../../data/GuildPersistedData";
|
||||
import { PersistedData } from "../../../data/entities/PersistedData";
|
||||
import { GuildMemberCachePlugin } from "../../GuildMemberCache/GuildMemberCachePlugin";
|
||||
import { persistEvt } from "../types";
|
||||
|
||||
export const StoreDataEvt = persistEvt({
|
||||
event: "guildMemberRemove",
|
||||
|
||||
async listener(meta) {
|
||||
const member = meta.args.member as GuildMember;
|
||||
const pluginData = meta.pluginData;
|
||||
|
||||
let persist = false;
|
||||
const persistData: IPartialPersistData = {};
|
||||
async listener({ pluginData, args: { member } }) {
|
||||
const config = await pluginData.config.getForUser(member.user);
|
||||
const persistData: Partial<PersistedData> = {};
|
||||
|
||||
const persistedRoles = config.persisted_roles;
|
||||
if (persistedRoles.length && member.roles) {
|
||||
const rolesToPersist = intersection(persistedRoles, [...member.roles.cache.keys()]);
|
||||
if (member.partial) {
|
||||
// Djs hasn't cached member data => use db cache
|
||||
const data = await pluginData.getPlugin(GuildMemberCachePlugin).getCachedMemberData(member.id);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rolesToPersist = config.persisted_roles.filter((roleId) => data.roles.includes(roleId));
|
||||
if (rolesToPersist.length) {
|
||||
persist = true;
|
||||
persistData.roles = rolesToPersist;
|
||||
}
|
||||
if (config.persist_nicknames && data.nickname) {
|
||||
persistData.nickname = data.nickname;
|
||||
}
|
||||
} else {
|
||||
// Djs has cached member data => use that
|
||||
const memberRoles = Array.from(member.roles.cache.keys());
|
||||
const rolesToPersist = config.persisted_roles.filter((roleId) => memberRoles.includes(roleId));
|
||||
if (rolesToPersist.length) {
|
||||
persistData.roles = rolesToPersist;
|
||||
}
|
||||
|
||||
if (config.persist_nicknames && member.nickname) {
|
||||
persist = true;
|
||||
persistData.nickname = member.nickname;
|
||||
persistData.nickname = member.nickname as any;
|
||||
}
|
||||
}
|
||||
|
||||
if (persist) {
|
||||
if (Object.keys(persistData).length) {
|
||||
pluginData.state.persistedData.set(member.id, persistData);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { GuildMember } from "discord.js";
|
||||
import { guildPluginEventListener } from "knub";
|
||||
import { SECONDS } from "../../../utils";
|
||||
import { parseCustomId } from "../../../utils/parseCustomId";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { getAllRolesInButtons } from "../functions/getAllRolesInButtons";
|
||||
import { RoleButtonsPluginType, TRoleButtonOption } from "../types";
|
||||
|
||||
const ROLE_BUTTON_CD = 5 * SECONDS;
|
||||
|
||||
export const onButtonInteraction = guildPluginEventListener<RoleButtonsPluginType>()({
|
||||
event: "interactionCreate",
|
||||
async listener({ pluginData, args }) {
|
||||
|
@ -33,6 +36,16 @@ export const onButtonInteraction = guildPluginEventListener<RoleButtonsPluginTyp
|
|||
return;
|
||||
}
|
||||
|
||||
const cdIdentifier = `${args.interaction.user.id}-${optionIndex}`;
|
||||
if (pluginData.cooldowns.isOnCooldown(cdIdentifier)) {
|
||||
args.interaction.reply({
|
||||
ephemeral: true,
|
||||
content: "Please wait before clicking the button again",
|
||||
});
|
||||
return;
|
||||
}
|
||||
pluginData.cooldowns.setCooldown(cdIdentifier, ROLE_BUTTON_CD);
|
||||
|
||||
const member = args.interaction.member as GuildMember;
|
||||
const role = pluginData.guild.roles.cache.get(option.role_id);
|
||||
const roleName = role?.name || option.role_id;
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { RoleQueueItem } from "../../../data/entities/RoleQueueItem";
|
||||
import { logger } from "../../../logger";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { RoleManagerPluginType } from "../types";
|
||||
|
||||
const ROLE_ASSIGNMENTS_PER_BATCH = 20;
|
||||
const ROLE_ASSIGNMENTS_PER_BATCH = 10;
|
||||
|
||||
export async function runRoleAssignmentLoop(pluginData: GuildPluginData<RoleManagerPluginType>) {
|
||||
if (pluginData.state.roleAssignmentLoopRunning || pluginData.state.abortRoleAssignmentLoop) {
|
||||
|
@ -30,21 +29,7 @@ export async function runRoleAssignmentLoop(pluginData: GuildPluginData<RoleMana
|
|||
return;
|
||||
}
|
||||
|
||||
// Remove assignments that cancel each other out (e.g. from spam-clicking a role button)
|
||||
const validAssignments = new Map<string, RoleQueueItem>();
|
||||
for (const assignment of nextAssignments) {
|
||||
const key = `${assignment.should_add ? 1 : 0}|${assignment.user_id}|${assignment.role_id}`;
|
||||
const oppositeKey = `${assignment.should_add ? 0 : 1}|${assignment.user_id}|${assignment.role_id}`;
|
||||
if (validAssignments.has(oppositeKey)) {
|
||||
validAssignments.delete(oppositeKey);
|
||||
continue;
|
||||
}
|
||||
validAssignments.set(key, assignment);
|
||||
}
|
||||
|
||||
// Apply batch in parallel
|
||||
await Promise.all(
|
||||
Array.from(validAssignments.values()).map(async (assignment) => {
|
||||
const member = await pluginData.guild.members.fetch(assignment.user_id).catch(() => null);
|
||||
if (!member) {
|
||||
return;
|
||||
|
@ -62,8 +47,7 @@ export async function runRoleAssignmentLoop(pluginData: GuildPluginData<RoleMana
|
|||
}\`) ${assignment.should_add ? "to" : "from"} <@!${assignment.user_id}> (\`${assignment.user_id}\`)`,
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
})());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { GuildLogs } from "../../data/GuildLogs";
|
|||
import { makeIoTsConfigParser } from "../../pluginUtils";
|
||||
import { trimPluginDescription } from "../../utils";
|
||||
import { LogsPlugin } from "../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../RoleManager/RoleManagerPlugin";
|
||||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
import { AddRoleCmd } from "./commands/AddRoleCmd";
|
||||
import { MassAddRoleCmd } from "./commands/MassAddRoleCmd";
|
||||
|
@ -43,7 +44,7 @@ export const RolesPlugin = zeppelinGuildPlugin<RolesPluginType>()({
|
|||
configSchema: ConfigSchema,
|
||||
},
|
||||
|
||||
dependencies: () => [LogsPlugin],
|
||||
dependencies: () => [LogsPlugin, RoleManagerPlugin],
|
||||
configParser: makeIoTsConfigParser(ConfigSchema),
|
||||
defaultOptions,
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { GuildChannel } from "discord.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { resolveRoleId, verboseUserMention } from "../../../utils";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { rolesCmd } from "../types";
|
||||
|
||||
export const AddRoleCmd = rolesCmd({
|
||||
|
@ -49,9 +49,7 @@ export const AddRoleCmd = rolesCmd({
|
|||
return;
|
||||
}
|
||||
|
||||
pluginData.state.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, args.member.id);
|
||||
|
||||
await args.member.roles.add(roleId);
|
||||
pluginData.getPlugin(RoleManagerPlugin).addRole(args.member.id, roleId);
|
||||
|
||||
pluginData.getPlugin(LogsPlugin).logMemberRoleAdd({
|
||||
mod: msg.author,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { GuildMember } from "discord.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { logger } from "../../../logger";
|
||||
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
|
||||
import { resolveMember, resolveRoleId, successMessage } from "../../../utils";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { rolesCmd } from "../types";
|
||||
|
||||
export const MassAddRoleCmd = rolesCmd({
|
||||
|
@ -72,8 +72,7 @@ export const MassAddRoleCmd = rolesCmd({
|
|||
|
||||
for (const member of membersWithoutTheRole) {
|
||||
try {
|
||||
pluginData.state.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, member.id);
|
||||
await member.roles.add(roleId);
|
||||
pluginData.getPlugin(RoleManagerPlugin).addRole(member.id, roleId);
|
||||
pluginData.getPlugin(LogsPlugin).logMemberRoleAdd({
|
||||
member,
|
||||
roles: [role],
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { GuildMember } from "discord.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { logger } from "../../../logger";
|
||||
import { canActOn, sendErrorMessage } from "../../../pluginUtils";
|
||||
import { resolveMember, resolveRoleId, successMessage } from "../../../utils";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { rolesCmd } from "../types";
|
||||
|
||||
export const MassRemoveRoleCmd = rolesCmd({
|
||||
|
@ -71,19 +70,13 @@ export const MassRemoveRoleCmd = rolesCmd({
|
|||
);
|
||||
|
||||
for (const member of membersWithTheRole) {
|
||||
try {
|
||||
pluginData.state.logs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, member.id);
|
||||
await member.roles.remove(roleId);
|
||||
pluginData.getPlugin(RoleManagerPlugin).removeRole(member.id, roleId);
|
||||
pluginData.getPlugin(LogsPlugin).logMemberRoleRemove({
|
||||
member,
|
||||
roles: [role],
|
||||
mod: msg.author,
|
||||
});
|
||||
assigned++;
|
||||
} catch (e) {
|
||||
logger.warn(`Error when removing role via !massremoverole: ${e.message}`);
|
||||
failed.push(member.id);
|
||||
}
|
||||
}
|
||||
|
||||
let resultMessage = `Removed role **${role.name}** from ${assigned} ${assigned === 1 ? "member" : "members"}!`;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { GuildChannel } from "discord.js";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { canActOn, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { resolveRoleId, verboseUserMention } from "../../../utils";
|
||||
import { LogsPlugin } from "../../Logs/LogsPlugin";
|
||||
import { RoleManagerPlugin } from "../../RoleManager/RoleManagerPlugin";
|
||||
import { rolesCmd } from "../types";
|
||||
|
||||
export const RemoveRoleCmd = rolesCmd({
|
||||
|
@ -49,10 +49,7 @@ export const RemoveRoleCmd = rolesCmd({
|
|||
return;
|
||||
}
|
||||
|
||||
pluginData.state.logs.ignoreLog(LogType.MEMBER_ROLE_REMOVE, args.member.id);
|
||||
|
||||
await args.member.roles.remove(roleId);
|
||||
|
||||
pluginData.getPlugin(RoleManagerPlugin).removeRole(args.member.id, roleId);
|
||||
pluginData.getPlugin(LogsPlugin).logMemberRoleRemove({
|
||||
mod: msg.author,
|
||||
member: args.member,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AutoDeletePlugin } from "./AutoDelete/AutoDeletePlugin";
|
||||
import { AutomodPlugin } from "./Automod/AutomodPlugin";
|
||||
import { AutoReactionsPlugin } from "./AutoReactions/AutoReactionsPlugin";
|
||||
import { AutomodPlugin } from "./Automod/AutomodPlugin";
|
||||
import { BotControlPlugin } from "./BotControl/BotControlPlugin";
|
||||
import { CasesPlugin } from "./Cases/CasesPlugin";
|
||||
import { CensorPlugin } from "./Censor/CensorPlugin";
|
||||
|
@ -12,6 +12,7 @@ import { CustomEventsPlugin } from "./CustomEvents/CustomEventsPlugin";
|
|||
import { GuildAccessMonitorPlugin } from "./GuildAccessMonitor/GuildAccessMonitorPlugin";
|
||||
import { GuildConfigReloaderPlugin } from "./GuildConfigReloader/GuildConfigReloaderPlugin";
|
||||
import { GuildInfoSaverPlugin } from "./GuildInfoSaver/GuildInfoSaverPlugin";
|
||||
import { GuildMemberCachePlugin } from "./GuildMemberCache/GuildMemberCachePlugin";
|
||||
import { InternalPosterPlugin } from "./InternalPoster/InternalPosterPlugin";
|
||||
import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin";
|
||||
import { LogsPlugin } from "./Logs/LogsPlugin";
|
||||
|
@ -53,6 +54,7 @@ export const guildPlugins: Array<ZeppelinGuildPluginBlueprint<any>> = [
|
|||
PostPlugin,
|
||||
ReactionRolesPlugin,
|
||||
MessageSaverPlugin,
|
||||
GuildMemberCachePlugin,
|
||||
ModActionsPlugin,
|
||||
NameHistoryPlugin,
|
||||
RemindersPlugin,
|
||||
|
@ -91,6 +93,7 @@ export const baseGuildPlugins: Array<ZeppelinGuildPluginBlueprint<any>> = [
|
|||
GuildInfoSaverPlugin,
|
||||
MessageSaverPlugin,
|
||||
NameHistoryPlugin,
|
||||
GuildMemberCachePlugin,
|
||||
CasesPlugin,
|
||||
MutesPlugin,
|
||||
TimeAndDatePlugin,
|
||||
|
|
10
backend/src/threadsSignalFix.ts
Normal file
10
backend/src/threadsSignalFix.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* Hack for wiping out the threads signal handlers
|
||||
* See: https://github.com/andywer/threads.js/issues/388
|
||||
* Make sure:
|
||||
* - This is imported before any real imports from "threads"
|
||||
* - This is imported as early as possible to avoid removing our own signal handlers
|
||||
*/
|
||||
import "threads";
|
||||
process.removeAllListeners("SIGINT");
|
||||
process.removeAllListeners("SIGTERM");
|
|
@ -20,6 +20,7 @@ import {
|
|||
MessageMentionOptions,
|
||||
PartialChannelData,
|
||||
PartialMessage,
|
||||
RoleResolvable,
|
||||
Snowflake,
|
||||
Sticker,
|
||||
TextBasedChannel,
|
||||
|
@ -680,7 +681,7 @@ export function parseInviteCodeInput(str: string): string {
|
|||
return getInviteCodesInString(str)[0];
|
||||
}
|
||||
|
||||
export function isNotNull(value): value is Exclude<typeof value, null> {
|
||||
export function isNotNull<T>(value: T): value is Exclude<T, null | undefined> {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
|
@ -1367,6 +1368,22 @@ export async function resolveRoleId(bot: Client, guildId: string, value: string)
|
|||
return null;
|
||||
}
|
||||
|
||||
export class UnknownRole {
|
||||
public id: string;
|
||||
public name: string;
|
||||
|
||||
constructor(props = {}) {
|
||||
for (const key in props) {
|
||||
this[key] = props[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRole(guild: Guild, roleResolvable: RoleResolvable) {
|
||||
const roleId = guild.roles.resolveId(roleResolvable);
|
||||
return guild.roles.resolve(roleId) ?? new UnknownRole({ id: roleId, name: roleId });
|
||||
}
|
||||
|
||||
const inviteCache = new SimpleCache<Promise<Invite | null>>(10 * MINUTES, 200);
|
||||
|
||||
type ResolveInviteReturnType<T extends boolean> = Promise<Invite | null>;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Pool, spawn, Worker } from "threads";
|
||||
import { env } from "../env";
|
||||
import "../threadsSignalFix";
|
||||
import { MINUTES } from "../utils";
|
||||
|
||||
const pool = Pool(() => spawn(new Worker("./cryptWorker"), { timeout: 10 * MINUTES }), 8);
|
||||
|
|
Loading…
Add table
Reference in a new issue