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
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ OmegaBot is a modular Discord bot designed to support development projects with
- Structured logging (pino)
- Welcome and onboarding flows triggered on member join (`guildMemberAdd`)
- Optional auto-role assignment for new members (`DISCORD_AUTO_ROLE_ID`)
- GitHub integration: issue and PR lookups plus polling-based announcements
- Check if Github is working
- GitHub integration with polling-based automation
- Health/status checks
- Issue and PR lookups
- New PR announcements
- Issue and PR assignee change announcements
- Issue and PR closed announcements
Expand Down Expand Up @@ -65,17 +66,21 @@ OmegaBot is a modular Discord bot designed to support development projects with

### Fun / utility commands

- chucknorris
- dadjoke
- coinflip
- dice
- weather
All fun commands are available under `/fun`:

- `/fun chucknorris` — Chuck Norris facts (random, category, or search)
- `/fun dadjoke` — Random or searched dad jokes
- `/fun coinflip` — Heads or tails
- `/fun dice` — Custom dice rolls
- `/fun weather` — Daily weather
- `/fun weather7` — 7-day forecast
- `/fun leaderboard` — Track fun command usage and top users

## Planned features

- /docs command for documentation lookups
- GitHub issues and pull request lookups
- Pull request announcements
- `/docs` command for documentation lookups
- Expanded GitHub automation (labels, reviews, merge events)
- Enhanced fun leaderboard views and stats
- Improved summary output (highlights, action items, structured sections)

---
Expand Down
26 changes: 19 additions & 7 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@

- `/ping` — Verify the bot is online and measure latency

## Summary & History

- `/summary` — Summarize recent messages (local or LLM mode)
- `/history` — DM recent channel history (file fallback if too long)
- `/playback` — Page through recent messages using buttons
- `/pagination` — Inline paginated view of recent messages

## GitHub

- `/gh issue` — Fetch a GitHub issue by number
Expand All @@ -19,6 +12,25 @@
- `/gh status` — Show GitHub integration status (configuration, polling, and channels)
- `/pr` — Fetch a single pull request by number (legacy shortcut)

## Fun

All fun commands are grouped under `/fun`.

- `/fun chucknorris` — Chuck Norris facts
- `/fun dadjoke` — Dad jokes (random or search)
- `/fun coinflip` — Flip a coin
- `/fun dice` — Roll dice with custom sides/count
- `/fun weather` — Today’s weather
- `/fun weather7` — 7-day forecast
- `/fun leaderboard` — Fun command usage leaderboard

## Summary & History

- `/summary` — Summarize recent messages (local or LLM mode)
- `/history` — DM recent channel history (file fallback if too long)
- `/playback` — Page through recent messages using buttons
- `/pagination` — Inline paginated view of recent messages

## Timezone

- `/timezone set` — Save your IANA timezone
Expand Down
180 changes: 175 additions & 5 deletions src/commands/fun/fun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ import { run as runDice } from "./subcommands/dice.js";
import { run as runWeather } from "./subcommands/weather.js";
import type { TempUnit, WeatherMode } from "../../services/weather/types.js";

import { run as runCoinflip } from "./subcommands/coinflip.js";
import { run as runPoll } from "./subcommands/poll.js";
import { run as runJava } from "./subcommands/java.js";

import { run as runLeaderboard } from "./subcommands/leaderboard.js";
import type { LeaderboardMode } from "./subcommands/leaderboard.js";

import { recordFunUsage, type FunCommandKey } from "./funUsageStore.js";

function parseTempUnit(raw: string | null): TempUnit {
return raw?.toLowerCase() === "c" ? "c" : "f";
}
Expand Down Expand Up @@ -90,6 +99,54 @@ export const data = new SlashCommandBuilder()
),
)

// /fun coinflip
.addSubcommand((s) =>
s
.setName("coinflip")
.setDescription("Flip a coin")
.addBooleanOption((o) =>
o
.setName("ephemeral")
.setDescription("Only show the result to you")
.setRequired(false),
),
)

// /fun java
.addSubcommand((s) =>
s
.setName("java")
.setDescription("Random Java jokes ☕")
.addBooleanOption((o) =>
o
.setName("ephemeral")
.setDescription("Only show the result to you")
.setRequired(false),
),
)

// /fun poll (2–4 options)
.addSubcommand((s) =>
s
.setName("poll")
.setDescription("Create a quick poll (2–4 options)")
.addStringOption((o) =>
o.setName("question").setDescription("Poll question").setRequired(true),
)
.addStringOption((o) =>
o.setName("option1").setDescription("Option 1").setRequired(true),
)
.addStringOption((o) =>
o.setName("option2").setDescription("Option 2").setRequired(true),
)
.addStringOption((o) =>
o.setName("option3").setDescription("Option 3 (optional)").setRequired(false),
)
.addStringOption((o) =>
o.setName("option4").setDescription("Option 4 (optional)").setRequired(false),
),
)

