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: configuration message #25

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4e14010
refactor: add structures for configuration message
goestav Aug 3, 2024
abc55d3
refactor: add configuration command
goestav Aug 3, 2024
fb59dfd
refactor: add basic settings view
goestav Aug 4, 2024
29fc638
refactor: support different tables
goestav Aug 6, 2024
1fb0c0c
refactor: split command up into subcommands
goestav Aug 6, 2024
ae3cd5e
refactor: add components and move settings to embed
goestav Aug 6, 2024
7a05539
refactor: remove unused type imports
goestav Aug 7, 2024
db5823a
refactor: handle text option interactions
goestav Aug 7, 2024
3959140
refactor: increase formatter line width to 120 characters
goestav Aug 8, 2024
ae857c4
refactor: remove unused imports
goestav Aug 8, 2024
83fe92e
refactor: simplify switch logic for the configuration component
goestav Aug 9, 2024
7953d1c
refactor: add required property to configuration manifest option
goestav Aug 9, 2024
3bbaf16
refactor: make not set text more consistent for paragraph text options
goestav Aug 9, 2024
513926f
refactor: use string select menu for configuration
goestav Aug 17, 2024
135779c
refactor: rename interaction-handlers to prompt-user-input
goestav Aug 18, 2024
9c1a7eb
refactor: rename getModalInput to promptModalValue
goestav Aug 18, 2024
9dab68e
refactor: remove unused imports
goestav Aug 18, 2024
0c0b9e3
refactor: add find placeholder utility
goestav Aug 18, 2024
08d6ca4
refactor: add placeholder replace utility function
goestav Aug 23, 2024
014823f
test: add test for invalid characters inside placeholder name
goestav Aug 23, 2024
ea5098f
refactor: update input placeholders for text configuration options
goestav Aug 23, 2024
4bfb77f
refactor: configuration command WIP
goestav Dec 7, 2024
fa46e26
build: bump better-sqlite3 dependency
goestav Dec 7, 2024
94b082b
build: bump discord.js dependency
goestav Dec 7, 2024
4b07c49
refactor: favor `subtext` from discord.js over custom `smallText` uti…
goestav Dec 7, 2024
7470493
refactor: track bun lockfile for version control
goestav Dec 16, 2024
5b21c17
refactor: fix configuration set logic + improve interaction reply mes…
goestav Jan 6, 2025
6ca0600
build: add common-tags dependency
goestav Jan 6, 2025
f0c7fdf
refactor: handle role configuration options
goestav Jan 6, 2025
4799395
refactor: improve interaction response message for value reset
goestav Jan 6, 2025
7f4480c
refactor: gracefully handle collector timeout
goestav Jan 6, 2025
e9f60c9
Merge branch 'main' into feature/configuration-message
MorganVonBrylan Jan 8, 2025
2b182be
refactor: favor bun's built-in test runner over vitest
goestav Jan 11, 2025
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
.DS_Store

node_modules
bun.lockb

