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
83 changes: 80 additions & 3 deletions docs-website/docs/server-features/discord.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
1. Open the [Discord developer portal](https://discord.com/developers/applications),
create a *New Application* and give it the name of your server.
2. Under *Bot*, click *Reset Token* and copy the token. Treat it like a password.
The bot doesn't need any of the *Privileged Gateway Intents*.
The bot doesn't need any of the *Privileged Gateway Intents*, unless the
[chat bridge](#writing-from-discord) should read the normal messages.
3. Under *OAuth2 → URL Generator*, select the scopes `bot` and
`applications.commands`, and the permissions *View Channels*, *Send Messages*,
*Embed Links* and *Read Message History*. For [`/openmu setup`](#setting-up-your-discord-server),
Expand All @@ -101,6 +102,8 @@
| `/link <code>` | Links the Discord user to the game account, see [Linking accounts](#linking-accounts) |
| `/unlink` | Removes the link to the game account |
| `/character <name>` | Selects the character of the account, as which the player appears |
| `/say <message>` | Sends a message to the chat of the game which is bound to the channel, see [Chat bridge](#chat-bridge) |
| `/guildchat bind`, `create`, `unbind` | Binds the chat of a guild or alliance to a channel, see [Chat bridge](#chat-bridge) |

The commands only show information; they don't change anything in the game.
Additionally, players can [link their account](#linking-accounts) with
Expand Down Expand Up @@ -132,8 +135,8 @@
[layout](#setting-up-your-discord-server) on your Discord server, so you can,
for example, open channels only for players. The bot needs the *Manage Roles*
permission for it, and its own role has to be above *Linked Player* in the role
list. When a link is removed, also in the admin panel of the all-in-one
deployment, the role is removed again.
list. When a link is removed, also in the admin panel, the role is removed
again.

The answers to these commands are only visible to the player who used them.
The codes are stored as hash in the database, and every code can only be used
Expand Down Expand Up @@ -207,6 +210,69 @@
members. After inviting the bot to it, `/openmu setup` adopts everything and
reports that nothing was missing.

## Chat bridge

The bot can mirror the chats of the game to Discord channels, in both directions:

* the **world chat**: players write into it in the game with `/world <message>`,
and all players of all game servers read it. It's mirrored to `#world-chat` of
the [layout](#setting-up-your-discord-server).
* the chat of a **guild** (`@` in the game) or an **alliance** (`@@`), when its
guild master binds it to a channel.

The chat of the players is private, so nothing is mirrored by default: enable
*Publish guild, alliance and world chat* in the configuration of the *Game Event
Publisher* plugin. Players who enter the game are told when the chat of their
guild or alliance is mirrored.

### Binding the chat of a guild

The guild master links the Discord user to the account (see
[Linking accounts](#linking-accounts)) and selects the guild master character
with `/character`. Then there are two ways, which you can restrict with
`Discord__Bot__ChatBridge__BindingMode`:

* **On your Discord server:** `/guildchat create` creates a channel like
`#guild-legends` in the category *Guilds* of the layout. Only the bot and the
members of the guild who linked their Discord user can see it. The bot keeps
this up to date when users link themselves and every 10 status updates. It
needs the *Manage Channels* permission for it.
* **On the Discord server of the guild:** the guild master invites the bot to
the Discord server of the guild, and uses `/guildchat bind` in the channel
which should be bound. This requires the permission to manage the channel. With
`Discord__Bot__ChatBridge__AllowedDiscordServerIds`, you can allow only some Discord
servers.

The option `scope` of these commands chooses the chat of the alliance instead,
which only the master of an alliance can bind. `/guildchat unbind` in the channel
removes the binding; the guild master and users who manage the channel can use it.
The binding is also removed when the guild is disbanded, the channel is deleted
or the bot is removed from the Discord server.

### Writing from Discord

Only Discord users who are linked to an account can write into the game, as
their selected character, which has to be a member of the guild or alliance. The
chat ban of the account applies, and a user can send 10 messages per minute
(`Discord__Bot__ChatBridge__MaximumMessagesPerMinute`). The messages go through the

Check notice on line 257 in docs-website/docs/server-features/discord.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs-website/docs/server-features/discord.md#L257

Expected: 80; Actual: 83
plugins of the plugin point *External chat message received*, e.g. a filter,
which can stop them; the plugins of *Chat message received* only get the
messages of players in the game.

In the game, the messages appear with the character name and the prefix `@`,
e.g. `@Hero`, so a Discord user can't pretend to be a character in the game.
Mentions, markdown and line breaks are removed, custom emojis become `:name:`,
and long messages are cut (`Discord__Bot__ChatBridge__MaximumMessageLength`).

To read the normal messages in the channels, the bot needs the privileged
*Message Content* intent: enable it for the bot in the Discord developer portal
under *Bot → Privileged Gateway Intents*, and set
`Discord__Bot__ChatBridge__ReadMessages` to `true`. When a message can't be sent to the

Check notice on line 270 in docs-website/docs/server-features/discord.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs-website/docs/server-features/discord.md#L270

Expected: 80; Actual: 87
game, the bot reacts with ❌ (permission *Add Reactions*), and `/say` tells why.
Without the intent, players use `/say <message>`. Discord only approves the
intent for verified bots, which are in more than 100 Discord servers, with a
reason.

## Settings

All settings are part of the `Discord` section of the configuration, so they can
Expand All @@ -225,6 +291,11 @@
| `Bot__Channels__<Category>` | the layout | The identifier of the channel per category, into which the bot posts the notifications. |
| `Bot__LayoutFile` | the default | The path of a JSON file with your own [layout](#your-own-layout). |
| `Bot__StatusUpdateInterval` | `00:01:00` | How often the status message and the presence are updated. |
| `Bot__ChatBridge__BindingMode` | `Both` | Where the chats of guilds can be bound: `Both`, `HostedOnly` (only channels on your Discord server) or `GuildOwnedOnly` (only on Discord servers of guilds). |
| `Bot__ChatBridge__AllowedDiscordServerIds__0`, `__1`, … | all | The Discord servers of guilds, on which chats can be bound. |
| `Bot__ChatBridge__ReadMessages` | `false` | Reads the normal messages in the bound channels. Requires the *Message Content* intent. |
| `Bot__ChatBridge__MaximumMessageLength` | `100` | The maximum length of a message from Discord to the game. |
| `Bot__ChatBridge__MaximumMessagesPerMinute` | `10` | The maximum number of messages per minute of a Discord user to the game. |

## Good to know

Expand All @@ -249,3 +320,9 @@
Private staff channels are skipped if their required roles or permissions cannot
be set. Fix the reported permissions and run setup again. Public channels with
staff channel names are not used automatically for notifications.

Commands are registered globally so guild-owned Discord servers can use `/link`,
`/guildchat` and `/say`. The main configured Discord server also gets
guild-scoped registration for immediate availability. Binding and unbinding as a
guild master require that the selected character still belongs to the linked
account.
3 changes: 3 additions & 0 deletions src/Dapr/AdminPanel.Host/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
.AddManageableServerRegistry()
.AddTransient<IDictionary<int, IGameServer>>(s => s.GetRequiredService<IServerProvider>().Servers.OfType<IGameServer>().ToDictionary(server => server.Id))
.AddSingleton<ILoginServer, LoginServer>()

// E.g. to notify the Discord bot when an administrator removes the link of an account.
.AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton<IGameServerInstanceManager, DockerGameServerInstanceManager>()
.AddSingleton<IConnectServerInstanceManager, DockerConnectServerInstanceManager>()
.AddSingleton<IBackupService, BackupService>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.GameServer.Host;
namespace MUnique.OpenMU.Dapr.Common;

using global::Dapr.Client;
using Microsoft.Extensions.Logging;
Expand All @@ -17,14 +17,14 @@ public class EventPublisher : IEventPublisher
{
private const string PubSubName = "pubsub";
private readonly DaprClient _daprClient;
private readonly ILogger<GameServerStatePublisher> _logger;
private readonly ILogger<EventPublisher> _logger;

/// <summary>
/// Initializes a new instance of the <see cref="EventPublisher"/> class.
/// </summary>
/// <param name="daprClient">The dapr client.</param>
/// <param name="logger">The logger.</param>
public EventPublisher(DaprClient daprClient, ILogger<GameServerStatePublisher> logger)
public EventPublisher(DaprClient daprClient, ILogger<EventPublisher> logger)
{
this._daprClient = daprClient;
this._logger = logger;
Expand Down Expand Up @@ -98,6 +98,23 @@ await this._daprClient
}
}

/// <inheritdoc />
public async ValueTask WorldChatMessageAsync(string sender, string message)
{
try
{
await this._daprClient
.PublishEventAsync(
PubSubName,
nameof(IGameServer.WorldChatMessageAsync),
new WorldChatMessageArguments(sender, message)).ConfigureAwait(false);
}
catch (Exception ex)
{
this._logger.LogError(ex, "Unexpected error when publishing a world chat message");
}
}

/// <summary>
/// Notifies that a client tried to log into an already logged-in account.
/// The connected player can be notified about that.
Expand Down
4 changes: 4 additions & 0 deletions src/Dapr/Common/GameServerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public ValueTask GuildPlayerKickedAsync(string playerName)
public ValueTask AllianceChatMessageAsync(uint guildId, string sender, string message)
=> this.PublishAsync(nameof(this.AllianceChatMessageAsync), new GuildMessageArguments(guildId, sender, message));

/// <inheritdoc />
public ValueTask WorldChatMessageAsync(string sender, string message)
=> this.PublishAsync(nameof(this.WorldChatMessageAsync), new WorldChatMessageArguments(sender, message));

/// <inheritdoc />
public ValueTask SendGlobalMessageAsync(string message, MessageType messageType)
=> this.PublishCommandAsync(nameof(this.SendGlobalMessageAsync), new MessageArguments(this.Id, message, messageType));
Expand Down
1 change: 1 addition & 0 deletions src/Dapr/Discord.Host/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
.AddManageableServerRegistry()
.AddSingleton<IFriendServer, FriendServer>()
.AddSingleton<IGuildServer, GuildServer>()
.AddSingleton<IEventPublisher, EventPublisher>()

// The game servers of the distributed deployment run in UTC, too.
.AddDiscord(settings, () => TimeZoneInfo.Utc);
Expand Down
11 changes: 11 additions & 0 deletions src/Dapr/GameServer.Host/GameServerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ public ValueTask AllianceChatMessageAsync([FromBody] GuildMessageArguments data)
return this._gameServer.AllianceChatMessageAsync(data.GuildId, data.Sender, data.Message);
}

/// <summary>
/// Sends a message of the world chat to all connected players.
/// </summary>
/// <param name="data">The message arguments.</param>
[HttpPost(nameof(IGameServer.WorldChatMessageAsync))]
[Topic("pubsub", nameof(IGameServer.WorldChatMessageAsync))]
public ValueTask WorldChatMessageAsync([FromBody] WorldChatMessageArguments data)
{
return this._gameServer.WorldChatMessageAsync(data.Sender, data.Message);
}

/// <summary>
/// Notifies the game server that a guild got deleted.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/Dapr/ServerClients/WorldChatMessageArguments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// <copyright file="WorldChatMessageArguments.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.ServerClients;

/// <summary>
/// Arguments for a message of the world chat.
/// </summary>
/// <param name="Sender">The sender.</param>
/// <param name="Message">The message.</param>
public record WorldChatMessageArguments(string Sender, string Message);
64 changes: 64 additions & 0 deletions src/DataModel/Entities/GuildChatBinding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// <copyright file="GuildChatBinding.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.DataModel.Entities;

/// <summary>
/// A binding of the chat of a guild or alliance to a channel of an external service, e.g. Discord,
/// so that the messages are mirrored in both directions.
/// </summary>
/// <remarks>
/// It's not part of the <see cref="Guild"/> aggregate, because it's managed by the external service.
/// That's why it refers to its guild by identifier.
/// </remarks>
[AggregateRoot]
public class GuildChatBinding
{
/// <summary>
/// Gets or sets the identifier.
/// </summary>
public Guid Id { get; set; }

/// <summary>
/// Gets or sets the identifier of the guild. For an alliance, it's the master guild of the alliance.
/// </summary>
public Guid GuildId { get; set; }

/// <summary>
/// Gets or sets the scope, which tells if the chat of the guild or of its alliance is bound.
/// </summary>
public GuildChatScope Scope { get; set; }

/// <summary>
/// Gets or sets the name of the external service, e.g. <c>discord</c>.
/// </summary>
public string Provider { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the identifier of the server of the external service, e.g. of the Discord server.
/// </summary>
public string ExternalServerId { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the identifier of the channel of the external service.
/// </summary>
public string ExternalChannelId { get; set; } = string.Empty;

/// <summary>
/// Gets or sets a value indicating whether the channel is hosted by the server owner,
/// i.e. it's in the Discord server of the game server and was created for the guild.
/// Otherwise, it's in a Discord server of the guild.
/// </summary>
public bool IsHosted { get; set; }

/// <summary>
/// Gets or sets the name of the character which bound the chat.
/// </summary>
public string? BoundBy { get; set; }

/// <summary>
/// Gets or sets the timestamp at which the chat was bound.
/// </summary>
public DateTime BoundAt { get; set; }
}
21 changes: 21 additions & 0 deletions src/DataModel/Entities/GuildChatScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// <copyright file="GuildChatScope.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.DataModel.Entities;

/// <summary>
/// The chat of a guild which is bound to a channel of an external service.
/// </summary>
public enum GuildChatScope
{
/// <summary>
/// The chat of the guild.
/// </summary>
Guild,

/// <summary>
/// The chat of the alliance whose master is the guild.
/// </summary>
Alliance,
}
51 changes: 51 additions & 0 deletions src/Discord/ChatBridge/DiscordChatBindResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// <copyright file="DiscordChatBindResult.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Discord.ChatBridge;

/// <summary>
/// The result of binding a chat of a guild or alliance to a Discord channel, or of removing the binding.
/// </summary>
public enum DiscordChatBindResult
{
/// <summary>
/// The binding was created, changed or removed.
/// </summary>
Success,

/// <summary>
/// The binding mode of the settings doesn't allow it.
/// </summary>
ModeNotAllowed,

/// <summary>
/// The Discord server isn't allowed by the settings.
/// </summary>
ServerNotAllowed,

/// <summary>
/// The Discord user isn't linked to a game account.
/// </summary>
NotLinked,

/// <summary>
/// The character of the Discord user isn't the guild master.
/// </summary>
NotGuildMaster,

/// <summary>
/// The guild of the Discord user isn't the master of an alliance.
/// </summary>
NotAllianceMaster,

/// <summary>
/// The channel is bound to the chat of another guild or alliance.
/// </summary>
ChannelInUse,

/// <summary>
/// The channel isn't bound.
/// </summary>
NotBound,
}
Loading
Loading