Merge pull request 'Implement XEP-0045 support in moxxmpp' (#46) from ikjot-2605/moxxmpp:xep_0045 into master

Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/46
This commit is contained in:
PapaTutuWawa 2023-07-01 19:42:12 +00:00
commit 1e7279e23b
8 changed files with 265 additions and 3 deletions

View File

@ -0,0 +1,80 @@
import 'package:cli_repl/cli_repl.dart';
import 'package:example_dart/arguments.dart';
import 'package:example_dart/socket.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
void main(List<String> args) async {
// Set up logging
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print(
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
);
});
final parser = ArgumentParser()
..parser.addOption('muc', help: 'The MUC to send messages to')
..parser.addOption('nick', help: 'The nickname with which to join the MUC');
final options = parser.handleArguments(args);
if (options == null) {
return;
}
// Connect
final muc = JID.fromString(options['muc']! as String).toBare();
final nick = options['nick']! as String;
final connection = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
ExampleTCPSocketWrapper(parser.srvRecord),
)..connectionSettings = parser.connectionSettings;
// Register the managers and negotiators
await connection.registerManagers([
PresenceManager(),
DiscoManager([]),
PubSubManager(),
MessageManager(),
MUCManager(),
]);
await connection.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StartTlsNegotiator(),
SaslScramNegotiator(10, '', '', ScramHashType.sha1),
]);
// Connect
Logger.root.info('Connecting...');
final result =
await connection.connect(shouldReconnect: false, waitUntilLogin: true);
if (!result.isType<bool>()) {
Logger.root.severe('Authentication failed!');
return;
}
Logger.root.info('Connected.');
// Join room
await connection.getManagerById<MUCManager>(mucManager)!.joinRoom(muc, nick);
final repl = Repl(prompt: '> ');
await for (final line in repl.runAsync()) {
await connection
.getManagerById<MessageManager>(messageManager)!
.sendMessage(
muc,
TypedMap<StanzaHandlerExtension>.fromList([
MessageBodyData(line),
]),
type: 'groupchat');
}
// Leave room
await connection.getManagerById<MUCManager>(mucManager)!.leaveRoom(muc);
// Disconnect
await connection.disconnect();
}

View File

@ -47,6 +47,9 @@ export 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0030/helpers.dart'; export 'package:moxxmpp/src/xeps/xep_0030/helpers.dart';
export 'package:moxxmpp/src/xeps/xep_0030/types.dart'; export 'package:moxxmpp/src/xeps/xep_0030/types.dart';
export 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; export 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
export 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0045/types.dart';
export 'package:moxxmpp/src/xeps/xep_0045/xep_0045.dart';
export 'package:moxxmpp/src/xeps/xep_0054.dart'; export 'package:moxxmpp/src/xeps/xep_0054.dart';
export 'package:moxxmpp/src/xeps/xep_0060/errors.dart'; export 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0060/helpers.dart'; export 'package:moxxmpp/src/xeps/xep_0060/helpers.dart';

View File

@ -33,3 +33,4 @@ const stickersManager = 'org.moxxmpp.stickersmanager';
const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities'; const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities';
const messageProcessingHintManager = 'org.moxxmpp.messageprocessinghint'; const messageProcessingHintManager = 'org.moxxmpp.messageprocessinghint';
const occupantIdManager = 'org.moxxmpp.occupantidmanager'; const occupantIdManager = 'org.moxxmpp.occupantidmanager';
const mucManager = 'org.moxxmpp.mucmanager';

View File

