feat(xep): Refactor sendMessage to allow groupchat

Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
This commit is contained in:
Ikjot Singh Dhody 2023-06-14 09:59:46 +05:30
parent 64a8de6caa
commit 05c41d3185
5 changed files with 140 additions and 332 deletions

View File

@ -48,6 +48,8 @@ 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_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';

View File

@ -1,89 +1,72 @@
import 'package:moxlib/moxlib.dart'; import 'package:collection/collection.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';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; 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/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/staging/file_upload_notification.dart'; import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0045/xep_0045.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/xeps/xep_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0184.dart';
import 'package:moxxmpp/src/xeps/xep_0308.dart';
import 'package:moxxmpp/src/xeps/xep_0333.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0448.dart'; import 'package:moxxmpp/src/xeps/xep_0449.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart';
/// Data used to build a message stanza. /// A callback that is called whenever a message is sent using
/// /// [MessageManager.sendMessage]. The input the typed map that is passed to
/// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be /// sendMessage.
/// added. This is recommended when sharing files but may cause issues when the message typedef MessageSendingCallback = List<XMLNode> Function(
/// stanza should include a SFS element without any fallbacks. TypedMap<StanzaHandlerExtension>,
class MessageDetails { );
const MessageDetails({
required this.to, /// The raw content of the <body /> element.
this.body, class MessageBodyData implements StanzaHandlerExtension {
this.requestDeliveryReceipt = false, const MessageBodyData(this.body);
this.requestChatMarkers = true,
this.id, /// The content of the <body /> element.
this.originId,
this.quoteBody,
this.quoteId,
this.quoteFrom,
this.chatState,
this.sfs,
this.fun,
this.funReplacement,
this.funCancellation,
this.shouldEncrypt = false,
this.messageRetraction,
this.lastMessageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
this.setOOBFallbackBody = true,
});
final String to;
final String? body; final String? body;
final bool requestDeliveryReceipt;
final bool requestChatMarkers; XMLNode toXML() {
final String? id; return XMLNode(
final String? originId; tag: 'body',
final String? quoteBody; text: body,
final String? quoteId; );
final String? quoteFrom; }
final ChatState? chatState; }
final StatelessFileSharingData? sfs;
final FileMetadataData? fun; /// The id attribute of the message stanza.
final String? funReplacement; class MessageIdData implements StanzaHandlerExtension {
final String? funCancellation; const MessageIdData(this.id);
final bool shouldEncrypt;
final MessageRetractionData? messageRetraction; /// The id attribute of the stanza.
final String? lastMessageCorrectionId; final String id;
final MessageReactions? messageReactions;
final String? stickerPackId;
final List<MessageProcessingHint>? messageProcessingHints;
final bool setOOBFallbackBody;
} }
class MessageManager extends XmppManagerBase { class MessageManager extends XmppManagerBase {
MessageManager() : super(messageManager); MessageManager() : super(messageManager);
/// The priority of the message handler. If a handler should run before this one,
/// which emits the [MessageEvent] event and terminates processing, make sure it
/// has a priority greater than [messageHandlerPriority].
static int messageHandlerPriority = -100;
/// A list of callbacks that are called when a message is sent in order to add
/// appropriate child elements.
final List<MessageSendingCallback> _messageSendingCallbacks =
List<MessageSendingCallback>.empty(growable: true);
void registerMessageSendingCallback(MessageSendingCallback callback) {
_messageSendingCallbacks.add(callback);
}
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
stanzaTag: 'message', stanzaTag: 'message',
callback: _onMessage, callback: _onMessage,
priority: -100, priority: messageHandlerPriority,
) )
]; ];
@ -94,237 +77,72 @@ class MessageManager extends XmppManagerBase {
Stanza _, Stanza _,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final message = state.stanza;
final body = message.firstTag('body');
final hints = List<MessageProcessingHint>.empty(growable: true);
for (final element
in message.findTagsByXmlns(messageProcessingHintsXmlns)) {
hints.add(messageProcessingHintFromXml(element));
}
getAttributes().sendEvent( getAttributes().sendEvent(
MessageEvent( MessageEvent(
body: body != null ? body.innerText() : '', JID.fromString(state.stanza.attributes['from']! as String),
fromJid: JID.fromString(message.attributes['from']! as String), JID.fromString(state.stanza.attributes['to']! as String),
toJid: JID.fromString(message.attributes['to']! as String), state.stanza.attributes['id']! as String,
sid: message.attributes['id']! as String, state.encrypted,
stanzaId: state.stableId ?? const StableStanzaId(), state.extensions,
isCarbon: state.isCarbon, type: state.stanza.attributes['type'] as String?,
deliveryReceiptRequested: state.deliveryReceiptRequested, error: StanzaError.fromStanza(state.stanza),
isMarkable: state.isMarkable, encryptionError: state.encryptionError,
type: message.attributes['type'] as String?,
oob: state.oob,
sfs: state.sfs,
sims: state.sims,
reply: state.reply,
chatState: state.chatState,
fun: state.fun,
funReplacement: state.funReplacement,
funCancellation: state.funCancellation,
encrypted: state.encrypted,
messageRetraction: state.messageRetraction,
messageCorrectionId: state.lastMessageCorrectionSid,
messageReactions: state.messageReactions,
messageProcessingHints: hints.isEmpty ? null : hints,
stickerPackId: state.stickerPackId,
other: state.other,
error: StanzaError.fromStanza(message),
), ),
); );
return state.copyWith(done: true); return state..done = true;
} }
/// Send a message to to with the content body. If deliveryRequest is true, then /// Send an unawaitable message to [to]. [extensions] is a typed map that contains
/// the message will also request a delivery receipt from the receiver. /// data for building the message.
/// If id is non-null, then it will be the id of the message stanza. Future<void> sendMessage(
/// element to this id. If originId is non-null, then it will create an "origin-id" JID to,
/// child in the message stanza and set its id to originId. TypedMap<StanzaHandlerExtension> extensions,
void sendMessage(MessageDetails details) { ) async {
assert( await getAttributes().sendStanza(
implies(
details.quoteBody != null,
details.quoteFrom != null && details.quoteId != null,
),
'When quoting a message, then quoteFrom and quoteId must also be non-null',
);
final stanza = Stanza.message(
to: details.to,
type: 'chat',
id: details.id,
children: [],
);
if (details.quoteBody != null) {
final quote = QuoteData.fromBodies(details.quoteBody!, details.body!);
stanza
..addChild(
XMLNode(tag: 'body', text: quote.body),
)
..addChild(
XMLNode.xmlns(
tag: 'reply',
xmlns: replyXmlns,
attributes: {'to': details.quoteFrom!, 'id': details.quoteId!},
),
)
..addChild(
XMLNode.xmlns(
tag: 'fallback',
xmlns: fallbackXmlns,
attributes: {'for': replyXmlns},
children: [
XMLNode(
tag: 'body',
attributes: <String, String>{
'start': '0',
'end': '${quote.fallbackLength}',
},
)
],
),
);
} else {
var body = details.body;
if (details.sfs != null && details.setOOBFallbackBody) {
// TODO(Unknown): Maybe find a better solution
final firstSource = details.sfs!.sources.first;
if (firstSource is StatelessFileSharingUrlSource) {
body = firstSource.url;
} else if (firstSource is StatelessFileSharingEncryptedSource) {
body = firstSource.source.url;
}
} else if (details.messageRetraction?.fallback != null) {
body = details.messageRetraction!.fallback;
}
if (body != null) {
stanza.addChild(
XMLNode(tag: 'body', text: body),
);
}
}
if (details.requestDeliveryReceipt) {
stanza.addChild(makeMessageDeliveryRequest());
}
if (details.requestChatMarkers) {
stanza.addChild(makeChatMarkerMarkable());
}
if (details.originId != null) {
stanza.addChild(makeOriginIdElement(details.originId!));
}
if (details.sfs != null) {
stanza.addChild(details.sfs!.toXML());
final source = details.sfs!.sources.first;
if (source is StatelessFileSharingUrlSource &&
details.setOOBFallbackBody) {
// SFS recommends OOB as a fallback
stanza.addChild(constructOOBNode(OOBData(url: source.url)));
}
}
if (details.chatState != null) {
stanza.addChild(
// TODO(Unknown): Move this into xep_0085.dart
XMLNode.xmlns(
tag: chatStateToString(details.chatState!),
xmlns: chatStateXmlns,
),
);
}
if (details.fun != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'file-upload',
xmlns: fileUploadNotificationXmlns,
children: [
details.fun!.toXML(),
],
),
);
}
if (details.funReplacement != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'replaces',
xmlns: fileUploadNotificationXmlns,
attributes: <String, String>{
'id': details.funReplacement!,
},
),
);
}
if (details.messageRetraction != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'apply-to',
xmlns: fasteningXmlns,
attributes: <String, String>{
'id': details.messageRetraction!.id,
},
children: [
XMLNode.xmlns(
tag: 'retract',
xmlns: messageRetractionXmlns,
),
],
),
);
if (details.messageRetraction!.fallback != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'fallback',
xmlns: fallbackIndicationXmlns,
),
);
}
}
if (details.lastMessageCorrectionId != null) {
stanza.addChild(
makeLastMessageCorrectionEdit(
details.lastMessageCorrectionId!,
),
);
}
if (details.messageReactions != null) {
stanza.addChild(details.messageReactions!.toXml());
}
if (details.messageProcessingHints != null) {
for (final hint in details.messageProcessingHints!) {
stanza.addChild(hint.toXml());
}
}
if (details.stickerPackId != null) {
stanza.addChild(
XMLNode.xmlns(
tag: 'sticker',
xmlns: stickersXmlns,
attributes: {
'pack': details.stickerPackId!,
},
),
);
}
getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
stanza, Stanza.message(
to: to.toString(),
id: extensions.get<MessageIdData>()?.id,
type: extensions.get<ConversationTypeData>()?.conversationType ==
ConversationType.groupchat
? 'groupchat'
: 'chat',
children: _messageSendingCallbacks
.map((c) => c(extensions))
.flattened
.toList(),
),
awaitable: false, awaitable: false,
), ),
); );
} }
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
if (extensions.get<ReplyData>() != null) {
return [];
}
if (extensions.get<StickersData>() != null) {
return [];
}
if (extensions.get<StatelessFileSharingData>() != null) {
return [];
}
if (extensions.get<OOBData>() != null) {
return [];
}
final data = extensions.get<MessageBodyData>();
return data != null ? [data.toXML()] : [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
registerMessageSendingCallback(_messageSendingCallback);
}
} }

