Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ To add the bot to your server, click [here](https://discord.com/oauth2/authorize
- un-signs you up for a voice channel, so you no longer get pinged
- **/ring**
- pings someone to join the voice channel you're currently in
- also available by right-clicking someone and picking Apps > Ring
- **/block**
- blocks someone, which prevents them from ringing you and makes it so you don't get pinged if they start a call
- **/unblock**
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.110.2",
"discord-embed-router": "^1.4.2",
"discord-embed-router": "^1.5.0",
"discord.js": "^14.22.1",
"dotenv": "^17.2.2",
"is-online": "^11.0.0"
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
allowBuilds:
esbuild: true
minimumReleaseAgeExclude:
- discord-embed-router@1.4.2
- discord-embed-router@1.5.0
56 changes: 0 additions & 56 deletions src/commands/commands.ts

This file was deleted.

24 changes: 24 additions & 0 deletions src/commands/context/ringUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
ApplicationCommandType,
ContextMenuCommandBuilder,
MessageFlags,
UserContextMenuCommandInteraction,
} from "discord.js";

import { RingRouter } from "@routes/types";

export const ringUser = {
data: new ContextMenuCommandBuilder()
.setName("Ring")
.setType(ApplicationCommandType.User),
async execute(
router: RingRouter,
interaction: UserContextMenuCommandInteraction,
) {
await router.dispatch(interaction, "/ring/user", {
method: "POST",
queryParams: { id: interaction.targetUser.id },
flags: [MessageFlags.Ephemeral],
});
},
};
50 changes: 50 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ringUser } from "@commands/context/ringUser";
import { about } from "@commands/slash/about";
import { catalog } from "@commands/slash/catalog";
import { defaultRingees } from "@commands/slash/defaultRingees";
import { defaultRingRecipients } from "@commands/slash/defaultRingRecipient";
import { deleteData } from "@commands/slash/deleteData";
import { block } from "@commands/slash/filter/block";
import { filter } from "@commands/slash/filter/filter";
import { unblock } from "@commands/slash/filter/unblock";
import { unwhitelist } from "@commands/slash/filter/unwhitelist";
import { whitelist } from "@commands/slash/filter/whitelist";
import { help } from "@commands/slash/help";
import { mode } from "@commands/slash/mode";
import { quit } from "@commands/slash/quit";
import { ring } from "@commands/slash/ring";
import { ringDefaults } from "@commands/slash/ringDefaults";
import { signup } from "@commands/slash/signup";
import { signuprole } from "@commands/slash/signuprole";
import { unsignup } from "@commands/slash/unsignup";
import { unsignuprole } from "@commands/slash/unsignuprole";
import {
CommandImplementation,
ContextMenuImplementation,
} from "@commands/types";

export const commands: CommandImplementation[] = [
help,
catalog,
about,
deleteData,
ring,
ringDefaults,
signup,
unsignup,
signuprole,
unsignuprole,
quit,
mode,
// filter commands
block,
unblock,
whitelist,
unwhitelist,
filter,
// default ringees
defaultRingees,
defaultRingRecipients,
];

export const contextMenuCommands: ContextMenuImplementation[] = [ringUser];
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defaultRingeesCommand } from "@commands/defaultRingees";
import { defaultRingeesCommand } from "@commands/slash/defaultRingees";

// the pre-rename name of /default_ringees: still registered so existing muscle
// memory works, but left out of /catalog and /help. Delete this file, its entry
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/commands/quit.ts → src/commands/slash/quit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SlashCommandBuilder,
} from "discord.js";

import { unsignup } from "@commands/unsignup";
import { unsignup } from "@commands/slash/unsignup";
import { RingRouter } from "@routes/types";

export const quit = {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
26 changes: 26 additions & 0 deletions src/commands/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
ChatInputCommandInteraction,
ContextMenuCommandBuilder,
SharedSlashCommand,
UserContextMenuCommandInteraction,
} from "discord.js";

import { RingRouter } from "@routes/types";

// commands are thin adapters: they parse their options and dispatch into
// the router, where all business rules live
export type CommandImplementation = {
data: SharedSlashCommand;
execute: (
router: RingRouter,
interaction: ChatInputCommandInteraction,
) => Promise<void>;
};

export type ContextMenuImplementation = {
data: ContextMenuCommandBuilder;
execute: (
router: RingRouter,
interaction: UserContextMenuCommandInteraction,
) => Promise<void>;
};
9 changes: 5 additions & 4 deletions src/deploy-commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { REST, Routes } from "discord.js";

import { commands } from "@commands/commands";
import { commands, contextMenuCommands } from "@commands/index";
import { DISCORD_CLIENT_ID, DISCORD_TOKEN } from "@config";

