feat(xep): Add MUC events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
814f99436b
commit
7ca648c478
@ -24,6 +24,7 @@ const extendedAddressingXmlns = 'http://jabber.org/protocol/address';
|
|||||||
|
|
||||||
// XEP-0045
|
// XEP-0045
|
||||||
const mucXmlns = 'http://jabber.org/protocol/muc';
|
const mucXmlns = 'http://jabber.org/protocol/muc';
|
||||||
|
const mucUserXmlns = 'http://jabber.org/protocol/muc#user';
|
||||||
const roomInfoFormType = 'http://jabber.org/protocol/muc#roominfo';
|
const roomInfoFormType = 'http://jabber.org/protocol/muc#roominfo';
|
||||||
|
|
||||||
// XEP-0054
|
// XEP-0054
|
||||||
|
@ -17,3 +17,9 @@ class NoNicknameSpecified extends MUCError {}
|
|||||||
/// them to be a member of a room, but they are not currently joined to
|
/// them to be a member of a room, but they are not currently joined to
|
||||||
/// that room.
|
/// that room.
|
||||||
class RoomNotJoinedError extends MUCError {}
|
class RoomNotJoinedError extends MUCError {}
|
||||||
|
|
||||||
|
/// Indicates that the MUC forbids us from joining, i.e. when we're banned.
|
||||||
|
class JoinForbiddenError extends MUCError {}
|
||||||
|
|
||||||
|
/// Indicates that an unspecific error occurred while joining.
|
||||||
|
class MUCUnspecificError extends MUCError {}
|
||||||
|
35
packages/moxxmpp/lib/src/xeps/xep_0045/events.dart
Normal file
35
packages/moxxmpp/lib/src/xeps/xep_0045/events.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:moxxmpp/src/events.dart';
|
||||||
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
|
||||||
|
|
||||||
|
/// Triggered when the MUC changes our nickname.
|
||||||
|
class NickChangedByMUCEvent extends XmppEvent {
|
||||||
|
NickChangedByMUCEvent(this.roomJid, this.nick);
|
||||||
|
|
||||||
|
/// The JID of the room.
|
||||||
|
final JID roomJid;
|
||||||
|
|
||||||
|
/// The new nickname.
|
||||||
|
final String nick;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when an entity joins the MUC.
|
||||||
|
class MemberJoinedEvent extends XmppEvent {
|
||||||
|
MemberJoinedEvent(this.roomJid, this.member);
|
||||||
|
|
||||||
|
/// The JID of the room.
|
||||||
|
final JID roomJid;
|
||||||
|
|
||||||
|
/// The new member.
|
||||||
|
final RoomMember member;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemberChangedEvent extends XmppEvent {
|
||||||
|
MemberChangedEvent(this.roomJid, this.member);
|
||||||
|
|
||||||
|
/// The JID of the room.
|
||||||
|
final JID roomJid;
|
||||||
|
|
||||||
|
/// The new member.
|
||||||
|
final RoomMember member;
|
||||||
|
}
|
@ -4,6 +4,66 @@ import 'package:moxxmpp/src/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
|
|
||||||
|
class InvalidAffiliationException implements Exception {}
|
||||||
|
class InvalidRoleException implements Exception {}
|
||||||
|
|
||||||
|
enum Affiliation {
|
||||||
|
owner('owner'),
|
||||||
|
admin('admin'),
|
||||||
|
member('member'),
|
||||||
|
outcast('outcast'),
|
||||||
|
none('none');
|
||||||
|
|
||||||
|
const Affiliation(this.value);
|
||||||
|
|
||||||
|
factory Affiliation.fromString(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'owner':
|
||||||
|
return Affiliation.owner;
|
||||||
|
case 'admin':
|
||||||
|
return Affiliation.admin;
|
||||||
|
case 'member':
|
||||||
|
return Affiliation.member;
|
||||||
|
case 'outcast':
|
||||||
|
return Affiliation.outcast;
|
||||||
|
case 'none':
|
||||||
|
return Affiliation.none;
|
||||||
|
default:
|
||||||
|
throw InvalidAffiliationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value to use for an attribute referring to this affiliation.
|
||||||
|
final String value;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
moderator('moderator'),
|
||||||
|
participant('participant'),
|
||||||
|
visitor('visitor'),
|
||||||
|
none('none');
|
||||||
|
|
||||||
|
const Role(this.value);
|
||||||
|
|
||||||
|
factory Role.fromString(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'moderator':
|
||||||
|
return Role.moderator;
|
||||||
|
case 'participant':
|
||||||
|
return Role.participant;
|
||||||
|
case 'visitor':
|
||||||
|
return Role.visitor;
|
||||||
|
case 'none':
|
||||||
|
return Role.none;
|
||||||
|
default:
|
||||||
|
throw InvalidRoleException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value to use for an attribute referring to this role.
|
||||||
|
final String value;
|
||||||
|
}
|
||||||
|
|
||||||
class RoomInformation {
|
class RoomInformation {
|
||||||
/// Represents information about a Multi-User Chat (MUC) room.
|
/// Represents information about a Multi-User Chat (MUC) room.
|
||||||
RoomInformation({
|
RoomInformation({
|
||||||
@ -48,6 +108,32 @@ class RoomInformation {
|
|||||||
/// The used message-id and an optional origin-id.
|
/// The used message-id and an optional origin-id.
|
||||||
typedef PendingMessage = (String, String?);
|
typedef PendingMessage = (String, String?);
|
||||||
|
|
||||||
|
/// An entity inside a MUC room. The name "member" here does not refer to an affiliation of member.
|
||||||
|
class RoomMember {
|
||||||
|
const RoomMember(this.nick, this.affiliation, this.role);
|
||||||
|
|
||||||
|
/// The entity's nickname.
|
||||||
|
final String nick;
|
||||||
|
|
||||||
|
/// The assigned affiliation.
|
||||||
|
final Affiliation affiliation;
|
||||||
|
|
||||||
|
/// The assigned role.
|
||||||
|
final Role role;
|
||||||
|
|
||||||
|
RoomMember copyWith({
|
||||||
|
String? nick,
|
||||||
|
Affiliation? affiliation,
|
||||||
|
Role? role,
|
||||||
|
}) {
|
||||||
|
return RoomMember(
|
||||||
|
nick ?? this.nick,
|
||||||
|
affiliation ?? this.affiliation,
|
||||||
|
role ?? this.role,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RoomState {
|
class RoomState {
|
||||||
RoomState({required this.roomJid, this.nick, required this.joined}) {
|
RoomState({required this.roomJid, this.nick, required this.joined}) {
|
||||||
pendingMessages = List<PendingMessage>.empty(growable: true);
|
pendingMessages = List<PendingMessage>.empty(growable: true);
|
||||||
@ -63,4 +149,7 @@ class RoomState {
|
|||||||
bool joined;
|
bool joined;
|
||||||
|
|
||||||
late final List<PendingMessage> pendingMessages;
|
late final List<PendingMessage> pendingMessages;
|
||||||
|
|
||||||
|
/// "List" of entities inside the MUC.
|
||||||
|
final Map<String, RoomMember> members = {};
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:moxlib/moxlib.dart';
|
import 'package:moxlib/moxlib.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
@ -6,11 +7,13 @@ import 'package:moxxmpp/src/managers/data.dart';
|
|||||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
|
import 'package:moxxmpp/src/presence.dart';
|
||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.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_0030/xep_0030.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
|
import 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0045/events.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
||||||
import 'package:synchronized/extension.dart';
|
import 'package:synchronized/extension.dart';
|
||||||
@ -25,9 +28,12 @@ class MUCManager extends XmppManagerBase {
|
|||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
/// Map full JID to RoomState
|
/// Map a room's JID to its RoomState
|
||||||
final Map<JID, RoomState> _mucRoomCache = {};
|
final Map<JID, RoomState> _mucRoomCache = {};
|
||||||
|
|
||||||
|
/// Mapp a room's JID to a completer waiting for the completion of the join process.
|
||||||
|
final Map<JID, Completer<Result<bool, MUCError>>> _mucRoomJoinCompleter = {};
|
||||||
|
|
||||||
/// Cache lock
|
/// Cache lock
|
||||||
final Lock _cacheLock = Lock();
|
final Lock _cacheLock = Lock();
|
||||||
|
|
||||||
@ -43,6 +49,14 @@ class MUCManager extends XmppManagerBase {
|
|||||||
// Before the message handler
|
// Before the message handler
|
||||||
priority: -99,
|
priority: -99,
|
||||||
),
|
),
|
||||||
|
StanzaHandler(
|
||||||
|
stanzaTag: 'presence',
|
||||||
|
callback: _onPresence,
|
||||||
|
tagName: 'x',
|
||||||
|
tagXmlns: mucUserXmlns,
|
||||||
|
// Before the PresenceManager
|
||||||
|
priority: PresenceManager.presenceHandlerPriority + 1,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -70,6 +84,7 @@ class MUCManager extends XmppManagerBase {
|
|||||||
// Mark all groupchats as not joined.
|
// Mark all groupchats as not joined.
|
||||||
for (final jid in _mucRoomCache.keys) {
|
for (final jid in _mucRoomCache.keys) {
|
||||||
_mucRoomCache[jid]!.joined = false;
|
_mucRoomCache[jid]!.joined = false;
|
||||||
|
_mucRoomJoinCompleter[jid] = Completer();
|
||||||
|
|
||||||
// Re-join all MUCs.
|
// Re-join all MUCs.
|
||||||
final state = _mucRoomCache[jid]!;
|
final state = _mucRoomCache[jid]!;
|
||||||
@ -149,18 +164,23 @@ class MUCManager extends XmppManagerBase {
|
|||||||
return Result(NoNicknameSpecified());
|
return Result(NoNicknameSpecified());
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cacheLock.synchronized(
|
final completer =
|
||||||
|
await _cacheLock.synchronized<Completer<Result<bool, MUCError>>>(
|
||||||
() {
|
() {
|
||||||
_mucRoomCache[roomJid] = RoomState(
|
_mucRoomCache[roomJid] = RoomState(
|
||||||
roomJid: roomJid,
|
roomJid: roomJid,
|
||||||
nick: nick,
|
nick: nick,
|
||||||
joined: false,
|
joined: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final completer = Completer<Result<bool, MUCError>>();
|
||||||
|
_mucRoomJoinCompleter[roomJid] = completer;
|
||||||
|
return completer;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await _sendMucJoin(roomJid, nick, maxHistoryStanzas);
|
await _sendMucJoin(roomJid, nick, maxHistoryStanzas);
|
||||||
return const Result(true);
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _sendMucJoin(
|
Future<void> _sendMucJoin(
|
||||||
@ -222,6 +242,129 @@ class MUCManager extends XmppManagerBase {
|
|||||||
return const Result(true);
|
return const Result(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RoomState?> getRoomState(JID roomJid) async {
|
||||||
|
return _cacheLock.synchronized(() => _mucRoomCache[roomJid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<StanzaHandlerData> _onPresence(
|
||||||
|
Stanza presence,
|
||||||
|
StanzaHandlerData state,
|
||||||
|
) async {
|
||||||
|
if (presence.from == null) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
final from = JID.fromString(presence.from!);
|
||||||
|
final bareFrom = from.toBare();
|
||||||
|
return _cacheLock.synchronized(() {
|
||||||
|
final room = _mucRoomCache[bareFrom];
|
||||||
|
if (room == null) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from.resource.isEmpty) {
|
||||||
|
// TODO(Unknown): Handle presence from the room itself.
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presence.type == 'error') {
|
||||||
|
final errorTag = presence.firstTag('error')!;
|
||||||
|
final error = errorTag.firstTagByXmlns(fullStanzaXmlns)!;
|
||||||
|
Result<bool, MUCError> result;
|
||||||
|
if (error.tag == 'forbidden') {
|
||||||
|
result = Result(JoinForbiddenError());
|
||||||
|
} else {
|
||||||
|
result = Result(MUCUnspecificError());
|
||||||
|
}
|
||||||
|
|
||||||
|
_mucRoomCache.remove(bareFrom);
|
||||||
|
_mucRoomJoinCompleter[bareFrom]!.complete(result);
|
||||||
|
_mucRoomJoinCompleter.remove(bareFrom);
|
||||||
|
return StanzaHandlerData(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
presence,
|
||||||
|
state.extensions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final x = presence.firstTag('x', xmlns: mucUserXmlns)!;
|
||||||
|
final item = x.firstTag('item')!;
|
||||||
|
final statuses = x
|
||||||
|
.findTags('status')
|
||||||
|
.map((s) => s.attributes['code']! as String)
|
||||||
|
.toList();
|
||||||
|
final role = Role.fromString(
|
||||||
|
item.attributes['role']! as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (statuses.contains('110')) {
|
||||||
|
if (room.nick != from.resource) {
|
||||||
|
// Notify us of the changed nick.
|
||||||
|
getAttributes().sendEvent(
|
||||||
|
NickChangedByMUCEvent(
|
||||||
|
bareFrom,
|
||||||
|
from.resource,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the nick to make sure we're in sync with the MUC.
|
||||||
|
room.nick = from.resource;
|
||||||
|
return StanzaHandlerData(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
presence,
|
||||||
|
state.extensions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presence.attributes['type'] == 'unavailable' && role == Role.none) {
|
||||||
|
// Cannot happen while joining, so we assume we are joined
|
||||||
|
assert(
|
||||||
|
room.joined,
|
||||||
|
'Should not receive unavailable with role="none" while joining',
|
||||||
|
);
|
||||||
|
room.members.remove(from.resource);
|
||||||
|
} else {
|
||||||
|
final member = RoomMember(
|
||||||
|
from.resource,
|
||||||
|
Affiliation.fromString(
|
||||||
|
item.attributes['affiliation']! as String,
|
||||||
|
),
|
||||||
|
role,
|
||||||
|
);
|
||||||
|
logger.finest('Got presence from ${from.resource} in $bareFrom');
|
||||||
|
if (room.joined) {
|
||||||
|
if (room.members.containsKey(from.resource)) {
|
||||||
|
getAttributes().sendEvent(
|
||||||
|
MemberJoinedEvent(
|
||||||
|
bareFrom,
|
||||||
|
member,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
getAttributes().sendEvent(
|
||||||
|
MemberChangedEvent(
|
||||||
|
bareFrom,
|
||||||
|
member,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
room.members[from.resource] = member;
|
||||||
|
}
|
||||||
|
|
||||||
|
return StanzaHandlerData(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
presence,
|
||||||
|
state.extensions,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<StanzaHandlerData> _onMessageSent(
|
Future<StanzaHandlerData> _onMessageSent(
|
||||||
Stanza message,
|
Stanza message,
|
||||||
StanzaHandlerData state,
|
StanzaHandlerData state,
|
||||||
@ -260,6 +403,10 @@ class MUCManager extends XmppManagerBase {
|
|||||||
if (!roomState.joined) {
|
if (!roomState.joined) {
|
||||||
// Mark the room as joined.
|
// Mark the room as joined.
|
||||||
_mucRoomCache[roomJid]!.joined = true;
|
_mucRoomCache[roomJid]!.joined = true;
|
||||||
|
_mucRoomJoinCompleter[roomJid]!.complete(
|
||||||
|
const Result(true),
|
||||||
|
);
|
||||||
|
_mucRoomJoinCompleter.remove(roomJid);
|
||||||
logger.finest('$roomJid is now joined');
|
logger.finest('$roomJid is now joined');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,4 +162,174 @@ void main() {
|
|||||||
expect(fakeSocket.getState(), 10);
|
expect(fakeSocket.getState(), 10);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Test joining a MUC with other members',
|
||||||
|
() async {
|
||||||
|
final fakeSocket = StubTCPSocket([
|
||||||
|
StringExpectation(
|
||||||
|
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||||
|
'''
|
||||||
|
<stream:stream
|
||||||
|
xmlns="jabber:client"
|
||||||
|
version="1.0"
|
||||||
|
xmlns:stream="http://etherx.jabber.org/streams"
|
||||||
|
from="test.server"
|
||||||
|
xml:lang="en">
|
||||||
|
<stream:features xmlns="http://etherx.jabber.org/streams">
|
||||||
|
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
|
||||||
|
<mechanism>PLAIN</mechanism>
|
||||||
|
</mechanisms>
|
||||||
|
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||||
|
<required/>
|
||||||
|
</bind>
|
||||||
|
</stream:features>''',
|
||||||
|
),
|
||||||
|
StringExpectation(
|
||||||
|
"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AHBvbHlub21kaXZpc2lvbgBhYWFh</auth>",
|
||||||
|
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
|
||||||
|
),
|
||||||
|
StringExpectation(
|
||||||
|
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
|
||||||
|
'''
|
||||||
|
<stream:stream
|
||||||
|
xmlns="jabber:client"
|
||||||
|
version="1.0"
|
||||||
|
xmlns:stream="http://etherx.jabber.org/streams"
|
||||||
|
from="test.server"
|
||||||
|
xml:lang="en">
|
||||||
|
<stream:features xmlns="http://etherx.jabber.org/streams">
|
||||||
|
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||||
|
<required/>
|
||||||
|
</bind>
|
||||||
|
<session xmlns="urn:ietf:params:xml:ns:xmpp-session">
|
||||||
|
<optional/>
|
||||||
|
</session>
|
||||||
|
<csi xmlns="urn:xmpp:csi:0"/>
|
||||||
|
<sm xmlns="urn:xmpp:sm:3"/>
|
||||||
|
</stream:features>
|
||||||
|
''',
|
||||||
|
),
|
||||||
|
StanzaExpectation(
|
||||||
|
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
|
||||||
|
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
|
||||||
|
ignoreId: true,
|
||||||
|
),
|
||||||
|
StanzaExpectation(
|
||||||
|
'<presence to="channel@muc.example.org/test" xmlns="jabber:client"><x xmlns="http://jabber.org/protocol/muc"><history maxstanzas="0"/></x></presence>',
|
||||||
|
'',
|
||||||
|
ignoreId: true,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
final conn = XmppConnection(
|
||||||
|
TestingSleepReconnectionPolicy(1),
|
||||||
|
AlwaysConnectedConnectivityManager(),
|
||||||
|
ClientToServerNegotiator(),
|
||||||
|
fakeSocket,
|
||||||
|
)
|
||||||
|
..connectionSettings = ConnectionSettings(
|
||||||
|
jid: JID.fromString('polynomdivision@test.server'),
|
||||||
|
password: 'aaaa',
|
||||||
|
)
|
||||||
|
..setResource('test-resource', triggerEvent: false);
|
||||||
|
await conn.registerManagers([
|
||||||
|
DiscoManager([]),
|
||||||
|
MUCManager(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await conn.registerFeatureNegotiators([
|
||||||
|
SaslPlainNegotiator(),
|
||||||
|
ResourceBindingNegotiator(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await conn.connect(
|
||||||
|
waitUntilLogin: true,
|
||||||
|
shouldReconnect: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Join a groupchat
|
||||||
|
final roomJid = JID.fromString('channel@muc.example.org');
|
||||||
|
final joinResult = conn.getManagerById<MUCManager>(mucManager)!.joinRoom(
|
||||||
|
roomJid,
|
||||||
|
'test',
|
||||||
|
maxHistoryStanzas: 0,
|
||||||
|
);
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
fakeSocket
|
||||||
|
..injectRawXml(
|
||||||
|
'''
|
||||||
|
<presence
|
||||||
|
from='channel@muc.example.org/firstwitch'
|
||||||
|
id='3DCB0401-D7CF-4E31-BE05-EDF8D057BFBD'>
|
||||||
|
<x xmlns='http://jabber.org/protocol/muc#user'>
|
||||||
|
<item affiliation='owner' role='moderator'/>
|
||||||
|
</x>
|
||||||
|
</presence>
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
..injectRawXml(
|
||||||
|
'''
|
||||||
|
<presence
|
||||||
|
from='channel@muc.example.org/secondwitch'
|
||||||
|
id='C2CD9EE3-8421-431E-854A-A2AD0CE2E23D'>
|
||||||
|
<x xmlns='http://jabber.org/protocol/muc#user'>
|
||||||
|
<item affiliation='admin' role='moderator'/>
|
||||||
|
</x>
|
||||||
|
</presence>
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
..injectRawXml(
|
||||||
|
'''
|
||||||
|
<presence
|
||||||
|
from='channel@muc.example.org/test'
|
||||||
|
id='C2CD9EE3-8421-431E-854A-A2AD0CE2E23E'>
|
||||||
|
<x xmlns='http://jabber.org/protocol/muc#user'>
|
||||||
|
<item affiliation='member' role='none'/>
|
||||||
|
<status code='110' />
|
||||||
|
</x>
|
||||||
|
</presence>
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
..injectRawXml(
|
||||||
|
'''
|
||||||
|
<message from="channel@muc.example.org" type="groupchat" xmlns="jabber:client">
|
||||||
|
<subject/>
|
||||||
|
</message>
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
|
||||||
|
await joinResult;
|
||||||
|
expect(fakeSocket.getState(), 5);
|
||||||
|
|
||||||
|
final room = (await conn
|
||||||
|
.getManagerById<MUCManager>(mucManager)!
|
||||||
|
.getRoomState(roomJid))!;
|
||||||
|
expect(room.joined, true);
|
||||||
|
expect(
|
||||||
|
room.members.length,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
room.members['test'],
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
room.members['secondwitch']!.role,
|
||||||
|
Role.moderator,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
room.members['secondwitch']!.affiliation,
|
||||||
|
Affiliation.admin,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
room.members['firstwitch']!.role,
|
||||||
|
Role.moderator,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
room.members['firstwitch']!.affiliation,
|
||||||
|
Affiliation.owner,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user