View File

@ -2,4 +2,6 @@ abstract class MUCError {}
class InvalidStanzaFormat extends MUCError {} class InvalidStanzaFormat extends MUCError {}
class InvalidDiscoInfoResponse extends MUCError {}
class NoNicknameSpecified extends MUCError {} class NoNicknameSpecified extends MUCError {}

View File

@ -8,29 +8,15 @@ class RoomInformation {
required this.name, required this.name,
}); });
factory RoomInformation.fromStanza({ factory RoomInformation.fromDiscoInfo({
required JID roomJID, required DiscoInfo discoInfo,
required XMLNode stanza, }) =>
}) { RoomInformation(
final featureNodes = stanza.children[0].findTags('feature'); jid: discoInfo.jid!,
final identityNodes = stanza.children[0].findTags('identity'); features: discoInfo.features,
name: discoInfo.identities[0].name!,
if (featureNodes.isNotEmpty && identityNodes.isNotEmpty) {
final features = featureNodes
.map((xmlNode) => xmlNode.attributes['var'].toString())
.toList();
final name = identityNodes[0].attributes['name'].toString();
return RoomInformation(
jid: roomJID,
features: features,
name: name,
); );
} else {
// ignore: only_throw_errors
throw InvalidStanzaFormat();
}
}
final JID jid; final JID jid;
final List<String> features; final List<String> features;
final String name; final String name;

