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:
commit
1e7279e23b
80
examples_dart/bin/muc_client.dart
Normal file
80
examples_dart/bin/muc_client.dart
Normal 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();
|
||||||
|
}
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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
|
||||||
|
@ -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';
|
||||||
|
19
packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart
Normal file
19
packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart
Normal 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 {}
|
43
packages/moxxmpp/lib/src/xeps/xep_0045/types.dart
Normal file
43
packages/moxxmpp/lib/src/xeps/xep_0045/types.dart
Normal 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;
|
||||||
|
}
|
112
packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart
Normal file
112
packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user