Discord Bot
Post race results to Discord automatically
Discord Bot for Race Results
Build a Discord bot that posts race results and standings to your sailing club's Discord server.
Prerequisites
- A Rhumby API key with
readscope - A Discord bot token (from the Discord Developer Portal)
- Node.js 18+
Setup
mkdir rhumby-discord-bot && cd rhumby-discord-bot
npm init -y
npm install discord.jsFull bot code
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
const RHUMBY_KEY = process.env.RHUMBY_API_KEY;
const RHUMBY_BASE = 'https://rhumby.com/api/v1';
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages]
});
async function fetchStandings(eventSlug) {
const res = await fetch(`${RHUMBY_BASE}/events/${eventSlug}/standings`, {
headers: { 'Authorization': `Bearer ${RHUMBY_KEY}` }
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
async function fetchResults(eventSlug) {
const res = await fetch(`${RHUMBY_BASE}/events/${eventSlug}/results`, {
headers: { 'Authorization': `Bearer ${RHUMBY_KEY}` }
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
function formatStandings(data) {
const embed = new EmbedBuilder()
.setTitle(`Standings: ${data.data.event}`)
.setColor(0x0066cc)
.setTimestamp();
let table = '```\n';
table += 'Pos Sailor Boat Pts\n';
table += '--- ------ ---- ---\n';
for (const row of data.data.standings.slice(0, 15)) {
const pos = String(row.position).padEnd(4);
const sailor = (row.sailor || 'Unknown').substring(0, 16).padEnd(18);
const boat = (row.boatName || '').substring(0, 16).padEnd(17);
const pts = String(row.totalPoints);
table += `${pos} ${sailor} ${boat} ${pts}\n`;
}
table += '```';
embed.setDescription(table);
return embed;
}
// Slash command: /standings <event-slug>
client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'standings') {
const slug = interaction.options.getString('event');
await interaction.deferReply();
try {
const data = await fetchStandings(slug);
const embed = formatStandings(data);
await interaction.editReply({ embeds: [embed] });
} catch (err) {
await interaction.editReply(`Failed to fetch standings: ${err.message}`);
}
}
if (interaction.commandName === 'results') {
const slug = interaction.options.getString('event');
const race = interaction.options.getInteger('race');
await interaction.deferReply();
try {
const data = await fetchResults(slug);
const raceData = data.data.races.find(r => r.raceNumber === race);
if (!raceData) {
await interaction.editReply(`Race ${race} not found.`);
return;
}
const embed = new EmbedBuilder()
.setTitle(`Race ${race} Results`)
.setColor(0x00cc66)
.setTimestamp();
let table = '```\n';
for (const r of raceData.results) {
const pos = r.penalty || String(r.finishPosition);
table += `${pos.padEnd(4)} ${(r.sailor || '').padEnd(18)} ${(r.boatName || '').padEnd(16)}\n`;
}
table += '```';
embed.setDescription(table);
await interaction.editReply({ embeds: [embed] });
} catch (err) {
await interaction.editReply(`Failed to fetch results: ${err.message}`);
}
}
});
client.login(process.env.DISCORD_TOKEN);Register slash commands
const { REST, Routes, SlashCommandBuilder } = require('discord.js');
const commands = [
new SlashCommandBuilder()
.setName('standings')
.setDescription('Get current series standings')
.addStringOption(opt =>
opt.setName('event').setDescription('Event slug').setRequired(true)
),
new SlashCommandBuilder()
.setName('results')
.setDescription('Get race results')
.addStringOption(opt =>
opt.setName('event').setDescription('Event slug').setRequired(true)
)
.addIntegerOption(opt =>
opt.setName('race').setDescription('Race number').setRequired(true)
),
];
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
rest.put(Routes.applicationCommands(process.env.DISCORD_APP_ID), {
body: commands.map(c => c.toJSON())
});Auto-post with webhooks
Instead of slash commands, use Rhumby webhooks to auto-post when results are published:
// Express endpoint for Rhumby webhook
app.post('/webhook', async (req, res) => {
// Verify signature first (see Webhook Signatures docs)
if (req.body.type === 'results.published') {
const channel = client.channels.cache.get(RESULTS_CHANNEL_ID);
const standings = await fetchStandings(req.body.data.eventSlug);
await channel.send({ embeds: [formatStandings(standings)] });
}
res.status(200).send('OK');
});