.env.*
!.env.example
Expand Down
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
}
},
"formatter": {
"indentStyle": "tab"
"indentStyle": "tab",
"lineWidth": 120
}
}
Binary file added bun.lockb
Binary file not shown.
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
"type": "module",
"devDependencies": {
"@biomejs/biome": "^1.4.1",
"better-sqlite3": "^9.2.2",
"@types/common-tags": "^1.8.4",
"better-sqlite3": "^11.6.0",
"bun-types": "^1.0.20",
"drizzle-kit": "^0.20.9"
"drizzle-kit": "^0.20.9",
"vitest": "^2.0.5"
},
"author": "Jacob Jackson",
"license": "GPL-3.0-or-later",
"dependencies": {
"bufferutil": "^4.0.8",
"cheerio": "^1.0.0-rc.12",
"discord.js": "^14.14.1",
"common-tags": "^1.8.2",
"discord.js": "^14.16.3",
"djs-fsrouter": "^0.0.12",
"drizzle-orm": "^0.29.2",
"entities-decode": "^2.0.0",
Expand Down
72 changes: 72 additions & 0 deletions src/commands/config/gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { PermissionFlagsBits, TextInputStyle } from "discord.js";
import { Config } from "../../schemas/config.ts";
import { createConfigurationManifest } from "../../structures/index.ts";
import { ConfigurationMessage } from "../../structures/index.ts";
import type { Command } from "djs-fsrouter";
import { eq } from "drizzle-orm";
import { checkIsValidTextChannel } from "../../utils/index.ts";

const manifest = createConfigurationManifest(Config, [
{
name: "Gateway channel",
description: "New members will be welcomed here.",
column: "gatewayChannel",
type: "channel",
placeholder: "Select a gateway channel",
validate: checkIsValidTextChannel,
},
// Join
{
name: "Gateway join title",
description: "Message title when a user joins.",
column: "gatewayJoinTitle",
type: "text",
placeholder: "Welcome [mention]!",
},
{
name: "Gateway join content",
description: "Message content when a user joins.",
column: "gatewayJoinContent",
type: "text",
placeholder: "We hope you enjoy your stay!",
style: TextInputStyle.Paragraph,
},
// Leave
{
name: "Gateway leave title",
description: "Message title when a user leaves.",
column: "gatewayLeaveTitle",
type: "text",
placeholder: "Goodbye [mention]!",
},
{
name: "Gateway leave content",
description: "Message content when a user leaves.",
column: "gatewayLeaveContent",
type: "text",
placeholder: "We are sorry to see you go [mention]",
style: TextInputStyle.Paragraph,
},
]);

const ConfigCommand: Command = {
description: "Configure the gateway",
defaultMemberPermissions: PermissionFlagsBits.Administrator,
async run(interaction) {
if (!interaction.inGuild()) {
interaction.reply({
content: "Run this command in a server to get server info",
ephemeral: true,
});
return;
}

const configurationMessage = new ConfigurationMessage(manifest, {
getWhereClause: ({ table, interaction }) => eq(table.id, interaction.guildId),
});

await configurationMessage.initialize(interaction);
},
};

export default ConfigCommand;
66 changes: 66 additions & 0 deletions src/commands/config/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { PermissionFlagsBits, type Channel } from "discord.js";
import { Config } from "../../schemas/config.ts";
import { createConfigurationManifest } from "../../structures/index.ts";
import { ConfigurationMessage } from "../../structures/index.ts";
import { checkIsValidTextChannel } from "../../utils/index.ts";
import type { Command } from "djs-fsrouter";
import { eq } from "drizzle-orm";
import { LogMode } from "../../types/logging.ts";

const LogModeValues = Object.keys(LogMode).filter((item) => !Number.isNaN(Number(item)));

const LogModeSelectOptions = Object.entries(LogMode)
.filter(([, value]) => typeof value === "number")
.map(([key, value]) => ({ label: key, value: value.toString() }));

const manifest = createConfigurationManifest(Config, [
{
name: "Logging mode",
description: "Determines what should be logged.",
column: "loggingMode",
type: "select",
placeholder: "Select a logging mode",
options: LogModeSelectOptions,
validate(value) {
if (!LogModeValues.includes(value)) return "The provided logging mode is invalid";

return true;
},
toDatabase(value): number {
return Number.parseInt(value);
},
fromDatabase(value): string {
return value ? (value as number).toString() : "";
},
},
{
name: "Logging channel",
description: "Log messages will be sent here.",
column: "loggingChannel",
type: "channel",
placeholder: "Select a logging channel",
validate: checkIsValidTextChannel,
},
]);

const ConfigCommand: Command = {
description: "Configure suggestion management",
defaultMemberPermissions: PermissionFlagsBits.Administrator,
async run(interaction) {
if (!interaction.inGuild()) {
interaction.reply({
content: "Run this command in a server to get server info",
ephemeral: true,
});
return;
}

const configurationMessage = new ConfigurationMessage(manifest, {
getWhereClause: ({ table, interaction }) => eq(table.id, interaction.guildId),
});

await configurationMessage.initialize(interaction);
},
};

export default ConfigCommand;
63 changes: 63 additions & 0 deletions src/commands/config/suggestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { PermissionFlagsBits } from "discord.js";
import { Config } from "../../schemas/config.ts";
import { createConfigurationManifest } from "../../structures/index.ts";
import { ConfigurationMessage } from "../../structures/index.ts";
import { checkIsValidTextChannel } from "../../utils/index.ts";
import type { Command } from "djs-fsrouter";
import { eq } from "drizzle-orm";

const manifest = createConfigurationManifest(Config, [
{
name: "Suggestion channel",
description: "Suggestions will be sent here.",
column: "suggestionChannel",
type: "channel",
placeholder: "Select a suggestion channel",
validate: checkIsValidTextChannel,
},
{
name: "Suggestion manager role",
description: "The role that can approve and reject suggestions.",
column: "suggestionManagerRole",
type: "role",
placeholder: "Select a manager role",
},
{
name: "Suggestion upvote emoji",
description: "The emoji for upvoting suggestions.",
column: "suggestionUpvoteEmoji",
type: "text",
label: "Set upvote emoji",
emoji: "👍",
},
{
name: "Suggestion downvote emoji",
description: "The emoji for downvoting suggestions.",
column: "suggestionDownvoteEmoji",
type: "text",
label: "Set downvote emoji",
emoji: "👎",
},
]);

const ConfigCommand: Command = {
description: "Configure suggestion management",
defaultMemberPermissions: PermissionFlagsBits.Administrator,
async run(interaction) {
if (!interaction.inGuild()) {
interaction.reply({
content: "Run this command in a server to get server info",
ephemeral: true,
});
return;
}

const configurationMessage = new ConfigurationMessage(manifest, {
getWhereClause: ({ table, interaction }) => eq(table.id, interaction.guildId),
});

await configurationMessage.initialize(interaction);
},
};

export default ConfigCommand;
6 changes: 1 addition & 5 deletions src/commands/delete-and-warn.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
PermissionFlagsBits,
ApplicationCommandType,
TextInputStyle,
} from "discord.js";
import { PermissionFlagsBits, ApplicationCommandType, TextInputStyle } from "discord.js";
const { ManageMessages, ModerateMembers } = PermissionFlagsBits;
import { modalInput } from "../components.ts";
import type { MessageCommand } from "djs-fsrouter";
Expand Down
15 changes: 3 additions & 12 deletions src/commands/info.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
type ChatInputCommandInteraction,
type APIEmbed,
ApplicationCommandType,
channelMention,
} from "discord.js";
import { type ChatInputCommandInteraction, type APIEmbed, ApplicationCommandType, channelMention } from "discord.js";
import type { Command } from "djs-fsrouter";

