Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ tsconfig.tsbuildinfo
.react-router
tailwind.css
userInfoCache.json
vite.config.ts*
*timestamp*
6 changes: 3 additions & 3 deletions .husky/post-merge
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ check_run() {
echo "$changed_files" | grep --quiet "$1" && eval "$2"
}

check_run package-lock.json 'echo "Deps have changed, run `npm i`"'
check_run migrations/.* 'echo "Migrations have changed, run `npm run knex migrate:latest`"'
check_run seeds/.* 'echo "Seeds have changed, run `npm run knex seed:run`"'
check_run package-lock.json 'echo "Deps have changed, run \`npm i\`"'
check_run migrations/.* 'echo "Migrations have changed, run \`npm run start:migrate\`"'
check_run seeds/.* 'echo "Seeds have changed, run \`npm run kysely:seed\`"'
5 changes: 1 addition & 4 deletions .lintstagedrc.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
export default {
"**/*.[tj]s?(x)": [
"eslint --no-warn-ignored --fix --max-warnings=0",
"prettier --check",
],
"**/*.[tj]s?(x)": ["npm run format:fix", "npm run lint:fix"],
"migrations/*.[tj]s": [
"npm run start:migrate",
"npm run generate:db-types",
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ npm i
npm run dev
```

## Development

The dev server uses Vite for intelligent hot reloading:

- **Frontend changes** (React components, routes, styles): Vite HMR updates instantly (<1s) without restarting the server
- **Server changes** (Discord bot, commands, helpers, models): Vite automatically reloads the server module to apply changes

Just run `npm run dev` and edit any file - the right reload strategy is applied automatically!

## Tech

- [Remix](https://remix.run/docs/en/v1)
Expand Down
4 changes: 2 additions & 2 deletions app/basics/login.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Form } from "react-router";
import type { ButtonHTMLAttributes } from "react";
import { Form } from "react-router";

interface LoginProps extends ButtonHTMLAttributes<Element> {
errors?: { [k: string]: string };
errors?: Record<string, string>;
redirectTo?: string;
}

Expand Down
2 changes: 1 addition & 1 deletion app/basics/logout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Form } from "react-router";
import type { ButtonHTMLAttributes } from "react";
import { Form } from "react-router";

type LoginProps = ButtonHTMLAttributes<Element>;

Expand Down
7 changes: 5 additions & 2 deletions app/commands/demo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { CommandInteraction } from "discord.js";
import { ContextMenuCommandBuilder, SlashCommandBuilder } from "discord.js";
import { ApplicationCommandType } from "discord-api-types/v10";
import {
ContextMenuCommandBuilder,
SlashCommandBuilder,
type CommandInteraction,
} from "discord.js";

export const command = new SlashCommandBuilder()
.setName("demo")
Expand Down
5 changes: 3 additions & 2 deletions app/commands/escalationControls.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { InteractionType, PermissionsBitField } from "discord.js";

import type { MessageComponentCommand } from "#~/helpers/discord";
import { applyRestriction, ban, kick, timeout } from "#~/models/discord.server";
import { fetchSettings, SETTINGS } from "#~/models/guilds.server";
import { deleteAllReportedForUser } from "#~/models/reportedMessages.server";
import { timeout, ban, kick, applyRestriction } from "#~/models/discord.server";

export const EscalationCommands = [
{
Expand Down Expand Up @@ -259,4 +260,4 @@ export const EscalationCommands = [
}
},
},
] as Array<MessageComponentCommand>;
] as MessageComponentCommand[];
11 changes: 7 additions & 4 deletions app/commands/force-ban.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { UserContextMenuCommandInteraction } from "discord.js";

import { PermissionFlagsBits, ContextMenuCommandBuilder } from "discord.js";
import { ApplicationCommandType } from "discord-api-types/v10";
import { log, trackPerformance } from "#~/helpers/observability";
import {
ContextMenuCommandBuilder,
PermissionFlagsBits,
type UserContextMenuCommandInteraction,
} from "discord.js";

import { commandStats } from "#~/helpers/metrics";
import { log, trackPerformance } from "#~/helpers/observability";

const command = new ContextMenuCommandBuilder()
.setName("Force Ban")
Expand Down
13 changes: 9 additions & 4 deletions app/commands/reacord/ModResponse.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { ComponentEventUser, ComponentEvent } from "reacord";
import { Button, ActionRow } from "reacord";
import {
ActionRow,
Button,
type ComponentEvent,
type ComponentEventUser,
} from "reacord";

import type { Resolution } from "#~/helpers/modResponse";
import {
humanReadableResolutions,
resolutions,
useVotes,
type Resolution,
} from "#~/helpers/modResponse";

const VOTES_TO_APPROVE = 3;
Expand Down Expand Up @@ -33,7 +37,7 @@ export const ModResponse = ({
label={label}
style={style}
onClick={async (event) => {
if (!event.guild?.member.roles?.includes(modRoleId)) {
if (!event.guild?.member.roles.includes(modRoleId)) {
return;
}
try {
Expand All @@ -47,6 +51,7 @@ export const ModResponse = ({
resolution,
event.user.id,
);

console.log(
`recording vote for ${resolution} from ${event.user.username}. ${leader} leads with ${voteCount} (needs ${votesRequired})`,
);
Expand Down
25 changes: 13 additions & 12 deletions app/commands/report.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { MessageContextMenuCommandInteraction } from "discord.js";
import { PermissionFlagsBits, ContextMenuCommandBuilder } from "discord.js";
import { ApplicationCommandType } from "discord-api-types/v10";
import {
ContextMenuCommandBuilder,
PermissionFlagsBits,
type MessageContextMenuCommandInteraction,
} from "discord.js";

import { commandStats } from "#~/helpers/metrics";
import { reportUser } from "#~/helpers/modLog";
import { ReportReasons } from "#~/models/reportedMessages.server";
import { log, trackPerformance } from "#~/helpers/observability";
import { commandStats } from "#~/helpers/metrics";
import { ReportReasons } from "#~/models/reportedMessages.server";

const command = new ContextMenuCommandBuilder()
.setName("Report")
Expand All @@ -20,7 +24,7 @@ const handler = async (interaction: MessageContextMenuCommandInteraction) => {
log("info", "Commands", "Report command executed", {
guildId: interaction.guildId,
reporterUserId: interaction.user.id,
targetUserId: message.author?.id,
targetUserId: message.author.id,
targetMessageId: message.id,
channelId: interaction.channelId,
});
Expand All @@ -35,16 +39,13 @@ const handler = async (interaction: MessageContextMenuCommandInteraction) => {
log("info", "Commands", "Report submitted successfully", {
guildId: interaction.guildId,
reporterUserId: interaction.user.id,
targetUserId: message.author?.id,
targetUserId: message.author.id,
targetMessageId: message.id,
reason: ReportReasons.anonReport,
});

// Track successful report in business analytics
commandStats.reportSubmitted(
interaction,
message.author?.id ?? "unknown",
);
commandStats.reportSubmitted(interaction, message.author.id);

// Track command success
commandStats.commandExecuted(interaction, "report", true);
Expand All @@ -59,7 +60,7 @@ const handler = async (interaction: MessageContextMenuCommandInteraction) => {
log("error", "Commands", "Report command failed", {
guildId: interaction.guildId,
reporterUserId: interaction.user.id,
targetUserId: message.author?.id,
targetUserId: message.author.id,
targetMessageId: message.id,
error: err.message,
stack: err.stack,
Expand All @@ -78,7 +79,7 @@ const handler = async (interaction: MessageContextMenuCommandInteraction) => {
commandName: "report",
guildId: interaction.guildId,
reporterUserId: interaction.user.id,
targetUserId: interaction.targetMessage.author?.id,
targetUserId: interaction.targetMessage.author.id,
},
);
};
Expand Down
11 changes: 7 additions & 4 deletions app/commands/setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { ChatInputCommandInteraction } from "discord.js";
import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
import {
PermissionFlagsBits,
SlashCommandBuilder,
type ChatInputCommandInteraction,
} from "discord.js";

import { SETTINGS, setSettings, registerGuild } from "#~/models/guilds.server";
import { log, trackPerformance } from "#~/helpers/observability";
import { commandStats } from "#~/helpers/metrics";
import { log, trackPerformance } from "#~/helpers/observability";
import { registerGuild, setSettings, SETTINGS } from "#~/models/guilds.server";

const command = new SlashCommandBuilder()
.setName("setup")
Expand Down
60 changes: 35 additions & 25 deletions app/commands/setupTickets.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
import { TextChannel, type ChatInputCommandInteraction } from "discord.js";
import { format } from "date-fns";
import { Routes, TextInputStyle } from "discord-api-types/v10";
import {
ChannelType,
ComponentType,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
PermissionFlagsBits,
SlashCommandBuilder,
MessageFlags,
ChannelType,
ComponentType,
InteractionType,
MessageFlags,
ModalBuilder,
PermissionFlagsBits,
SlashCommandBuilder,
TextInputBuilder,
type ChatInputCommandInteraction,
} from "discord.js";

import { REST } from "@discordjs/rest";
import { Routes, TextInputStyle } from "discord-api-types/v10";

import { discordToken } from "#~/helpers/env.server";
import { SETTINGS, fetchSettings } from "#~/models/guilds.server";
import { format } from "date-fns";
import type {
AnyCommand,
MessageComponentCommand,
ModalCommand,
SlashCommand,
} from "#~/helpers/discord";
import { quoteMessageContent } from "#~/helpers/discord";
import db from "#~/db.server.js";
import {
quoteMessageContent,
type AnyCommand,
type MessageComponentCommand,
type ModalCommand,
type SlashCommand,
} from "#~/helpers/discord";
import { discordToken } from "#~/helpers/env.server";
import { fetchSettings, SETTINGS } from "#~/models/guilds.server";

const rest = new REST({ version: "10" }).setToken(discordToken);

Expand Down Expand Up @@ -70,7 +71,7 @@ export const Command = [
const pingableRole = interaction.options.getRole("role");
const ticketChannel = interaction.options.getChannel("channel");
const buttonText =
interaction.options.getString("button-text") || DEFAULT_BUTTON_TEXT;
interaction.options.getString("button-text") ?? DEFAULT_BUTTON_TEXT;

if (ticketChannel && ticketChannel.type !== ChannelType.GuildText) {
await interaction.reply({
Expand Down Expand Up @@ -146,7 +147,6 @@ export const Command = [
if (
!interaction.channel ||
interaction.channel.type !== ChannelType.GuildText ||
!interaction.user ||
!interaction.guild ||
!interaction.message
) {
Expand All @@ -157,7 +157,7 @@ export const Command = [
return;
}
const { channel, fields, user } = interaction;
const concern = fields.getField("concern").value;
const concern = fields.getTextInputValue("concern");
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed from fields.getField('concern').value to fields.getTextInputValue('concern'). Verify this API exists in the Discord.js version being used. The getTextInputValue method may not be available in older versions.

Copilot uses AI. Check for mistakes.

let config = await db
.selectFrom("tickets_config")
Expand All @@ -180,12 +180,22 @@ export const Command = [
}
}

// If channel_id is configured but fetch returns null (channel deleted),
// this will error, which is intended - the configured channel is invalid
const ticketsChannel = config.channel_id
? ((await interaction.guild.channels.fetch(
config.channel_id,
)) as TextChannel) || channel
? await interaction.guild.channels.fetch(config.channel_id)
: channel;

if (
!ticketsChannel?.isTextBased() ||
ticketsChannel.type !== ChannelType.GuildText
) {
void interaction.reply(
"Couldn’t make a ticket! Tell the admins that their ticket channel is misconfigured.",
);
return;
}

const thread = await ticketsChannel.threads.create({
name: `${user.username} – ${format(new Date(), "PP kk:mmX")}`,
autoArchiveDuration: 60 * 24 * 7,
Expand Down Expand Up @@ -218,7 +228,7 @@ ${quoteMessageContent(concern)}`);
],
});

