Skip to content
Open
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
11 changes: 8 additions & 3 deletions docs-website/docs/admin-panel/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,23 @@ documented separately:

The *Game Event Publisher* plugin publishes notable events of the game to the
whole server, so that external systems can react on them, for example a Discord
integration. It's disabled by default; activate it when such a system is
connected.
integration (see [Discord notifications](../server-features/discord.md)). It's
disabled by default; activate it when such a system is connected.

It publishes:

* the opening, start and end of mini games, like Blood Castle, with the winner,
* the start and end of invasions, with the invaded maps,
* the state changes of the castle siege, with the owner of the castle,
* excellent and ancient items which are dropped by monsters,
* the kills of boss monsters,
* characters which reach a level or master level milestone,
* global notices which game masters send with the `!` chat prefix.

Each kind of event can be switched off in the plugin configuration.
Each kind of event can be switched off in the plugin configuration. There you
also define which monsters are bosses, by their monster numbers (e.g. 275 for
Kundun), and which levels and master levels are milestones (by default, level
400).

## Server announcements

Expand Down
78 changes: 78 additions & 0 deletions docs-website/docs/server-features/discord.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: Discord notifications
sidebar_label: Discord
sidebar_position: 5
description: Post events of the game, like mini games, castle siege and rare drops, to Discord channels.
---

# Discord notifications

OpenMU can post what happens in the game to the channels of your Discord server:

| Category | What is posted |
|---|---|
| `Events` | Mini games which open (e.g. Blood Castle), winners of mini games, invasions which start and end |
| `CastleSiege` | Guild registration, the start and the end of the battle, and the new owner of the castle |
| `WorldNews` | Boss kills, excellent and ancient items which monsters dropped, characters which reach a level milestone |
| `Notices` | Global notices which game masters send with the `!` chat prefix |

Each category is posted to its own channel through a Discord *webhook*. Categories
without webhook aren't posted.

:::note
This works in the all-in-one deployment. In the distributed deployment, the
events are already published, but there is no service yet which posts them to
Discord.
:::

## Setting it up

1. **Create the webhooks.** In Discord, open the settings of a channel, go to
*Integrations → Webhooks*, create a webhook and copy its URL. Do this for
each channel which should receive messages.
2. **Configure the webhooks** for the server, through environment variables,
e.g. in your `docker-compose.override.yml`:

```yaml
environment:
- Discord__Webhooks__Events=https://discord.com/api/webhooks/...
- Discord__Webhooks__CastleSiege=https://discord.com/api/webhooks/...
- Discord__Webhooks__WorldNews=https://discord.com/api/webhooks/...
- Discord__Webhooks__Notices=https://discord.com/api/webhooks/...
```