export const type = ApplicationCommandType.ChatInput;
Expand Down Expand Up @@ -35,15 +30,11 @@ JavaScripters is a well known JavaScript focused server with over 10k members`,
},
{
name: "Rules channel",
value: interaction.guild?.rulesChannelId
? channelMention(interaction.guild?.rulesChannelId)
: "None",
value: interaction.guild?.rulesChannelId ? channelMention(interaction.guild?.rulesChannelId) : "None",
},
{
name: "Created",
value: `<t:${Math.floor(
(interaction.guild?.createdTimestamp as number) / 1000,
)}:d>`,
value: `<t:${Math.floor((interaction.guild?.createdTimestamp as number) / 1000)}:d>`,
},
],
};
Expand Down
2 changes: 1 addition & 1 deletion src/commands/logging/$info.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export default {
description: "Handles logging deleted & edited messages",
dmPermission: false,
defaultMemberPermissions: "0",
};
};
36 changes: 8 additions & 28 deletions src/commands/logging/clear.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { getConfig } from "../../logging.ts";

import {
ApplicationCommandOptionType,
ApplicationCommandType,
GuildMessageManager,
Message,
User,
} from "discord.js";
import { ApplicationCommandOptionType, ApplicationCommandType, GuildMessageManager, Message, User } from "discord.js";
import type { Command } from "djs-fsrouter";
import { deleteColor, editColor } from "../../listeners/logging.ts";

Expand All @@ -29,15 +23,10 @@ const Config: Command = {

await interaction.deferReply().catch(console.error);
const { channel } = getConfig(guild) || {};
if (!channel)
return interaction
.editReply("Error: No logging channel has been set.")
.catch(console.error);
if (!channel) return interaction.editReply("Error: No logging channel has been set.").catch(console.error);
const logs = await guild.channels.fetch(channel);
if (!logs || !logs.isTextBased())
return interaction
.editReply("Error: Could not retrieve the logging channel.")
.catch(console.error);
return interaction.editReply("Error: Could not retrieve the logging channel.").catch(console.error);

const target = interaction.options.getUser("user", true);
const targetMention = target.toString();
Expand All @@ -52,8 +41,7 @@ const Config: Command = {
if (member !== me || !embeds.length) return false;

const [{ description: embed, color }] = embeds;
if (!embed || (color !== deleteColor && color !== editColor))
return false;
if (!embed || (color !== deleteColor && color !== editColor)) return false;
if (embeds.length > 1) {
bulkPurges++;
promises.push(purgeBulk(target, message));
Expand All @@ -66,15 +54,10 @@ const Config: Command = {
if (targetLogs.size) toDelete.push(...targetLogs.values());
}

for (let i = 0; i < toDelete.length; i += 100)
promises.push(logs.bulkDelete(toDelete.slice(i, i + 100)));
for (let i = 0; i < toDelete.length; i += 100) promises.push(logs.bulkDelete(toDelete.slice(i, i + 100)));

await Promise.allSettled(promises);
interaction
.editReply(
`Erased ${toDelete.length} logs and purged ${bulkPurges} bulk logs.`,
)
.catch(console.error);
interaction.editReply(`Erased ${toDelete.length} logs and purged ${bulkPurges} bulk logs.`).catch(console.error);
},
};
export default Config;
Expand All @@ -84,9 +67,7 @@ async function* fetchTill14days(messageManager: GuildMessageManager) {
let chunk = await messageManager.fetch({ limit: 100, cache: false });
let last = chunk.last();
while (last) {
yield chunk.filter(
({ createdTimestamp }) => now - createdTimestamp < _14_DAYS,
);
yield chunk.filter(({ createdTimestamp }) => now - createdTimestamp < _14_DAYS);

if (now - last.createdTimestamp > _14_DAYS) return;

Expand All @@ -107,6 +88,5 @@ async function* fetchTill14days(messageManager: GuildMessageManager) {
*/
function purgeBulk({ tag }: User, message: Message) {
const embeds = message.embeds.filter(({ author }) => author?.name !== tag);
if (embeds.length !== message.embeds.length)
return embeds.length ? message.edit({ embeds }) : message.delete();
if (embeds.length !== message.embeds.length) return embeds.length ? message.edit({ embeds }) : message.delete();
}
8 changes: 2 additions & 6 deletions src/commands/mdn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,8 @@ const Mdn: Command = {
: `${MDN_ROOT}${search(query, 1)[0]?.url}`;

const crawler = await scrape(url);
const intro = crawler(
".main-page-content > .section-content:first-of-type > *",
);
const links = crawler(
".main-page-content > .section-content:first-of-type a",
);
const intro = crawler(".main-page-content > .section-content:first-of-type > *");
const links = crawler(".main-page-content > .section-content:first-of-type a");
Array.prototype.forEach.call(links, makeLinkAbsolute);
let title: string = crawler("head title").text();
if (title.endsWith(" | MDN")) title = title.slice(0, -6);
Expand Down
4 changes: 1 addition & 3 deletions src/commands/purge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ const Purge: Command = {
} = channel;
const myself = members.me || (await members.fetchMe());
if (!channel.permissionsFor(myself).has(ManageMessages)) {
return reply(
"Error: I do not have the permission to delete messages in this channel.",
);
return reply("Error: I do not have the permission to delete messages in this channel.");
}

let messages = Array.from(
Expand Down
Loading