Skip to content
This repository was archived by the owner on May 27, 2025. It is now read-only.
Draft
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ node_modules
dist
.turbo
.env
!.env.example
.env.*
!.env.example
2 changes: 2 additions & 0 deletions apps/typicalbot/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DISCORD_APPLICATION_ID=
DISCORD_TOKEN=
24 changes: 24 additions & 0 deletions apps/typicalbot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@trpkit/typicalbot",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "tsc && tsc-alias",
"cmd:register": "node --env-file=.env dist/register.js",
"cmd:unregister": "node --env-file=.env dist/unregister.js",
"dev": "pnpm build && node --env-file=.env.dev dist/bot.js",
"dev:cmds": "pnpm build && node --env-file=.env.dev dist/register.js",
"start": "node --env-file=.env dist/index.js"
},
"dependencies": {
"discord.js": "14.19.2",
"fast-glob": "3.3.3",
"tslib": "2.8.1",
"zod": "3.24.3"
},
"devDependencies": {
"@types/node": "22.15.3",
"tsc-alias": "1.8.15",
"typescript": "5.8.3"
}
}
24 changes: 24 additions & 0 deletions apps/typicalbot/src/bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Client, GatewayIntentBits } from "discord.js";
import { env } from "./env";
import { loadEvents, registerEvents } from "./lib/EventHandler";

const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});

const events = loadEvents();
registerEvents(client, events);

(async () => {
try {
await client.login(env.DISCORD_TOKEN);
} catch (e) {
console.error(e);
await client.destroy();
}
})();
66 changes: 66 additions & 0 deletions apps/typicalbot/src/commands/moderation/ban.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createCommand } from "@/lib/CommandHandler";
import { ApplicationCommandType, MessageFlags } from "discord.js";

export default createCommand({
metadata: {
options: {
name: "ban",
description: "Ban a user from the server",
type: ApplicationCommandType.ChatInput,
options: [
{
name: "user",
description: "The user to ban",
type: 6,
required: true,
},
{
name: "reason",
description: "The reason for the ban",
type: 3,
required: false,
},
],
},
},
chatInput: async (client, interaction) => {
if (!interaction.guild) {
await interaction.reply({
content: "This command can only be used in a server.",
flags: [MessageFlags.Ephemeral],
});
return;
}

const user = interaction.options.getUser("user", true);
const reason = interaction.options.getString("reason") || "No reason provided";
const member = interaction.guild.members.cache.get(user.id);

if (!member) {
await interaction.reply({
content: "That user is not in this server.",
flags: [MessageFlags.Ephemeral],
});
return;
}

if (!member.bannable) {
await interaction.reply({
content: "I cannot ban that user.",
flags: [MessageFlags.Ephemeral],
});
return;
}

try {
await member.ban({ reason });
await interaction.reply(`Banned ${user.tag} for: ${reason}`);
} catch (error) {
console.error(error);
await interaction.reply({
content: "There was an error trying to ban that user.",
flags: [MessageFlags.Ephemeral],
});
}
},
});
66 changes: 66 additions & 0 deletions apps/typicalbot/src/commands/moderation/kick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createCommand } from "@/lib/CommandHandler";
import { ApplicationCommandType, MessageFlags } from "discord.js";

export default createCommand({
metadata: {
options: {
name: "kick",
description: "Kick a user from the server",
type: ApplicationCommandType.ChatInput,
options: [
{
name: "user",
description: "The user to kick",
type: 6,
required: true,
},
{
name: "reason",
description: "The reason for the kick",
type: 3,
required: false,
},
],
},
},
chatInput: async (client, interaction) => {
if (!interaction.guild) {
await interaction.reply({
content: "This command can only be used in a server.",
flags: [MessageFlags.Ephemeral],
});
return;
}

const user = interaction.options.getUser("user", true);
const reason = interaction.options.getString("reason") || "No reason provided";
const member = interaction.guild.members.cache.get(user.id);

if (!member) {
await interaction.reply({
content: "That user is not in this server.",
flags: [MessageFlags.Ephemeral],
});
return;
}

if (!member.kickable) {
await interaction.reply({
content: "I cannot kick that user.",
flags: [MessageFlags.Ephemeral],
});
return;
}

try {
await member.kick(reason);
await interaction.reply(`Kicked ${user.tag} for: ${reason}`);
} catch (error) {
console.error(error);
await interaction.reply({
content: "There was an error trying to kick that user.",
flags: [MessageFlags.Ephemeral],
});
}
},
});
73 changes: 73 additions & 0 deletions apps/typicalbot/src/commands/moderation/timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createCommand } from "@/lib/CommandHandler";
import { ApplicationCommandType, MessageFlags } from "discord.js";