// /fun weather (daily)
.addSubcommand((s) =>
s
Expand Down Expand Up @@ -140,10 +197,75 @@ export const data = new SlashCommandBuilder()
.setDescription("Only show the result to you")
.setRequired(false),
),
)

// /fun leaderboard
.addSubcommand((s) =>
s
.setName("leaderboard")
.setDescription("Show fun command leaderboard")
.addStringOption((o) =>
o
.setName("view")
.setDescription("What leaderboard view to show")
.setRequired(false)
.addChoices(
{ name: "Top users", value: "users" },
{ name: "Top commands", value: "commands" },
{ name: "Single user", value: "user" },
),
)
.addUserOption((o) =>
o
.setName("user")
.setDescription("User to inspect (used with view: Single user)")
.setRequired(false),
)
.addIntegerOption((o) =>
o
.setName("limit")
.setDescription("How many results to show (default 10, max 25)")
.setMinValue(1)
.setMaxValue(25)
.setRequired(false),
),
);

function funKeyFromSub(sub: string): FunCommandKey | null {
const allowed: Record<string, FunCommandKey> = {
chucknorris: "chucknorris",
dadjoke: "dadjoke",
dice: "dice",
coinflip: "coinflip",
java: "java",
poll: "poll",
weather: "weather",
weather7: "weather7",
leaderboard: "leaderboard",
};

return allowed[sub] ?? null;
}

async function maybeRecordUsage(
interaction: ChatInputCommandInteraction,
sub: string,
): Promise<void> {
const key = funKeyFromSub(sub);
if (!key) return;

try {
await recordFunUsage({ userId: interaction.user.id, command: key });
} catch (err) {
// Do not fail the command if usage tracking fails
logger.warn({ err, sub }, "[fun] failed to record usage");
}
}

export async function execute(interaction: ChatInputCommandInteraction): Promise<void> {
const sub = interaction.options.getSubcommand(true);

// NOTE: most subcommands accept "ephemeral". Poll does not.
const ephemeral = interaction.options.getBoolean("ephemeral") ?? false;

// Parent command owns the interaction lifecycle
Expand All @@ -160,19 +282,65 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise
? { kind: "category", category }
: { kind: "random" };

return await runChuckNorris(interaction, mode);
await runChuckNorris(interaction, mode);
await maybeRecordUsage(interaction, sub);
return;
}

if (sub === "dadjoke") {
const query = interaction.options.getString("search")?.trim() ?? "";
// NOTE: option name is "query" (not "search")
const query = interaction.options.getString("query")?.trim() ?? "";

const mode: DadJokeMode = query ? { kind: "search", query } : { kind: "random" };

return await runDadJoke(interaction, mode);
await runDadJoke(interaction, mode);
await maybeRecordUsage(interaction, sub);
return;
}

if (sub === "dice") {
return await runDice(interaction);
await runDice(interaction);
await maybeRecordUsage(interaction, sub);
return;
}

if (sub === "coinflip") {
await runCoinflip(interaction);
await maybeRecordUsage(interaction, sub);
return;
}

if (sub === "java") {
await runJava(interaction);
await maybeRecordUsage(interaction, sub);
return;
}

if (sub === "poll") {
await runPoll(interaction);
await maybeRecordUsage(interaction, sub);
return;
}

if (sub === "leaderboard") {
const view = interaction.options.getString("view") ?? "users";
const limit = interaction.options.getInteger("limit") ?? 10;

if (view === "commands") {
const mode: LeaderboardMode = { kind: "commands", limit };
await runLeaderboard(interaction, mode);
} else if (view === "user") {
const u = interaction.options.getUser("user");
const targetId = u?.id ?? interaction.user.id;
const mode: LeaderboardMode = { kind: "user", userId: targetId };
await runLeaderboard(interaction, mode);
} else {
const mode: LeaderboardMode = { kind: "users", limit };
await runLeaderboard(interaction, mode);
}

await maybeRecordUsage(interaction, sub);
return;
}

if (sub === "weather" || sub === "weather7") {
Expand All @@ -184,7 +352,9 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise
? { kind: "daily", location, unit }
: { kind: "7day", location, unit };

return await runWeather(interaction, mode);
await runWeather(interaction, mode);
await maybeRecordUsage(interaction, sub);
return;
}

await interaction.editReply("Unknown subcommand.");
Expand Down
Loading