@ -103,14 +103,15 @@ class MessageManager extends XmppManagerBase {
/// data for building the message. /// data for building the message.
Future<void> sendMessage( Future<void> sendMessage(
JID to, JID to,
TypedMap<StanzaHandlerExtension> extensions, TypedMap<StanzaHandlerExtension> extensions, {
) async { String type = 'chat',
}) async {
await getAttributes().sendStanza( await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
Stanza.message( Stanza.message(
to: to.toString(), to: to.toString(),
id: extensions.get<MessageIdData>()?.id, id: extensions.get<MessageIdData>()?.id,
type: 'chat', type: type,
children: _messageSendingCallbacks children: _messageSendingCallbacks
.map((c) => c(extensions)) .map((c) => c(extensions))
.flattened .flattened

View File

@ -21,6 +21,9 @@ const discoItemsXmlns = 'http://jabber.org/protocol/disco#items';
// XEP-0033 // XEP-0033
const extendedAddressingXmlns = 'http://jabber.org/protocol/address'; const extendedAddressingXmlns = 'http://jabber.org/protocol/address';
// XEP-0045
const mucXmlns = 'http://jabber.org/protocol/muc';
// XEP-0054 // XEP-0054
const vCardTempXmlns = 'vcard-temp'; const vCardTempXmlns = 'vcard-temp';
const vCardTempUpdate = 'vcard-temp:x:update'; const vCardTempUpdate = 'vcard-temp:x:update';

View File

@ -0,0 +1,19 @@
/// Represents an error related to Multi-User Chat (MUC).
abstract class MUCError {}
/// Error indicating an invalid (non-supported) stanza received while going
/// through normal operation/flow of an MUC.
class InvalidStanzaFormat extends MUCError {}
/// Represents an error indicating an abnormal condition while parsing
/// the DiscoInfo response stanza in Multi-User Chat (MUC).
class InvalidDiscoInfoResponse extends MUCError {}
/// Returned when no nickname was specified from the client side while trying to
/// perform some actions on the MUC, such as joining the room.
class NoNicknameSpecified extends MUCError {}
/// This error occurs when a user attempts to perform an action that requires
/// them to be a member of a room, but they are not currently joined to
/// that room.
class RoomNotJoinedError extends MUCError {}

View File

@ -0,0 +1,43 @@
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
class RoomInformation {
/// Represents information about a Multi-User Chat (MUC) room.
RoomInformation({
required this.jid,
required this.features,
required this.name,
});
/// Constructs a [RoomInformation] object from a [DiscoInfo] object.
/// The [DiscoInfo] object contains the necessary information to populate
/// the [RoomInformation] fields.
factory RoomInformation.fromDiscoInfo({
required DiscoInfo discoInfo,
}) =>
RoomInformation(
jid: discoInfo.jid!,
features: discoInfo.features,
name: discoInfo.identities
.firstWhere((i) => i.category == 'conference')
.name!,
);
/// The JID of the Multi-User Chat (MUC) room.
final JID jid;
/// A list of features supported by the Multi-User Chat (MUC) room.
final List<String> features;
/// The name or title of the Multi-User Chat (MUC) room.
final String name;
}
class RoomState {
RoomState({
required this.roomJid,
this.nick,
});
final JID roomJid;
String? nick;
}

View File

@ -0,0 +1,112 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
import 'package:synchronized/synchronized.dart';
class MUCManager extends XmppManagerBase {
MUCManager() : super(mucManager);
@override
Future<bool> isSupported() async => true;
/// Map full JID to RoomState
final Map<JID, RoomState> _mucRoomCache = {};
/// Cache lock
final Lock _cacheLock = Lock();
/// Queries the information of a Multi-User Chat room.
///
/// Retrieves the information about the specified MUC room by performing a
/// disco info query. Returns a [Result] with the [RoomInformation] on success
/// or an appropriate [MUCError] on failure.
Future<Result<RoomInformation, MUCError>> queryRoomInformation(
JID roomJID,
) async {
final result = await getAttributes()
.getManagerById<DiscoManager>(discoManager)!
.discoInfoQuery(roomJID);
if (result.isType<StanzaError>()) {
return Result(InvalidStanzaFormat());
}
try {
final roomInformation = RoomInformation.fromDiscoInfo(
discoInfo: result.get<DiscoInfo>(),
);
return Result(roomInformation);
} catch (e) {
return Result(InvalidDiscoInfoResponse);
}
}
/// Joins a Multi-User Chat room.
///
/// Joins the specified MUC room using the provided nickname. Sends a presence
/// stanza with the appropriate attributes to join the room. Returns a [Result]
/// with a boolean value indicating success or failure, or an [MUCError]
/// if applicable.
Future<Result<bool, MUCError>> joinRoom(
JID roomJid,
String nick,
) async {
if (nick.isEmpty) {
return Result(NoNicknameSpecified());
}
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
to: roomJid.withResource(nick).toString(),
children: [
XMLNode.xmlns(
tag: 'x',
xmlns: mucXmlns,
)
],
),
),
);
await _cacheLock.synchronized(
() {
_mucRoomCache[roomJid] = RoomState(roomJid: roomJid, nick: nick);
},
);
return const Result(true);
}
/// Leaves a Multi-User Chat room.
///
/// Leaves the specified MUC room by sending an 'unavailable' presence stanza.
/// Removes the corresponding room entry from the cache. Returns a [Result]
/// with a boolean value indicating success or failure, or an [MUCError]
/// if applicable.
Future<Result<bool, MUCError>> leaveRoom(
JID roomJid,
) async {
final nick = await _cacheLock.synchronized(() {
final nick = _mucRoomCache[roomJid]?.nick;
_mucRoomCache.remove(roomJid);
return nick;
});
if (nick == null) {
return Result(RoomNotJoinedError);
}
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
to: roomJid.withResource(nick).toString(),
type: 'unavailable',
),
),
);
return const Result(true);
}
}