Discord Integration for Jotihunt
Learn how to integrate Jotihunt with Discord using either a full bot or simple webhooks
This guide shows you how to integrate Jotihunt with Discord. There are two approaches, each with its own benefits:
Discord Bot
A full-featured bot with interactive commands, real-time updates, and complete control over functionality. Best for teams that need advanced features and can handle hosting.
Webhook Integration
A simple notification system with no hosting required. Perfect for teams that just need basic updates without interactive features.
Feature Comparison
Choose the integration method that best fits your team’s needs and technical capabilities. Below we compare both approaches in detail.
Both methods support core features like area status updates and article notifications. The main difference lies in the ability to interact with users and implement custom features.
Best for teams that want full control and interactive features
- Interactive Commands - Respond to user queries and commands
- Real-time Updates - Instant notifications for game events
- Rich Formatting - Full Discord embed support
- Advanced Features - Custom commands, map generation, statistics
- Complete Control - Customize every aspect of the integration
- Requires server hosting
- More complex setup
- Needs ongoing maintenance
- Technical knowledge required
Best for teams that want a simple, reliable notification system
- Simple Setup - Quick to implement
- No Hosting - Works out of the box
- Real-time Updates - Instant notifications
- Low Maintenance - Set and forget
- Basic Formatting - Support for Discord embeds
- No interactive features
- Limited customization
- One-way communication only
- Basic feature set
Getting Started
Now with that out of the way, let’s get started with the integration. You can choose between two options:
Overview
The Discord bot will help your team stay updated with:
- 🔔 Real-time notifications for new articles and hints
- 🗺️ Area status changes and updates
- 📊 Team rankings and points
- 🎯 Photo assignment tracking
- 🤖 Custom commands for quick info access
Prerequisites
Before starting, make sure you have:
- Node.js v16.9.0 or higher installed
- A Discord account and admin access to a server
- Basic knowledge of JavaScript/TypeScript
Creating the Discord Bot
Create a Discord Application
- Go to the Discord Developer Portal
- Click “New Application” and give it a name
- Navigate to the “Bot” section
- Click “Add Bot”
- Copy your bot token (keep this secret!)
Set Up the Project
Create a new directory and initialize the project:
mkdir jotihunt-discord-bot
cd jotihunt-discord-bot
npm init -y
Install required dependencies:
npm install discord.js dotenv node-fetch
Create Basic Bot Structure
Create a new file bot.js
:
require('dotenv').config();
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
]
});
client.once('ready', () => {
console.log('Bot is ready!');
});
client.login(process.env.DISCORD_TOKEN);
Create a .env
file:
Never commit your .env
file to version control. Make sure to add .env
to your .gitignore
.
DISCORD_TOKEN=your_bot_token_here
NOTIFICATION_CHANNEL=your_channel_id
Implementing Core Features
Area Status Monitor
This feature checks for area status changes and sends notifications to a designated channel.
const JOTIHUNT_API = 'https://jotihunt.nl/api/2.0';
let lastAreaStates = new Map();
async function checkAreaUpdates() {
try {
const response = await fetch(`${JOTIHUNT_API}/areas`);
const data = await response.json();
for (const area of data.data) {
const lastStatus = lastAreaStates.get(area.name);
if (lastStatus && lastStatus !== area.status) {
const embed = new EmbedBuilder()
.setTitle(`🔄 Area Status Change: ${area.name}`)
.setColor(area.status === 'red' ? '#ff0000' :
area.status === 'orange' ? '#ffa500' : '#00ff00')
.setDescription(`Status changed from ${lastStatus} to ${area.status}`)
.setFooter({ text: `Last updated: ${new Date(area.updated_at).toLocaleString()}` });
const channel = client.channels.cache.get(process.env.NOTIFICATION_CHANNEL);
await channel.send({ embeds: [embed] });
}
lastAreaStates.set(area.name, area.status);
}
} catch (error) {
console.error('Error checking area updates:', error);
}
}
// Check every 30 seconds
setInterval(checkAreaUpdates, 30000);
Article Notifications
Monitor and notify about new articles and hints:
let lastArticleId = null;
async function checkNewArticles() {
try {
const response = await fetch(`${JOTIHUNT_API}/articles`);
const data = await response.json();
if (!data.data.length) return;
const latestArticle = data.data[0];
if (lastArticleId !== null && lastArticleId !== latestArticle.id) {
const embed = new EmbedBuilder()
.setTitle(`📰 New Article: ${latestArticle.title}`)
.setDescription(latestArticle.summary || 'No summary available')
.setURL(`https://jotihunt.nl/article/${latestArticle.id}`)
.setTimestamp(new Date(latestArticle.published_at));
const channel = client.channels.cache.get(process.env.NOTIFICATION_CHANNEL);
await channel.send({ embeds: [embed] });
}
lastArticleId = latestArticle.id;
} catch (error) {
console.error('Error checking articles:', error);
}
}
// Check every minute
setInterval(checkNewArticles, 60000);
Team Information Commands
Add slash commands to query team information:
const { SlashCommandBuilder } = require('@discordjs/builders');
const commands = [
new SlashCommandBuilder()
.setName('team')
.setDescription('Get information about a team')
.addStringOption(option =>
option.setName('name')
.setDescription('Team name to search for')
.setRequired(true)),
new SlashCommandBuilder()
.setName('areas')
.setDescription('Get current status of all areas'),
].map(command => command.toJSON());
client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
const { commandName } = interaction;
if (commandName === 'team') {
const teamName = interaction.options.getString('name');
try {
const response = await fetch(`${JOTIHUNT_API}/subscriptions`);
const data = await response.json();
const team = data.data.find(t =>
t.name.toLowerCase().includes(teamName.toLowerCase())
);
if (!team) {
await interaction.reply('Team not found!');
return;
}
const embed = new EmbedBuilder()
.setTitle(`Team: ${team.name}`)
.addFields([
{ name: 'Location', value: `${team.city} (${team.postcode})` },
{ name: 'Area', value: team.area || 'Not assigned' },
{ name: 'Photo Points', value: team.photo_assignment_points?.toString() || '0' }
]);
await interaction.reply({ embeds: [embed] });
} catch (error) {
await interaction.reply('Error fetching team information!');
}
}
});
Advanced Features
Area Status Map
Create a dynamic map showing area statuses:
const Canvas = require('canvas');
async function generateAreaMap() {
const canvas = Canvas.createCanvas(800, 600);
const ctx = canvas.getContext('2d');
// Load background map image
const background = await Canvas.loadImage('map.png');
ctx.drawImage(background, 0, 0, 800, 600);
// Draw area overlays
const areas = await fetch(`${JOTIHUNT_API}/areas`).then(r => r.json());
for (const area of areas.data) {
// Add colored overlay for each area
ctx.fillStyle = area.status === 'red' ? 'rgba(255,0,0,0.3)' :
area.status === 'orange' ? 'rgba(255,165,0,0.3)' :
'rgba(0,255,0,0.3)';
// Draw area polygon (coordinates would need to be defined)
// ctx.fill();
}
return canvas.toBuffer();
}
client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
if (interaction.commandName === 'map') {
const mapBuffer = await generateAreaMap();
await interaction.reply({ files: [{ attachment: mapBuffer, name: 'map.png' }] });
}
});
Point Tracking System
Track and display team points:
class PointTracker {
constructor() {
this.points = new Map();
}
async updatePoints() {
const response = await fetch(`${JOTIHUNT_API}/subscriptions`);
const data = await response.json();
for (const team of data.data) {
this.points.set(team.name, {
photo: team.photo_assignment_points || 0,
total: this.calculateTotalPoints(team)
});
}
}
getLeaderboard() {
return Array.from(this.points.entries())
.sort((a, b) => b[1].total - a[1].total)
.slice(0, 10);
}
}
const pointTracker = new PointTracker();
// Update points every 5 minutes
setInterval(() => pointTracker.updatePoints(), 300000);
Error Handling and Rate Limiting
Implement proper error handling and respect API rate limits:
class RateLimiter {
constructor(maxRequests = 30, timeWindow = 60000) {
this.requests = [];
this.maxRequests = maxRequests;
this.timeWindow = timeWindow;
}
async checkLimit() {
const now = Date.now();
this.requests = this.requests.filter(time => now - time < this.timeWindow);
if (this.requests.length >= this.maxRequests) {
const oldestRequest = this.requests[0];
const waitTime = this.timeWindow - (now - oldestRequest);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.requests.push(now);
}
}
const rateLimiter = new RateLimiter();
async function fetchWithRateLimit(url) {
await rateLimiter.checkLimit();
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
Testing
Here’s how to test your bot’s functionality:
async function runTests() {
console.log('Running bot tests...');
// Test area status monitoring
try {
await checkAreaUpdates();
console.log('✅ Area status check passed');
} catch (error) {
console.error('❌ Area status check failed:', error);
}
// Test article notifications
try {
await checkNewArticles();
console.log('✅ Article check passed');
} catch (error) {
console.error('❌ Article check failed:', error);
}
// Test rate limiting
try {
const limiter = new RateLimiter(2, 1000);
await limiter.checkLimit();
await limiter.checkLimit();
console.log('✅ Rate limiting passed');
} catch (error) {
console.error('❌ Rate limiting failed:', error);
}
}
// Run tests when starting the bot in test mode
if (process.env.NODE_ENV === 'test') {
runTests();
}
Deployment
To deploy your bot:
- Set up a server (e.g., DigitalOcean, Heroku)
- Configure environment variables
- Set up process management (PM2)
- Enable logging and monitoring
Example PM2 configuration (ecosystem.config.js
):
module.exports = {
apps: [{
name: 'jotihunt-bot',
script: 'bot.js',
watch: true,
env: {
NODE_ENV: 'production',
DISCORD_TOKEN: 'your_token_here',
NOTIFICATION_CHANNEL: 'your_channel_id'
}
}]
};
Never commit your .env
file to version control. Make sure to add .env
to your .gitignore
.
Useful Commands
Here’s a list of available bot commands:
Command | Description | Example |
---|---|---|
/team | Get team information | /team ScoutingXYZ |
/areas | Show all area statuses | /areas |
/map | Display area status map | /map |
/points | Show team rankings | /points |
/subscribe | Subscribe to notifications | /subscribe articles |
/help | List available commands | /help |
Troubleshooting
Common issues and solutions:
- Check if the bot is online
- Verify bot permissions
- Ensure slash commands are registered
- Check error logs
- Implement proper rate limiting
- Reduce update check frequency
- Cache responses when possible
- Verify channel IDs
- Check bot permissions
- Ensure WebSocket connection is stable
Additional Resources
Remember to regularly update your bot’s dependencies and keep an eye on the Jotihunt API for any changes or updates.
If you don’t need interactive features, a webhook-based integration is simpler to set up and requires no hosting.
Features
- 📢 Automatic notifications for new articles and hints
- 🚦 Area status change alerts
- 📱 No server hosting required
- ⚡ Quick setup process
Setting Up Discord Webhook
- In your Discord server, go to Server Settings > Integrations
- Click “Create Webhook”
- Give it a name (e.g., “Jotihunt Updates”)
- Choose the channel for notifications
- Copy the webhook URL (keep it secret!)
Implementation
Create a new file webhook.js
:
require('dotenv').config();
const fetch = require('node-fetch');
const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;
const JOTIHUNT_API = 'https://jotihunt.nl/api/2.0';
// Track last seen states
let lastArticleId = null;
let lastAreaStates = new Map();
async function sendWebhook(data) {
try {
await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} catch (error) {
console.error('Error sending webhook:', error);
}
}
async function checkArticles() {
try {
const response = await fetch(`${JOTIHUNT_API}/articles`);
const data = await response.json();
if (!data.data.length) return;
const latest = data.data[0];
if (lastArticleId !== null && lastArticleId !== latest.id) {
await sendWebhook({
embeds: [{
title: `📰 New Article: ${latest.title}`,
description: latest.summary || 'No summary available',
url: `https://jotihunt.nl/article/${latest.id}`,
color: 3447003,
timestamp: new Date().toISOString()
}]
});
}
lastArticleId = latest.id;
} catch (error) {
console.error('Error checking articles:', error);
}
}
async function checkAreas() {
try {
const response = await fetch(`${JOTIHUNT_API}/areas`);
const data = await response.json();
for (const area of data.data) {
const lastStatus = lastAreaStates.get(area.name);
if (lastStatus && lastStatus !== area.status) {
await sendWebhook({
embeds: [{
title: `🔄 Area Status Change: ${area.name}`,
description: `Status changed from ${lastStatus} to ${area.status}`,
color: area.status === 'red' ? 15158332 :
area.status === 'orange' ? 15105570 : 3066993,
timestamp: new Date().toISOString()
}]
});
}
lastAreaStates.set(area.name, area.status);
}
} catch (error) {
console.error('Error checking areas:', error);
}
}
// Run checks periodically
setInterval(checkArticles, 60000); // Every minute
setInterval(checkAreas, 30000); // Every 30 seconds
Create a .env
file:
Never commit your .env
file to version control. Make sure to add .env
to your .gitignore
.
DISCORD_WEBHOOK_URL=your_webhook_url_here
Install dependencies and run:
npm init -y
npm install dotenv node-fetch
node webhook.js
The webhook-based approach is perfect for teams that just need notifications without interactive features. It’s easier to set up and maintain.