// Construct and prepare an instance of the REST module
Expand All @@ -9,17 +9,18 @@ const rest = new REST().setToken(DISCORD_TOKEN);
// and deploy your commands!
(async () => {
try {
const allCommands = [...commands, ...contextMenuCommands];
console.log(
`Started refreshing ${commands.length} application (/) commands.`,
`Started refreshing ${allCommands.length} application commands.`,
);

// The put method is used to fully refresh all commands in the guild with the current set
const data = await rest.put(Routes.applicationCommands(DISCORD_CLIENT_ID), {
body: commands.map((command) => command.data.toJSON()),
body: allCommands.map((command) => command.data.toJSON()),
});

console.log(
`Successfully reloaded ${(data as Array<unknown>).length} application (/) commands.`,
`Successfully reloaded ${(data as Array<unknown>).length} application commands.`,
);
} catch (error) {
console.error(error);
Expand Down
33 changes: 24 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EmbedRouter } from "discord-embed-router";
import {
ChatInputCommandInteraction,
ApplicationCommandType,
Client,
Collection,
GatewayIntentBits,
Expand All @@ -10,9 +10,13 @@ import {

import { CommandName, isCommandName } from "@commands/commandNames";
import {
CommandImplementation,
commands as commandsArray,
} from "@commands/commands";
contextMenuCommands as contextMenuCommandsArray,
} from "@commands/index";
import {
CommandImplementation,
ContextMenuImplementation,
} from "@commands/types";
import { DISCORD_TOKEN } from "@config";
import { recordError, recordUsage } from "@db/diagnostics";
import { observeRouter } from "@main/diagnostics";
Expand Down Expand Up @@ -59,6 +63,10 @@ const commands = new Collection<string, CommandImplementation>();
for (const command of commandsArray) {
commands.set(command.data.name, command);
}
const contextMenuCommands = new Collection<string, ContextMenuImplementation>();
for (const command of contextMenuCommandsArray) {
contextMenuCommands.set(command.data.name, command);
}

// When the client is ready, set status
client.once("clientReady", async () => {
Expand All @@ -69,6 +77,8 @@ client.once("clientReady", async () => {
});

(await client.application?.commands.fetch())?.forEach((command, key) => {
// only chat input commands are mentionable, so only they need ids
if (command.type !== ApplicationCommandType.ChatInput) return;
if (isCommandName(command.name)) {
commandIds.set(command.name, key);
} else {
Expand All @@ -93,17 +103,22 @@ client.on("shardResume", () => {
client.on("interactionCreate", async (interaction) => {
if (!interaction.isCommand()) return;

const command = commands.get(interaction.commandName);

if (!command) return;
const label = `COMMAND ${interaction.isChatInputCommand() ? "/" : ""}${interaction.commandName}`;

try {
if (interaction instanceof ChatInputCommandInteraction) {
recordUsage(`COMMAND /${interaction.commandName}`);
if (interaction.isChatInputCommand()) {
const command = commands.get(interaction.commandName);
if (!command) return;
recordUsage(label);
await command.execute(router, interaction);
} else if (interaction.isUserContextMenuCommand()) {
const command = contextMenuCommands.get(interaction.commandName);
if (!command) return;
recordUsage(label);
await command.execute(router, interaction);
}
} catch (error) {
recordError(`COMMAND /${interaction.commandName}`, error);
recordError(label, error);
console.error(error);
await interaction
.reply({
Expand Down
1 change: 1 addition & 0 deletions src/routes/about/about.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ vi.mock("@db/feedback", () => ({

const interaction = {
user: { id: "caller" },
isCommand: () => false,
isChatInputCommand: () => false,
} as unknown as Interaction;

Expand Down
1 change: 1 addition & 0 deletions src/routes/delete-data/delete-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ vi.mock("@db/users", () => ({

const interaction = {
user: { id: "caller" },
isCommand: () => false,
isChatInputCommand: () => false,
} as unknown as Interaction;

Expand Down
1 change: 1 addition & 0 deletions src/routes/filter/filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ vi.mock("@db/filters", async (importOriginal) => ({

const interaction = {
user: { id: "caller" },
isCommand: () => false,
isChatInputCommand: () => false,
} as unknown as Interaction;

Expand Down
12 changes: 6 additions & 6 deletions src/routes/help/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@ export const helpGet: Handler<"GET"> = (router, interaction, state) => {
const mention = (name: CommandName) => commandMention(state.globals, name);

const lines = [
`**Ring** · ${mention("ring")} pings someone to join the voice channel you're in. Save default people and ping them with ${mention("ring_defaults")}, or turn on auto-ring to ping them automatically whenever you start a call.`,
`**Sign up** · ${mention("signup")} in a voice channel's text chat signs you up, so you get pinged when someone starts a call there (joins it while it's empty). Manage several from the Signups panel.`,
`**Ring**\nRight-click someone and pick Apps > Ring to ping them into the voice channel you're in, or run ${mention("ring")}. Save your default ringees with ${mention("default_ringees")} and ping them with ${mention("ring_defaults")}, or turn on auto-ring to ping them automatically whenever you start a call.`,
`**Sign up**\n${mention("signup")} in a voice channel's text chat signs you up, so you get pinged when someone starts a call there (joins it while it's empty). Manage several from the Signups panel.`,
// role signups are a Manage Roles action, so the guidance only shows to
// members who can use it
...(canManageRoleSignups(interaction)
? [
`**Role signups** · with Manage Roles, sign a whole role up for a channel via ${mention("signuprole")} so its members get pinged.`,
`**Role signups**\nwith Manage Roles, sign a whole role up for a channel via ${mention("signuprole")} so its members get pinged.`,
]
: []),
`**Filters** · ${mention("block")} stops someone from ringing you, and ${mention("whitelist")} restricts ringing to only the people you list.`,
`**Modes** · Normal pings your signed-up people when you start a call, Stealth pings no one, Auto goes stealth only while you're invisible. Set it with ${mention("mode")}.`,
`**Filters**\n${mention("block")} stops someone from ringing you, and ${mention("whitelist")} restricts ringing to only the people you list.`,
`**Modes**\nNormal pings your signed-up people when you start a call, Stealth pings no one, Auto goes stealth only while you're invisible. Set it with ${mention("mode")}.`,
];

const description = withFlash(
state.queryParams,
"RingVC replicates group-chat calls in Discord servers. A server can't ring people the way a group chat does, so RingVC pings them in the voice channel's text chat instead.\n\n" +
`${lines.join("\n")}\n\n` +
`${lines.join("\n\n")}\n\n` +
"-# Full command list on the Catalog tab. Jump to any panel with the section menu below.",
);

Expand Down
Loading
Loading