View File

@ -2,42 +2,42 @@ import 'package:moxxmpp/moxxmpp.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/types.dart'; import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
enum ConversationType { chat, groupchat, groupchatprivate }
class ConversationTypeData extends StanzaHandlerExtension {
ConversationTypeData(this.conversationType);
final ConversationType conversationType;
}
class MUCManager extends XmppManagerBase { class MUCManager extends XmppManagerBase {
MUCManager() : super(mucManager); MUCManager() : super(mucManager);
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<Result<RoomInformation, MUCError>> queryRoomInformation({ Future<Result<RoomInformation, MUCError>> queryRoomInformation(
required JID roomJID, JID roomJID,
}) async { ) async {
final attrs = getAttributes();
try { try {
final result = await attrs.sendStanza( final attrs = getAttributes();
StanzaDetails( final result = await attrs
Stanza.iq( .getManagerById<DiscoManager>(discoManager)
type: 'get', ?.discoInfoQuery(roomJID);
to: roomJID.toString(), if (result!.isType<DiscoError>()) {
children: [ return Result(InvalidStanzaFormat());
XMLNode.xmlns( }
tag: 'query', final roomInformation = RoomInformation.fromDiscoInfo(
xmlns: discoInfoXmlns, discoInfo: result.get(),
)
],
),
),
); );
final roomInformation =
RoomInformation.fromStanza(roomJID: roomJID, stanza: result!);
return Result(roomInformation); return Result(roomInformation);
} catch (e) { } catch (e) {
return Result(InvalidStanzaFormat()); return Result(InvalidDiscoInfoResponse);
} }
} }
Future<Result<bool, MUCError>> joinRoom({ Future<Result<bool, MUCError>> joinRoom(
required JID roomJIDWithNickname, JID roomJIDWithNickname,
}) async { ) async {
if (roomJIDWithNickname.resource.isEmpty) { if (roomJIDWithNickname.resource.isEmpty) {
return Result(NoNicknameSpecified()); return Result(NoNicknameSpecified());
} }
@ -62,9 +62,9 @@ class MUCManager extends XmppManagerBase {
} }
} }
Future<Result<bool, MUCError>> leaveRoom({ Future<Result<bool, MUCError>> leaveRoom(
required JID roomJIDWithNickname, JID roomJIDWithNickname,
}) async { ) async {
if (roomJIDWithNickname.resource.isEmpty) { if (roomJIDWithNickname.resource.isEmpty) {
return Result(NoNicknameSpecified()); return Result(NoNicknameSpecified());
} }