export default createCommand({
metadata: {
options: {
name: "timeout",
description: "Timeout a user in the server",
type: ApplicationCommandType.ChatInput,
options: [
{
name: "user",
description: "The user to timeout",
type: 6,
required: true,
},
{
name: "duration",
description: "The duration of the timeout in minutes",
type: 4,
required: true,
},
{
name: "reason",
description: "The reason for the timeout",
type: 3,
required: false,
},
],
},
},
chatInput: async (client, interaction) => {
if (!interaction.guild) {
await interaction.reply({
content: "This command can only be used in a server.",
flags: [MessageFlags.Ephemeral],
});
return;
}

const user = interaction.options.getUser("user", true);
const duration = interaction.options.getInteger("duration", true);
const reason = interaction.options.getString("reason") || "No reason provided";
const member = interaction.guild.members.cache.get(user.id);

if (!member) {
await interaction.reply({
content: "That user is not in this server.",
flags: [MessageFlags.Ephemeral],
});
return;
}

if (!member.moderatable) {
await interaction.reply({
content: "I cannot timeout that user.",
flags: [MessageFlags.Ephemeral],
});
return;
}

try {
await member.timeout(duration * 60 * 1000, reason);
await interaction.reply(`Timed out ${user.tag} for ${duration} minutes. Reason: ${reason}`);
} catch (error) {
console.error(error);
await interaction.reply({
content: "There was an error trying to timeout that user.",
flags: [MessageFlags.Ephemeral],
});
}
},
});
52 changes: 52 additions & 0 deletions apps/typicalbot/src/commands/moderation/unban.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createCommand } from "@/lib/CommandHandler";
import { ApplicationCommandType, MessageFlags } from "discord.js";

export default createCommand({
metadata: {
options: {
name: "unban",
description: "Unban a user from the server",
type: ApplicationCommandType.ChatInput,
options: [
{
name: "user",
description: "The user ID to unban",
type: 3,
required: true,
},
],
},
},
chatInput: async (client, interaction) => {
if (!interaction.guild) {
await interaction.reply({
content: "This command can only be used in a server.",
flags: [MessageFlags.Ephemeral],
});
return;
}

const userId = interaction.options.getString("user", true);
const bans = await interaction.guild.bans.fetch();
const bannedUser = bans.find((ban) => ban.user.id === userId);

if (!bannedUser) {
await interaction.reply({
content: "That user is not banned from this server.",
flags: [MessageFlags.Ephemeral],
});
return;
}

try {
await interaction.guild.members.unban(userId);
await interaction.reply(`Unbanned ${bannedUser.user.tag}`);
} catch (error) {
console.error(error);
await interaction.reply({
content: "There was an error trying to unban that user.",
flags: [MessageFlags.Ephemeral],
});
}
},
});
22 changes: 22 additions & 0 deletions apps/typicalbot/src/commands/system/ping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createCommand } from "@/lib/CommandHandler";
import { ApplicationCommandType, MessageFlags } from "discord.js";

export default createCommand({
metadata: {
options: {
name: "ping",
description: "Healthcheck",
type: ApplicationCommandType.ChatInput,
},
},
chatInput: async (client, interaction) => {
const sent = await interaction.deferReply({
flags: [MessageFlags.Ephemeral],
withResponse: true,
});

await interaction.editReply({
content: `Pong! Heartbeat: ${interaction.client.ws.ping.toFixed(0)}ms | Roundtrip: ${sent.interaction.createdTimestamp - interaction.createdTimestamp}ms`,
});
},
});
27 changes: 27 additions & 0 deletions apps/typicalbot/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from "zod";

const envSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),

// Discord
DISCORD_APPLICATION_ID: z.string(),
DISCORD_TOKEN: z.string(),
});

const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
console.error(
`Missing or invalid environment variable${parsed.error.errors.length > 1 ? "s" : ""}:
${parsed.error.errors.map((error) => ` ${error.path}: ${error.message}`).join("\n")}`
);
process.exit(1);
}

const secretEnvs: Array<keyof typeof envSchema.shape> = ["DISCORD_TOKEN"];

for (const secretEnv of secretEnvs) {
delete process.env[secretEnv];
}

export const env = Object.freeze(parsed.data);
Loading