Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/application commands #455

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
771ed76
feat: Context menu mod menu command
Obliie Jul 15, 2023
24b1180
fix: plugin dependencies and cleanup
Obliie Jul 15, 2023
454bec6
feat: Add user context menu commands for notes, warns, mutes and bans
Obliie Jul 15, 2023
7f2f2c8
fix: modal id conflicts causing collectors to respond to unrelated su…
Obliie Jul 15, 2023
8fcbd50
fix: interaction error handling
Obliie Jul 15, 2023
cdcca8c
style(ContextMenus): improve ux
Obliie Jul 16, 2023
26bf936
fix(ContextMenus): correct ban modal custom id
Obliie Jul 16, 2023
6689f91
feat(ContextMenus): add new field to modals for case evidence
Obliie Jul 16, 2023
740aa39
feat: add clean message context menu command
Obliie Aug 5, 2023
91339bb
Merge branch 'feat/context-menu-mod-menu' of github.com:Obliie/Zeppelin
LilyBergonzat Jan 22, 2024
dfb0e2c
WIP: Note Slash Command
LilyBergonzat Jan 22, 2024
a4c4b17
feat: first batch of emojis 🎉
LilyBergonzat Feb 14, 2024
592d037
Added Discord attachment link reaction, fixed emoji configuration and…
LilyBergonzat Feb 16, 2024
2c0e4b3
Merge branch 'master' of github.com:ZeppelinBot/Zeppelin into feat/ap…
LilyBergonzat Feb 16, 2024
cafcc28
Various fixes
LilyBergonzat Feb 17, 2024
b428e18
Fixed config parsing error with tags
LilyBergonzat Feb 17, 2024
ba65ecb
Made cases commands ephemeral by default
LilyBergonzat Feb 17, 2024
e4e7e1c
Fixed race condition
LilyBergonzat Feb 18, 2024
0fee24e
Fixed supposed-to-be-ephemeral message not being ephemeral
LilyBergonzat Feb 18, 2024
174e5cc
Fixed mute command not updating case when no reason
LilyBergonzat Feb 20, 2024
2874a0c
Fixed case type filters
LilyBergonzat Feb 22, 2024
a49bb81
Fixed ban command
LilyBergonzat Feb 22, 2024
4d8b6b5
Made confirms ephemeral and fixed slash command duration options
LilyBergonzat Feb 24, 2024
91025d8
Made mass action reason part of the slash command options
LilyBergonzat Feb 24, 2024
e879a15
Fixed incomplete attachment list for mass action message commands
LilyBergonzat Feb 24, 2024
ee861bb
Fixed fatal error for massbans
LilyBergonzat Feb 24, 2024
408f1b9
Added slash command deferral to avoid timeouts
LilyBergonzat Feb 26, 2024
7eff7bc
Fixed show option for case and cases commands
LilyBergonzat Feb 26, 2024
cbec610
Used guildPluginSlashCommand instead of raw blueprints
LilyBergonzat Mar 5, 2024
0be5491
Merge branch 'master' of github.com:ZeppelinBot/Zeppelin into feat/ap…
LilyBergonzat Apr 15, 2024
893a77d
Fixes, refactoring and PR feedback
LilyBergonzat Apr 15, 2024
1f0c7a4
Ran prettier to fix style issues
LilyBergonzat May 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions backend/src/api/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ app.use(multer().none());

const rootRouter = express.Router();

initAuth(app);
initGuildsAPI(app);
initArchives(app);
initDocs(app);
initAuth(rootRouter);
initGuildsAPI(rootRouter);
initArchives(rootRouter);
initDocs(rootRouter);

