How to automate multi-platform event announcements on your MU Online server
Set up a pipeline that reads events from the GameServer (Blood Castle, Devil Square, Chaos Castle, Invasions) and automatically publishes the announcement on Discord, Telegram, and your website, without relying on an admin being logged in.
Manually announcing events like Blood Castle, Devil Square, Chaos Castle, and Invasions is one of the most repetitive — and easiest to forget — tasks in running a MU Online server. An admin who has to watch the clock to post "Devil Square in 5 minutes!" on Discord, Telegram, and the website wastes t
Manually announcing events like Blood Castle, Devil Square, Chaos Castle, and Invasions is one of the most repetitive — and easiest to forget — tasks in running a MU Online server. An admin who has to watch the clock to post "Devil Square in 5 minutes!" on Discord, Telegram, and the website wastes time and, inevitably, misses announcements during peak hours. This tutorial shows how to build a simple pipeline that reads event state directly from the database or GameServer logs and automatically publishes the announcement across all three channels, with minimal infrastructure. You'll learn the script structure, each platform's webhooks, the "T-minus" calculation before an event, and how to avoid duplicate messages.
Why automate event announcements
Events with a fixed schedule (Devil Square every hour, Blood Castle at specific times) and dynamic events (Kundun Invasion, Red Dragon) share one thing in common: players only join if they know it's happening. Communities that announce manually have inconsistent participation rates — it drops sharply at night, in the early hours, or whenever the admin is away. An automated pipeline guarantees 24/7 consistency, reduces the team's operational load, and makes the server look more professional to players.
Solution architecture
The simplest and most robust design has three components: a collector (a script that reads event state), a normalizer (turns the raw data into a standard message), and publishers (one per platform: Discord, Telegram, website). Running the collector every 30–60 seconds via cron is enough for most servers — it doesn't need to be real-time.
| Component | Function | Suggested technology |
|---|---|---|
| Collector | Reads the event table from MySQL/MSSQL or parses the GameServer log | PHP, Python, or Node.js |
| Normalizer | Converts data into {event, map, time, status} | Pure function, no I/O |
| Discord publisher | Sends an embed via Webhook | curl or an HTTP library |
| Telegram publisher | Sends a message via Bot API | curl or an HTTP library |
| Web publisher | Writes to an active_events table read by the website | Direct SQL query |
Identifying the event data source
Each emulator stores event state differently. In IGCN, many events log the time in the EVENT_LOG table or in GameServerInfo columns; in MuEMU, it's common to have a MEMB_STAT-adjacent table or a rolling log file (Event.log) recording start/end times. Before writing any script, run an exploratory query:
SELECT TOP 20 * FROM EVENT_LOG ORDER BY dtCreate DESC;
If your emulator doesn't expose this in a table, the alternative is to read the GameServer log file with a tail -f-equivalent, filtering for keywords like BloodCastle, DevilSquare, ChaosCastle, Invasion.
Calculating the advance notice (T-minus)
Announcing only once an event has already started has limited value — the player needs time to travel and get in. Ideally, calculate the scheduled time and fire warnings at T-5min and T-1min, plus the event opening. If your events are cyclical (e.g., Devil Square every 60 minutes, on the hour), the calculation is straightforward:
from datetime import datetime, timedelta
def next_cycle(interval_minutes=60):
now = datetime.now()
current_minute = now.minute
next_time = now.replace(second=0, microsecond=0) + timedelta(minutes=(interval_minutes - current_minute % interval_minutes))
return next_time
For dynamic events (Invasions triggered by a server script), there's no T-minus known in advance — in that case, the announcement is always "event started now," fired the instant the collector detects the state change.
Publishing to Discord via Webhook
The Discord Webhook is the simplest way to publish without keeping a bot running. Create the webhook under Channel Settings → Integrations → Webhooks and use the generated URL:
curl -X POST "$DISCORD_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "⚔️ Devil Square starts in 5 minutes!",
"description": "Recommended level: 380+\nLocation: Devias 2\nEntrance: NPC Devil Messenger",
"color": 15158332
}]
}'
Use embeds instead of plain text — they stand out visually and let you set color, title, and description separately, which helps players quickly identify which event is starting.
Publishing to Telegram via Bot API
Create a bot with @BotFather, grab the token and the channel/group chat_id, and publish with a simple HTTP call:
curl -X POST "https://api.telegram.org/bot$TELEGRAM_TOKEN/sendMessage" \
-d chat_id="$TELEGRAM_CHAT_ID" \
-d parse_mode="Markdown" \
-d text="*Chaos Castle* starts in 5 minutes! Recommended level: 400+."
Unlike Discord, Telegram doesn't require creating a "channel webhook" — the bot itself just needs to be an admin of the destination channel/group to be able to post.
Syncing with the website ("live event" banner)
Besides social channels, it's worth writing the current state to a simple table read by the site's front end, to show a "Devil Square live now!" banner on the homepage:
INSERT INTO active_events (event, map, start_time, end_time)
VALUES ('Devil Square', 'Devias 2', NOW(), DATE_ADD(NOW(), INTERVAL 15 MINUTE));
The front end polls this table via API every few seconds (or uses a websocket, if the stack already supports it) and shows/hides the banner as end_time expires.
Avoiding duplicate announcements
The biggest risk of a script running at short intervals is publishing the same announcement multiple times. Solve it with a control table or file that records the last "event hash" already announced (e.g., event + scheduled_time), and the collector only publishes if the hash is new:
CREATE TABLE announced_events (
event_hash VARCHAR(64) PRIMARY KEY,
announced_at DATETIME DEFAULT NOW()
);
Before publishing, try to insert the hash; if it fails with a duplicate key error, the script simply skips it and moves on — a cheap and reliable way to achieve idempotency.
Scheduling the run (cron)
On Linux, a cron job every minute is enough:
* * * * * /usr/bin/php /var/www/scripts/announce_events.php >> /var/log/mu/announcements.log 2>&1
On Windows, use Task Scheduler with a "repeat every 1 minute" trigger calling the PHP/Python script from the command line. Make sure the log is rotated (see the log cleanup tutorial) so it doesn't accumulate gigabytes of repeated runs.
Handling network failures and retries
Calls to external APIs eventually fail — Discord and Telegram have rate limits and occasional instability. Implement a simple retry with exponential backoff (1s, 2s, 4s) and a retry limit (e.g., 3), logging the final failure without blocking the rest of the flow:
| Attempt | Wait before | Action if it fails |
|---|---|---|
| 1 | 0s | Retry |
| 2 | 1s | Retry |
| 3 | 2s | Retry |
| 4 (final) | 4s | Log the failure and continue |
Customizing the message per event type
Each event has a different audience and urgency — it's worth having a template per type, with recommended level, location, and expected reward, so the player can quickly decide whether it's worth joining:
| Event | Recommended level | Typical reward |
|---|---|---|
| Blood Castle | Varies by BC level | Box of Luck, Jewels |
| Devil Square | 380+ | Jewels, rare items |
| Chaos Castle | 400+ | Chaos combination, PK reset |
| Kundun Invasion | All | Boss item drops |
| Red Dragon Invasion | All | Exclusive event boxes |
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Duplicate announcement on Discord | No idempotency control | Implement an announced-event hash table |
| No announcement is published | Query points to the wrong table/column for your emulator | Confirm the real schema with an exploratory query |
| Webhook returns 401/404 | Webhook URL expired or channel deleted | Generate a new webhook and update the environment variable |
| Message arrives late | Cron interval too long | Reduce the interval to 30–60s |
| Telegram bot doesn't post | Bot isn't an admin of the channel | Add the bot as a channel/group administrator |
| Script hangs the server | Network call without a timeout | Set a timeout of a few seconds on every HTTP call |
Checklist for event automation
- Event data source identified and tested (SQL table or log).
- Collector script running via cron/Task Scheduler at a 30–60s interval.
- Discord webhook configured and tested with a real message.
- Telegram bot created, added as admin, and tested.
- Idempotency table (
announced_events) created and working. - Website banner synced with the
active_eventstable. - Retry with backoff implemented for network failures.
- Execution logs with rotation configured.
With event announcements running on their own, your team frees up time to focus on content and support instead of watching the clock. If you're still setting up the server's foundation before automating this flow, start with the MU Online server creation tutorial.
Frequently asked questions
Do I need my own bot, or can I get away with just webhooks?
For most cases, webhooks (Discord Webhook and Telegram Bot API) do the job without needing to host a full bot. A dedicated bot is only worth it if you want interactive commands like '/nextevent' in addition to the automatic announcement.
Can the announcement warn players in advance (e.g., 5 minutes before)?
Yes, as long as your script reads the event's scheduled time (via a database table or GameServer config) instead of just the event already in progress. The tutorial shows how to calculate T-5min from the configured cycle.
Does this work with any emulator (IGCN, MuEMU, X-Team)?
The logic is the same, but the event table/column names differ between emulators. You'll need to identify where your emulator stores the event state (SQL table, log file, or shared memory) and adjust the query accordingly.
Can I announce on more than one server (different languages) with the same script?
Yes. Just parameterize the script by server/language and run one instance (or a loop) per configuration, pointing to the corresponding webhook for each community.
What happens if Discord or Telegram is down at the moment of the event?
The best approach is to implement retry with backoff and failure logging. If the call fails, log it to a file and try again in a few seconds — never let the script hang the rest of the pipeline because of a network failure.