Treat the URLs like passwords: everybody who knows one can post into the
channel. That's why they are not part of the game configuration in the
database, but of the configuration of the server process.
3. **Activate the *Game Event Publisher* plugin** in the admin panel
([Plugins](../admin-panel/plugins.md#game-event-publisher)). It's disabled by
default. In its configuration, you choose which events are published, which
monsters are bosses and which levels are milestones.
4. Restart the server.

## Settings

All settings are part of the `Discord` section of the configuration, so they can
also be set in the `appsettings.json` of the server.

| Setting | Default | Description |
|---|---|---|
| `Webhooks__<Category>` | — | The webhook URL per category, see the table above. |
| `Language` | `en` | The language of the messages. English (`en`) and German (`de`) are available. Names of maps, monsters and items are translated, too. |
| `EventAnnouncingServerIds__0`, `__1`, … | all | The identifiers of the game servers which announce mini games and invasions. When several game servers run the same events, set it to one of them to avoid duplicate messages. |
| `MaximumQueuedMessages` | `100` | The number of messages per webhook which can wait to be sent. When there are more, the oldest ones are dropped. |

## Good to know

* **Times** like "the entrance closes in 5 minutes" are posted in a way that each
Discord user sees them in their own time zone and language.
* **Rate limits:** Discord accepts about 30 messages per minute per webhook.
When more happens, OpenMU combines up to 10 messages into one post and waits
for Discord, so nothing gets lost unless the queue overflows.
* **Mini games** like Blood Castle open all their levels at the same time; this
is announced only once.
* **Mentions** are disabled, so a text of a player like `@everyone` can't ping
anybody.
* Make your notices channel an *Announcement channel*: other Discord servers,
e.g. the ones of guilds, can then *follow* it and get the messages copied into
their own channel.
3 changes: 2 additions & 1 deletion docs-website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ const sidebars = {
'server-features/test-actors',
'server-features/gens',
'server-features/cash-shop',
"server-features/player-shop-currencies",
'server-features/discord',
'server-features/player-shop-currencies',
],
},
{
Expand Down
31 changes: 31 additions & 0 deletions src/Discord/DiscordChannelCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// <copyright file="DiscordChannelCategory.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Discord;

/// <summary>
/// The categories of notifications, each of which is posted to its own Discord channel.
/// </summary>
public enum DiscordChannelCategory
{
/// <summary>
/// Events like mini games and invasions.
/// </summary>
Events,

/// <summary>
/// The state changes of the castle siege.
/// </summary>
CastleSiege,

/// <summary>
/// News about players, like boss kills, rare drops and level milestones.
/// </summary>
WorldNews,

/// <summary>
/// Notices of game masters.
/// </summary>
Notices,
}
64 changes: 64 additions & 0 deletions src/Discord/DiscordEmbed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// <copyright file="DiscordEmbed.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Discord;

/// <summary>
/// A message which is posted as embed to a Discord channel.
/// </summary>
/// <param name="Title">The title.</param>
/// <param name="Description">The description, which may contain Discord markdown.</param>
/// <param name="Color">The color of the bar on the left side, as RGB value.</param>
/// <param name="TimestampUtc">The timestamp, which is shown below the message.</param>
/// <param name="Footer">The footer text.</param>
public sealed record DiscordEmbed(string Title, string Description, int Color, DateTime TimestampUtc, string? Footer)
{
/// <summary>
/// The maximum length of the title, which Discord accepts.
/// </summary>
public const int MaximumTitleLength = 256;

/// <summary>
/// The maximum length of the description, which Discord accepts.
/// </summary>
public const int MaximumDescriptionLength = 4096;

/// <summary>
/// The maximum length of the footer, which Discord accepts.
/// </summary>
public const int MaximumFooterLength = 2048;

/// <summary>
/// The maximum length of the texts of all embeds of one message, which Discord accepts.
/// </summary>
public const int MaximumTotalLength = 6000;

/// <summary>
/// Gets the length of all texts of the embed, which counts against <see cref="MaximumTotalLength"/>.
/// </summary>
public int TextLength => this.Title.Length + this.Description.Length + (this.Footer?.Length ?? 0);

/// <summary>
/// Gets the embed with texts which are cut to the lengths which Discord accepts.
/// </summary>
/// <returns>The embed, or a copy with shortened texts.</returns>
public DiscordEmbed Limit()
{
if (this.Title.Length <= MaximumTitleLength
&& this.Description.Length <= MaximumDescriptionLength
&& (this.Footer?.Length ?? 0) <= MaximumFooterLength)
{
return this;
}

return this with
{
Title = Cut(this.Title, MaximumTitleLength),
Description = Cut(this.Description, MaximumDescriptionLength),
Footer = this.Footer is { } footer ? Cut(footer, MaximumFooterLength) : null,
};
}

private static string Cut(string text, int maximumLength) => text.Length <= maximumLength ? text : text[..(maximumLength - 1)] + "…";
}
26 changes: 26 additions & 0 deletions src/Discord/DiscordHttpClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// <copyright file="DiscordHttpClient.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Discord;

using System.Net.Http;

/// <summary>
/// Creates the HTTP client for the webhooks.
/// </summary>
public static class DiscordHttpClient
{
/// <summary>
/// The time after which a request to Discord is aborted.
/// A sender waits for its request, so a hanging connection would stall its queue.
/// The senders retry a request which timed out.
/// </summary>
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);

/// <summary>
/// Creates the HTTP client. It lives as long as the process, so it isn't disposed.
/// </summary>
/// <returns>The HTTP client.</returns>
public static HttpClient Create() => new() { Timeout = Timeout };
}
Loading
Loading