Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4814caf
refactor: add structures for configuration message
goestav Aug 3, 2024
d0ceedc
refactor: add configuration command
goestav Aug 3, 2024
4071dbb
refactor: add basic settings view
goestav Aug 4, 2024
d99c8d1
refactor: support different tables
goestav Aug 6, 2024
28bf0a3
refactor: split command up into subcommands
goestav Aug 6, 2024
5da6eee
refactor: add components and move settings to embed
goestav Aug 6, 2024
48505d5
refactor: remove unused type imports
goestav Aug 7, 2024
fa4ede1
refactor: handle text option interactions
goestav Aug 7, 2024
33293a0
refactor: increase formatter line width to 120 characters
goestav Aug 8, 2024
dc339a4
refactor: remove unused imports
goestav Aug 8, 2024
2ac43fa
refactor: simplify switch logic for the configuration component
goestav Aug 9, 2024
dbc55f4
refactor: add required property to configuration manifest option
goestav Aug 9, 2024
eada538
refactor: make not set text more consistent for paragraph text options
goestav Aug 9, 2024
f70c89e
refactor: use string select menu for configuration
goestav Aug 17, 2024
523d4ae
refactor: rename interaction-handlers to prompt-user-input
goestav Aug 18, 2024
031cd57
refactor: rename getModalInput to promptModalValue
goestav Aug 18, 2024
51b5761
refactor: remove unused imports
goestav Aug 18, 2024
09e1422
refactor: add find placeholder utility
goestav Aug 18, 2024
eb5e4c0
refactor: add placeholder replace utility function
goestav Aug 23, 2024
ed74b5e
test: add test for invalid characters inside placeholder name
goestav Aug 23, 2024
3e65150
refactor: update input placeholders for text configuration options
goestav Aug 23, 2024
f5f6d6f
refactor: configuration command WIP
goestav Dec 7, 2024
9e2800c
build: bump discord.js dependency
goestav Dec 7, 2024
caa6f91
refactor: favor `subtext` from discord.js over custom `smallText` uti…
goestav Dec 7, 2024
d42a15a
refactor: track bun lockfile for version control
goestav Dec 16, 2024
1362d16
refactor: fix configuration set logic + improve interaction reply mes…
goestav Jan 6, 2025
d71cad1
build: add common-tags dependency
goestav Jan 6, 2025
9f431f2
refactor: handle role configuration options
goestav Jan 6, 2025
f610ae4
refactor: improve interaction response message for value reset
goestav Jan 6, 2025
7f46129
refactor: gracefully handle collector timeout
goestav Jan 6, 2025
b69a96d
refactor: favor bun's built-in test runner over vitest
goestav Jan 11, 2025
56fcaf0
refactor: rename (guild) config database schema to guild
goestav Apr 3, 2025
62d60bb
refactor: ensure guild exists before running a configuration command
goestav Apr 3, 2025
27268b5
refactor: rename getWhereClause to getConfigurationRowFilter to impro…
goestav Jun 25, 2025
46bf30e
refactor: remove redundant and SQL utility
goestav Jun 25, 2025
8e193c0
refactor: add constant for prompt user input utility
goestav Jun 25, 2025
8407850
refactor: use ephemeral message flag instead of ephemeral boolean
goestav Jun 25, 2025
8af27e9
refactor: fix typescript errors for type imports
goestav Jun 26, 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 @@ -14,6 +14,7 @@
}
},
"formatter": {
"indentStyle": "tab"
"indentStyle": "tab",
"lineWidth": 120
}
}
Binary file added bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"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.41.0",
"entities-decode": "^2.0.0",
Expand Down
75 changes: 75 additions & 0 deletions src/commands/config/gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { MessageFlags, PermissionFlagsBits, TextInputStyle } from "discord.js";
import { GuildSchema } from "../../schemas/guild.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";
import { ensureGuild } from "#repository";

const manifest = createConfigurationManifest(GuildSchema, [
{
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",
flags: MessageFlags.Ephemeral,
});
return;
}

await ensureGuild(interaction.guildId);

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

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

export default ConfigCommand;
69 changes: 69 additions & 0 deletions src/commands/config/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { PermissionFlagsBits, type Channel } from "discord.js";
import { GuildSchema } from "../../schemas/guild.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";
import { ensureGuild } from "#repository";

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(GuildSchema, [
{
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;
}

await ensureGuild(interaction.guildId);

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

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

export default ConfigCommand;
66 changes: 66 additions & 0 deletions src/commands/config/suggestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { PermissionFlagsBits } from "discord.js";
import { GuildSchema } from "../../schemas/guild.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 { ensureGuild } from "#repository";

const manifest = createConfigurationManifest(GuildSchema, [
{
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;
}

await ensureGuild(interaction.guildId);

const configurationMessage = new ConfigurationMessage(manifest, {
getConfigurationRowFilter: ({ 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
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();
}
14 changes: 4 additions & 10 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 Expand Up @@ -97,15 +93,13 @@ setInterval(refreshIndex, 3600_000 * REFRESH_INTERVAL);

function search(term: string, limit = 10) {
if (!Number.isInteger(limit) || limit < 1 || limit > 10)
throw new RangeError(
`The number of results must be an integer between 1 and 10, inclusive (got ${limit}).`,
);
throw new RangeError(`The number of results must be an integer between 1 and 10, inclusive (got ${limit}).`);

return searcher.search(term, limit).map((id) => index[id as number]);
}

function itemToChoice({ title, url }: IndexEntry) {
const category = url.match(/^\/en-US\/docs\/([^\/]+)\//)?.[1] || "Web";
const category = url.match(/^\/en-US\/docs\/([^/]+)\//)?.[1] || "Web";
if (category !== "Web") title += ` - ${category}`;
return {
name: truncate(title, 100),
Expand Down
Loading