Skip to content
Tildev0.6.1

A chat plugin

This page shows how a plugin adds a chat network: a factory that creates one client per account, the client itself, and the way its events reach Tilde and the scripts.

For each [[accounts.chat]], Tilde asks the factory whose Protocol is the protocol of [accounts.chat.connection], ignoring case. The factory implements IConnectionFactory<IChatClient>. From IrcChatClientFactory.cs, without its logger:

public sealed class IrcChatClientFactory : IConnectionFactory<IChatClient>
{
private readonly IEventHubService _hub;
public string Protocol => "irc";
public IrcChatClientFactory(IEventHubService hub)
{
_hub = hub;
}
public IChatClient Create(string accountId, ProtocolSettings settings)
{
var irc = IrcSettings.From(settings);
return new IrcChatClient(accountId, irc, _hub);
}
}

The plugin registers it in RegisterAsync, from IrcPlugin.cs:

container.Register<IConnectionFactory<IChatClient>, IrcChatClientFactory>(
Reuse.Singleton,
ifAlreadyRegistered: IfAlreadyRegistered.AppendNotKeyed
);

Create returns a client that is not connected yet. How to read settings is on Configuration sections.

The client implements IChatClient, which extends IConnection (State, ConnectAsync, DisconnectAsync, DisposeAsync):

Method What it does
JoinAsync(channelId) joins a channel or a room and returns it as a ChatChannel
LeaveAsync(channelId) leaves it
SendAsync(channelId, text) sends a text; a text that starts with /me is sent as an action
MarkReadAsync(channelId, messageId) tells the network the user read up to a message; nothing is sent for a room
SetPresenceAsync(presence, statusText) shows the user as Online, Away, ExtendedAway, DoNotDisturb or Offline
ReadEventsAsync() streams the events of the network until the token is cancelled

Like a mail store, the client publishes a ConnectionStateChangedEvent on the event hub each time its State changes. The /me rule is in ChatActionText.cs: each plugin turns it into what its network understands.

The client does not publish its chat events on the hub. It yields them from ReadEventsAsync, and Tilde reads that stream, after the chat middlewares, and publishes each event. The IRC client does it this way: the only thing it publishes itself is the change of its connection state (IrcChatClient.cs).

Tilde calls ReadEventsAsync once per account, when the application starts, at the same time as it connects, and reads it until it ends. So the stream lives across reconnects: when it ends, no other event of that account arrives. A queue such as Channel<IChatEvent> does this, as in the IRC client. This is a part of a small client I built against the interfaces, which echoes what it is sent:

private readonly Channel<IChatEvent> _events = Channel.CreateUnbounded<IChatEvent>();
public async ValueTask SendAsync(string channelId, string text, CancellationToken cancellationToken = default)
{
var message = new ChatMessage(
Guid.NewGuid().ToString("N"),
new ChatChannel(channelId, channelId),
new ChatUser("echo", "Echo"),
text,
DateTimeOffset.UtcNow
);
await _events.Writer.WriteAsync(new ChatMessageReceivedEvent(_accountId, message), cancellationToken);
}
public async IAsyncEnumerable<IChatEvent> ReadEventsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default
)
{
await foreach (var chatEvent in _events.Reader.ReadAllAsync(cancellationToken))
{
yield return chatEvent;
}
}

The events are records in src/Tilde.Abstraction/Data/Events/Chat, each one an IChatEvent that starts with the id of the account: ChatMessageReceivedEvent, ChatMentionEvent, ChatChannelJoinedEvent, ChatUserJoinedEvent, ChatPresenceChangedEvent, ChatErrorEvent and the others in that folder. ChatMessageSentEvent is the exception: a middleware of Tilde.Core publishes it after a message was sent, so the client does not.

A ChatMessage says more than its text:

  • IsAction is true for an action; Text then holds the action without the /me.
  • IsNotice is true for a notice (IRC NOTICE), a message nobody is expected to answer.
  • Mentioned is true when a message in a room calls the user by nick. A direct message is never mentioned.
  • IsHistory is true for a message replayed from the history of a room: it calls for nobody’s attention.
  • ChatChannel.IsDirect is true for a conversation with one person.

Tilde turns most of these events into the events of the Lua scripts, such as chat_message and chat_mention (see the events of the scripts).

An IChatMiddleware sits around the client of every account, like the mail middlewares on A mail plugin. It has one method for each method of IChatClient. Its ReadEventsAsync(next) returns the stream that Tilde publishes, so it can drop, change or add events. Register it with AppendNotKeyed; the middlewares of the plugins run in the order they were registered, and the first one is the outermost.

A chat account with the protocol of the plugin:

[[accounts.chat]]
id = "echo"
name = "Echo"
[accounts.chat.connection]
protocol = "echo"

Once the plugin is added in TildeHostBuilder (see The plugin class), tcli check creates its client and tcli check --connect connects it too. The chat accounts and their keys are on Chat accounts.

Next: Configuration sections, or test the plugin.