interaction.reply({
void interaction.reply({
content: `A private thread with the moderation team has been opened for you: <#${thread.id}>`,
ephemeral: true,
});
Expand Down Expand Up @@ -267,4 +277,4 @@ ${quoteMessageContent(concern)}`);
return;
},
} as MessageComponentCommand,
] as Array<AnyCommand>;
] as AnyCommand[];
15 changes: 9 additions & 6 deletions app/commands/track.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { MessageContextMenuCommandInteraction } from "discord.js";
import { PermissionFlagsBits, ContextMenuCommandBuilder } from "discord.js";
import { ApplicationCommandType } from "discord-api-types/v10";
import {
ContextMenuCommandBuilder,
PermissionFlagsBits,
type MessageContextMenuCommandInteraction,
} from "discord.js";
import { Button } from "reacord";
import { reacord } from "#~/discord/client.server";

import { reacord } from "#~/discord/client.server";
import { reportUser } from "#~/helpers/modLog";
import {
ReportReasons,
markMessageAsDeleted,
ReportReasons,
} from "#~/models/reportedMessages.server";

const command = new ContextMenuCommandBuilder()
Expand All @@ -24,7 +27,7 @@ const handler = async (interaction: MessageContextMenuCommandInteraction) => {
staff: user,
});

const instance = await reacord.ephemeralReply(
const instance = reacord.ephemeralReply(
interaction,
<>
Tracked
Expand All @@ -46,7 +49,7 @@ const handler = async (interaction: MessageContextMenuCommandInteraction) => {
content: `deleted by ${user.username}`,
}),
]);
instance.render(`Tracked ${thread ? `<#${thread.id}>` : ""}`);
instance.render(`Tracked <#${thread.id}>`);
}}
/>
</>,
Expand Down
Loading
Loading