Hello readers!
Welcome back to the GSoC series of blogs. This one is about my implementation of the MUC XEP (XEP-0045) in Moxxmpp.
Introduction
Well, as you probably know, Moxxy is the frontend app that is being developed as a modern new XMPP client.
Moxxmpp is a Dart package that is used by Moxxy for multiple XMPP services. Essentially, the conversation between Moxxy and the XMPP server is handled by Moxxmpp. It is like the middleman that helps ease the communication between the two, keeping Moxxy code clean and free from the intricacies it handles.
Since my GSoC project is all about adding MUC support to Moxxy, it was essential to first add the required support in Moxxmpp before proceeding to Moxxy. This blog will give you an idea of what the responsibilities of Moxxmpp are, and why it was important to separate it from Moxxy.
What is Moxxmpp?
In the words of it’s developer, Moxxmpp is “a pure-Dart XMPP library”. Moxxmpp could be thought of as the backbone of Moxxy. The entire communication/networking part of Moxxy, i.e. how it (as a client) communicates with the user’s XMPP server is outsourced to this Dart library.
Not only this, used as per it’s rules - Moxxmpp could be used by any client to perform XMPP tasks that are currently supported by it. It is a re-usable and versatile XMPP library.
Listed below are some of it’s core responsibilities:
- Establishing the connection between the client and the XMPP server via the user’s JID and password.
- Allowing Moxxy/any front-end client to register managers that provide support for a particular XEP/part of the XMPP flow. For example: the messaging service is handled by a MessageManager.
Similarly here are some other managers with their use:
- PresenceManager: Handling the presence related stanzas/use.
- PubSubManager: Provides support for XEP-0060.
- DiscoManager: Provides support for the service discovery: XEP-0030.
- RosterManager: Handling roster related tasks.
- BlockingManager: For blocking related tasks.
As of June 17 2023, there are 35 such managers and 35 XEPs supported in Moxxmpp.
- Managing cache: Moxxmpp manages cache of common requests, such as service discovery requests. If a request is duplicated, the result is provided from the cache - saving bandwidth and time.
As explained below, new XEP support is related to adding new managers to Moxxmpp and I will be doing the same for XEP-0045 support in Moxxy. My design plan is elaborated in the next section.
Here is my implementation plan for adding XEP-0045 support to Moxxmpp.
GSoC Moxxmpp Implementation Design Plan
Directory Structure
- xep_0045
- errors.dart
- helpers.dart
- types.dart
- xep_0045.dart
- Here, the errors.dart file will contain the abstract classes for different errors that one could get whilst going through all the lifecycle use-cases of the MUC feature.
- The helpers.dart file will have some helper functions to maintain (DRY code avoiding repetition) and (modularized code).
- types.dart will contain model classes for the events and other entities.
- The xep_0045.dart is the main file which will contain the exposed routines as shown below, as well as the incoming stanza handlers.
Phase 1 - Query Room Information
Future<Result<RoomInformation,MUCError>> queryRoomInformation(JID roomJID)
This routine will allow the users to view the features of the room. These will include whether the room is open, moderated, password protected, etc. All this will be stored in a RoomInformation object and sent back as a Result object. It is important to note that the MUC server’s address (for the ‘to’ attribute) will be parsed from the latter half of the room JID (the part post the ‘@’).
The parameters of the RoomInformation class will be represented in the following way:
JID roomJID
List<String> feature_list
Phase 2 - Join/Leave Room(Handle errors)
To join/leave a room we can send an appropriate presence stanza that will handle the same for us. So we will have two simple routines for the same as well.
Routines
Future<Result>bool, MUCError>> joinRoom(JID roomJID)
This routine will send a presence stanza to the MUC server asking it to join the server. It takes the roomJID
as a parameter that will essentially contain the bare JID as well as the nickname (prompted from the user) as the resource. This will be preceded by the queryRoomInformation() routine call to obtain necessary information about the room, for example: if the room is password protected (in which case the user will see a not-implemented error dialog till a time it is supported).
Important to note is that the user’s nickname will be used as the ‘resource’ in the roomJID sent by the frontend. This is because the ‘to’ attribute of a presence stanza to join a room looks as such - {local/MUC}@{domain/chat server}/{nickname}.
Future<Result<bool, MUCError>> leaveRoom(JID roomJID)
Similarly this is a routine to allow for leaving the room with a presence stanza.
The boolean return type is just to allow for a simple return object with the Result type. We could move forward with a simple Future
Phase 3 - Refactor sendMessage to handle MUCs
To be able to send messages to groupchats/MUCs, the main requirement is to be able to change the type
attribute in the message stanzas from chat
to groupchat
. This can be done in one of two ways. The first way will be to change the sendMessage
routine’s description to include a type
parameter that will simply transfer over to the stanza that will be sent from moxxmpp as a string. Another was is described below, where a new extension called ConversationTypeData will be added to the StanzaHandlerExtension
object whilst calling the sendMessage routine. The former solution is simpler and will most probably be implemented. The design for both these possibilities is listed below.
Solution 1
The sendMessage
routine will transform from
Future<void> sendMessage(
JID to,
TypedMap<StanzaHandlerExtension> extensions,
) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.message(
to: to.toString(),
id: extensions.get<MessageIdData>()?.id,
type: 'chat',
children: _messageSendingCallbacks
.map((c) => c(extensions))
.flattened
.toList(),
),
awaitable: false,
),
);
}
to
Future<void> sendMessage(
JID to,
TypedMap<StanzaHandlerExtension> extensions,
String type,
) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.message(
to: to.toString(),
id: extensions.get<MessageIdData>()?.id,
type: type,
children: _messageSendingCallbacks
.map((c) => c(extensions))
.flattened
.toList(),
),
awaitable: false,
),
);
}
Solution 2
enum ConversationType {
chat, groupchat, groupchatprivate
}
The ConversationTypeData
class essentially will act as a wrapper over the above enum.
Hence the new type attribute value will be:
extensions.get<ConversationTypeData>()?.conversationType ==
ConversationType.groupchat
? 'groupchat'
: 'chat',
Here, ConversationTypeData
is a simple wrapper over ConversationType
so that it can be recognized as a subclass of the StanzaHandlerExtensions
class.
For private MUC messages to a single participant, the type
attribute of the message stanza should be set to chat
. This can be handled very easily in case of either solution implementation.
For MUC messages with a stanza reflecting a chat server (type: ‘groupchat’), the type in the MessageEvent will be set to ‘groupchat’ automatically. The rest will be handled similarly to 1:1 chats.
Phase 4 - Miscellaneous requirements
Some of the miscellaneous requirements from Moxxmpp could include viewing member list, group subject, change availability status, and more. For now, I have identified the below as the most crucial requirements for the Moxxmpp feature additions for MUC.
Below are the use-cases and how they are planned to be handled:
- Get member list
After one joins a room, the user receives a presence from all the connected users to the chat. The best way to get the member list is to capture/handle the incoming presence stanzas and send an appropriate MUCPresenceReceived
event to the UI.
This way, whenever a user joins a room, they can see the current member list, and on subsequent presence updates, also change that list.
- Handling the room subject
This is a very simple usecase. When a disco info query about the MUC goes out to the XMPP server, the response contains an identity node containing the chat name. This would be the best way to extract the rom subject. As of now, the queryRoomInformation
routine will be triggered from the UI and the response will be sent to the same. This will be encapsulated in a simple RoomInformation
class which is now updated as follows:
JID roomJID
List<String> feature_list
String name
Note: Changing your status seemed like a task that would be slighlty out of scope for the project duration, so that is kept as a stretch goal for now.
Errors
Frontend checks while joining a room to prevent avoidable errors:
- Nickname must be specified in accordance with the rules of the XEP - to prevent the no nickname specified error.
- Validations for JID input field.
- Validation for checking empty nickname.
Some of the possible errors that can be faced by the user in an MUC workflow are:
- Invalid/Already in use nickname
- Not allowed to register
- Invalid stanza format
- Invalid Disco Info Response: In case of some rare edge cases (not handled currently) where the response does not contain some information like the room name, or the features of the room.
Challenges noted
Some challenges/room for error were noted along the way. They are enumerated below:
-
How will Moxxy/Moxxmpp handle messages from oneself?
When a user sends a message on the MUC, they receive the message themselves as well. How will moxxmpp/moxxy differentiate between other live messages and this echoed message?
The solution to this problem is to use the origin-id of the message. If the origin-id of the response matches with the sent origin-id, then the message is to be ignored and not shown in the UI.
-
In anonymous MUCs, one can easily act as someone else by using their nickname once they leave. How will this be handled?
This is fixed by using the occupant-id property as described in XEP-0421. A user (with their unique JID) when joins the group, is assigned a unique occupant-id, which can be used to actually track which message is from which user.
This makes the implementation of reactions/retractions simpler.
-
How will discussion history (sent on joining an MUC) be handled when someone rejoins an MUC?
When a user joins a MUC, they are sent a set of stanzas in the following order:
1. Errors (if any) 2. Self presence with status code 3. Room history 4. Room subject 5. Live message/presence updates
Now, when a user gets disconnected from an MUC and joins again, then the room history will be sent again as well. To prevent this history from getting appended to the conversation in the UI, we will need to wait till we receive the room subject and only then start listening for message stanzas (since room subject is sent after room history).
Conclusion and future work
In conclusion, this blog post discussed the implementation plan for adding Multi-User Chat (MUC) support, specifically XEP-0045, to Moxxmpp. Moxxmpp serves as the backbone for the Moxxy XMPP client, handling the communication between the client and XMPP server. The blog outlined the directory structure, design plan, and key routines for implementing the MUC support in Moxxmpp. It also addressed some challenges and considerations related to handling MUC features.
Implementation for this has begun, and a PR is already underway. Once this PR is completed, then work will begin on the front-end and the changes will be integrated into Moxxy.