// Default route
rootRouter.get("/", (req, res) => {
Expand Down
14 changes: 3 additions & 11 deletions backend/src/configValidator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub";
import moment from "moment-timezone";
import { BaseConfig, ConfigValidationError, GuildPluginBlueprint, PluginConfigManager } from "knub";
import { ZodError } from "zod";
import { guildPlugins } from "./plugins/availablePlugins";
import { ZeppelinGuildConfig, zZeppelinGuildConfig } from "./types";
import { zZeppelinGuildConfig } from "./types";
import { formatZodIssue } from "./utils/formatZodIssue";

const pluginNameToPlugin = new Map<string, GuildPluginBlueprint<any, any>>();
Expand All @@ -16,14 +15,7 @@ export async function validateGuildConfig(config: any): Promise<string | null> {
return validationResult.error.issues.map(formatZodIssue).join("\n");
}

const guildConfig = config as ZeppelinGuildConfig;

if (guildConfig.timezone) {
const validTimezones = moment.tz.names();
if (!validTimezones.includes(guildConfig.timezone)) {
return `Invalid timezone: ${guildConfig.timezone}`;
}
}
const guildConfig = config as BaseConfig;

if (guildConfig.plugins) {
for (const [pluginName, pluginOptions] of Object.entries(guildConfig.plugins)) {
Expand Down
52 changes: 44 additions & 8 deletions backend/src/data/GuildCases.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { In, InsertResult, Repository } from "typeorm";
import { FindOptionsWhere } from "typeorm/find-options/FindOptionsWhere";
import { Queue } from "../Queue";
import { chunkArray } from "../utils";
import { BaseGuildRepository } from "./BaseGuildRepository";
Expand Down Expand Up @@ -73,34 +74,69 @@ export class GuildCases extends BaseGuildRepository {
});
}

async getByUserId(userId: string): Promise<Case[]> {
async getByUserId(
userId: string,
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "user_id"> = {},
): Promise<Case[]> {
return this.cases.find({
relations: this.getRelations(),
where: {
guild_id: this.guildId,
user_id: userId,
...filters,
},
});
}

async getTotalCasesByModId(modId: string): Promise<number> {
return this.cases.count({
async getRecentByUserId(userId: string, count: number, skip = 0): Promise<Case[]> {
return this.cases.find({
relations: this.getRelations(),
where: {
guild_id: this.guildId,
mod_id: modId,
is_hidden: false,
user_id: userId,
},
skip,
take: count,
order: {
case_number: "DESC",
},
});
}

async getRecentByModId(modId: string, count: number, skip = 0): Promise<Case[]> {
return this.cases.find({
relations: this.getRelations(),
async getTotalCasesByModId(
modId: string,
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id" | "is_hidden"> = {},
): Promise<number> {
return this.cases.count({
where: {
guild_id: this.guildId,
mod_id: modId,
is_hidden: false,
...filters,
},
});
}

async getRecentByModId(
modId: string,
count: number,
skip = 0,
filters: Omit<FindOptionsWhere<Case>, "guild_id" | "mod_id"> = {},
): Promise<Case[]> {
const where: FindOptionsWhere<Case> = {
guild_id: this.guildId,
mod_id: modId,
is_hidden: false,
...filters,
};

if (where.is_hidden === true) {
delete where.is_hidden;
}

return this.cases.find({
relations: this.getRelations(),
where,
skip,
take: count,
order: {
Expand Down
18 changes: 18 additions & 0 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,27 @@ connect().then(async () => {
if (row) {
try {
const loaded = loadYamlSafely(row.config);

if (loaded.success_emoji || loaded.error_emoji) {
const deprecatedKeys = [] as string[];
const exampleConfig = `plugins:\n common:\n config:\n success_emoji: "👍"\n error_emoji: "👎"`;

if (loaded.success_emoji) {
deprecatedKeys.push("success_emoji");
}

if (loaded.error_emoji) {
deprecatedKeys.push("error_emoji");
}

logger.warn(`Deprecated config properties found in "${key}": ${deprecatedKeys.join(", ")}`);
logger.warn(`You can now configure those emojis in the "common" plugin config\n${exampleConfig}`);
}

// Remove deprecated properties some may still have in their config
delete loaded.success_emoji;
delete loaded.error_emoji;

return loaded;
} catch (err) {
logger.error(`Error while loading config "${key}": ${err.message}`);
Expand Down
39 changes: 24 additions & 15 deletions backend/src/migrations/1573248462469-MoveStarboardsToConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ import { MigrationInterface, QueryRunner, Table, TableColumn } from "typeorm";
export class MoveStarboardsToConfig1573248462469 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
// Create a new column for the channel's id
await queryRunner.addColumn("starboard_messages", new TableColumn({
name: "starboard_channel_id",
type: "bigint",
unsigned: true,
}));
await queryRunner.addColumn(
"starboard_messages",
new TableColumn({
name: "starboard_channel_id",
type: "bigint",
unsigned: true,
}),
);

// Since we are removing the guild_id with the starboards table, we might want it here
await queryRunner.addColumn("starboard_messages", new TableColumn({
name: "guild_id",
type: "bigint",
unsigned: true,
}));
await queryRunner.addColumn(
"starboard_messages",
new TableColumn({
name: "guild_id",
type: "bigint",
unsigned: true,
}),
);

// Migrate the old starboard_id to the new starboard_channel_id
await queryRunner.query(`
Expand Down Expand Up @@ -43,11 +49,14 @@ export class MoveStarboardsToConfig1573248462469 implements MigrationInterface {
await queryRunner.dropColumn("starboard_messages", "starboard_channel_id");
await queryRunner.dropColumn("starboard_messages", "guild_id");

await queryRunner.addColumn("starboard_messages", new TableColumn({
name: "starboard_id",
type: "int",
unsigned: true,
}));
await queryRunner.addColumn(
"starboard_messages",
new TableColumn({
name: "starboard_id",
type: "int",
unsigned: true,
}),
);

await queryRunner.query(`
ALTER TABLE starboard_messages
Expand Down
93 changes: 52 additions & 41 deletions backend/src/pluginUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
*/

import {
ChatInputCommandInteraction,
GuildMember,
InteractionReplyOptions,
Message,
MessageCreateOptions,
MessageMentionOptions,
PermissionsBitField,
TextBasedChannel,
User,
} from "discord.js";
import { AnyPluginData, BasePluginData, CommandContext, ExtendedMatchParams, GuildPluginData, helpers } from "knub";
import { logger } from "./logger";
import { isStaff } from "./staff";
import { TZeppelinKnub } from "./types";
import { errorMessage, successMessage } from "./utils";
import { Tail } from "./utils/typeUtils";

const { getMemberLevel } = helpers;
Expand Down Expand Up @@ -49,46 +49,57 @@ export async function hasPermission(
return helpers.hasPermission(config, permission);
}

export async function sendSuccessMessage(
pluginData: AnyPluginData<any>,
channel: TextBasedChannel,
body: string,
allowedMentions?: MessageMentionOptions,
): Promise<Message | undefined> {
const emoji = pluginData.fullConfig.success_emoji || undefined;
const formattedBody = successMessage(body, emoji);
const content: MessageCreateOptions = allowedMentions
? { content: formattedBody, allowedMentions }
: { content: formattedBody };

return channel
.send({ ...content }) // Force line break
.catch((err) => {
const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id;
logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`);
return undefined;
});
export function isContextInteraction(
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
): context is ChatInputCommandInteraction {
return "commandId" in context && !!context.commandId;
}

export async function sendErrorMessage(
pluginData: AnyPluginData<any>,
channel: TextBasedChannel,
body: string,
allowedMentions?: MessageMentionOptions,
): Promise<Message | undefined> {
const emoji = pluginData.fullConfig.error_emoji || undefined;
const formattedBody = errorMessage(body, emoji);
const content: MessageCreateOptions = allowedMentions
? { content: formattedBody, allowedMentions }
: { content: formattedBody };

return channel
.send({ ...content }) // Force line break
.catch((err) => {
const channelInfo = "guild" in channel ? `${channel.id} (${channel.guild.id})` : channel.id;
logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`);
return undefined;
});
export function isContextMessage(
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
): context is Message {
return "content" in context || "embeds" in context;
}

export async function getContextChannel(
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
): Promise<TextBasedChannel> {
if (isContextInteraction(context)) {
// context is ChatInputCommandInteraction
return context.channel!;
} else if ("username" in context) {
// context is User
return await (context as User).createDM();
} else if ("send" in context) {
// context is TextBaseChannel
return context as TextBasedChannel;
} else {
// context is Message
return context.channel;
}
}

export async function sendContextResponse(
context: TextBasedChannel | Message | User | ChatInputCommandInteraction,
response: string | Omit<MessageCreateOptions, "flags"> | InteractionReplyOptions,
): Promise<Message> {
if (isContextInteraction(context)) {
const options = { ...(typeof response === "string" ? { content: response } : response), fetchReply: true };

return (
context.replied
? context.followUp(options)
: context.deferred
? context.editReply(options)
: context.reply(options)
) as Promise<Message>;
}

if (typeof response !== "string" && "ephemeral" in response) {
delete response.ephemeral;
}

return (await getContextChannel(context)).send(response as string | Omit<MessageCreateOptions, "flags">);
}

export function getBaseUrl(pluginData: AnyPluginData<any>) {
Expand Down
5 changes: 5 additions & 0 deletions backend/src/plugins/AutoReactions/AutoReactionsPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PluginOptions, guildPlugin } from "knub";
import { GuildAutoReactions } from "../../data/GuildAutoReactions";
import { GuildSavedMessages } from "../../data/GuildSavedMessages";
import { CommonPlugin } from "../Common/CommonPlugin";
import { LogsPlugin } from "../Logs/LogsPlugin";
import { DisableAutoReactionsCmd } from "./commands/DisableAutoReactionsCmd";
import { NewAutoReactionsCmd } from "./commands/NewAutoReactionsCmd";
Expand Down Expand Up @@ -50,4 +51,8 @@ export const AutoReactionsPlugin = guildPlugin<AutoReactionsPluginType>()({
state.autoReactions = GuildAutoReactions.getGuildInstance(guild.id);
state.cache = new Map();
},

beforeStart(pluginData) {
pluginData.state.common = pluginData.getPlugin(CommonPlugin);
},
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { commandTypeHelpers as ct } from "../../../commandTypes";
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
import { autoReactionsCmd } from "../types";

export const DisableAutoReactionsCmd = autoReactionsCmd({
Expand All @@ -14,12 +13,12 @@ export const DisableAutoReactionsCmd = autoReactionsCmd({
async run({ message: msg, args, pluginData }) {
const autoReaction = await pluginData.state.autoReactions.getForChannel(args.channelId);
if (!autoReaction) {
sendErrorMessage(pluginData, msg.channel, `Auto-reactions aren't enabled in <#${args.channelId}>`);
void pluginData.state.common.sendErrorMessage(msg, `Auto-reactions aren't enabled in <#${args.channelId}>`);
return;
}

await pluginData.state.autoReactions.removeFromChannel(args.channelId);
pluginData.state.cache.delete(args.channelId);
sendSuccessMessage(pluginData, msg.channel, `Auto-reactions disabled in <#${args.channelId}>`);
void pluginData.state.common.sendSuccessMessage(msg, `Auto-reactions disabled in <#${args.channelId}>`);
},
});
Loading
Loading