feat: Initial code
This commit is contained in:
		
							parent
							
								
									999e6c521a
								
							
						
					
					
						commit
						a3ced3a6da
					
				
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,2 +1,12 @@ | ||||
| .envrc | ||||
| .direnv/ | ||||
| # Files and directories created by pub. | ||||
| .dart_tool/ | ||||
| .packages | ||||
| 
 | ||||
| # Conventional directory for build outputs. | ||||
| build/ | ||||
| 
 | ||||
| # Omit committing pubspec.lock for library packages; see | ||||
| # https://dart.dev/guides/libraries/private-files#pubspeclock. | ||||
| pubspec.lock | ||||
|  | ||||
							
								
								
									
										3
									
								
								moxxmpp/CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								moxxmpp/CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| ## 0.1.0 | ||||
| 
 | ||||
| - Initial version copied over from Moxxyv2 | ||||
							
								
								
									
										7
									
								
								moxxmpp/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								moxxmpp/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| # moxxmpp | ||||
| 
 | ||||
| A pure-Dart XMPP library written for Moxxy. | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| See `../LICENSE`. | ||||
							
								
								
									
										14
									
								
								moxxmpp/analysis_options.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								moxxmpp/analysis_options.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| include: package:very_good_analysis/analysis_options.yaml | ||||
| linter: | ||||
|   rules: | ||||
|     public_member_api_docs: false | ||||
|     lines_longer_than_80_chars: false | ||||
|     use_setters_to_change_properties: false | ||||
|     avoid_positional_boolean_parameters: false | ||||
|     avoid_bool_literals_in_conditional_expressions: false | ||||
| 
 | ||||
| analyzer: | ||||
|   exclude: | ||||
|     - "**/*.g.dart" | ||||
|     - "**/*.freezed.dart" | ||||
|     - "test/" | ||||
							
								
								
									
										6
									
								
								moxxmpp/example/moxxmpp_example.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								moxxmpp/example/moxxmpp_example.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| import 'package:moxxmpp/moxxmpp.dart'; | ||||
| 
 | ||||
| void main() { | ||||
|   var awesome = Awesome(); | ||||
|   print('awesome: ${awesome.isAwesome}'); | ||||
| } | ||||
							
								
								
									
										76
									
								
								moxxmpp/lib/moxxmpp.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								moxxmpp/lib/moxxmpp.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| library moxxmpp; | ||||
| 
 | ||||
| export 'package:moxxmpp/src/connection.dart'; | ||||
| export 'package:moxxmpp/src/events.dart'; | ||||
| export 'package:moxxmpp/src/iq.dart'; | ||||
| export 'package:moxxmpp/src/jid.dart'; | ||||
| export 'package:moxxmpp/src/managers/attributes.dart'; | ||||
| export 'package:moxxmpp/src/managers/base.dart'; | ||||
| export 'package:moxxmpp/src/managers/data.dart'; | ||||
| export 'package:moxxmpp/src/managers/handlers.dart'; | ||||
| export 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| export 'package:moxxmpp/src/managers/priorities.dart'; | ||||
| export 'package:moxxmpp/src/message.dart'; | ||||
| export 'package:moxxmpp/src/namespaces.dart'; | ||||
| export 'package:moxxmpp/src/negotiators/manager.dart'; | ||||
| export 'package:moxxmpp/src/negotiators/namespaces.dart'; | ||||
| export 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| export 'package:moxxmpp/src/negotiators/resource_binding.dart'; | ||||
| export 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; | ||||
| export 'package:moxxmpp/src/negotiators/sasl/plain.dart'; | ||||
| export 'package:moxxmpp/src/negotiators/sasl/scram.dart'; | ||||
| export 'package:moxxmpp/src/negotiators/starttls.dart'; | ||||
| export 'package:moxxmpp/src/ping.dart'; | ||||
| export 'package:moxxmpp/src/presence.dart'; | ||||
| export 'package:moxxmpp/src/reconnect.dart'; | ||||
| export 'package:moxxmpp/src/rfcs/rfc_2782.dart'; | ||||
| export 'package:moxxmpp/src/rfcs/rfc_4790.dart'; | ||||
| export 'package:moxxmpp/src/roster.dart'; | ||||
| export 'package:moxxmpp/src/settings.dart'; | ||||
| export 'package:moxxmpp/src/socket.dart'; | ||||
| export 'package:moxxmpp/src/stanza.dart'; | ||||
| export 'package:moxxmpp/src/stringxml.dart'; | ||||
| export 'package:moxxmpp/src/types/error.dart'; | ||||
| export 'package:moxxmpp/src/types/resultv2.dart'; | ||||
| export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; | ||||
| export 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0004.dart'; | ||||
| 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/types.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0030/xep_0030.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/helpers.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0066.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0084.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0085.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0115.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0184.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0191.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0198/state.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0203.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0280.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0297.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0300.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0333.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0334.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0352.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0359.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0363.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0380.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0384/crypto.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0384/errors.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0384/helpers.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0384/types.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0385.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0414.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0446.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0447.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0448.dart'; | ||||
| export 'package:moxxmpp/src/xeps/xep_0461.dart'; | ||||
							
								
								
									
										27
									
								
								moxxmpp/lib/src/buffer.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								moxxmpp/lib/src/buffer.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| import 'dart:async'; | ||||
| 
 | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| import 'package:xml/xml.dart'; | ||||
| import 'package:xml/xml_events.dart'; | ||||
| 
 | ||||
| class XmlStreamBuffer extends StreamTransformerBase<String, XMLNode> { | ||||
| 
 | ||||
|   XmlStreamBuffer() : _streamController = StreamController(), _decoder = const XmlNodeDecoder(); | ||||
|   final StreamController<XMLNode> _streamController; | ||||
|   final XmlNodeDecoder _decoder; | ||||
| 
 | ||||
|   @override | ||||
|   Stream<XMLNode> bind(Stream<String> stream) { | ||||
|     stream.toXmlEvents().selectSubtreeEvents((event) { | ||||
|       return event.qualifiedName != 'stream:stream'; | ||||
|     }).transform(_decoder).listen((nodes) { | ||||
|       for (final node in nodes) { | ||||
|         if (node.nodeType == XmlNodeType.ELEMENT) { | ||||
|           _streamController.add(XMLNode.fromXmlElement(node as XmlElement)); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|     return _streamController.stream; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										1033
									
								
								moxxmpp/lib/src/connection.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1033
									
								
								moxxmpp/lib/src/connection.dart
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										206
									
								
								moxxmpp/lib/src/events.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								moxxmpp/lib/src/events.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,206 @@ | ||||
| import 'package:moxxmpp/src/connection.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0066.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0085.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0359.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0385.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0446.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0447.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0461.dart'; | ||||
| 
 | ||||
| abstract class XmppEvent {} | ||||
| 
 | ||||
| /// Triggered when the connection state of the XmppConnection has | ||||
| /// changed. | ||||
| class ConnectionStateChangedEvent extends XmppEvent { | ||||
|   ConnectionStateChangedEvent(this.state, this.before, this.resumed); | ||||
|   final XmppConnectionState before; | ||||
|   final XmppConnectionState state; | ||||
|   final bool resumed; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when we encounter a stream error. | ||||
| class StreamErrorEvent extends XmppEvent { | ||||
|   StreamErrorEvent({ required this.error }); | ||||
|   final String error; | ||||
| } | ||||
| 
 | ||||
| /// Triggered after the SASL authentication has failed. | ||||
| class AuthenticationFailedEvent extends XmppEvent { | ||||
|   AuthenticationFailedEvent(this.saslError); | ||||
|   final String saslError; | ||||
| } | ||||
| 
 | ||||
| /// Triggered after the SASL authentication has succeeded. | ||||
| class AuthenticationSuccessEvent extends XmppEvent {} | ||||
| 
 | ||||
| /// Triggered when we want to ping the connection open | ||||
| class SendPingEvent extends XmppEvent {} | ||||
| 
 | ||||
| /// Triggered when the stream resumption was successful | ||||
| class StreamResumedEvent extends XmppEvent { | ||||
|   StreamResumedEvent({ required this.h }); | ||||
|   final int h; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when stream resumption failed | ||||
| class StreamResumeFailedEvent extends XmppEvent {} | ||||
| 
 | ||||
| class MessageEvent extends XmppEvent { | ||||
|   MessageEvent({ | ||||
|     required this.body, | ||||
|     required this.fromJid, | ||||
|     required this.toJid, | ||||
|     required this.sid, | ||||
|     required this.stanzaId, | ||||
|     required this.isCarbon, | ||||
|     required this.deliveryReceiptRequested, | ||||
|     required this.isMarkable, | ||||
|     required this.encrypted, | ||||
|     required this.other, | ||||
|     this.type, | ||||
|     this.oob, | ||||
|     this.sfs, | ||||
|     this.sims, | ||||
|     this.reply, | ||||
|     this.chatState, | ||||
|     this.fun, | ||||
|     this.funReplacement, | ||||
|     this.funCancellation, | ||||
|   }); | ||||
|   final String body; | ||||
|   final JID fromJid; | ||||
|   final JID toJid; | ||||
|   final String sid; | ||||
|   final String? type; | ||||
|   final StableStanzaId stanzaId; | ||||
|   final bool isCarbon; | ||||
|   final bool deliveryReceiptRequested; | ||||
|   final bool isMarkable; | ||||
|   final OOBData? oob; | ||||
|   final StatelessFileSharingData? sfs; | ||||
|   final StatelessMediaSharingData? sims; | ||||
|   final ReplyData? reply; | ||||
|   final ChatState? chatState; | ||||
|   final FileMetadataData? fun; | ||||
|   final String? funReplacement; | ||||
|   final String? funCancellation; | ||||
|   final bool encrypted; | ||||
|   final Map<String, dynamic> other; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when a client responds to our delivery receipt request | ||||
| class DeliveryReceiptReceivedEvent extends XmppEvent { | ||||
|   DeliveryReceiptReceivedEvent({ required this.from, required this.id }); | ||||
|   final JID from; | ||||
|   final String id; | ||||
| } | ||||
| 
 | ||||
| class ChatMarkerEvent extends XmppEvent { | ||||
|   ChatMarkerEvent({ | ||||
|     required this.type, | ||||
|     required this.from, | ||||
|     required this.id, | ||||
|   }); | ||||
|   final JID from; | ||||
|   final String type; | ||||
|   final String id; | ||||
| } | ||||
| 
 | ||||
| // Triggered when we received a Stream resumption ID | ||||
| class StreamManagementEnabledEvent extends XmppEvent { | ||||
|   StreamManagementEnabledEvent({ | ||||
|       required this.resource, | ||||
|       this.id, | ||||
|       this.location, | ||||
|   }); | ||||
|   final String resource; | ||||
|   final String? id; | ||||
|   final String? location; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when we bound a resource | ||||
| class ResourceBindingSuccessEvent extends XmppEvent { | ||||
|   ResourceBindingSuccessEvent({ required this.resource }); | ||||
|   final String resource; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when we receive presence | ||||
| class PresenceReceivedEvent extends XmppEvent { | ||||
|   PresenceReceivedEvent(this.jid, this.presence); | ||||
|   final JID jid; | ||||
|   final Stanza presence; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when we are starting an connection attempt | ||||
| class ConnectingEvent extends XmppEvent {} | ||||
| 
 | ||||
| /// Triggered when we found out what the server supports | ||||
| class ServerDiscoDoneEvent extends XmppEvent {} | ||||
| 
 | ||||
| class ServerItemDiscoEvent extends XmppEvent { | ||||
|   ServerItemDiscoEvent(this.info); | ||||
|   final DiscoInfo info; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when we receive a subscription request | ||||
| class SubscriptionRequestReceivedEvent extends XmppEvent { | ||||
|   SubscriptionRequestReceivedEvent({ required this.from }); | ||||
|   final JID from; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when we receive a new or updated avatar | ||||
| class AvatarUpdatedEvent extends XmppEvent { | ||||
|   AvatarUpdatedEvent({ required this.jid, required this.base64, required this.hash }); | ||||
|   final String jid; | ||||
|   final String base64; | ||||
|   final String hash; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when a PubSub notification has been received | ||||
| class PubSubNotificationEvent extends XmppEvent { | ||||
|   PubSubNotificationEvent({ required this.item, required this.from }); | ||||
|   final PubSubItem item; | ||||
|   final String from; | ||||
| } | ||||
| 
 | ||||
| /// Triggered by the StreamManagementManager if a stanza has been acked | ||||
| class StanzaAckedEvent extends XmppEvent { | ||||
|   StanzaAckedEvent(this.stanza); | ||||
|   final Stanza stanza; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when receiving a push of the blocklist | ||||
| class BlocklistBlockPushEvent extends XmppEvent { | ||||
|   BlocklistBlockPushEvent({ required this.items }); | ||||
|   final List<String> items; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when receiving a push of the blocklist | ||||
| class BlocklistUnblockPushEvent extends XmppEvent { | ||||
|   BlocklistUnblockPushEvent({ required this.items }); | ||||
|   final List<String> items; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when receiving a push of the blocklist | ||||
| class BlocklistUnblockAllPushEvent extends XmppEvent { | ||||
|   BlocklistUnblockAllPushEvent(); | ||||
| } | ||||
| 
 | ||||
| /// Triggered when a stanza has not been sent because a stanza handler | ||||
| /// wanted to cancel the entire process. | ||||
| class StanzaSendingCancelledEvent extends XmppEvent { | ||||
|   StanzaSendingCancelledEvent(this.data); | ||||
|   final StanzaHandlerData data; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when the device list of a Jid is updated | ||||
| class OmemoDeviceListUpdatedEvent extends XmppEvent { | ||||
|   OmemoDeviceListUpdatedEvent(this.jid, this.deviceList); | ||||
|   final JID jid; | ||||
|   final List<int> deviceList; | ||||
| } | ||||
							
								
								
									
										10
									
								
								moxxmpp/lib/src/iq.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								moxxmpp/lib/src/iq.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import 'package:moxxmpp/src/connection.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| 
 | ||||
| bool handleUnhandledStanza(XmppConnection conn, Stanza stanza) { | ||||
|   if (stanza.type != 'error' && stanza.type != 'result') { | ||||
|     conn.sendStanza(stanza.errorReply('cancel', 'feature-not-implemented')); | ||||
|   } | ||||
| 
 | ||||
|   return true; | ||||
| } | ||||
							
								
								
									
										108
									
								
								moxxmpp/lib/src/jid.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								moxxmpp/lib/src/jid.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| import 'package:meta/meta.dart'; | ||||
| 
 | ||||
| @immutable | ||||
| class JID { | ||||
| 
 | ||||
|   const JID(this.local, this.domain, this.resource); | ||||
| 
 | ||||
|   factory JID.fromString(String jid) { | ||||
|     // 0: Parsing either the local or domain part | ||||
|     // 1: Parsing the domain part | ||||
|     // 2: Parsing the resource | ||||
|     var state = 0; | ||||
|     var buffer = ''; | ||||
|     var local_ = ''; | ||||
|     var domain_ = ''; | ||||
|     var resource_ = ''; | ||||
|      | ||||
|     for (var i = 0; i < jid.length; i++) { | ||||
|       final c = jid[i]; | ||||
|       final eol = i == jid.length - 1; | ||||
|        | ||||
|       switch (state) { | ||||
|         case 0: { | ||||
|           if (c == '@') { | ||||
|             local_ = buffer; | ||||
|             buffer = ''; | ||||
|             state = 1; | ||||
|           } else if (c == '/') { | ||||
|             domain_ = buffer; | ||||
|             buffer = ''; | ||||
|             state = 2; | ||||
|           } else if (eol) { | ||||
|             domain_ = buffer + c; | ||||
|           } else { | ||||
|             buffer += c; | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
|         case 1: { | ||||
|           if (c == '/') { | ||||
|             domain_ = buffer; | ||||
|             buffer = ''; | ||||
|             state = 2; | ||||
|           } else if (eol) { | ||||
|             domain_ = buffer; | ||||
| 
 | ||||
|             if (c != ' ') { | ||||
|               domain_ = domain_ + c; | ||||
|             } | ||||
|           } else if (c != ' ') { | ||||
|             buffer += c; | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
|         case 2: { | ||||
|           if (eol) { | ||||
|             resource_ = buffer; | ||||
| 
 | ||||
|             if (c != ' ') { | ||||
|               resource_ = resource_ + c; | ||||
|             } | ||||
|           } else if (c != ''){ | ||||
|             buffer += c; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return JID(local_, domain_, resource_); | ||||
|   } | ||||
|   final String local; | ||||
|   final String domain; | ||||
|   final String resource; | ||||
| 
 | ||||
|   bool isBare() => resource.isEmpty; | ||||
|   bool isFull() => resource.isNotEmpty; | ||||
| 
 | ||||
|   JID toBare() => JID(local, domain, ''); | ||||
|   JID withResource(String resource) => JID(local, domain, resource); | ||||
|    | ||||
|   @override | ||||
|   String toString() { | ||||
|     var result = ''; | ||||
| 
 | ||||
|     if (local.isNotEmpty) { | ||||
|       result += '$local@$domain'; | ||||
|     } else { | ||||
|       result += domain; | ||||
|     } | ||||
|     if (isFull()) { | ||||
|       result += '/$resource'; | ||||
|     } | ||||
| 
 | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (other is JID) { | ||||
|       return other.local == local && other.domain == domain && other.resource == resource; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => local.hashCode ^ domain.hashCode ^ resource.hashCode; | ||||
| } | ||||
							
								
								
									
										55
									
								
								moxxmpp/lib/src/managers/attributes.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								moxxmpp/lib/src/managers/attributes.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| import 'dart:async'; | ||||
| 
 | ||||
| import 'package:moxxmpp/src/connection.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/settings.dart'; | ||||
| import 'package:moxxmpp/src/socket.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| class XmppManagerAttributes { | ||||
|    | ||||
|   XmppManagerAttributes({ | ||||
|     required this.sendStanza, | ||||
|     required this.sendNonza, | ||||
|     required this.getManagerById, | ||||
|     required this.sendEvent, | ||||
|     required this.getConnectionSettings, | ||||
|     required this.isFeatureSupported, | ||||
|     required this.getFullJID, | ||||
|     required this.getSocket, | ||||
|     required this.getConnection, | ||||
|     required this.getNegotiatorById, | ||||
|   }); | ||||
|   /// Send a stanza whose response can be awaited. | ||||
|   final Future<XMLNode> Function(Stanza stanza, { StanzaFromType addFrom, bool addId, bool awaitable, bool encrypted}) sendStanza; | ||||
| 
 | ||||
|   /// Send a nonza. | ||||
|   final void Function(XMLNode) sendNonza; | ||||
| 
 | ||||
|   /// Send an event to the connection's event channel. | ||||
|   final void Function(XmppEvent) sendEvent; | ||||
| 
 | ||||
|   /// Get the connection settings of the attached connection. | ||||
|   final ConnectionSettings Function() getConnectionSettings; | ||||
| 
 | ||||
|   /// (Maybe) Get a Manager attached to the connection by its Id. | ||||
|   final T? Function<T extends XmppManagerBase>(String) getManagerById; | ||||
| 
 | ||||
|   /// Returns true if a server feature is supported | ||||
|   final bool Function(String) isFeatureSupported; | ||||
|    | ||||
|   /// Returns the full JID of the current account | ||||
|   final JID Function() getFullJID; | ||||
| 
 | ||||
|   /// Returns the current socket. MUST NOT be used to send data. | ||||
|   final BaseSocketWrapper Function() getSocket; | ||||
| 
 | ||||
|   /// Return the [XmppConnection] the manager is registered against. | ||||
|   final XmppConnection Function() getConnection; | ||||
| 
 | ||||
|   final T? Function<T extends XmppFeatureNegotiatorBase>(String) getNegotiatorById; | ||||
| } | ||||
							
								
								
									
										72
									
								
								moxxmpp/lib/src/managers/base.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								moxxmpp/lib/src/managers/base.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/managers/attributes.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| abstract class XmppManagerBase { | ||||
|   late final XmppManagerAttributes _managerAttributes; | ||||
|   late final Logger _log; | ||||
| 
 | ||||
|   /// Registers the callbacks from XmppConnection with the manager | ||||
|   void register(XmppManagerAttributes attributes) { | ||||
|     _managerAttributes = attributes; | ||||
|     _log = Logger(getName()); | ||||
|   } | ||||
|    | ||||
|   /// Returns the attributes that are registered with the manager. | ||||
|   /// Must only be called after register has been called on it. | ||||
|   XmppManagerAttributes getAttributes() { | ||||
|     return _managerAttributes; | ||||
|   } | ||||
| 
 | ||||
|   /// Return the StanzaHandlers associated with this manager that deal with stanzas we | ||||
|   /// send. These are run before the stanza is sent. | ||||
|   List<StanzaHandler> getOutgoingPreStanzaHandlers() => []; | ||||
| 
 | ||||
|   /// Return the StanzaHandlers associated with this manager that deal with stanzas we | ||||
|   /// send. These are run after the stanza is sent. | ||||
|   List<StanzaHandler> getOutgoingPostStanzaHandlers() => []; | ||||
|    | ||||
|   /// Return the StanzaHandlers associated with this manager that deal with stanzas we | ||||
|   /// receive. | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => []; | ||||
|    | ||||
|   /// Return the NonzaHandlers associated with this manager. | ||||
|   List<NonzaHandler> getNonzaHandlers() => []; | ||||
| 
 | ||||
|   /// Return a list of features that should be included in a disco response. | ||||
|   List<String> getDiscoFeatures() => []; | ||||
|    | ||||
|   /// Return the Id (akin to xmlns) of this manager. | ||||
|   String getId(); | ||||
| 
 | ||||
|   /// Return a name that will be used for logging. | ||||
|   String getName(); | ||||
| 
 | ||||
|   /// Return the logger for this manager. | ||||
|   Logger get logger => _log; | ||||
|    | ||||
|   /// Called when XmppConnection triggers an event | ||||
|   Future<void> onXmppEvent(XmppEvent event) async {} | ||||
| 
 | ||||
|   /// Returns true if the XEP is supported on the server. If not, returns false | ||||
|   Future<bool> isSupported(); | ||||
|    | ||||
|   /// Runs all NonzaHandlers of this Manager which match the nonza. Resolves to true if | ||||
|   /// the nonza has been handled by one of the handlers. Resolves to false otherwise. | ||||
|   Future<bool> runNonzaHandlers(XMLNode nonza) async { | ||||
|     var handled = false; | ||||
|     await Future.forEach( | ||||
|       getNonzaHandlers(), | ||||
|       (NonzaHandler handler) async { | ||||
|         if (handler.matches(nonza)) { | ||||
|           handled = true; | ||||
|           await handler.callback(nonza); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
| 
 | ||||
|     return handled; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										60
									
								
								moxxmpp/lib/src/managers/data.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								moxxmpp/lib/src/managers/data.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0066.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0085.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0203.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0359.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0380.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0385.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0446.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0447.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0461.dart'; | ||||
| 
 | ||||
| part 'data.freezed.dart'; | ||||
| 
 | ||||
| @freezed | ||||
| class StanzaHandlerData with _$StanzaHandlerData { | ||||
|   factory StanzaHandlerData( | ||||
|     // Indicates to the runner that processing is now done. This means that all | ||||
|     // pre-processing is done and no other handlers should be consulted. | ||||
|     bool done, | ||||
|     // Indicates to the runner that processing is to be cancelled and no further handlers | ||||
|     // should run. The stanza also will not be sent. | ||||
|     bool cancel, | ||||
|     // The reason why we cancelled the processing and sending | ||||
|     dynamic cancelReason, | ||||
|     // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely | ||||
|     // necessary, e.g. with Message Carbons or OMEMO | ||||
|     Stanza stanza, | ||||
|     { | ||||
|       // Whether the stanza is retransmitted. Only useful in the context of outgoing | ||||
|       // stanza handlers. MUST NOT be overwritten. | ||||
|       @Default(false) bool retransmitted, | ||||
|       StatelessMediaSharingData? sims, | ||||
|       StatelessFileSharingData? sfs, | ||||
|       OOBData? oob, | ||||
|       StableStanzaId? stableId, | ||||
|       ReplyData? reply, | ||||
|       ChatState? chatState, | ||||
|       @Default(false) bool isCarbon, | ||||
|       @Default(false) bool deliveryReceiptRequested, | ||||
|       @Default(false) bool isMarkable, | ||||
|       // File Upload Notifications | ||||
|       // A notification | ||||
|       FileMetadataData? fun, | ||||
|       // The stanza id this replaces | ||||
|       String? funReplacement, | ||||
|       // The stanza id this cancels | ||||
|       String? funCancellation, | ||||
|       // Whether the stanza was received encrypted | ||||
|       @Default(false) bool encrypted, | ||||
|       // The stated type of encryption used, if any was used | ||||
|       ExplicitEncryptionType? encryptionType, | ||||
|       // Delayed Delivery | ||||
|       DelayedDelivery? delayedDelivery, | ||||
|       // This is for stanza handlers that are not part of the XMPP library but still need | ||||
|       // pass data around. | ||||
|       @Default(<String, dynamic>{}) Map<String, dynamic> other, | ||||
|     } | ||||
|   ) = _StanzaHandlerData; | ||||
| } | ||||
							
								
								
									
										613
									
								
								moxxmpp/lib/src/managers/data.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										613
									
								
								moxxmpp/lib/src/managers/data.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,613 @@ | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target | ||||
| 
 | ||||
| part of 'data.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| T _$identity<T>(T value) => value; | ||||
| 
 | ||||
| final _privateConstructorUsedError = UnsupportedError( | ||||
|     'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); | ||||
| 
 | ||||
| /// @nodoc | ||||
| mixin _$StanzaHandlerData { | ||||
| // Indicates to the runner that processing is now done. This means that all | ||||
| // pre-processing is done and no other handlers should be consulted. | ||||
|   bool get done => | ||||
|       throw _privateConstructorUsedError; // Indicates to the runner that processing is to be cancelled and no further handlers | ||||
| // should run. The stanza also will not be sent. | ||||
|   bool get cancel => | ||||
|       throw _privateConstructorUsedError; // The reason why we cancelled the processing and sending | ||||
|   dynamic get cancelReason => | ||||
|       throw _privateConstructorUsedError; // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely | ||||
| // necessary, e.g. with Message Carbons or OMEMO | ||||
|   Stanza get stanza => | ||||
|       throw _privateConstructorUsedError; // Whether the stanza is retransmitted. Only useful in the context of outgoing | ||||
| // stanza handlers. MUST NOT be overwritten. | ||||
|   bool get retransmitted => throw _privateConstructorUsedError; | ||||
|   StatelessMediaSharingData? get sims => throw _privateConstructorUsedError; | ||||
|   StatelessFileSharingData? get sfs => throw _privateConstructorUsedError; | ||||
|   OOBData? get oob => throw _privateConstructorUsedError; | ||||
|   StableStanzaId? get stableId => throw _privateConstructorUsedError; | ||||
|   ReplyData? get reply => throw _privateConstructorUsedError; | ||||
|   ChatState? get chatState => throw _privateConstructorUsedError; | ||||
|   bool get isCarbon => throw _privateConstructorUsedError; | ||||
|   bool get deliveryReceiptRequested => throw _privateConstructorUsedError; | ||||
|   bool get isMarkable => | ||||
|       throw _privateConstructorUsedError; // File Upload Notifications | ||||
| // A notification | ||||
|   FileMetadataData? get fun => | ||||
|       throw _privateConstructorUsedError; // The stanza id this replaces | ||||
|   String? get funReplacement => | ||||
|       throw _privateConstructorUsedError; // The stanza id this cancels | ||||
|   String? get funCancellation => | ||||
|       throw _privateConstructorUsedError; // Whether the stanza was received encrypted | ||||
|   bool get encrypted => | ||||
|       throw _privateConstructorUsedError; // The stated type of encryption used, if any was used | ||||
|   ExplicitEncryptionType? get encryptionType => | ||||
|       throw _privateConstructorUsedError; // Delayed Delivery | ||||
|   DelayedDelivery? get delayedDelivery => | ||||
|       throw _privateConstructorUsedError; // This is for stanza handlers that are not part of the XMPP library but still need | ||||
| // pass data around. | ||||
|   Map<String, dynamic> get other => throw _privateConstructorUsedError; | ||||
| 
 | ||||
|   @JsonKey(ignore: true) | ||||
|   $StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| abstract class $StanzaHandlerDataCopyWith<$Res> { | ||||
|   factory $StanzaHandlerDataCopyWith( | ||||
|           StanzaHandlerData value, $Res Function(StanzaHandlerData) then) = | ||||
|       _$StanzaHandlerDataCopyWithImpl<$Res>; | ||||
|   $Res call( | ||||
|       {bool done, | ||||
|       bool cancel, | ||||
|       dynamic cancelReason, | ||||
|       Stanza stanza, | ||||
|       bool retransmitted, | ||||
|       StatelessMediaSharingData? sims, | ||||
|       StatelessFileSharingData? sfs, | ||||
|       OOBData? oob, | ||||
|       StableStanzaId? stableId, | ||||
|       ReplyData? reply, | ||||
|       ChatState? chatState, | ||||
|       bool isCarbon, | ||||
|       bool deliveryReceiptRequested, | ||||
|       bool isMarkable, | ||||
|       FileMetadataData? fun, | ||||
|       String? funReplacement, | ||||
|       String? funCancellation, | ||||
|       bool encrypted, | ||||
|       ExplicitEncryptionType? encryptionType, | ||||
|       DelayedDelivery? delayedDelivery, | ||||
|       Map<String, dynamic> other}); | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| class _$StanzaHandlerDataCopyWithImpl<$Res> | ||||
|     implements $StanzaHandlerDataCopyWith<$Res> { | ||||
|   _$StanzaHandlerDataCopyWithImpl(this._value, this._then); | ||||
| 
 | ||||
|   final StanzaHandlerData _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function(StanzaHandlerData) _then; | ||||
| 
 | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? done = freezed, | ||||
|     Object? cancel = freezed, | ||||
|     Object? cancelReason = freezed, | ||||
|     Object? stanza = freezed, | ||||
|     Object? retransmitted = freezed, | ||||
|     Object? sims = freezed, | ||||
|     Object? sfs = freezed, | ||||
|     Object? oob = freezed, | ||||
|     Object? stableId = freezed, | ||||
|     Object? reply = freezed, | ||||
|     Object? chatState = freezed, | ||||
|     Object? isCarbon = freezed, | ||||
|     Object? deliveryReceiptRequested = freezed, | ||||
|     Object? isMarkable = freezed, | ||||
|     Object? fun = freezed, | ||||
|     Object? funReplacement = freezed, | ||||
|     Object? funCancellation = freezed, | ||||
|     Object? encrypted = freezed, | ||||
|     Object? encryptionType = freezed, | ||||
|     Object? delayedDelivery = freezed, | ||||
|     Object? other = freezed, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       done: done == freezed | ||||
|           ? _value.done | ||||
|           : done // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       cancel: cancel == freezed | ||||
|           ? _value.cancel | ||||
|           : cancel // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       cancelReason: cancelReason == freezed | ||||
|           ? _value.cancelReason | ||||
|           : cancelReason // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|       stanza: stanza == freezed | ||||
|           ? _value.stanza | ||||
|           : stanza // ignore: cast_nullable_to_non_nullable | ||||
|               as Stanza, | ||||
|       retransmitted: retransmitted == freezed | ||||
|           ? _value.retransmitted | ||||
|           : retransmitted // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       sims: sims == freezed | ||||
|           ? _value.sims | ||||
|           : sims // ignore: cast_nullable_to_non_nullable | ||||
|               as StatelessMediaSharingData?, | ||||
|       sfs: sfs == freezed | ||||
|           ? _value.sfs | ||||
|           : sfs // ignore: cast_nullable_to_non_nullable | ||||
|               as StatelessFileSharingData?, | ||||
|       oob: oob == freezed | ||||
|           ? _value.oob | ||||
|           : oob // ignore: cast_nullable_to_non_nullable | ||||
|               as OOBData?, | ||||
|       stableId: stableId == freezed | ||||
|           ? _value.stableId | ||||
|           : stableId // ignore: cast_nullable_to_non_nullable | ||||
|               as StableStanzaId?, | ||||
|       reply: reply == freezed | ||||
|           ? _value.reply | ||||
|           : reply // ignore: cast_nullable_to_non_nullable | ||||
|               as ReplyData?, | ||||
|       chatState: chatState == freezed | ||||
|           ? _value.chatState | ||||
|           : chatState // ignore: cast_nullable_to_non_nullable | ||||
|               as ChatState?, | ||||
|       isCarbon: isCarbon == freezed | ||||
|           ? _value.isCarbon | ||||
|           : isCarbon // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       deliveryReceiptRequested: deliveryReceiptRequested == freezed | ||||
|           ? _value.deliveryReceiptRequested | ||||
|           : deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       isMarkable: isMarkable == freezed | ||||
|           ? _value.isMarkable | ||||
|           : isMarkable // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       fun: fun == freezed | ||||
|           ? _value.fun | ||||
|           : fun // ignore: cast_nullable_to_non_nullable | ||||
|               as FileMetadataData?, | ||||
|       funReplacement: funReplacement == freezed | ||||
|           ? _value.funReplacement | ||||
|           : funReplacement // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       funCancellation: funCancellation == freezed | ||||
|           ? _value.funCancellation | ||||
|           : funCancellation // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       encrypted: encrypted == freezed | ||||
|           ? _value.encrypted | ||||
|           : encrypted // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       encryptionType: encryptionType == freezed | ||||
|           ? _value.encryptionType | ||||
|           : encryptionType // ignore: cast_nullable_to_non_nullable | ||||
|               as ExplicitEncryptionType?, | ||||
|       delayedDelivery: delayedDelivery == freezed | ||||
|           ? _value.delayedDelivery | ||||
|           : delayedDelivery // ignore: cast_nullable_to_non_nullable | ||||
|               as DelayedDelivery?, | ||||
|       other: other == freezed | ||||
|           ? _value.other | ||||
|           : other // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| abstract class _$$_StanzaHandlerDataCopyWith<$Res> | ||||
|     implements $StanzaHandlerDataCopyWith<$Res> { | ||||
|   factory _$$_StanzaHandlerDataCopyWith(_$_StanzaHandlerData value, | ||||
|           $Res Function(_$_StanzaHandlerData) then) = | ||||
|       __$$_StanzaHandlerDataCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   $Res call( | ||||
|       {bool done, | ||||
|       bool cancel, | ||||
|       dynamic cancelReason, | ||||
|       Stanza stanza, | ||||
|       bool retransmitted, | ||||
|       StatelessMediaSharingData? sims, | ||||
|       StatelessFileSharingData? sfs, | ||||
|       OOBData? oob, | ||||
|       StableStanzaId? stableId, | ||||
|       ReplyData? reply, | ||||
|       ChatState? chatState, | ||||
|       bool isCarbon, | ||||
|       bool deliveryReceiptRequested, | ||||
|       bool isMarkable, | ||||
|       FileMetadataData? fun, | ||||
|       String? funReplacement, | ||||
|       String? funCancellation, | ||||
|       bool encrypted, | ||||
|       ExplicitEncryptionType? encryptionType, | ||||
|       DelayedDelivery? delayedDelivery, | ||||
|       Map<String, dynamic> other}); | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| class __$$_StanzaHandlerDataCopyWithImpl<$Res> | ||||
|     extends _$StanzaHandlerDataCopyWithImpl<$Res> | ||||
|     implements _$$_StanzaHandlerDataCopyWith<$Res> { | ||||
|   __$$_StanzaHandlerDataCopyWithImpl( | ||||
|       _$_StanzaHandlerData _value, $Res Function(_$_StanzaHandlerData) _then) | ||||
|       : super(_value, (v) => _then(v as _$_StanzaHandlerData)); | ||||
| 
 | ||||
|   @override | ||||
|   _$_StanzaHandlerData get _value => super._value as _$_StanzaHandlerData; | ||||
| 
 | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? done = freezed, | ||||
|     Object? cancel = freezed, | ||||
|     Object? cancelReason = freezed, | ||||
|     Object? stanza = freezed, | ||||
|     Object? retransmitted = freezed, | ||||
|     Object? sims = freezed, | ||||
|     Object? sfs = freezed, | ||||
|     Object? oob = freezed, | ||||
|     Object? stableId = freezed, | ||||
|     Object? reply = freezed, | ||||
|     Object? chatState = freezed, | ||||
|     Object? isCarbon = freezed, | ||||
|     Object? deliveryReceiptRequested = freezed, | ||||
|     Object? isMarkable = freezed, | ||||
|     Object? fun = freezed, | ||||
|     Object? funReplacement = freezed, | ||||
|     Object? funCancellation = freezed, | ||||
|     Object? encrypted = freezed, | ||||
|     Object? encryptionType = freezed, | ||||
|     Object? delayedDelivery = freezed, | ||||
|     Object? other = freezed, | ||||
|   }) { | ||||
|     return _then(_$_StanzaHandlerData( | ||||
|       done == freezed | ||||
|           ? _value.done | ||||
|           : done // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       cancel == freezed | ||||
|           ? _value.cancel | ||||
|           : cancel // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       cancelReason == freezed | ||||
|           ? _value.cancelReason | ||||
|           : cancelReason // ignore: cast_nullable_to_non_nullable | ||||
|               as dynamic, | ||||
|       stanza == freezed | ||||
|           ? _value.stanza | ||||
|           : stanza // ignore: cast_nullable_to_non_nullable | ||||
|               as Stanza, | ||||
|       retransmitted: retransmitted == freezed | ||||
|           ? _value.retransmitted | ||||
|           : retransmitted // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       sims: sims == freezed | ||||
|           ? _value.sims | ||||
|           : sims // ignore: cast_nullable_to_non_nullable | ||||
|               as StatelessMediaSharingData?, | ||||
|       sfs: sfs == freezed | ||||
|           ? _value.sfs | ||||
|           : sfs // ignore: cast_nullable_to_non_nullable | ||||
|               as StatelessFileSharingData?, | ||||
|       oob: oob == freezed | ||||
|           ? _value.oob | ||||
|           : oob // ignore: cast_nullable_to_non_nullable | ||||
|               as OOBData?, | ||||
|       stableId: stableId == freezed | ||||
|           ? _value.stableId | ||||
|           : stableId // ignore: cast_nullable_to_non_nullable | ||||
|               as StableStanzaId?, | ||||
|       reply: reply == freezed | ||||
|           ? _value.reply | ||||
|           : reply // ignore: cast_nullable_to_non_nullable | ||||
|               as ReplyData?, | ||||
|       chatState: chatState == freezed | ||||
|           ? _value.chatState | ||||
|           : chatState // ignore: cast_nullable_to_non_nullable | ||||
|               as ChatState?, | ||||
|       isCarbon: isCarbon == freezed | ||||
|           ? _value.isCarbon | ||||
|           : isCarbon // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       deliveryReceiptRequested: deliveryReceiptRequested == freezed | ||||
|           ? _value.deliveryReceiptRequested | ||||
|           : deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       isMarkable: isMarkable == freezed | ||||
|           ? _value.isMarkable | ||||
|           : isMarkable // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       fun: fun == freezed | ||||
|           ? _value.fun | ||||
|           : fun // ignore: cast_nullable_to_non_nullable | ||||
|               as FileMetadataData?, | ||||
|       funReplacement: funReplacement == freezed | ||||
|           ? _value.funReplacement | ||||
|           : funReplacement // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       funCancellation: funCancellation == freezed | ||||
|           ? _value.funCancellation | ||||
|           : funCancellation // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       encrypted: encrypted == freezed | ||||
|           ? _value.encrypted | ||||
|           : encrypted // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|       encryptionType: encryptionType == freezed | ||||
|           ? _value.encryptionType | ||||
|           : encryptionType // ignore: cast_nullable_to_non_nullable | ||||
|               as ExplicitEncryptionType?, | ||||
|       delayedDelivery: delayedDelivery == freezed | ||||
|           ? _value.delayedDelivery | ||||
|           : delayedDelivery // ignore: cast_nullable_to_non_nullable | ||||
|               as DelayedDelivery?, | ||||
|       other: other == freezed | ||||
|           ? _value._other | ||||
|           : other // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| 
 | ||||
| class _$_StanzaHandlerData implements _StanzaHandlerData { | ||||
|   _$_StanzaHandlerData(this.done, this.cancel, this.cancelReason, this.stanza, | ||||
|       {this.retransmitted = false, | ||||
|       this.sims, | ||||
|       this.sfs, | ||||
|       this.oob, | ||||
|       this.stableId, | ||||
|       this.reply, | ||||
|       this.chatState, | ||||
|       this.isCarbon = false, | ||||
|       this.deliveryReceiptRequested = false, | ||||
|       this.isMarkable = false, | ||||
|       this.fun, | ||||
|       this.funReplacement, | ||||
|       this.funCancellation, | ||||
|       this.encrypted = false, | ||||
|       this.encryptionType, | ||||
|       this.delayedDelivery, | ||||
|       final Map<String, dynamic> other = const <String, dynamic>{}}) | ||||
|       : _other = other; | ||||
| 
 | ||||
| // Indicates to the runner that processing is now done. This means that all | ||||
| // pre-processing is done and no other handlers should be consulted. | ||||
|   @override | ||||
|   final bool done; | ||||
| // Indicates to the runner that processing is to be cancelled and no further handlers | ||||
| // should run. The stanza also will not be sent. | ||||
|   @override | ||||
|   final bool cancel; | ||||
| // The reason why we cancelled the processing and sending | ||||
|   @override | ||||
|   final dynamic cancelReason; | ||||
| // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely | ||||
| // necessary, e.g. with Message Carbons or OMEMO | ||||
|   @override | ||||
|   final Stanza stanza; | ||||
| // Whether the stanza is retransmitted. Only useful in the context of outgoing | ||||
| // stanza handlers. MUST NOT be overwritten. | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   final bool retransmitted; | ||||
|   @override | ||||
|   final StatelessMediaSharingData? sims; | ||||
|   @override | ||||
|   final StatelessFileSharingData? sfs; | ||||
|   @override | ||||
|   final OOBData? oob; | ||||
|   @override | ||||
|   final StableStanzaId? stableId; | ||||
|   @override | ||||
|   final ReplyData? reply; | ||||
|   @override | ||||
|   final ChatState? chatState; | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   final bool isCarbon; | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   final bool deliveryReceiptRequested; | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   final bool isMarkable; | ||||
| // File Upload Notifications | ||||
| // A notification | ||||
|   @override | ||||
|   final FileMetadataData? fun; | ||||
| // The stanza id this replaces | ||||
|   @override | ||||
|   final String? funReplacement; | ||||
| // The stanza id this cancels | ||||
|   @override | ||||
|   final String? funCancellation; | ||||
| // Whether the stanza was received encrypted | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   final bool encrypted; | ||||
| // The stated type of encryption used, if any was used | ||||
|   @override | ||||
|   final ExplicitEncryptionType? encryptionType; | ||||
| // Delayed Delivery | ||||
|   @override | ||||
|   final DelayedDelivery? delayedDelivery; | ||||
| // This is for stanza handlers that are not part of the XMPP library but still need | ||||
| // pass data around. | ||||
|   final Map<String, dynamic> _other; | ||||
| // This is for stanza handlers that are not part of the XMPP library but still need | ||||
| // pass data around. | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   Map<String, dynamic> get other { | ||||
|     // ignore: implicit_dynamic_type | ||||
|     return EqualUnmodifiableMapView(_other); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other)'; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(dynamic other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$_StanzaHandlerData && | ||||
|             const DeepCollectionEquality().equals(other.done, done) && | ||||
|             const DeepCollectionEquality().equals(other.cancel, cancel) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.cancelReason, cancelReason) && | ||||
|             const DeepCollectionEquality().equals(other.stanza, stanza) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.retransmitted, retransmitted) && | ||||
|             const DeepCollectionEquality().equals(other.sims, sims) && | ||||
|             const DeepCollectionEquality().equals(other.sfs, sfs) && | ||||
|             const DeepCollectionEquality().equals(other.oob, oob) && | ||||
|             const DeepCollectionEquality().equals(other.stableId, stableId) && | ||||
|             const DeepCollectionEquality().equals(other.reply, reply) && | ||||
|             const DeepCollectionEquality().equals(other.chatState, chatState) && | ||||
|             const DeepCollectionEquality().equals(other.isCarbon, isCarbon) && | ||||
|             const DeepCollectionEquality().equals( | ||||
|                 other.deliveryReceiptRequested, deliveryReceiptRequested) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.isMarkable, isMarkable) && | ||||
|             const DeepCollectionEquality().equals(other.fun, fun) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.funReplacement, funReplacement) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.funCancellation, funCancellation) && | ||||
|             const DeepCollectionEquality().equals(other.encrypted, encrypted) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.encryptionType, encryptionType) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.delayedDelivery, delayedDelivery) && | ||||
|             const DeepCollectionEquality().equals(other._other, this._other)); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => Object.hashAll([ | ||||
|         runtimeType, | ||||
|         const DeepCollectionEquality().hash(done), | ||||
|         const DeepCollectionEquality().hash(cancel), | ||||
|         const DeepCollectionEquality().hash(cancelReason), | ||||
|         const DeepCollectionEquality().hash(stanza), | ||||
|         const DeepCollectionEquality().hash(retransmitted), | ||||
|         const DeepCollectionEquality().hash(sims), | ||||
|         const DeepCollectionEquality().hash(sfs), | ||||
|         const DeepCollectionEquality().hash(oob), | ||||
|         const DeepCollectionEquality().hash(stableId), | ||||
|         const DeepCollectionEquality().hash(reply), | ||||
|         const DeepCollectionEquality().hash(chatState), | ||||
|         const DeepCollectionEquality().hash(isCarbon), | ||||
|         const DeepCollectionEquality().hash(deliveryReceiptRequested), | ||||
|         const DeepCollectionEquality().hash(isMarkable), | ||||
|         const DeepCollectionEquality().hash(fun), | ||||
|         const DeepCollectionEquality().hash(funReplacement), | ||||
|         const DeepCollectionEquality().hash(funCancellation), | ||||
|         const DeepCollectionEquality().hash(encrypted), | ||||
|         const DeepCollectionEquality().hash(encryptionType), | ||||
|         const DeepCollectionEquality().hash(delayedDelivery), | ||||
|         const DeepCollectionEquality().hash(_other) | ||||
|       ]); | ||||
| 
 | ||||
|   @JsonKey(ignore: true) | ||||
|   @override | ||||
|   _$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith => | ||||
|       __$$_StanzaHandlerDataCopyWithImpl<_$_StanzaHandlerData>( | ||||
|           this, _$identity); | ||||
| } | ||||
| 
 | ||||
| abstract class _StanzaHandlerData implements StanzaHandlerData { | ||||
|   factory _StanzaHandlerData(final bool done, final bool cancel, | ||||
|       final dynamic cancelReason, final Stanza stanza, | ||||
|       {final bool retransmitted, | ||||
|       final StatelessMediaSharingData? sims, | ||||
|       final StatelessFileSharingData? sfs, | ||||
|       final OOBData? oob, | ||||
|       final StableStanzaId? stableId, | ||||
|       final ReplyData? reply, | ||||
|       final ChatState? chatState, | ||||
|       final bool isCarbon, | ||||
|       final bool deliveryReceiptRequested, | ||||
|       final bool isMarkable, | ||||
|       final FileMetadataData? fun, | ||||
|       final String? funReplacement, | ||||
|       final String? funCancellation, | ||||
|       final bool encrypted, | ||||
|       final ExplicitEncryptionType? encryptionType, | ||||
|       final DelayedDelivery? delayedDelivery, | ||||
|       final Map<String, dynamic> other}) = _$_StanzaHandlerData; | ||||
| 
 | ||||
|   @override // Indicates to the runner that processing is now done. This means that all | ||||
| // pre-processing is done and no other handlers should be consulted. | ||||
|   bool get done; | ||||
|   @override // Indicates to the runner that processing is to be cancelled and no further handlers | ||||
| // should run. The stanza also will not be sent. | ||||
|   bool get cancel; | ||||
|   @override // The reason why we cancelled the processing and sending | ||||
|   dynamic get cancelReason; | ||||
|   @override // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely | ||||
| // necessary, e.g. with Message Carbons or OMEMO | ||||
|   Stanza get stanza; | ||||
|   @override // Whether the stanza is retransmitted. Only useful in the context of outgoing | ||||
| // stanza handlers. MUST NOT be overwritten. | ||||
|   bool get retransmitted; | ||||
|   @override | ||||
|   StatelessMediaSharingData? get sims; | ||||
|   @override | ||||
|   StatelessFileSharingData? get sfs; | ||||
|   @override | ||||
|   OOBData? get oob; | ||||
|   @override | ||||
|   StableStanzaId? get stableId; | ||||
|   @override | ||||
|   ReplyData? get reply; | ||||
|   @override | ||||
|   ChatState? get chatState; | ||||
|   @override | ||||
|   bool get isCarbon; | ||||
|   @override | ||||
|   bool get deliveryReceiptRequested; | ||||
|   @override | ||||
|   bool get isMarkable; | ||||
|   @override // File Upload Notifications | ||||
| // A notification | ||||
|   FileMetadataData? get fun; | ||||
|   @override // The stanza id this replaces | ||||
|   String? get funReplacement; | ||||
|   @override // The stanza id this cancels | ||||
|   String? get funCancellation; | ||||
|   @override // Whether the stanza was received encrypted | ||||
|   bool get encrypted; | ||||
|   @override // The stated type of encryption used, if any was used | ||||
|   ExplicitEncryptionType? get encryptionType; | ||||
|   @override // Delayed Delivery | ||||
|   DelayedDelivery? get delayedDelivery; | ||||
|   @override // This is for stanza handlers that are not part of the XMPP library but still need | ||||
| // pass data around. | ||||
|   Map<String, dynamic> get other; | ||||
|   @override | ||||
|   @JsonKey(ignore: true) | ||||
|   _$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
							
								
								
									
										93
									
								
								moxxmpp/lib/src/managers/handlers.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								moxxmpp/lib/src/managers/handlers.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | ||||
| import 'package:moxlib/moxlib.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| abstract class Handler { | ||||
| 
 | ||||
|   const Handler(this.matchStanzas, { this.nonzaTag, this.nonzaXmlns }); | ||||
|   final String? nonzaTag; | ||||
|   final String? nonzaXmlns; | ||||
|   final bool matchStanzas; | ||||
| 
 | ||||
|   /// Returns true if the node matches the description provided by this [Handler]. | ||||
|   bool matches(XMLNode node) { | ||||
|     var matches = false; | ||||
| 
 | ||||
|     if (nonzaTag == null && nonzaXmlns == null) { | ||||
|       matches = true; | ||||
|     } | ||||
| 
 | ||||
|     if (nonzaXmlns != null && nonzaTag != null) { | ||||
|       matches = (node.attributes['xmlns'] ?? '') == nonzaXmlns! && node.tag == nonzaTag!; | ||||
|     } | ||||
|      | ||||
|     if (matchStanzas && nonzaTag == null) { | ||||
|       matches = [ 'iq', 'presence', 'message' ].contains(node.tag); | ||||
|     } | ||||
| 
 | ||||
|     return matches; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class NonzaHandler extends Handler { | ||||
| 
 | ||||
|   NonzaHandler({ | ||||
|       required this.callback, | ||||
|       String? nonzaTag, | ||||
|       String? nonzaXmlns, | ||||
|   }) : super( | ||||
|     false, | ||||
|     nonzaTag: nonzaTag, | ||||
|     nonzaXmlns: nonzaXmlns, | ||||
|   ); | ||||
|   final Future<bool> Function(XMLNode) callback; | ||||
| } | ||||
| 
 | ||||
| class StanzaHandler extends Handler { | ||||
| 
 | ||||
|   StanzaHandler({ | ||||
|       required this.callback, | ||||
|       this.tagXmlns, | ||||
|       this.tagName,      | ||||
|       this.priority = 0, | ||||
|       String? stanzaTag, | ||||
|   }) : super( | ||||
|       true, | ||||
|       nonzaTag: stanzaTag, | ||||
|       nonzaXmlns: stanzaXmlns, | ||||
|     ); | ||||
|   final String? tagName; | ||||
|   final String? tagXmlns; | ||||
|   final int priority; | ||||
|   final Future<StanzaHandlerData> Function(Stanza, StanzaHandlerData) callback; | ||||
|      | ||||
|   @override | ||||
|   bool matches(XMLNode node) { | ||||
|     var matches = super.matches(node); | ||||
|      | ||||
|     if (matches == false) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     if (tagName != null) { | ||||
|       final firstTag = node.firstTag(tagName!, xmlns: tagXmlns); | ||||
| 
 | ||||
|       matches = firstTag != null; | ||||
|     } else if (tagXmlns != null) { | ||||
|       return listContains( | ||||
|         node.children, | ||||
|         (XMLNode _node) => _node.attributes.containsKey('xmlns') && _node.attributes['xmlns'] == tagXmlns, | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (tagName == null && tagXmlns == null) { | ||||
|       matches = true; | ||||
|     } | ||||
|      | ||||
|     return matches; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| int stanzaHandlerSortComparator(StanzaHandler a, StanzaHandler b) => b.priority.compareTo(a.priority); | ||||
							
								
								
									
										26
									
								
								moxxmpp/lib/src/managers/namespaces.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								moxxmpp/lib/src/managers/namespaces.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| const smManager = 'im.moxxy.streammangementmanager'; | ||||
| const discoManager = 'im.moxxy.discomanager'; | ||||
| const messageManager = 'im.moxxy.messagemanager'; | ||||
| const rosterManager = 'im.moxxy.rostermanager'; | ||||
| const presenceManager = 'im.moxxy.presencemanager'; | ||||
| const csiManager = 'im.moxxy.csimanager'; | ||||
| const carbonsManager = 'im.moxxy.carbonsmanager'; | ||||
| const vcardManager = 'im.moxxy.vcardmanager'; | ||||
| const pubsubManager = 'im.moxxy.pubsubmanager'; | ||||
| const userAvatarManager = 'im.moxxy.useravatarmanager'; | ||||
| const stableIdManager = 'im.moxxy.stableidmanager'; | ||||
| const simsManager = 'im.moxxy.simsmanager'; | ||||
| const messageDeliveryReceiptManager = 'im.moxxy.messagedeliveryreceiptmanager'; | ||||
| const chatMarkerManager = 'im.moxxy.chatmarkermanager'; | ||||
| const oobManager = 'im.moxxy.oobmanager'; | ||||
| const sfsManager = 'im.moxxy.sfsmanager'; | ||||
| const messageRepliesManager = 'im.moxxy.messagerepliesmanager'; | ||||
| const blockingManager = 'im.moxxy.blockingmanager'; | ||||
| const httpFileUploadManager = 'im.moxxy.httpfileuploadmanager'; | ||||
| const chatStateManager = 'im.moxxy.chatstatemanager'; | ||||
| const pingManager = 'im.moxxy.ping'; | ||||
| const fileUploadNotificationManager = 'im.moxxy.fileuploadnotificationmanager'; | ||||
| const omemoManager = 'org.moxxy.omemomanager'; | ||||
| const emeManager = 'org.moxxy.ememanager'; | ||||
| const cryptographicHashManager = 'org.moxxy.cryptographichashmanager'; | ||||
| const delayedDeliveryManager = 'org.moxxy.delayeddeliverymanager'; | ||||
							
								
								
									
										0
									
								
								moxxmpp/lib/src/managers/priorities.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								moxxmpp/lib/src/managers/priorities.dart
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										223
									
								
								moxxmpp/lib/src/message.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								moxxmpp/lib/src/message.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,223 @@ | ||||
| import 'package:moxlib/moxlib.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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/staging/file_upload_notification.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_0333.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0359.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0446.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0447.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0448.dart'; | ||||
| 
 | ||||
| class MessageDetails { | ||||
| 
 | ||||
|   const MessageDetails({ | ||||
|     required this.to, | ||||
|     this.body, | ||||
|     this.requestDeliveryReceipt = false, | ||||
|     this.requestChatMarkers = true, | ||||
|     this.id, | ||||
|     this.originId, | ||||
|     this.quoteBody, | ||||
|     this.quoteId, | ||||
|     this.quoteFrom, | ||||
|     this.chatState, | ||||
|     this.sfs, | ||||
|     this.fun, | ||||
|     this.funReplacement, | ||||
|     this.funCancellation, | ||||
|     this.shouldEncrypt = false, | ||||
|   }); | ||||
|   final String to; | ||||
|   final String? body; | ||||
|   final bool requestDeliveryReceipt; | ||||
|   final bool requestChatMarkers; | ||||
|   final String? id; | ||||
|   final String? originId; | ||||
|   final String? quoteBody; | ||||
|   final String? quoteId; | ||||
|   final String? quoteFrom; | ||||
|   final ChatState? chatState; | ||||
|   final StatelessFileSharingData? sfs; | ||||
|   final FileMetadataData? fun; | ||||
|   final String? funReplacement; | ||||
|   final String? funCancellation; | ||||
|   final bool shouldEncrypt; | ||||
| } | ||||
| 
 | ||||
| class MessageManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getId() => messageManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'MessageManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       callback: _onMessage, | ||||
|       priority: -100, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onMessage(Stanza _, StanzaHandlerData state) async { | ||||
|     final message = state.stanza; | ||||
|     final body = message.firstTag('body'); | ||||
| 
 | ||||
|     getAttributes().sendEvent(MessageEvent( | ||||
|       body: body != null ? body.innerText() : '', | ||||
|       fromJid: JID.fromString(message.attributes['from']! as String), | ||||
|       toJid: JID.fromString(message.attributes['to']! as String), | ||||
|       sid: message.attributes['id']! as String, | ||||
|       stanzaId: state.stableId ?? const StableStanzaId(), | ||||
|       isCarbon: state.isCarbon, | ||||
|       deliveryReceiptRequested: state.deliveryReceiptRequested, | ||||
|       isMarkable: state.isMarkable, | ||||
|       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, | ||||
|       other: state.other, | ||||
|     ),); | ||||
| 
 | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
| 
 | ||||
|   /// Send a message to to with the content body. If deliveryRequest is true, then | ||||
|   /// the message will also request a delivery receipt from the receiver. | ||||
|   /// If id is non-null, then it will be the id of the message stanza. | ||||
|   /// element to this id. If originId is non-null, then it will create an "origin-id" | ||||
|   /// child in the message stanza and set its id to originId. | ||||
|   void sendMessage(MessageDetails details) { | ||||
|     final stanza = Stanza.message( | ||||
|       to: details.to, | ||||
|       type: 'chat', | ||||
|       id: details.id, | ||||
|       children: [], | ||||
|     ); | ||||
| 
 | ||||
|     if (details.quoteBody != null) { | ||||
|       final fallback = '> ${details.quoteBody!}'; | ||||
| 
 | ||||
|       stanza | ||||
|         ..addChild( | ||||
|           XMLNode(tag: 'body', text: '$fallback\n${details.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': '${fallback.length}' | ||||
|                 }, | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
|         ); | ||||
|     } else { | ||||
|       var body = details.body; | ||||
|       if (details.sfs != null) { | ||||
|         // 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; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       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) { | ||||
|         // 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!, | ||||
|           }, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|      | ||||
|     getAttributes().sendStanza(stanza, awaitable: false); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										134
									
								
								moxxmpp/lib/src/namespaces.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								moxxmpp/lib/src/namespaces.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,134 @@ | ||||
| // RFC 6120 | ||||
| const saslXmlns = 'urn:ietf:params:xml:ns:xmpp-sasl'; | ||||
| const stanzaXmlns = 'jabber:client'; | ||||
| const streamXmlns = 'http://etherx.jabber.org/streams'; | ||||
| const bindXmlns = 'urn:ietf:params:xml:ns:xmpp-bind'; | ||||
| const startTlsXmlns = 'urn:ietf:params:xml:ns:xmpp-tls'; | ||||
| const fullStanzaXmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'; | ||||
| 
 | ||||
| // RFC 6121 | ||||
| const rosterXmlns = 'jabber:iq:roster'; | ||||
| const rosterVersioningXmlns = 'urn:xmpp:features:rosterver'; | ||||
| 
 | ||||
| // XEP-0004 | ||||
| const dataFormsXmlns = 'jabber:x:data'; | ||||
| 
 | ||||
| // XEP-0030 | ||||
| const discoInfoXmlns = 'http://jabber.org/protocol/disco#info'; | ||||
| const discoItemsXmlns = 'http://jabber.org/protocol/disco#items'; | ||||
| 
 | ||||
| // XEP-0033 | ||||
| const extendedAddressingXmlns = 'http://jabber.org/protocol/address'; | ||||
| 
 | ||||
| // XEP-0054 | ||||
| const vCardTempXmlns = 'vcard-temp'; | ||||
| const vCardTempUpdate = 'vcard-temp:x:update'; | ||||
| 
 | ||||
| // XEP-0060 | ||||
| const pubsubXmlns = 'http://jabber.org/protocol/pubsub'; | ||||
| const pubsubEventXmlns = 'http://jabber.org/protocol/pubsub#event'; | ||||
| const pubsubOwnerXmlns = 'http://jabber.org/protocol/pubsub#owner'; | ||||
| const pubsubPublishOptionsXmlns = 'http://jabber.org/protocol/pubsub#publish-options'; | ||||
| const pubsubNodeConfigMax = 'http://jabber.org/protocol/pubsub#config-node-max'; | ||||
| const pubsubNodeConfigMultiItems = 'http://jabber.org/protocol/pubsub#multi-items'; | ||||
| 
 | ||||
| // XEP-0066 | ||||
| const oobDataXmlns = 'jabber:x:oob'; | ||||
| 
 | ||||
| // XEP-0084 | ||||
| const userAvatarDataXmlns = 'urn:xmpp:avatar:data'; | ||||
| const userAvatarMetadataXmlns = 'urn:xmpp:avatar:metadata'; | ||||
| 
 | ||||
| // XEP-0085 | ||||
| const chatStateXmlns = 'http://jabber.org/protocol/chatstates'; | ||||
| 
 | ||||
| // XEP-0115 | ||||
| const capsXmlns = 'http://jabber.org/protocol/caps'; | ||||
| 
 | ||||
| // XEP-0184 | ||||
| const deliveryXmlns = 'urn:xmpp:receipts'; | ||||
| 
 | ||||
| // XEP-0191 | ||||
| const blockingXmlns = 'urn:xmpp:blocking'; | ||||
| 
 | ||||
| // XEP-0198 | ||||
| const smXmlns = 'urn:xmpp:sm:3'; | ||||
| 
 | ||||
| // XEP-0203 | ||||
| const delayedDeliveryXmlns = 'urn:xmpp:delay'; | ||||
| 
 | ||||
| // XEP-0234 | ||||
| const jingleFileTransferXmlns = 'urn:xmpp:jingle:apps:file-transfer:5'; | ||||
| 
 | ||||
| // XEP-0280 | ||||
| const carbonsXmlns = 'urn:xmpp:carbons:2'; | ||||
| 
 | ||||
| // XEP-0297 | ||||
| const forwardedXmlns = 'urn:xmpp:forward:0'; | ||||
| 
 | ||||
| // XEP-0300 | ||||
| const hashXmlns = 'urn:xmpp:hashes:2'; | ||||
| const hashFunctionNameBaseXmlns = 'urn:xmpp:hash-function-text-names'; | ||||
| const hashSha256 = 'sha-256'; | ||||
| const hashSha512 = 'sha-512'; | ||||
| const hashSha3256 = 'sha3-256'; | ||||
| const hashSha3512 = 'sha3-512'; | ||||
| const hashBlake2b256 = 'blake2b-256'; | ||||
| const hashBlake2b512 = 'blake2b-512'; | ||||
| 
 | ||||
| // XEP-0333 | ||||
| const chatMarkersXmlns = 'urn:xmpp:chat-markers:0'; | ||||
| 
 | ||||
| // XEP-0334 | ||||
| const messageProcessingHintsXmlns = 'urn:xmpp:hints'; | ||||
| 
 | ||||
| // XEP-0352 | ||||
| const csiXmlns = 'urn:xmpp:csi:0'; | ||||
| 
 | ||||
| // XEP-0359 | ||||
| const stableIdXmlns = 'urn:xmpp:sid:0'; | ||||
| 
 | ||||
| // XEP-0363 | ||||
| const httpFileUploadXmlns = 'urn:xmpp:http:upload:0'; | ||||
| 
 | ||||
| // XEP-0372 | ||||
| const referenceXmlns = 'urn:xmpp:reference:0'; | ||||
| 
 | ||||
| // XEP-380 | ||||
| const emeXmlns = 'urn:xmpp:eme:0'; | ||||
| const emeOtr = 'urn:xmpp:otr:0'; | ||||
| const emeLegacyOpenPGP = 'jabber:x:encrypted'; | ||||
| const emeOpenPGP = 'urn:xmpp:openpgp:0'; | ||||
| const emeOmemo = 'eu.siacs.conversations.axolotl'; | ||||
| const emeOmemo1 = 'urn:xmpp:omemo:1'; | ||||
| const emeOmemo2 = 'urn:xmpp:omemo:2'; | ||||
| 
 | ||||
| // XEP-0384 | ||||
| const omemoXmlns = 'urn:xmpp:omemo:2'; | ||||
| const omemoDevicesXmlns = 'urn:xmpp:omemo:2:devices'; | ||||
| const omemoBundlesXmlns = 'urn:xmpp:omemo:2:bundles'; | ||||
| 
 | ||||
| // XEP-0385 | ||||
| const simsXmlns = 'urn:xmpp:sims:1'; | ||||
| 
 | ||||
| // XEP-0420 | ||||
| const sceXmlns = 'urn:xmpp:sce:1'; | ||||
| 
 | ||||
| // XEP-0446 | ||||
| const fileMetadataXmlns = 'urn:xmpp:file:metadata:0'; | ||||
| 
 | ||||
| // XEP-0447 | ||||
| const sfsXmlns = 'urn:xmpp:sfs:0'; | ||||
| 
 | ||||
| // XEP-0448 | ||||
| const sfsEncryptionXmlns = 'urn:xmpp:esfs:0'; | ||||
| const sfsEncryptionAes128GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-128-gcm-nopadding:0'; | ||||
| const sfsEncryptionAes256GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-256-gcm-nopadding:0'; | ||||
| const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0'; | ||||
| 
 | ||||
| // XEP-0461 | ||||
| const replyXmlns = 'urn:xmpp:reply:0'; | ||||
| const fallbackXmlns = 'urn:xmpp:feature-fallback:0'; | ||||
| 
 | ||||
| // ??? | ||||
| const urlDataXmlns = 'http://jabber.org/protocol/url-data'; | ||||
							
								
								
									
										0
									
								
								moxxmpp/lib/src/negotiators/manager.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								moxxmpp/lib/src/negotiators/manager.dart
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										9
									
								
								moxxmpp/lib/src/negotiators/namespaces.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								moxxmpp/lib/src/negotiators/namespaces.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| const saslPlainNegotiator = 'im.moxxy.sasl.plain'; | ||||
| const saslScramSha1Negotiator = 'im.moxxy.sasl.scram.sha1'; | ||||
| const saslScramSha256Negotiator = 'im.moxxy.sasl.scram.sha256'; | ||||
| const saslScramSha512Negotiator = 'im.moxxy.sasl.scram.sha512'; | ||||
| const csiNegotiator = 'im.moxxy.xeps.csi'; | ||||
| const rosterNegotiator = 'im.moxxy.core.roster'; | ||||
| const resourceBindingNegotiator = 'im.moxxy.core.resource'; | ||||
| const streamManagementNegotiator = 'im.moxxy.xeps.sm'; | ||||
| const startTlsNegotiator = 'im.moxxy.core.starttls'; | ||||
							
								
								
									
										108
									
								
								moxxmpp/lib/src/negotiators/negotiator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								moxxmpp/lib/src/negotiators/negotiator.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| import 'package:moxlib/moxlib.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/settings.dart'; | ||||
| import 'package:moxxmpp/src/socket.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| /// The state a negotiator is currently in | ||||
| enum NegotiatorState { | ||||
|   // Ready to negotiate the feature | ||||
|   ready, | ||||
|   // Feature negotiated; negotiator must not be used again | ||||
|   done, | ||||
|   // Cancel the current attempt but we are not done | ||||
|   retryLater, | ||||
|   // The negotiator is in an error state | ||||
|   error, | ||||
|   // Skip the rest of the negotiation and assume the stream ready. Only use this when | ||||
|   // using stream restoration XEPs, like Stream Management. | ||||
|   skipRest, | ||||
| } | ||||
| 
 | ||||
| class NegotiatorAttributes { | ||||
| 
 | ||||
|   const NegotiatorAttributes( | ||||
|     this.sendNonza, | ||||
|     this.getConnectionSettings, | ||||
|     this.sendEvent, | ||||
|     this.getNegotiatorById, | ||||
|     this.getManagerById, | ||||
|     this.getFullJID, | ||||
|     this.getSocket, | ||||
|     this.isAuthenticated, | ||||
|   ); | ||||
|   /// Sends the nonza nonza and optionally redacts it in logs if redact is not null. | ||||
|   final void Function(XMLNode nonza, {String? redact}) sendNonza; | ||||
|   /// Returns the connection settings. | ||||
|   final ConnectionSettings Function() getConnectionSettings; | ||||
|   /// Send an event event to the connection's event bus | ||||
|   final Future<void> Function(XmppEvent event) sendEvent; | ||||
|   /// Returns the negotiator with id id of the connection or null. | ||||
|   final T? Function<T extends XmppFeatureNegotiatorBase>(String) getNegotiatorById; | ||||
|   /// Returns the manager with id id of the connection or null. | ||||
|   final T? Function<T extends XmppManagerBase>(String) getManagerById; | ||||
|   /// Returns the full JID of the current account | ||||
|   final JID Function() getFullJID; | ||||
|   /// Returns the socket the negotiator is attached to | ||||
|   final BaseSocketWrapper Function() getSocket; | ||||
|   /// Returns true if the stream is authenticated. Returns false if not. | ||||
|   final bool Function() isAuthenticated; | ||||
| } | ||||
| 
 | ||||
| abstract class XmppFeatureNegotiatorBase { | ||||
| 
 | ||||
|   XmppFeatureNegotiatorBase(this.priority, this.sendStreamHeaderWhenDone, this.negotiatingXmlns, this.id) | ||||
|     : state = NegotiatorState.ready; | ||||
|   /// The priority regarding other negotiators. The higher, the earlier will the | ||||
|   /// negotiator be used | ||||
|   final int priority; | ||||
| 
 | ||||
|   /// If true, then a new stream header will be sent when the negotiator switches its | ||||
|   /// state to done. If false, no stream header will be sent. | ||||
|   final bool sendStreamHeaderWhenDone; | ||||
| 
 | ||||
|   /// The XMLNS the negotiator will negotiate | ||||
|   final String negotiatingXmlns; | ||||
| 
 | ||||
|   /// The Id of the negotiator | ||||
|   final String id; | ||||
|    | ||||
|   /// The state the negotiator is currently in | ||||
|   NegotiatorState state; | ||||
|    | ||||
|   late NegotiatorAttributes _attributes; | ||||
| 
 | ||||
|   /// Register the negotiator against a connection class by means of [attributes]. | ||||
|   void register(NegotiatorAttributes attributes) { | ||||
|     _attributes = attributes; | ||||
|   } | ||||
|    | ||||
|   /// Returns true if a feature in [features], which are the children of the | ||||
|   /// <stream:features /> nonza, can be negotiated. Otherwise, returns false. | ||||
|   bool matchesFeature(List<XMLNode> features) { | ||||
|     return firstWhereOrNull( | ||||
|       features, | ||||
|       (XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns, | ||||
|     ) != null; | ||||
|   } | ||||
| 
 | ||||
|   /// Called with the currently received nonza [nonza] when the negotiator is active. | ||||
|   /// If the negotiator is just elected to be the next one, then [nonza] is equal to | ||||
|   /// the <stream:features /> nonza. | ||||
|   /// | ||||
|   /// Returns the next state of the negotiator. If done or retryLater is selected, then | ||||
|   /// negotiator won't be called again. If retryLater is returned, then the negotiator | ||||
|   /// must switch some internal state to prevent getting matched immediately again. | ||||
|   /// If ready is returned, then the negotiator indicates that it is not done with | ||||
|   /// negotiation. | ||||
|   Future<void> negotiate(XMLNode nonza); | ||||
| 
 | ||||
|   /// Reset the negotiator to a state that negotation can happen again. | ||||
|   void reset() { | ||||
|     state = NegotiatorState.ready; | ||||
|   } | ||||
|    | ||||
|   NegotiatorAttributes get attributes => _attributes; | ||||
| } | ||||
							
								
								
									
										66
									
								
								moxxmpp/lib/src/negotiators/resource_binding.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								moxxmpp/lib/src/negotiators/resource_binding.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
| 
 | ||||
| class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase { | ||||
| 
 | ||||
|   ResourceBindingNegotiator() : _requestSent = false, super(0, false, bindXmlns, resourceBindingNegotiator); | ||||
|   bool _requestSent; | ||||
| 
 | ||||
|   @override | ||||
|   bool matchesFeature(List<XMLNode> features) { | ||||
|     final sm = attributes.getManagerById<StreamManagementManager>(smManager); | ||||
|     if (sm != null) { | ||||
|       return super.matchesFeature(features) && !sm.streamResumed && attributes.isAuthenticated(); | ||||
|     } | ||||
| 
 | ||||
|     return super.matchesFeature(features) && attributes.isAuthenticated(); | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   Future<void> negotiate(XMLNode nonza) async { | ||||
|     if (!_requestSent) { | ||||
|       final stanza = XMLNode.xmlns( | ||||
|         tag: 'iq', | ||||
|         xmlns: stanzaXmlns, | ||||
|         attributes: { | ||||
|           'type': 'set', | ||||
|           'id': const Uuid().v4(), | ||||
|         }, | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'bind', | ||||
|             xmlns: bindXmlns, | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
| 
 | ||||
|       _requestSent = true; | ||||
|       attributes.sendNonza(stanza); | ||||
|     } else { | ||||
|       if (nonza.tag != 'iq' || nonza.attributes['type'] != 'result') { | ||||
|         state = NegotiatorState.error; | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       final bind = nonza.firstTag('bind')!; | ||||
|       final jid = bind.firstTag('jid')!; | ||||
|       final resource = jid.innerText().split('/')[1]; | ||||
| 
 | ||||
|       await attributes.sendEvent(ResourceBindingSuccessEvent(resource: resource)); | ||||
|       state = NegotiatorState.done; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   void reset() { | ||||
|     _requestSent = false; | ||||
| 
 | ||||
|     super.reset(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										46
									
								
								moxxmpp/lib/src/negotiators/sasl/kv.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								moxxmpp/lib/src/negotiators/sasl/kv.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| enum ParserState { | ||||
|   variableName, | ||||
|   variableValue | ||||
| } | ||||
| 
 | ||||
| /// Parse a string like "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL" into | ||||
| /// { "n": "user", "r": "fyko+d2lbbFgONRv9qkxdawL"}. | ||||
| Map<String, String> parseKeyValue(String keyValueString) { | ||||
|   var state = ParserState.variableName; | ||||
|   var name = ''; | ||||
|   var value = ''; | ||||
|   final values = <String, String>{}; | ||||
| 
 | ||||
|   for (var i = 0; i < keyValueString.length; i++) { | ||||
|     final char = keyValueString[i]; | ||||
|     switch (state) { | ||||
|       case ParserState.variableName: { | ||||
|         if (char == '=') { | ||||
|           state = ParserState.variableValue;  | ||||
|         } else if (char == ',') { | ||||
|           name = ''; | ||||
|         } else { | ||||
|           name += char; | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|       case ParserState.variableValue: { | ||||
|         if (char == ',' || i == keyValueString.length - 1) { | ||||
|           if (char != ',') { | ||||
|             value += char; | ||||
|           } | ||||
| 
 | ||||
|           values[name] = value; | ||||
|           value = ''; | ||||
|           name = ''; | ||||
|           state = ParserState.variableName; | ||||
|         } else { | ||||
|           value += char; | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return values; | ||||
| } | ||||
							
								
								
									
										27
									
								
								moxxmpp/lib/src/negotiators/sasl/negotiator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								moxxmpp/lib/src/negotiators/sasl/negotiator.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| import 'package:moxlib/moxlib.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| abstract class SaslNegotiator extends XmppFeatureNegotiatorBase { | ||||
| 
 | ||||
|   SaslNegotiator(int priority, String id, this.mechanismName) : super(priority, true, saslXmlns, id); | ||||
|   /// The name inside the <mechanism /> element | ||||
|   final String mechanismName; | ||||
|    | ||||
|   @override | ||||
|   bool matchesFeature(List<XMLNode> features) { | ||||
|     // Is SASL advertised? | ||||
|     final mechanisms = firstWhereOrNull( | ||||
|       features, | ||||
|       (XMLNode feature) => feature.attributes['xmlns'] == saslXmlns, | ||||
|     ); | ||||
|     if (mechanisms == null) return false; | ||||
| 
 | ||||
|     // Is SASL PLAIN advertised? | ||||
|     return firstWhereOrNull( | ||||
|       mechanisms.children, | ||||
|       (XMLNode mechanism) => mechanism.text == mechanismName, | ||||
|     ) != null; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								moxxmpp/lib/src/negotiators/sasl/nonza.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								moxxmpp/lib/src/negotiators/sasl/nonza.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| class SaslAuthNonza extends XMLNode { | ||||
|   SaslAuthNonza(String mechanism, String body) : super( | ||||
|     tag: 'auth', | ||||
|     attributes: <String, String>{ | ||||
|       'xmlns': saslXmlns, | ||||
|       'mechanism': mechanism , | ||||
|     }, | ||||
|     text: body, | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										73
									
								
								moxxmpp/lib/src/negotiators/sasl/plain.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								moxxmpp/lib/src/negotiators/sasl/plain.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| import 'dart:convert'; | ||||
| 
 | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/sasl/nonza.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| class SaslPlainAuthNonza extends SaslAuthNonza { | ||||
|   SaslPlainAuthNonza(String username, String password) : super( | ||||
|     'PLAIN', base64.encode(utf8.encode('\u0000$username\u0000$password')), | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class SaslPlainNegotiator extends SaslNegotiator { | ||||
|    | ||||
|   SaslPlainNegotiator() | ||||
|     : _authSent = false, | ||||
|       _log = Logger('SaslPlainNegotiator'), | ||||
|       super(0, saslPlainNegotiator, 'PLAIN'); | ||||
|   bool _authSent; | ||||
| 
 | ||||
|   final Logger _log; | ||||
| 
 | ||||
|   @override | ||||
|   bool matchesFeature(List<XMLNode> features) { | ||||
|     if (!attributes.getConnectionSettings().allowPlainAuth) return false; | ||||
|      | ||||
|     if (super.matchesFeature(features)) { | ||||
|       if (!attributes.getSocket().isSecure()) { | ||||
|         _log.warning('Refusing to match SASL feature due to unsecured connection'); | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> negotiate(XMLNode nonza) async { | ||||
|     if (!_authSent) { | ||||
|       final settings = attributes.getConnectionSettings(); | ||||
|       attributes.sendNonza( | ||||
|         SaslPlainAuthNonza(settings.jid.local, settings.password), | ||||
|         redact: SaslPlainAuthNonza('******', '******').toXml(), | ||||
|       ); | ||||
|       _authSent = true; | ||||
|     } else { | ||||
|       final tag = nonza.tag; | ||||
|       if (tag == 'success') { | ||||
|         await attributes.sendEvent(AuthenticationSuccessEvent()); | ||||
|         state = NegotiatorState.done; | ||||
|       } else { | ||||
|         // We assume it's a <failure/> | ||||
|         final error = nonza.children.first.tag; | ||||
|         await attributes.sendEvent(AuthenticationFailedEvent(error)); | ||||
|          | ||||
|         state = NegotiatorState.error; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void reset() { | ||||
|     _authSent = false; | ||||
| 
 | ||||
|     super.reset(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										259
									
								
								moxxmpp/lib/src/negotiators/sasl/scram.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								moxxmpp/lib/src/negotiators/sasl/scram.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,259 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:math' show Random; | ||||
| 
 | ||||
| import 'package:cryptography/cryptography.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/sasl/kv.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/sasl/nonza.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:random_string/random_string.dart'; | ||||
| import 'package:saslprep/saslprep.dart'; | ||||
| 
 | ||||
| // NOTE: Inspired by https://github.com/vukoye/xmpp_dart/blob/3b1a0588562b9e591488c99d834088391840911d/lib/src/features/sasl/ScramSaslHandler.dart | ||||
| 
 | ||||
| enum ScramHashType { | ||||
|   sha1, | ||||
|   sha256, | ||||
|   sha512 | ||||
| } | ||||
| 
 | ||||
| HashAlgorithm hashFromType(ScramHashType type) { | ||||
|   switch (type) { | ||||
|     case ScramHashType.sha1: return Sha1(); | ||||
|     case ScramHashType.sha256: return Sha256(); | ||||
|     case ScramHashType.sha512: return Sha512(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const scramSha1Mechanism = 'SCRAM-SHA-1'; | ||||
| const scramSha256Mechanism = 'SCRAM-SHA-256'; | ||||
| const scramSha512Mechanism = 'SCRAM-SHA-512'; | ||||
| 
 | ||||
| String mechanismNameFromType(ScramHashType type) { | ||||
|   switch (type) { | ||||
|     case ScramHashType.sha1: return scramSha1Mechanism; | ||||
|     case ScramHashType.sha256: return scramSha256Mechanism; | ||||
|     case ScramHashType.sha512: return scramSha512Mechanism; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| String namespaceFromType(ScramHashType type) { | ||||
|   switch (type) { | ||||
|     case ScramHashType.sha1: return saslScramSha1Negotiator; | ||||
|     case ScramHashType.sha256: return saslScramSha256Negotiator; | ||||
|     case ScramHashType.sha512: return saslScramSha512Negotiator; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class SaslScramAuthNonza extends SaslAuthNonza { | ||||
|   // This subclassing makes less sense here, but this is since the auth nonza here | ||||
|   // requires knowledge of the inner state of the Negotiator. | ||||
|   SaslScramAuthNonza({ required ScramHashType type, required String body }) : super( | ||||
|     mechanismNameFromType(type), body, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class SaslScramResponseNonza extends XMLNode { | ||||
|   SaslScramResponseNonza({ required String body }) : super( | ||||
|     tag: 'response', | ||||
|     attributes: <String, String>{ | ||||
|       'xmlns': saslXmlns, | ||||
|     }, | ||||
|     text: body, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| enum ScramState { | ||||
|   preSent, | ||||
|   initialMessageSent, | ||||
|   challengeResponseSent, | ||||
|   error | ||||
| } | ||||
| 
 | ||||
| const gs2Header = 'n,,'; | ||||
| 
 | ||||
| class SaslScramNegotiator extends SaslNegotiator { | ||||
|    | ||||
|   // NOTE: NEVER, and I mean, NEVER set clientNonce or initalMessageNoGS2. They are just there for testing | ||||
|   SaslScramNegotiator( | ||||
|     int priority, | ||||
|     this.initialMessageNoGS2, | ||||
|     this.clientNonce, | ||||
|     this.hashType, | ||||
|   ) : | ||||
|     _hash = hashFromType(hashType), | ||||
|     _serverSignature = '', | ||||
|     _scramState = ScramState.preSent, | ||||
|     _log = Logger('SaslScramNegotiator(${mechanismNameFromType(hashType)})'), | ||||
|     super(priority, namespaceFromType(hashType), mechanismNameFromType(hashType)); | ||||
|   String? clientNonce; | ||||
|   String initialMessageNoGS2; | ||||
|   final ScramHashType hashType; | ||||
|   final HashAlgorithm _hash; | ||||
|   String _serverSignature; | ||||
| 
 | ||||
|   // The internal state for performing the negotiation | ||||
|   ScramState _scramState; | ||||
| 
 | ||||
|   final Logger _log; | ||||
| 
 | ||||
|   Future<List<int>> calculateSaltedPassword(String salt, int iterations) async { | ||||
|     final pbkdf2 = Pbkdf2( | ||||
|       macAlgorithm: Hmac(_hash), | ||||
|       iterations: iterations, | ||||
|       bits: 160, // NOTE: RFC says 20 octets => 20 octets * 8 bits/octet | ||||
|     ); | ||||
| 
 | ||||
|     final saltedPasswordRaw = await pbkdf2.deriveKey( | ||||
|       secretKey: SecretKey( | ||||
|         utf8.encode(Saslprep.saslprep(attributes.getConnectionSettings().password)), | ||||
|       ), | ||||
|       nonce: base64.decode(salt), | ||||
|     ); | ||||
|     return saltedPasswordRaw.extractBytes(); | ||||
|   } | ||||
| 
 | ||||
|   Future<List<int>> calculateClientKey(List<int> saltedPassword) async { | ||||
|     return (await Hmac(_hash).calculateMac( | ||||
|         utf8.encode('Client Key'), secretKey: SecretKey(saltedPassword), | ||||
|     )).bytes; | ||||
|   } | ||||
| 
 | ||||
|   Future<List<int>> calculateClientSignature(String authMessage, List<int> storedKey) async { | ||||
|     return (await Hmac(_hash).calculateMac( | ||||
|         utf8.encode(authMessage), | ||||
|         secretKey: SecretKey(storedKey), | ||||
|     )).bytes; | ||||
|   } | ||||
| 
 | ||||
|   Future<List<int>> calculateServerKey(List<int> saltedPassword) async { | ||||
|     return (await Hmac(_hash).calculateMac( | ||||
|         utf8.encode('Server Key'), | ||||
|         secretKey: SecretKey(saltedPassword), | ||||
|     )).bytes; | ||||
|   } | ||||
| 
 | ||||
|   Future<List<int>> calculateServerSignature(String authMessage, List<int> serverKey) async { | ||||
|     return (await Hmac(_hash).calculateMac( | ||||
|         utf8.encode(authMessage), | ||||
|         secretKey: SecretKey(serverKey), | ||||
|     )).bytes; | ||||
|   } | ||||
| 
 | ||||
|   List<int> calculateClientProof(List<int> clientKey, List<int> clientSignature) { | ||||
|     final clientProof = List<int>.filled(clientKey.length, 0); | ||||
|     for (var i = 0; i < clientKey.length; i++) { | ||||
|       clientProof[i] = clientKey[i] ^ clientSignature[i]; | ||||
|     } | ||||
| 
 | ||||
|     return clientProof; | ||||
|   } | ||||
|    | ||||
|   Future<String> calculateChallengeResponse(String base64Challenge) async { | ||||
|     final challengeString = utf8.decode(base64.decode(base64Challenge)); | ||||
|     final challenge = parseKeyValue(challengeString); | ||||
|     final clientFinalMessageBare = 'c=biws,r=${challenge['r']!}'; | ||||
|      | ||||
|     final saltedPassword = await calculateSaltedPassword(challenge['s']!, int.parse(challenge['i']!)); | ||||
|     final clientKey = await calculateClientKey(saltedPassword); | ||||
|     final storedKey = (await _hash.hash(clientKey)).bytes; | ||||
|     final authMessage = '$initialMessageNoGS2,$challengeString,$clientFinalMessageBare'; | ||||
|     final clientSignature = await calculateClientSignature(authMessage, storedKey); | ||||
|     final clientProof = calculateClientProof(clientKey, clientSignature); | ||||
|     final serverKey = await calculateServerKey(saltedPassword); | ||||
|     _serverSignature = base64.encode(await calculateServerSignature(authMessage, serverKey)); | ||||
| 
 | ||||
|     return '$clientFinalMessageBare,p=${base64.encode(clientProof)}'; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool matchesFeature(List<XMLNode> features) { | ||||
|     if (super.matchesFeature(features)) { | ||||
|       if (!attributes.getSocket().isSecure()) { | ||||
|         _log.warning('Refusing to match SASL feature due to unsecured connection'); | ||||
|         return false; | ||||
|       } | ||||
| 
 | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   Future<void> negotiate(XMLNode nonza) async { | ||||
|     switch (_scramState) { | ||||
|       case ScramState.preSent: | ||||
|         if (clientNonce == null || clientNonce == '') { | ||||
|           clientNonce = randomAlphaNumeric(40, provider: CoreRandomProvider.from(Random.secure())); | ||||
|         } | ||||
|          | ||||
|         initialMessageNoGS2 = 'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce'; | ||||
| 
 | ||||
|         _scramState = ScramState.initialMessageSent; | ||||
|         attributes.sendNonza( | ||||
|           SaslScramAuthNonza(body: base64.encode(utf8.encode(gs2Header + initialMessageNoGS2)), type: hashType), | ||||
|           redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(), | ||||
|         ); | ||||
|         break; | ||||
|       case ScramState.initialMessageSent: | ||||
|         if (nonza.tag != 'challenge') { | ||||
|           final error = nonza.children.first.tag; | ||||
|           await attributes.sendEvent(AuthenticationFailedEvent(error)); | ||||
| 
 | ||||
|           state = NegotiatorState.error; | ||||
|           _scramState = ScramState.error; | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         final challengeBase64 = nonza.innerText(); | ||||
|         final response = await calculateChallengeResponse(challengeBase64); | ||||
|         final responseBase64 = base64.encode(utf8.encode(response)); | ||||
|         _scramState = ScramState.challengeResponseSent; | ||||
|         attributes.sendNonza( | ||||
|           SaslScramResponseNonza(body: responseBase64), | ||||
|           redact: SaslScramResponseNonza(body: '******').toXml(), | ||||
|         ); | ||||
|         return; | ||||
|       case ScramState.challengeResponseSent: | ||||
|         if (nonza.tag != 'success') { | ||||
|           // We assume it's a <failure /> | ||||
|           final error = nonza.children.first.tag; | ||||
|           await attributes.sendEvent(AuthenticationFailedEvent(error)); | ||||
|           _scramState = ScramState.error; | ||||
|           state = NegotiatorState.error; | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         // NOTE: This assumes that the string is always "v=..." and contains no other parameters | ||||
|         final signature = parseKeyValue(utf8.decode(base64.decode(nonza.innerText()))); | ||||
|         if (signature['v']! != _serverSignature) { | ||||
|           // TODO(Unknown): Notify of a signature mismatch | ||||
|           //final error = nonza.children.first.tag; | ||||
|           //attributes.sendEvent(AuthenticationFailedEvent(error)); | ||||
|           _scramState = ScramState.error; | ||||
|           state = NegotiatorState.error; | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         await attributes.sendEvent(AuthenticationSuccessEvent()); | ||||
|         state = NegotiatorState.done; | ||||
|         return; | ||||
|       case ScramState.error: | ||||
|         state = NegotiatorState.error; | ||||
|         return; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void reset() { | ||||
|     _scramState = ScramState.preSent; | ||||
| 
 | ||||
|     super.reset(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										65
									
								
								moxxmpp/lib/src/negotiators/starttls.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								moxxmpp/lib/src/negotiators/starttls.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| enum _StartTlsState { | ||||
|   ready, | ||||
|   requested | ||||
| } | ||||
| 
 | ||||
| class StartTLSNonza extends XMLNode { | ||||
|   StartTLSNonza() : super.xmlns( | ||||
|     tag: 'starttls', | ||||
|     xmlns: startTlsXmlns, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class StartTlsNegotiator extends XmppFeatureNegotiatorBase { | ||||
|    | ||||
|   StartTlsNegotiator() | ||||
|     : _state = _StartTlsState.ready, | ||||
|       _log = Logger('StartTlsNegotiator'), | ||||
|       super(10, true, startTlsXmlns, startTlsNegotiator); | ||||
|   _StartTlsState _state; | ||||
| 
 | ||||
|   final Logger _log; | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> negotiate(XMLNode nonza) async { | ||||
|     switch (_state) { | ||||
|       case _StartTlsState.ready: | ||||
|         _log.fine('StartTLS is available. Performing StartTLS upgrade...'); | ||||
|         _state = _StartTlsState.requested; | ||||
|         attributes.sendNonza(StartTLSNonza()); | ||||
|         break; | ||||
|       case _StartTlsState.requested: | ||||
|         if (nonza.tag != 'proceed' || nonza.attributes['xmlns'] != startTlsXmlns) { | ||||
|           _log.severe('Failed to perform StartTLS negotiation'); | ||||
|           state = NegotiatorState.error; | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         _log.fine('Securing socket'); | ||||
|         final result = await attributes.getSocket() | ||||
|           .secure(attributes.getConnectionSettings().jid.domain); | ||||
|         if (!result) { | ||||
|           _log.severe('Failed to secure stream'); | ||||
|           state = NegotiatorState.error; | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         _log.fine('Stream is now TLS secured'); | ||||
|         state = NegotiatorState.done; | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void reset() { | ||||
|     _state = _StartTlsState.ready; | ||||
| 
 | ||||
|     super.reset(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										52
									
								
								moxxmpp/lib/src/ping.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								moxxmpp/lib/src/ping.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; | ||||
| 
 | ||||
| class PingManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getId() => pingManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'PingManager'; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   void _logWarning() { | ||||
|     logger.warning('Cannot send keepalives as SM is not available, the socket disallows whitespace pings and does not manage its own keepalives. Cannot guarantee that the connection survives.'); | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   Future<void> onXmppEvent(XmppEvent event) async { | ||||
|     if (event is SendPingEvent) { | ||||
|       logger.finest('Received ping event.'); | ||||
|       final attrs = getAttributes(); | ||||
|       final socket = attrs.getSocket(); | ||||
| 
 | ||||
|       if (socket.managesKeepalives()) { | ||||
|         logger.finest('Not sending ping as the socket manages it.'); | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       final stream = attrs.getManagerById(smManager) as StreamManagementManager?; | ||||
|       if (stream != null) { | ||||
|         if (stream.isStreamManagementEnabled() /*&& stream.getUnackedStanzaCount() > 0*/) { | ||||
|           logger.finest('Sending an ack ping as Stream Management is enabled'); | ||||
|           stream.sendAckRequestPing(); | ||||
|         } else if (attrs.getSocket().whitespacePingAllowed()) { | ||||
|           logger.finest('Sending a whitespace ping as Stream Management is not enabled'); | ||||
|           attrs.getConnection().sendWhitespacePing(); | ||||
|         } else { | ||||
|           _logWarning(); | ||||
|         } | ||||
|       } else { | ||||
|         if (attrs.getSocket().whitespacePingAllowed()) { | ||||
|           attrs.getConnection().sendWhitespacePing(); | ||||
|         } else { | ||||
|           _logWarning(); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										158
									
								
								moxxmpp/lib/src/presence.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								moxxmpp/lib/src/presence.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,158 @@ | ||||
| import 'package:moxxmpp/src/connection.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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/types.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0115.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0414.dart'; | ||||
| 
 | ||||
| class PresenceManager extends XmppManagerBase { | ||||
| 
 | ||||
|   PresenceManager() : _capabilityHash = null, super(); | ||||
|   String? _capabilityHash; | ||||
|    | ||||
|   @override | ||||
|   String getId() => presenceManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'PresenceManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'presence', | ||||
|       callback: _onPresence, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [ capsXmlns ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onPresence(Stanza presence, StanzaHandlerData state) async { | ||||
|     final attrs = getAttributes(); | ||||
|     switch (presence.type) { | ||||
|       case 'subscribe': | ||||
|       case 'subscribed': { | ||||
|         attrs.sendEvent( | ||||
|           SubscriptionRequestReceivedEvent(from: JID.fromString(presence.from!)), | ||||
|         ); | ||||
|         return state.copyWith(done: true); | ||||
|       } | ||||
|       default: break; | ||||
|     } | ||||
| 
 | ||||
|     if (presence.from != null) { | ||||
|       logger.finest("Received presence from '${presence.from}'"); | ||||
| 
 | ||||
|       getAttributes().sendEvent(PresenceReceivedEvent(JID.fromString(presence.from!), presence)); | ||||
|       return state.copyWith(done: true); | ||||
|     }  | ||||
| 
 | ||||
|     return state; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns the capability hash. | ||||
|   Future<String> getCapabilityHash() async { | ||||
|     final manager = getAttributes().getManagerById(discoManager)! as DiscoManager; | ||||
|     _capabilityHash ??= await calculateCapabilityHash( | ||||
|       DiscoInfo( | ||||
|         manager.getRegisteredDiscoFeatures(), | ||||
|         manager.getIdentities(), | ||||
|         [], | ||||
|         getAttributes().getFullJID(), | ||||
|       ), | ||||
|       getHashByName('sha-1')!, | ||||
|     ); | ||||
| 
 | ||||
|     return _capabilityHash!; | ||||
|   } | ||||
|    | ||||
|   /// Sends the initial presence to enable receiving messages. | ||||
|   Future<void> sendInitialPresence() async { | ||||
|     final attrs = getAttributes(); | ||||
|     attrs.sendNonza( | ||||
|       Stanza.presence( | ||||
|         from: attrs.getFullJID().toString(), | ||||
|         children: [ | ||||
|           XMLNode( | ||||
|             tag: 'show', | ||||
|             text: 'chat', | ||||
|           ), | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'c', | ||||
|             xmlns: capsXmlns, | ||||
|             attributes: { | ||||
|               'hash': 'sha-1', | ||||
|               'node': 'http://moxxy.im', | ||||
|               'ver': await getCapabilityHash() | ||||
|             }, | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Send an unavailable presence with no 'to' attribute. | ||||
|   void sendUnavailablePresence() { | ||||
|     getAttributes().sendStanza( | ||||
|       Stanza.presence( | ||||
|         type: 'unavailable', | ||||
|       ), | ||||
|       addFrom: StanzaFromType.full, | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   /// Sends a subscription request to [to]. | ||||
|   void sendSubscriptionRequest(String to) { | ||||
|     getAttributes().sendStanza( | ||||
|       Stanza.presence( | ||||
|         type: 'subscribe', | ||||
|         to: to, | ||||
|       ), | ||||
|       addFrom: StanzaFromType.none, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Sends an unsubscription request to [to]. | ||||
|   void sendUnsubscriptionRequest(String to) { | ||||
|     getAttributes().sendStanza( | ||||
|       Stanza.presence( | ||||
|         type: 'unsubscribe', | ||||
|         to: to, | ||||
|       ), | ||||
|       addFrom: StanzaFromType.none, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Accept a presence subscription request for [to]. | ||||
|   void sendSubscriptionRequestApproval(String to) { | ||||
|     getAttributes().sendStanza( | ||||
|       Stanza.presence( | ||||
|         type: 'subscribed', | ||||
|         to: to, | ||||
|       ), | ||||
|       addFrom: StanzaFromType.none, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Reject a presence subscription request for [to]. | ||||
|   void sendSubscriptionRequestRejection(String to) { | ||||
|     getAttributes().sendStanza( | ||||
|       Stanza.presence( | ||||
|         type: 'unsubscribed', | ||||
|         to: to, | ||||
|       ), | ||||
|       addFrom: StanzaFromType.none, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										150
									
								
								moxxmpp/lib/src/reconnect.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								moxxmpp/lib/src/reconnect.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,150 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:math'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:synchronized/synchronized.dart'; | ||||
| 
 | ||||
| abstract class ReconnectionPolicy { | ||||
| 
 | ||||
|   ReconnectionPolicy() | ||||
|     : _shouldAttemptReconnection = false, | ||||
|       _isReconnecting = false, | ||||
|       _isReconnectingLock = Lock(); | ||||
|   /// Function provided by XmppConnection that allows the policy | ||||
|   /// to perform a reconnection. | ||||
|   Future<void> Function()? performReconnect; | ||||
|   /// Function provided by XmppConnection that allows the policy | ||||
|   /// to say that we lost the connection. | ||||
|   void Function()? triggerConnectionLost; | ||||
|   /// Indicate if should try to reconnect. | ||||
|   bool _shouldAttemptReconnection; | ||||
|   /// Indicate if a reconnection attempt is currently running. | ||||
|   bool _isReconnecting; | ||||
|   /// And the corresponding lock | ||||
|   final Lock _isReconnectingLock; | ||||
|    | ||||
|   /// Called by XmppConnection to register the policy. | ||||
|   void register(Future<void> Function() performReconnect, void Function() triggerConnectionLost) { | ||||
|     this.performReconnect = performReconnect; | ||||
|     this.triggerConnectionLost = triggerConnectionLost; | ||||
| 
 | ||||
|     unawaited(reset()); | ||||
|   } | ||||
|    | ||||
|   /// In case the policy depends on some internal state, this state must be reset | ||||
|   /// to an initial state when reset is called. In case timers run, they must be | ||||
|   /// terminated. | ||||
|   Future<void> reset(); | ||||
| 
 | ||||
|   /// Called by the XmppConnection when the reconnection failed. | ||||
|   Future<void> onFailure() async {} | ||||
| 
 | ||||
|   /// Caled by the XmppConnection when the reconnection was successful. | ||||
|   Future<void> onSuccess(); | ||||
| 
 | ||||
|   bool get shouldReconnect => _shouldAttemptReconnection; | ||||
| 
 | ||||
|   /// Set whether a reconnection attempt should be made. | ||||
|   void setShouldReconnect(bool value) { | ||||
|     _shouldAttemptReconnection = value; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns true if the manager is currently triggering a reconnection. If not, returns | ||||
|   /// false. | ||||
|   Future<bool> isReconnectionRunning() async { | ||||
|     return _isReconnectingLock.synchronized(() => _isReconnecting); | ||||
|   } | ||||
| 
 | ||||
|   /// Set the _isReconnecting state to [value]. | ||||
|   @protected | ||||
|   Future<void> setIsReconnecting(bool value) async { | ||||
|     await _isReconnectingLock.synchronized(() async { | ||||
|       _isReconnecting = value; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @protected | ||||
|   Future<bool> testAndSetIsReconnecting() async { | ||||
|     return _isReconnectingLock.synchronized(() { | ||||
|       if (_isReconnecting) { | ||||
|         return false; | ||||
|       } else { | ||||
|         _isReconnecting = true; | ||||
|         return true; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// A simple reconnection strategy: Make the reconnection delays exponentially longer | ||||
| /// for every failed attempt. | ||||
| class ExponentialBackoffReconnectionPolicy extends ReconnectionPolicy { | ||||
| 
 | ||||
|   ExponentialBackoffReconnectionPolicy() | ||||
|   : _counter = 0, | ||||
|     _log = Logger('ExponentialBackoffReconnectionPolicy'), | ||||
|     super(); | ||||
|   int _counter; | ||||
|   Timer? _timer; | ||||
|   final Logger _log; | ||||
| 
 | ||||
|   /// Called when the backoff expired | ||||
|   Future<void> _onTimerElapsed() async { | ||||
|     final isReconnecting = await isReconnectionRunning(); | ||||
|     if (shouldReconnect) { | ||||
|       if (!isReconnecting) { | ||||
|         await performReconnect!(); | ||||
|       } else { | ||||
|         // Should never happen. | ||||
|         _log.fine('Backoff timer expired but reconnection is running, so doing nothing.'); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   Future<void> reset() async { | ||||
|     _log.finest('Resetting internal state'); | ||||
|     _counter = 0; | ||||
|     await setIsReconnecting(false); | ||||
| 
 | ||||
|     if (_timer != null) { | ||||
|       _timer!.cancel(); | ||||
|       _timer = null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> onFailure() async { | ||||
|     _log.finest('Failure occured. Starting exponential backoff'); | ||||
|     _counter++; | ||||
|     await setIsReconnecting(true); | ||||
| 
 | ||||
|     if (_timer != null) { | ||||
|       _timer!.cancel(); | ||||
|     } | ||||
| 
 | ||||
|     // Wait at max 80 seconds. | ||||
|     final seconds = min(pow(2, _counter).toInt(), 80); | ||||
|     _timer = Timer(Duration(seconds: seconds), _onTimerElapsed); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> onSuccess() async { | ||||
|     await reset(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// A stub reconnection policy for tests | ||||
| @visibleForTesting | ||||
| class TestingReconnectionPolicy extends ReconnectionPolicy { | ||||
|   TestingReconnectionPolicy() : super(); | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> onSuccess() async {} | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> onFailure() async {} | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> reset() async {} | ||||
| } | ||||
							
								
								
									
										22
									
								
								moxxmpp/lib/src/rfcs/rfc_2782.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								moxxmpp/lib/src/rfcs/rfc_2782.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| /*import 'package:moxdns/moxdns.dart'; | ||||
| 
 | ||||
| /// Sorts the SRV records according to priority and weight. | ||||
| int srvRecordSortComparator(SrvRecord a, SrvRecord b) { | ||||
|   if (a.priority < b.priority) { | ||||
|     return -1; | ||||
|   } else { | ||||
|     if (a.priority > b.priority) { | ||||
|       return 1; | ||||
|     } | ||||
| 
 | ||||
|     // a.priority == b.priority | ||||
|     if (a.weight < b.weight) { | ||||
|       return -1; | ||||
|     } else if (a.weight > b.weight) { | ||||
|       return 1; | ||||
|     } else { | ||||
|       return 0; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| */ | ||||
							
								
								
									
										26
									
								
								moxxmpp/lib/src/rfcs/rfc_4790.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								moxxmpp/lib/src/rfcs/rfc_4790.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| /// A sort comparator using the i;octet collation defined by RFC 4790 | ||||
| // TODO(Unknown): Maybe enforce utf8? | ||||
| int ioctetSortComparator(String a, String b) { | ||||
|   if (a.isEmpty && b.isEmpty) { | ||||
|     return 0; | ||||
|   } | ||||
| 
 | ||||
|   if (a.isEmpty && b.isNotEmpty) { | ||||
|     return -1; | ||||
|   } | ||||
| 
 | ||||
|   if (a.isNotEmpty && b.isEmpty) { | ||||
|     return 1; | ||||
|   } | ||||
| 
 | ||||
|   if (a[0] == b[0]) { | ||||
|     return ioctetSortComparator(a.substring(1), b.substring(1)); | ||||
|   } | ||||
| 
 | ||||
|   // TODO(Unknown): Is this correct? | ||||
|   if (a.codeUnitAt(0) < b.codeUnitAt(0)) { | ||||
|     return -1; | ||||
|   } | ||||
|    | ||||
|   return 1; | ||||
| } | ||||
							
								
								
									
										322
									
								
								moxxmpp/lib/src/roster.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										322
									
								
								moxxmpp/lib/src/roster.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,322 @@ | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:moxxmpp/src/types/error.dart'; | ||||
| 
 | ||||
| const rosterErrorNoQuery = 1; | ||||
| const rosterErrorNonResult = 2; | ||||
| 
 | ||||
| class XmppRosterItem { | ||||
| 
 | ||||
|   XmppRosterItem({ required this.jid, required this.subscription, this.ask, this.name, this.groups = const [] }); | ||||
|   final String jid; | ||||
|   final String? name; | ||||
|   final String subscription; | ||||
|   final String? ask; | ||||
|   final List<String> groups; | ||||
| } | ||||
| 
 | ||||
| enum RosterRemovalResult { | ||||
|   okay, | ||||
|   error, | ||||
|   itemNotFound | ||||
| } | ||||
| 
 | ||||
| class RosterRequestResult { | ||||
| 
 | ||||
|   RosterRequestResult({ required this.items, this.ver }); | ||||
|   List<XmppRosterItem> items; | ||||
|   String? ver; | ||||
| } | ||||
| 
 | ||||
| class RosterPushEvent extends XmppEvent { | ||||
| 
 | ||||
|   RosterPushEvent({ required this.item, this.ver }); | ||||
|   final XmppRosterItem item; | ||||
|   final String? ver; | ||||
| } | ||||
| 
 | ||||
| /// A Stub feature negotiator for finding out whether roster versioning is supported. | ||||
| class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase { | ||||
|   RosterFeatureNegotiator() : _supported = false, super(11, false, rosterVersioningXmlns, rosterNegotiator); | ||||
| 
 | ||||
|   /// True if rosterVersioning is supported. False otherwise. | ||||
|   bool _supported; | ||||
|   bool get isSupported => _supported; | ||||
|    | ||||
|   @override | ||||
|   Future<void> negotiate(XMLNode nonza) async { | ||||
|     // negotiate is only called when the negotiator matched, meaning the server | ||||
|     // advertises roster versioning. | ||||
|     _supported = true; | ||||
|     state = NegotiatorState.done; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void reset() { | ||||
|     _supported = false; | ||||
| 
 | ||||
|     super.reset(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// This manager requires a RosterFeatureNegotiator to be registered. | ||||
| class RosterManager extends XmppManagerBase { | ||||
| 
 | ||||
|   RosterManager() : _rosterVersion = null, super(); | ||||
|   String? _rosterVersion; | ||||
|    | ||||
|   @override | ||||
|   String getId() => rosterManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'RosterManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'iq', | ||||
|       tagName: 'query', | ||||
|       tagXmlns: rosterXmlns, | ||||
|       callback: _onRosterPush, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true;  | ||||
|    | ||||
|   /// Override-able functions | ||||
|   Future<void> commitLastRosterVersion(String version) async {} | ||||
|   Future<void> loadLastRosterVersion() async {} | ||||
| 
 | ||||
|   void setRosterVersion(String ver) { | ||||
|     assert(_rosterVersion == null, 'A roster version must not be empty'); | ||||
| 
 | ||||
|     _rosterVersion = ver; | ||||
|   } | ||||
|   | ||||
|   Future<StanzaHandlerData> _onRosterPush(Stanza stanza, StanzaHandlerData state) async { | ||||
|     final attrs = getAttributes(); | ||||
|     final from = stanza.attributes['from'] as String?; | ||||
|     final selfJid = attrs.getConnectionSettings().jid; | ||||
| 
 | ||||
|     logger.fine('Received roster push'); | ||||
| 
 | ||||
|     // Only allow the push if the from attribute is either | ||||
|     // - empty, i.e. not set | ||||
|     // - a full JID of our own | ||||
|     if (from != null && JID.fromString(from).toBare() != selfJid) { | ||||
|       logger.warning('Roster push invalid! Unexpected from attribute: ${stanza.toXml()}'); | ||||
|       return state.copyWith(done: true); | ||||
|     } | ||||
| 
 | ||||
|     final query = stanza.firstTag('query', xmlns: rosterXmlns)!; | ||||
|     final item = query.firstTag('item'); | ||||
| 
 | ||||
|     if (item == null) { | ||||
|       logger.warning('Received empty roster push'); | ||||
|       return state.copyWith(done: true); | ||||
|     } | ||||
| 
 | ||||
|     if (query.attributes['ver'] != null) { | ||||
|       final ver = query.attributes['ver']! as String; | ||||
|       await commitLastRosterVersion(ver); | ||||
|       _rosterVersion = ver; | ||||
|     } | ||||
|      | ||||
|     attrs.sendEvent(RosterPushEvent( | ||||
|       item: XmppRosterItem( | ||||
|         jid: item.attributes['jid']! as String, | ||||
|         subscription: item.attributes['subscription']! as String, | ||||
|         ask: item.attributes['ask'] as String?, | ||||
|         name: item.attributes['name'] as String?,  | ||||
|       ), | ||||
|       ver: query.attributes['ver'] as String?, | ||||
|     ),); | ||||
|     await attrs.sendStanza(stanza.reply()); | ||||
| 
 | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
| 
 | ||||
|   /// Shared code between requesting rosters without and with roster versioning, if | ||||
|   /// the server deems a regular roster response more efficient than n roster pushes. | ||||
|   Future<MayFail<RosterRequestResult>> _handleRosterResponse(XMLNode? query) async { | ||||
|     final List<XmppRosterItem> items; | ||||
|     if (query != null) { | ||||
|       items = query.children.map((item) => XmppRosterItem( | ||||
|         name: item.attributes['name'] as String?, | ||||
|         jid: item.attributes['jid']! as String, | ||||
|         subscription: item.attributes['subscription']! as String, | ||||
|         ask: item.attributes['ask'] as String?, | ||||
|         groups: item.findTags('group').map((groupNode) => groupNode.innerText()).toList(), | ||||
|       ),).toList(); | ||||
| 
 | ||||
|       if (query.attributes['ver'] != null) { | ||||
|         final ver_ = query.attributes['ver']! as String; | ||||
|         await commitLastRosterVersion(ver_); | ||||
|         _rosterVersion = ver_; | ||||
|       } | ||||
|     } else { | ||||
|       logger.warning('Server response to roster request without roster versioning does not contain a <query /> element, while the type is not error. This violates RFC6121'); | ||||
|       return MayFail.failure(rosterErrorNoQuery); | ||||
|     } | ||||
| 
 | ||||
|     final ver = query.attributes['ver'] as String?; | ||||
|     if (ver != null) { | ||||
|       _rosterVersion = ver; | ||||
|       await commitLastRosterVersion(ver); | ||||
|     } | ||||
|      | ||||
|     return MayFail.success( | ||||
|       RosterRequestResult( | ||||
|         items: items, | ||||
|         ver: ver, | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|   } | ||||
|    | ||||
|   /// Requests the roster following RFC 6121 without using roster versioning. | ||||
|   Future<MayFail<RosterRequestResult>> requestRoster() async { | ||||
|     final attrs = getAttributes(); | ||||
|     final response = await attrs.sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'get', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'query', | ||||
|             xmlns: rosterXmlns, | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (response.attributes['type'] != 'result') { | ||||
|       logger.warning('Error requesting roster without roster versioning: ${response.toXml()}'); | ||||
|       return MayFail.failure(rosterErrorNonResult); | ||||
|     } | ||||
| 
 | ||||
|     final query = response.firstTag('query', xmlns: rosterXmlns); | ||||
|     return _handleRosterResponse(query); | ||||
|   } | ||||
| 
 | ||||
|   /// Requests a series of roster pushes according to RFC6121. Requires that the server | ||||
|   /// advertises urn:xmpp:features:rosterver in the stream features. | ||||
|   Future<MayFail<RosterRequestResult?>> requestRosterPushes() async { | ||||
|     if (_rosterVersion == null) { | ||||
|       await loadLastRosterVersion(); | ||||
|     } | ||||
| 
 | ||||
|     final attrs = getAttributes(); | ||||
|     final result = await attrs.sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'get', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'query', | ||||
|             xmlns: rosterXmlns, | ||||
|             attributes: { | ||||
|               'ver': _rosterVersion ?? '' | ||||
|             }, | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (result.attributes['type'] != 'result') { | ||||
|       logger.warning('Requesting roster pushes failed: ${result.toXml()}'); | ||||
|       return MayFail.failure(rosterErrorNonResult); | ||||
|     } | ||||
| 
 | ||||
|     final query = result.firstTag('query', xmlns: rosterXmlns); | ||||
|     return _handleRosterResponse(query); | ||||
|   } | ||||
| 
 | ||||
|   bool rosterVersioningAvailable() { | ||||
|     return getAttributes().getNegotiatorById<RosterFeatureNegotiator>(rosterNegotiator)!.isSupported; | ||||
|   } | ||||
|    | ||||
|   /// Attempts to add [jid] with a title of [title] and groups [groups] to the roster. | ||||
|   /// Returns true if the process was successful, false otherwise. | ||||
|   Future<bool> addToRoster(String jid, String title, { List<String>? groups }) async { | ||||
|     final attrs = getAttributes(); | ||||
|     final response = await attrs.sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'query', | ||||
|             xmlns: rosterXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'item', | ||||
|                 attributes: <String, String>{ | ||||
|                   'jid': jid, | ||||
|                   ...title == jid.split('@')[0] ? <String, String>{} : <String, String>{ 'name': title } | ||||
|                 }, | ||||
|                 children: (groups ?? []).map((group) => XMLNode(tag: 'group', text: group)).toList(), | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (response.attributes['type'] != 'result') { | ||||
|       logger.severe('Error adding $jid to roster: $response'); | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   /// Attempts to remove [jid] from the roster. Returns true if the process was successful, | ||||
|   /// false otherwise. | ||||
|   Future<RosterRemovalResult> removeFromRoster(String jid) async { | ||||
|     final attrs = getAttributes(); | ||||
|     final response = await attrs.sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'query', | ||||
|             xmlns: rosterXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'item', | ||||
|                 attributes: <String, String>{ | ||||
|                   'jid': jid, | ||||
|                   'subscription': 'remove' | ||||
|                 }, | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (response.attributes['type'] != 'result') { | ||||
|       logger.severe('Failed to remove roster item: ${response.toXml()}'); | ||||
| 
 | ||||
|       final error = response.firstTag('error')!; | ||||
|       final notFound = error.firstTag('item-not-found') != null; | ||||
| 
 | ||||
|       if (notFound) { | ||||
|         logger.warning('Item was not found'); | ||||
|         return RosterRemovalResult.itemNotFound; | ||||
|       } | ||||
| 
 | ||||
|       return RosterRemovalResult.error; | ||||
|     } | ||||
| 
 | ||||
|     return RosterRemovalResult.okay; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										6
									
								
								moxxmpp/lib/src/routing.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								moxxmpp/lib/src/routing.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| enum RoutingState { | ||||
|   error, | ||||
|   preConnection, | ||||
|   negotiating, | ||||
|   handleStanzas | ||||
| } | ||||
							
								
								
									
										10
									
								
								moxxmpp/lib/src/settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								moxxmpp/lib/src/settings.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| 
 | ||||
| class ConnectionSettings { | ||||
| 
 | ||||
|   ConnectionSettings({ required this.jid, required this.password, required this.useDirectTLS, required this.allowPlainAuth }); | ||||
|   final JID jid; | ||||
|   final String password; | ||||
|   final bool useDirectTLS; | ||||
|   final bool allowPlainAuth; | ||||
| } | ||||
							
								
								
									
										343
									
								
								moxxmpp/lib/src/socket.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								moxxmpp/lib/src/socket.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,343 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:io'; | ||||
| import 'package:logging/logging.dart'; | ||||
| //import 'package:moxdns/moxdns.dart'; | ||||
| //import 'package:moxxmpp/src/rfcs/rfc_2782.dart'; | ||||
| 
 | ||||
| // NOTE: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids | ||||
| const xmppClientALPNId = 'xmpp-client'; | ||||
| 
 | ||||
| abstract class XmppSocketEvent {} | ||||
| 
 | ||||
| /// Triggered by the socket when an error occurs. | ||||
| class XmppSocketErrorEvent extends XmppSocketEvent { | ||||
| 
 | ||||
|   XmppSocketErrorEvent(this.error); | ||||
|   final Object error; | ||||
| } | ||||
| 
 | ||||
| /// Triggered when the socket is closed | ||||
| class XmppSocketClosureEvent extends XmppSocketEvent {} | ||||
| 
 | ||||
| /// This class is the base for a socket that XmppConnection can use. | ||||
| abstract class BaseSocketWrapper { | ||||
|   /// This must return the unbuffered string stream that the socket receives. | ||||
|   Stream<String> getDataStream(); | ||||
| 
 | ||||
|   /// This must return events generated by the socket. | ||||
|   /// See sub-classes of [XmppSocketEvent] for possible events. | ||||
|   Stream<XmppSocketEvent> getEventStream(); | ||||
|    | ||||
|   /// This must close the socket but not the streams so that the same class can be | ||||
|   /// reused by calling [this.connect] again. | ||||
|   void close(); | ||||
| 
 | ||||
|   /// Write [data] into the socket. If [redact] is not null, then [redact] will be | ||||
|   /// logged instead of [data]. | ||||
|   void write(String data, { String? redact }); | ||||
|    | ||||
|   /// This must connect to [host]:[port] and initialize the streams accordingly. | ||||
|   /// [domain] is the domain that TLS should be validated against, in case the Socket | ||||
|   /// provides TLS encryption. Returns true if the connection has been successfully | ||||
|   /// established. Returns false if the connection has failed. | ||||
|   Future<bool> connect(String domain, { String? host, int? port }); | ||||
| 
 | ||||
|   /// Returns true if the socket is secured, e.g. using TLS. | ||||
|   bool isSecure(); | ||||
| 
 | ||||
|   /// Upgrades the connection into a secure version, e.g. by performing a TLS upgrade. | ||||
|   /// May do nothing if the connection is always secure. | ||||
|   /// Returns true if the socket has been successfully upgraded. False otherwise. | ||||
|   Future<bool> secure(String domain); | ||||
| 
 | ||||
|   /// Returns true if whitespace pings are allowed. False if not. | ||||
|   bool whitespacePingAllowed(); | ||||
| 
 | ||||
|   /// Returns true if it manages its own keepalive pings, like websockets. False if not. | ||||
|   bool managesKeepalives(); | ||||
| 
 | ||||
|   /// Brings the socket into a state that allows it to close without triggering any errors | ||||
|   /// to the XmppConnection. | ||||
|   void prepareDisconnect() {} | ||||
| } | ||||
| 
 | ||||
| /// TCP socket implementation for XmppConnection | ||||
| /*class TCPSocketWrapper extends BaseSocketWrapper { | ||||
|    | ||||
|   TCPSocketWrapper(this._logData) | ||||
|   : _log = Logger('TCPSocketWrapper'), | ||||
|     _dataStream = StreamController.broadcast(), | ||||
|     _eventStream = StreamController.broadcast(), | ||||
|     _secure = false, | ||||
|     _ignoreSocketClosure = false; | ||||
|   Socket? _socket; | ||||
|   bool _ignoreSocketClosure; | ||||
|   final StreamController<String> _dataStream; | ||||
|   final StreamController<XmppSocketEvent> _eventStream; | ||||
|   StreamSubscription<dynamic>? _socketSubscription; | ||||
| 
 | ||||
|   final Logger _log; | ||||
|   final bool _logData; | ||||
| 
 | ||||
|   bool _secure; | ||||
| 
 | ||||
|   @override | ||||
|   bool isSecure() => _secure; | ||||
| 
 | ||||
|   @override | ||||
|   bool whitespacePingAllowed() => true; | ||||
| 
 | ||||
|   @override | ||||
|   bool managesKeepalives() => false; | ||||
| 
 | ||||
|   /// Allow the socket to be destroyed by cancelling internal subscriptions. | ||||
|   void destroy() { | ||||
|     _socketSubscription?.cancel(); | ||||
|   } | ||||
|    | ||||
|   bool _onBadCertificate(dynamic certificate, String domain) { | ||||
|     _log.fine('Bad certificate: ${certificate.toString()}'); | ||||
|     //final isExpired = certificate.endValidity.isAfter(DateTime.now()); | ||||
|     // TODO(Unknown): Either validate the certificate ourselves or use a platform native | ||||
|     //                hostname verifier (or Dart adds it themselves) | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   Future<bool> _xep368Connect(String domain) async { | ||||
|     // TODO(Unknown): Maybe do DNSSEC one day | ||||
|     final results = await MoxdnsPlugin.srvQuery('_xmpps-client._tcp.$domain', false); | ||||
|     if (results.isEmpty) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     results.sort(srvRecordSortComparator); | ||||
|     for (final srv in results) { | ||||
|       try { | ||||
|         _log.finest('Attempting secure connection to ${srv.target}:${srv.port}...'); | ||||
|         _ignoreSocketClosure = true; | ||||
|         _socket = await SecureSocket.connect( | ||||
|           srv.target, | ||||
|           srv.port, | ||||
|           timeout: const Duration(seconds: 5), | ||||
|           supportedProtocols: const [ xmppClientALPNId ], | ||||
|           onBadCertificate: (cert) => _onBadCertificate(cert, domain), | ||||
|         ); | ||||
| 
 | ||||
|         _ignoreSocketClosure = false; | ||||
|         _secure = true; | ||||
|         _log.finest('Success!'); | ||||
|         return true; | ||||
|       } on SocketException catch(e) { | ||||
|         _log.finest('Failure! $e'); | ||||
|         _ignoreSocketClosure = false; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   Future<bool> _rfc6120Connect(String domain) async { | ||||
|     // TODO(Unknown): Maybe do DNSSEC one day | ||||
|     final results = await MoxdnsPlugin.srvQuery('_xmpp-client._tcp.$domain', false); | ||||
|     results.sort(srvRecordSortComparator); | ||||
| 
 | ||||
|     for (final srv in results) { | ||||
|       try { | ||||
|         _log.finest('Attempting connection to ${srv.target}:${srv.port}...'); | ||||
|         _ignoreSocketClosure = true; | ||||
|         _socket = await Socket.connect( | ||||
|           srv.target, | ||||
|           srv.port, | ||||
|           timeout: const Duration(seconds: 5), | ||||
|         ); | ||||
| 
 | ||||
|         _ignoreSocketClosure = false; | ||||
|         _log.finest('Success!'); | ||||
|         return true; | ||||
|       } on SocketException catch(e) { | ||||
|         _log.finest('Failure! $e'); | ||||
|         _ignoreSocketClosure = false; | ||||
|         continue; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return _rfc6120FallbackConnect(domain); | ||||
|   } | ||||
| 
 | ||||
|   /// Connect to [host] with port [port] and returns true if the connection | ||||
|   /// was successfully established. Does not setup the streams as this has | ||||
|   /// to be done by the caller. | ||||
|   Future<bool> _hostPortConnect(String host, int port) async { | ||||
|     try { | ||||
|       _log.finest('Attempting fallback connection to $host:$port...'); | ||||
|       _ignoreSocketClosure = true; | ||||
|       _socket = await Socket.connect( | ||||
|         host, | ||||
|         port, | ||||
|         timeout: const Duration(seconds: 5), | ||||
|       ); | ||||
|       _log.finest('Success!'); | ||||
|       return true; | ||||
|     } on SocketException catch(e) { | ||||
|       _log.finest('Failure! $e'); | ||||
|       _ignoreSocketClosure = false; | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Connect to [domain] using the default C2S port of XMPP. Returns | ||||
|   /// true if the connection was successful. Does not setup the streams | ||||
|   /// as [_rfc6120FallbackConnect] should only be called from | ||||
|   /// [_rfc6120Connect], which already sets the streams up on a successful | ||||
|   /// connection. | ||||
|   Future<bool> _rfc6120FallbackConnect(String domain) async { | ||||
|     return _hostPortConnect(domain, 5222); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> secure(String domain) async { | ||||
|     if (_secure) { | ||||
|       _log.warning('Connection is already marked as secure. Doing nothing'); | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     if (_socket == null) { | ||||
|       _log.severe('Failed to secure socket since _socket is null'); | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     _ignoreSocketClosure = true; | ||||
|      | ||||
|     try { | ||||
|       _socket = await SecureSocket.secure( | ||||
|         _socket!, | ||||
|         supportedProtocols: const [ xmppClientALPNId ], | ||||
|         onBadCertificate: (cert) => _onBadCertificate(cert, domain), | ||||
|       ); | ||||
| 
 | ||||
|       _secure = true; | ||||
|       _ignoreSocketClosure = false; | ||||
|       _setupStreams(); | ||||
|       return true; | ||||
|     } on SocketException { | ||||
|       _ignoreSocketClosure = false; | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   void _setupStreams() { | ||||
|     if (_socket == null) { | ||||
|       _log.severe('Failed to setup streams as _socket is null'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     _socketSubscription = _socket!.listen( | ||||
|       (List<int> event) { | ||||
|         final data = utf8.decode(event); | ||||
|         if (_logData) { | ||||
|           _log.finest('<== $data'); | ||||
|         } | ||||
|         _dataStream.add(data); | ||||
|       }, | ||||
|       onError: (Object error) { | ||||
|         _log.severe(error.toString()); | ||||
|         _eventStream.add(XmppSocketErrorEvent(error)); | ||||
|       }, | ||||
|     ); | ||||
|     // ignore: implicit_dynamic_parameter | ||||
|     _socket!.done.then((_) { | ||||
|       if (!_ignoreSocketClosure) { | ||||
|         _eventStream.add(XmppSocketClosureEvent()); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   Future<bool> connect(String domain, { String? host, int? port }) async { | ||||
|     _ignoreSocketClosure = false; | ||||
|     _secure = false; | ||||
| 
 | ||||
|     // Connection order: | ||||
|     // 1. host:port, if given | ||||
|     // 2. XEP-0368 | ||||
|     // 3. RFC 6120 | ||||
|     // 4. RFC 6120 fallback | ||||
| 
 | ||||
|     if (host != null && port != null) { | ||||
|       _log.finest('Specific host and port given'); | ||||
|       if (await _hostPortConnect(host, port)) { | ||||
|         _setupStreams(); | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (await _xep368Connect(domain)) { | ||||
|       _setupStreams(); | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     // NOTE: _rfc6120Connect already attempts the fallback | ||||
|     if (await _rfc6120Connect(domain)) { | ||||
|       _setupStreams(); | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void close() { | ||||
|     if (_socketSubscription != null) { | ||||
|       _log.finest('Closing socket subscription'); | ||||
|       _socketSubscription!.cancel(); | ||||
|     } | ||||
| 
 | ||||
|     if (_socket == null) { | ||||
|       _log.warning('Failed to close socket since _socket is null'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     _ignoreSocketClosure = true; | ||||
|     try { | ||||
|       _socket!.close(); | ||||
|     } catch(e) { | ||||
|       _log.warning('Closing socket threw exception: $e'); | ||||
|     } | ||||
|     _ignoreSocketClosure = false; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Stream<String> getDataStream() => _dataStream.stream.asBroadcastStream(); | ||||
| 
 | ||||
|   @override | ||||
|   Stream<XmppSocketEvent> getEventStream() => _eventStream.stream.asBroadcastStream(); | ||||
| 
 | ||||
|   @override | ||||
|   void write(Object? data, { String? redact }) { | ||||
|     if (_socket == null) { | ||||
|       _log.severe('Failed to write to socket as _socket is null'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (data != null && data is String && _logData) { | ||||
|       if (redact != null) { | ||||
|         _log.finest('**> $redact'); | ||||
|       } else { | ||||
|         _log.finest('==> $data'); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       _socket!.write(data); | ||||
|     } on SocketException catch (e) { | ||||
|       _log.severe(e); | ||||
|       _eventStream.add(XmppSocketErrorEvent(e)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void prepareDisconnect() { | ||||
|     _ignoreSocketClosure = true; | ||||
|   } | ||||
| }*/ | ||||
							
								
								
									
										131
									
								
								moxxmpp/lib/src/stanza.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								moxxmpp/lib/src/stanza.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| class Stanza extends XMLNode { | ||||
| 
 | ||||
|   Stanza({ this.to, this.from, this.type, this.id, List<XMLNode> children = const [], required String tag, Map<String, String> attributes = const {} }) : super( | ||||
|     tag: tag, | ||||
|     attributes: <String, dynamic>{ | ||||
|       ...attributes, | ||||
|       ...type != null ? <String, dynamic>{ 'type': type } : <String, dynamic>{}, | ||||
|       ...id != null ? <String, dynamic>{ 'id': id } : <String, dynamic>{}, | ||||
|       ...to != null ? <String, dynamic>{ 'to': to } : <String, dynamic>{}, | ||||
|       ...from != null ? <String, dynamic>{ 'from': from } : <String, dynamic>{}, | ||||
|       'xmlns': stanzaXmlns | ||||
|     }, | ||||
|     children: children, | ||||
|   ); | ||||
| 
 | ||||
|   factory Stanza.iq({ String? to, String? from, String? type, String? id, List<XMLNode> children = const [], Map<String, String>? attributes = const {} }) { | ||||
|     return Stanza( | ||||
|       tag: 'iq', | ||||
|       from: from, | ||||
|       to: to, | ||||
|       id: id, | ||||
|       type: type, | ||||
|       attributes: <String, String>{ | ||||
|         ...attributes!, | ||||
|         'xmlns': stanzaXmlns | ||||
|       }, | ||||
|       children: children, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   factory Stanza.presence({ String? to, String? from, String? type, String? id, List<XMLNode> children = const [], Map<String, String>? attributes = const {} }) { | ||||
|     return Stanza( | ||||
|       tag: 'presence', | ||||
|       from: from, | ||||
|       to: to, | ||||
|       id: id, | ||||
|       type: type, | ||||
|       attributes: <String, String>{ | ||||
|         ...attributes!, | ||||
|         'xmlns': stanzaXmlns | ||||
|       }, | ||||
|       children: children, | ||||
|     ); | ||||
|   } | ||||
|   factory Stanza.message({ String? to, String? from, String? type, String? id, List<XMLNode> children = const [], Map<String, String>? attributes = const {} }) { | ||||
|     return Stanza( | ||||
|       tag: 'message', | ||||
|       from: from, | ||||
|       to: to, | ||||
|       id: id, | ||||
|       type: type, | ||||
|       attributes: <String, String>{ | ||||
|         ...attributes!, | ||||
|         'xmlns': stanzaXmlns | ||||
|       }, | ||||
|       children: children, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   factory Stanza.fromXMLNode(XMLNode node) { | ||||
|     return Stanza( | ||||
|       to: node.attributes['to'] as String?, | ||||
|       from: node.attributes['from'] as String?, | ||||
|       id: node.attributes['id'] as String?, | ||||
|       tag: node.tag, | ||||
|       type: node.attributes['type'] as String?, | ||||
|       children: node.children, | ||||
|       // TODO(Unknown): Remove to, from, id, and type | ||||
|       // TODO(Unknown): Not sure if this is the correct way to approach this | ||||
|       attributes: node.attributes | ||||
|         .map<String, String>((String key, dynamic value) { | ||||
|           return MapEntry(key, value.toString()); | ||||
|         }), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   String? to; | ||||
|   String? from; | ||||
|   String? type; | ||||
|   String? id; | ||||
| 
 | ||||
|   Stanza copyWith({ String? id, String? from, String? to, String? type, List<XMLNode>? children }) { | ||||
|     return Stanza( | ||||
|       tag: tag, | ||||
|       to: to ?? this.to, | ||||
|       from: from ?? this.from, | ||||
|       id: id ?? this.id, | ||||
|       type: type ?? this.type, | ||||
|       children: children ?? this.children, | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   Stanza reply({ List<XMLNode> children = const [] }) { | ||||
|     return copyWith( | ||||
|       from: attributes['to'] as String?, | ||||
|       to: attributes['from'] as String?, | ||||
|       type: tag == 'iq' ? 'result' : attributes['type'] as String?, | ||||
|       children: children, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Stanza errorReply(String type, String condition, { String? text }) { | ||||
|    return copyWith( | ||||
|       from: attributes['to'] as String?, | ||||
|       to: attributes['from'] as String?, | ||||
|       type: 'error', | ||||
|       children: [ | ||||
|         XMLNode( | ||||
|           tag: 'error', | ||||
|           attributes: <String, dynamic>{ 'type': type }, | ||||
|           children: [ | ||||
|             XMLNode.xmlns( | ||||
|               tag: condition, | ||||
|               xmlns: fullStanzaXmlns, | ||||
|               children: text != null ?[ | ||||
|                 XMLNode.xmlns( | ||||
|                   tag: 'text', | ||||
|                   xmlns: fullStanzaXmlns, | ||||
|                   text: text, | ||||
|                 ) | ||||
|               ] : [], | ||||
|             ) | ||||
|           ], | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										136
									
								
								moxxmpp/lib/src/stringxml.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								moxxmpp/lib/src/stringxml.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,136 @@ | ||||
| import 'package:xml/xml.dart'; | ||||
| 
 | ||||
| class XMLNode { | ||||
| 
 | ||||
|   XMLNode({ | ||||
|     required this.tag, | ||||
|     this.attributes = const <String, dynamic>{}, | ||||
|     this.children = const [], | ||||
|     this.closeTag = true, | ||||
|     this.text, | ||||
|     this.isDeclaration = false, | ||||
|   }); | ||||
|   XMLNode.xmlns({ | ||||
|       required this.tag, | ||||
|       required String xmlns, | ||||
|       Map<String, String> attributes = const <String, String>{}, | ||||
|       this.children = const [], | ||||
|       this.closeTag = true, | ||||
|       this.text, | ||||
|   }) : attributes = <String, String>{ 'xmlns': xmlns, ...attributes }, isDeclaration = false; | ||||
|   /// Because this API is better ;) | ||||
|   /// Don't use in production. Just for testing | ||||
|   factory XMLNode.fromXmlElement(XmlElement element) { | ||||
|     final attributes = <String, String>{}; | ||||
| 
 | ||||
|     for (final attribute in element.attributes) { | ||||
|       attributes[attribute.name.qualified] = attribute.value; | ||||
|     } | ||||
| 
 | ||||
|     if (element.childElements.isEmpty) { | ||||
|       return XMLNode( | ||||
|         tag: element.name.qualified, | ||||
|         attributes: attributes, | ||||
|         text: element.innerText, | ||||
|       ); | ||||
|     } else { | ||||
|       return XMLNode( | ||||
|         tag: element.name.qualified, | ||||
|         attributes: attributes, | ||||
|         children: element.childElements.toList().map(XMLNode.fromXmlElement).toList(), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|   /// Just for testing purposes | ||||
|   factory XMLNode.fromString(String str) { | ||||
|     return XMLNode.fromXmlElement( | ||||
|       XmlDocument.parse(str).firstElementChild!, | ||||
|     ); | ||||
|   } | ||||
|   final String tag; | ||||
|   Map<String, dynamic> attributes; | ||||
|   List<XMLNode> children; | ||||
|   bool closeTag; | ||||
|   String? text; | ||||
|   bool isDeclaration; | ||||
| 
 | ||||
|   /// Adds a child to this node. | ||||
|   void addChild(XMLNode child) { | ||||
|     children.add(child); | ||||
|   } | ||||
| 
 | ||||
|   /// Renders the attributes of the node into "attr1=\"value\" attr2=...". | ||||
|   String renderAttributes() { | ||||
|     return attributes.keys.map((String key) { | ||||
|         final dynamic value = attributes[key]; | ||||
|         assert(value is String || value is int, 'XML values must either be string or int'); | ||||
|         if (value is String) { | ||||
|           return "$key='$value'"; | ||||
|         } else { | ||||
|           return '$key=$value'; | ||||
|         } | ||||
|     }).join(' '); | ||||
|   } | ||||
| 
 | ||||
|   /// Renders the entire node, including its children, into an XML string. | ||||
|   String toXml() { | ||||
|     final decl = isDeclaration ? '?' : ''; | ||||
|     if (children.isEmpty) { | ||||
|       if (text != null && text!.isNotEmpty) { | ||||
|         final attrString = attributes.isEmpty ? '' : ' ${renderAttributes()}'; | ||||
|         return '<$tag$attrString>$text</$tag>'; | ||||
|       } else { | ||||
|         return '<$decl$tag ${renderAttributes()}${closeTag ? " />" : "$decl>"}'; | ||||
|       }  | ||||
|     } else {  | ||||
|       final childXml = children.map((child) => child.toXml()).join(); | ||||
|       final xml = '<$decl$tag ${renderAttributes()}$decl>$childXml'; | ||||
|       return xml + (closeTag ? '</$tag>' : ''); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Returns the first child for which [test] returns true. If none is found, returns | ||||
|   /// null. | ||||
|   XMLNode? _firstTag(bool Function(XMLNode) test) { | ||||
|     try { | ||||
|       return children.firstWhere(test); | ||||
|     } catch(e) { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /// Returns the first xml node that matches the description: | ||||
|   /// - node's tag is equal to [tag] | ||||
|   /// - (optional) node's xmlns attribute is equal to [xmlns] | ||||
|   /// Returns null if none is found. | ||||
|   XMLNode? firstTag(String tag, { String? xmlns}) { | ||||
|     return _firstTag((node) { | ||||
|       if (xmlns != null) { | ||||
|         return node.tag == tag && node.attributes['xmlns'] == xmlns; | ||||
|       } | ||||
| 
 | ||||
|       return node.tag == tag; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /// Returns the first child whose xmlns attribute is equal to [xmlns]. Returns null | ||||
|   /// if none is found. | ||||
|   XMLNode? firstTagByXmlns(String xmlns) { | ||||
|     return _firstTag((node) { | ||||
|       return node.attributes['xmlns'] == xmlns; | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /// Returns all children whose tag is equal to [tag]. | ||||
|   List<XMLNode> findTags(String tag, { String? xmlns }) { | ||||
|     return children.where((element) { | ||||
|       final xmlnsMatches = xmlns != null ? element.attributes['xmlns'] == xmlns : true; | ||||
|       return element.tag == tag && xmlnsMatches; | ||||
|     }).toList(); | ||||
|   } | ||||
|    | ||||
|   /// Returns the inner text of the node. If none is set, returns the "". | ||||
|   String innerText() { | ||||
|     return text ?? ''; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										19
									
								
								moxxmpp/lib/src/types/error.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								moxxmpp/lib/src/types/error.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| /// A wrapper class that can be used to indicate that a function may return a valid | ||||
| /// instance of [T] but may also fail. | ||||
| /// The way [MayFail] is intended to be used to to have function specific - or application | ||||
| /// specific - error codes that can be either handled by code or be translated into a | ||||
| /// localised error message for the user. | ||||
| class MayFail<T> { | ||||
| 
 | ||||
|   MayFail({ this.result, this.errorCode }); | ||||
|   MayFail.success(this.result); | ||||
|   MayFail.failure(this.errorCode); | ||||
|   T? result; | ||||
|   int? errorCode; | ||||
| 
 | ||||
|   bool isError() => result == null && errorCode != null; | ||||
| 
 | ||||
|   T getValue() => result!; | ||||
| 
 | ||||
|   int getErrorCode() => errorCode!; | ||||
| } | ||||
							
								
								
									
										13
									
								
								moxxmpp/lib/src/types/result.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								moxxmpp/lib/src/types/result.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| /// Class that is supposed to by used with a state type S and a value type V. | ||||
| /// The state indicates if an action was successful or not, while the value | ||||
| /// type indicates the return value, i.e. a result in a computation or the | ||||
| /// actual error description. | ||||
| class Result<S, V> { | ||||
| 
 | ||||
|   Result(S state, V value) : _state = state, _value = value; | ||||
|   final S _state; | ||||
|   final V _value; | ||||
| 
 | ||||
|   S getState() => _state; | ||||
|   V getValue() => _value; | ||||
| } | ||||
							
								
								
									
										13
									
								
								moxxmpp/lib/src/types/resultv2.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								moxxmpp/lib/src/types/resultv2.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| class Result<T, V> { | ||||
| 
 | ||||
|   const Result(this._data) : assert(_data is T || _data is V, 'Invalid data type: Must be either $T or $V'); | ||||
|   final dynamic _data; | ||||
| 
 | ||||
|   bool isType<S>() => _data is S; | ||||
| 
 | ||||
|   S get<S>() { | ||||
|     assert(_data is S, 'Data is not $S'); | ||||
| 
 | ||||
|     return _data as S; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										54
									
								
								moxxmpp/lib/src/xeps/staging/extensible_file_thumbnails.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								moxxmpp/lib/src/xeps/staging/extensible_file_thumbnails.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| /// NOTE: Specified by https://codeberg.org/moxxy/custom-xeps/src/branch/master/xep-xxxx-extensible-file-thumbnails.md | ||||
| 
 | ||||
| const fileThumbnailsXmlns = 'proto:urn:xmpp:eft:0'; | ||||
| const blurhashThumbnailType = '$fileThumbnailsXmlns:blurhash'; | ||||
| 
 | ||||
| abstract class Thumbnail {} | ||||
| 
 | ||||
| class BlurhashThumbnail extends Thumbnail { | ||||
| 
 | ||||
|   BlurhashThumbnail(this.hash); | ||||
|   final String hash; | ||||
| } | ||||
| 
 | ||||
| Thumbnail? parseFileThumbnailElement(XMLNode node) { | ||||
|   assert(node.attributes['xmlns'] == fileThumbnailsXmlns, 'Invalid element xmlns'); | ||||
|   assert(node.tag == 'file-thumbnail', 'Invalid element name'); | ||||
| 
 | ||||
|   switch (node.attributes['type']!) { | ||||
|     case blurhashThumbnailType: { | ||||
|       final hash = node.firstTag('blurhash')!.innerText(); | ||||
|       return BlurhashThumbnail(hash); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return null; | ||||
| } | ||||
| 
 | ||||
| XMLNode? _fromThumbnail(Thumbnail thumbnail) { | ||||
|   if (thumbnail is BlurhashThumbnail) { | ||||
|     return XMLNode( | ||||
|       tag: 'blurhash', | ||||
|       text: thumbnail.hash, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return null; | ||||
| } | ||||
| 
 | ||||
| XMLNode constructFileThumbnailElement(Thumbnail thumbnail) { | ||||
|   final node = _fromThumbnail(thumbnail)!; | ||||
|   var type = ''; | ||||
|   if (thumbnail is BlurhashThumbnail) { | ||||
|     type = blurhashThumbnailType; | ||||
|   } | ||||
| 
 | ||||
|   return XMLNode.xmlns( | ||||
|     tag: 'file-thumbnail', | ||||
|     xmlns: fileThumbnailsXmlns, | ||||
|     attributes: { 'type': type }, | ||||
|     children: [ node ], | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										72
									
								
								moxxmpp/lib/src/xeps/staging/file_upload_notification.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								moxxmpp/lib/src/xeps/staging/file_upload_notification.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0446.dart'; | ||||
| 
 | ||||
| /// NOTE: Specified by https://github.com/PapaTutuWawa/custom-xeps/blob/master/xep-xxxx-file-upload-notifications.md | ||||
| 
 | ||||
| const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0'; | ||||
| 
 | ||||
| class FileUploadNotificationManager extends XmppManagerBase { | ||||
|   FileUploadNotificationManager() : super(); | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => fileUploadNotificationManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'FileUploadNotificationManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'file-upload', | ||||
|       tagXmlns: fileUploadNotificationXmlns, | ||||
|       callback: _onFileUploadNotificationReceived, | ||||
|       priority: -99, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'replaces', | ||||
|       tagXmlns: fileUploadNotificationXmlns, | ||||
|       callback: _onFileUploadNotificationReplacementReceived, | ||||
|       priority: -99, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'cancelled', | ||||
|       tagXmlns: fileUploadNotificationXmlns, | ||||
|       callback: _onFileUploadNotificationCancellationReceived, | ||||
|       priority: -99, | ||||
|     ), | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
| 
 | ||||
|   Future<StanzaHandlerData> _onFileUploadNotificationReceived(Stanza message, StanzaHandlerData state) async { | ||||
|     final funElement = message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!; | ||||
|     return state.copyWith( | ||||
|       fun: FileMetadataData.fromXML( | ||||
|         funElement.firstTag('file', xmlns: fileMetadataXmlns)!, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived(Stanza message, StanzaHandlerData state) async { | ||||
|     final element = message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!; | ||||
|     return state.copyWith( | ||||
|       funReplacement: element.attributes['id']! as String, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived(Stanza message, StanzaHandlerData state) async { | ||||
|     final element = message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!; | ||||
|     return state.copyWith( | ||||
|       funCancellation: element.attributes['id']! as String, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										147
									
								
								moxxmpp/lib/src/xeps/xep_0004.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								moxxmpp/lib/src/xeps/xep_0004.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,147 @@ | ||||
| import 'package:moxlib/moxlib.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| class DataFormOption { | ||||
| 
 | ||||
|   const DataFormOption({ required this.value, this.label }); | ||||
|   final String? label; | ||||
|   final String value; | ||||
| 
 | ||||
|   XMLNode toXml() { | ||||
|     return XMLNode( | ||||
|       tag: 'option', | ||||
|       attributes: label != null ? <String, dynamic>{ 'label': label } : <String, dynamic>{}, | ||||
|       children: [ | ||||
|         XMLNode( | ||||
|           tag: 'value', | ||||
|           text: value, | ||||
|         ) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class DataFormField { | ||||
| 
 | ||||
|   const DataFormField({ | ||||
|       required this.options, | ||||
|       required this.values, | ||||
|       required this.isRequired, | ||||
|       this.varAttr, | ||||
|       this.type, | ||||
|       this.description, | ||||
|       this.label, | ||||
|   }); | ||||
|   final String? description; | ||||
|   final bool isRequired; | ||||
|   final List<String> values; | ||||
|   final List<DataFormOption> options; | ||||
|   final String? type; | ||||
|   final String? varAttr; | ||||
|   final String? label; | ||||
| 
 | ||||
|   XMLNode toXml() { | ||||
|     return XMLNode( | ||||
|       tag: 'field', | ||||
|       attributes: <String, dynamic>{ | ||||
|         ...varAttr != null ? <String, dynamic>{ 'var': varAttr } : <String, dynamic>{}, | ||||
|         ...type != null ? <String, dynamic>{ 'type': type } : <String, dynamic>{}, | ||||
|         ...label != null ? <String, dynamic>{ 'label': label } : <String, dynamic>{} | ||||
|       }, | ||||
|       children: [ | ||||
|         ...description != null ? [XMLNode(tag: 'desc', text: description)] : [], | ||||
|         ...isRequired ? [XMLNode(tag: 'required')] : [], | ||||
|         ...values.map((value) => XMLNode(tag: 'value', text: value)).toList(), | ||||
|         ...options.map((option) => option.toXml()) | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class DataForm { | ||||
| 
 | ||||
|   const DataForm({ | ||||
|       required this.type, | ||||
|       required this.instructions, | ||||
|       required this.fields, | ||||
|       required this.reported, | ||||
|       required this.items, | ||||
|       this.title, | ||||
|   }); | ||||
|   final String type; | ||||
|   final String? title; | ||||
|   final List<String> instructions; | ||||
|   final List<DataFormField> fields; | ||||
|   final List<DataFormField> reported; | ||||
|   final List<List<DataFormField>> items; | ||||
| 
 | ||||
|   DataFormField? getFieldByVar(String varAttr) { | ||||
|     return firstWhereOrNull(fields, (field) => field.varAttr == varAttr); | ||||
|   } | ||||
|    | ||||
|   XMLNode toXml() { | ||||
|     return XMLNode.xmlns( | ||||
|       tag: 'x', | ||||
|       xmlns: dataFormsXmlns, | ||||
|       attributes: { | ||||
|         'type': type | ||||
|       }, | ||||
|       children: [ | ||||
|         ...instructions.map((i) => XMLNode(tag: 'instruction', text: i)).toList(), | ||||
|         ...title != null ? [XMLNode(tag: 'title', text: title)] : [], | ||||
|         ...fields.map((field) => field.toXml()).toList(), | ||||
|         ...reported.map((report) => report.toXml()).toList(), | ||||
|         ...items.map((item) => XMLNode( | ||||
|               tag: 'item', | ||||
|               children: item.map((i) => i.toXml()).toList(), | ||||
|           ),).toList(), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| DataFormOption _parseDataFormOption(XMLNode option) { | ||||
|   return DataFormOption( | ||||
|     label: option.attributes['label'] as String?, | ||||
|     value: option.firstTag('value')!.innerText(), | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| DataFormField _parseDataFormField(XMLNode field) { | ||||
|   final desc = field.firstTag('desc')?.innerText(); | ||||
|   final isRequired = field.firstTag('required') != null; | ||||
|   final values = field.findTags('value').map((i) => i.innerText()).toList(); | ||||
|   final options = field.findTags('option').map(_parseDataFormOption).toList(); | ||||
| 
 | ||||
|   return DataFormField( | ||||
|     varAttr: field.attributes['var'] as String?, | ||||
|     type: field.attributes['type'] as String?, | ||||
|     options: options, | ||||
|     values: values, | ||||
|     isRequired: isRequired, | ||||
|     description: desc, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| /// Parse a Data Form declaration. | ||||
| DataForm parseDataForm(XMLNode x) { | ||||
|   assert(x.attributes['xmlns'] == dataFormsXmlns, 'Invalid element xmlns'); | ||||
|   assert(x.tag == 'x', 'Invalid element name'); | ||||
| 
 | ||||
|   final type = x.attributes['type']! as String; | ||||
|   final title = x.firstTag('title')?.innerText(); | ||||
|   final instructions = x.findTags('instructions').map((i) => i.innerText()).toList(); | ||||
|   final fields = x.findTags('field').map(_parseDataFormField).toList(); | ||||
|   final reported = x.firstTag('reported')?.findTags('field').map((i) => _parseDataFormField(i.firstTag('field')!)).toList() ?? []; | ||||
|   final items = x.findTags('item').map((i) => i.findTags('field').map(_parseDataFormField).toList()).toList(); | ||||
| 
 | ||||
|   return DataForm( | ||||
|     type: type, | ||||
|     instructions: instructions, | ||||
|     fields: fields, | ||||
|     reported: reported, | ||||
|     items: items, | ||||
|     title: title, | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										7
									
								
								moxxmpp/lib/src/xeps/xep_0030/errors.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								moxxmpp/lib/src/xeps/xep_0030/errors.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| abstract class DiscoError {} | ||||
| 
 | ||||
| class UnknownDiscoError extends DiscoError {} | ||||
| 
 | ||||
| class InvalidResponseDiscoError extends DiscoError {} | ||||
| 
 | ||||
| class ErrorResponseDiscoError extends DiscoError {} | ||||
							
								
								
									
										25
									
								
								moxxmpp/lib/src/xeps/xep_0030/helpers.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								moxxmpp/lib/src/xeps/xep_0030/helpers.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| // TODO(PapaTutuWawa): Move types into types.dart | ||||
| 
 | ||||
| Stanza buildDiscoInfoQueryStanza(String entity, String? node) { | ||||
|   return Stanza.iq(to: entity, type: 'get', children: [ | ||||
|       XMLNode.xmlns( | ||||
|         tag: 'query', | ||||
|         xmlns: discoInfoXmlns, | ||||
|         attributes: node != null ? { 'node': node } : {}, | ||||
|       ) | ||||
|   ],); | ||||
| } | ||||
| 
 | ||||
| Stanza buildDiscoItemsQueryStanza(String entity, { String? node }) { | ||||
|   return Stanza.iq(to: entity, type: 'get', children: [ | ||||
|       XMLNode.xmlns( | ||||
|         tag: 'query', | ||||
|         xmlns: discoItemsXmlns, | ||||
|         attributes: node != null ? { 'node': node } : {}, | ||||
|       ) | ||||
|   ],); | ||||
| } | ||||
							
								
								
									
										46
									
								
								moxxmpp/lib/src/xeps/xep_0030/types.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								moxxmpp/lib/src/xeps/xep_0030/types.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0004.dart'; | ||||
| 
 | ||||
| class Identity { | ||||
| 
 | ||||
|   const Identity({ required this.category, required this.type, this.name, this.lang }); | ||||
|   final String category; | ||||
|   final String type; | ||||
|   final String? name; | ||||
|   final String? lang; | ||||
| 
 | ||||
|   XMLNode toXMLNode() { | ||||
|     return XMLNode( | ||||
|       tag: 'identity', | ||||
|       attributes: <String, dynamic>{ | ||||
|         'category': category, | ||||
|         'type': type, | ||||
|         'name': name, | ||||
|         ...lang == null ? <String, dynamic>{} : <String, dynamic>{ 'xml:lang': lang } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class DiscoInfo { | ||||
| 
 | ||||
|   const DiscoInfo( | ||||
|     this.features, | ||||
|     this.identities, | ||||
|     this.extendedInfo, | ||||
|     this.jid, | ||||
|   ); | ||||
|   final List<String> features; | ||||
|   final List<Identity> identities; | ||||
|   final List<DataForm> extendedInfo; | ||||
|   final JID jid; | ||||
| } | ||||
| 
 | ||||
| class DiscoItem { | ||||
| 
 | ||||
|   const DiscoItem({ required this.jid, this.node, this.name }); | ||||
|   final String jid; | ||||
|   final String? node; | ||||
|   final String? name; | ||||
| } | ||||
							
								
								
									
										439
									
								
								moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										439
									
								
								moxxmpp/lib/src/xeps/xep_0030/xep_0030.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,439 @@ | ||||
| import 'dart:async'; | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/presence.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:moxxmpp/src/types/resultv2.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0004.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0030/helpers.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0115.dart'; | ||||
| import 'package:synchronized/synchronized.dart'; | ||||
| 
 | ||||
| @immutable | ||||
| class DiscoCacheKey { | ||||
| 
 | ||||
|   const DiscoCacheKey(this.jid, this.node); | ||||
|   final String jid; | ||||
|   final String? node; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is DiscoCacheKey && jid == other.jid && node == other.node; | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   int get hashCode => jid.hashCode ^ node.hashCode; | ||||
| } | ||||
| 
 | ||||
| class DiscoManager extends XmppManagerBase { | ||||
|   | ||||
|   DiscoManager() | ||||
|     : _features = List.empty(growable: true), | ||||
|       _capHashCache = {}, | ||||
|       _capHashInfoCache = {}, | ||||
|       _discoInfoCache = {}, | ||||
|       _runningInfoQueries = {}, | ||||
|       _cacheLock = Lock(), | ||||
|       super(); | ||||
|   /// Our features | ||||
|   final List<String> _features; | ||||
| 
 | ||||
|   // Map full JID to Capability hashes | ||||
|   final Map<String, CapabilityHashInfo> _capHashCache; | ||||
|   // Map capability hash to the disco info | ||||
|   final Map<String, DiscoInfo> _capHashInfoCache; | ||||
|   // Map full JID to Disco Info | ||||
|   final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache; | ||||
|   // Mapping the full JID to a list of running requests | ||||
|   final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries; | ||||
|   // Cache lock | ||||
|   final Lock _cacheLock; | ||||
| 
 | ||||
|   @visibleForTesting | ||||
|   bool hasInfoQueriesRunning() => _runningInfoQueries.isNotEmpty; | ||||
| 
 | ||||
|   @visibleForTesting | ||||
|   List<Completer<Result<DiscoError, DiscoInfo>>> getRunningInfoQueries(DiscoCacheKey key) => _runningInfoQueries[key]!; | ||||
|    | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       tagName: 'query', | ||||
|       tagXmlns: discoInfoXmlns, | ||||
|       stanzaTag: 'iq', | ||||
|       callback: _onDiscoInfoRequest, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       tagName: 'query', | ||||
|       tagXmlns: discoItemsXmlns, | ||||
|       stanzaTag: 'iq', | ||||
|       callback: _onDiscoItemsRequest, | ||||
|     ), | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => discoManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'DiscoManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [ discoInfoXmlns, discoItemsXmlns ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> onXmppEvent(XmppEvent event) async { | ||||
|     if (event is PresenceReceivedEvent) { | ||||
|       await _onPresence(event.jid, event.presence); | ||||
|     } else if (event is StreamResumeFailedEvent) { | ||||
|       await _cacheLock.synchronized(() async { | ||||
|         // Clear the cache | ||||
|         _discoInfoCache.clear(); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /// Adds a list of features to the possible disco info response. | ||||
|   /// This function only adds features that are not already present in the disco features. | ||||
|   void addDiscoFeatures(List<String> features) { | ||||
|     for (final feat in features) { | ||||
|       if (!_features.contains(feat)) { | ||||
|         _features.add(feat); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _onPresence(JID from, Stanza presence) async { | ||||
|     final c = presence.firstTag('c', xmlns: capsXmlns); | ||||
|     if (c == null) return; | ||||
| 
 | ||||
|     final info = CapabilityHashInfo( | ||||
|       c.attributes['ver']! as String, | ||||
|       c.attributes['node']! as String, | ||||
|       c.attributes['hash']! as String, | ||||
|     ); | ||||
|      | ||||
|     // Check if we already know of that cache | ||||
|     var cached = false; | ||||
|     await _cacheLock.synchronized(() async { | ||||
|       if (!_capHashCache.containsKey(info.ver)) { | ||||
|         cached = true; | ||||
|       } | ||||
|     }); | ||||
|     if (cached) return; | ||||
| 
 | ||||
|     // Request the cap hash | ||||
|     logger.finest("Received capability hash we don't know about. Requesting it..."); | ||||
|     final result = await discoInfoQuery(from.toString(), node: '${info.node}#${info.ver}'); | ||||
|     if (result.isType<DiscoError>()) return; | ||||
| 
 | ||||
|     await _cacheLock.synchronized(() async { | ||||
|       _capHashCache[from.toString()] = info; | ||||
|       _capHashInfoCache[info.ver] = result.get<DiscoInfo>(); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /// Returns the list of disco features registered. | ||||
|   List<String> getRegisteredDiscoFeatures() => _features; | ||||
|    | ||||
|   /// May be overriden. Specifies the identities which will be returned in a disco info response. | ||||
|   List<Identity> getIdentities() => const [ Identity(category: 'client', type: 'pc', name: 'moxxmpp', lang: 'en') ]; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onDiscoInfoRequest(Stanza stanza, StanzaHandlerData state) async { | ||||
|     if (stanza.type != 'get') return state; | ||||
| 
 | ||||
|     final presence = getAttributes().getManagerById(presenceManager)! as PresenceManager; | ||||
|     final query = stanza.firstTag('query')!; | ||||
|     final node = query.attributes['node'] as String?; | ||||
|     final capHash = await presence.getCapabilityHash(); | ||||
|     final isCapabilityNode = node == 'http://moxxy.im#$capHash'; | ||||
| 
 | ||||
|     if (!isCapabilityNode && node != null) { | ||||
|       await getAttributes().sendStanza(Stanza.iq( | ||||
|             to: stanza.from, | ||||
|             from: stanza.to, | ||||
|             id: stanza.id, | ||||
|             type: 'error', | ||||
|             children: [ | ||||
|               XMLNode.xmlns( | ||||
|                 tag: 'query', | ||||
|                 // TODO(PapaTutuWawa): Why are we copying the xmlns? | ||||
|                 xmlns: query.attributes['xmlns']! as String, | ||||
|                 attributes: <String, String>{ | ||||
|                   'node': node | ||||
|                 }, | ||||
|               ), | ||||
|               XMLNode( | ||||
|                 tag: 'error', | ||||
|                 attributes: <String, String>{ | ||||
|                   'type': 'cancel' | ||||
|                 }, | ||||
|                 children: [ | ||||
|                   XMLNode.xmlns( | ||||
|                     tag: 'not-allowed', | ||||
|                     xmlns: fullStanzaXmlns, | ||||
|                   ) | ||||
|                 ], | ||||
|               ) | ||||
|             ], | ||||
|           ) | ||||
|       ,); | ||||
| 
 | ||||
|       return state.copyWith(done: true); | ||||
|     } | ||||
| 
 | ||||
|     await getAttributes().sendStanza(stanza.reply( | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'query', | ||||
|             xmlns: discoInfoXmlns, | ||||
|             attributes: { | ||||
|               ...!isCapabilityNode ? {} : { | ||||
|                   'node': 'http://moxxy.im#$capHash' | ||||
|               } | ||||
|             }, | ||||
|             children: [ | ||||
|               ...getIdentities().map((identity) => identity.toXMLNode()).toList(), | ||||
|               ..._features.map((feat) { | ||||
|                 return XMLNode( | ||||
|                   tag: 'feature', | ||||
|                   attributes: <String, dynamic>{ 'var': feat }, | ||||
|                 ); | ||||
|               }).toList(), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|     ),); | ||||
| 
 | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
| 
 | ||||
|   Future<StanzaHandlerData> _onDiscoItemsRequest(Stanza stanza, StanzaHandlerData state) async { | ||||
|     if (stanza.type != 'get') return state; | ||||
| 
 | ||||
|     final query = stanza.firstTag('query')!; | ||||
|     if (query.attributes['node'] != null) { | ||||
|       // TODO(Unknown): Handle the node we specified for XEP-0115 | ||||
|       await getAttributes().sendStanza( | ||||
|         Stanza.iq( | ||||
|           to: stanza.from, | ||||
|           from: stanza.to, | ||||
|           id: stanza.id, | ||||
|           type: 'error', | ||||
|           children: [ | ||||
|             XMLNode.xmlns( | ||||
|               tag: 'query', | ||||
|               // TODO(PapaTutuWawa): Why copy the xmlns? | ||||
|               xmlns: query.attributes['xmlns']! as String, | ||||
|               attributes: <String, String>{ | ||||
|                 'node': query.attributes['node']! as String, | ||||
|               }, | ||||
|             ), | ||||
|             XMLNode( | ||||
|               tag: 'error', | ||||
|               attributes: <String, dynamic>{ | ||||
|                 'type': 'cancel' | ||||
|               }, | ||||
|               children: [ | ||||
|                 XMLNode.xmlns( | ||||
|                   tag: 'not-allowed', | ||||
|                   xmlns: fullStanzaXmlns, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|       return state.copyWith(done: true); | ||||
|     } | ||||
| 
 | ||||
|     await getAttributes().sendStanza( | ||||
|       stanza.reply( | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'query', | ||||
|             xmlns: discoItemsXmlns, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async { | ||||
|     return _cacheLock.synchronized(() async { | ||||
|       // Complete all futures | ||||
|       for (final completer in _runningInfoQueries[key]!) { | ||||
|         completer.complete(result); | ||||
|       } | ||||
| 
 | ||||
|       // Add to cache if it is a result | ||||
|       if (result.isType<DiscoInfo>()) { | ||||
|         _discoInfoCache[key] = result.get<DiscoInfo>(); | ||||
|       } | ||||
|        | ||||
|       // Remove from the request cache | ||||
|       _runningInfoQueries.remove(key); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /// Sends a disco info query to the (full) jid [entity], optionally with node=[node]. | ||||
|   Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node}) async { | ||||
|     final cacheKey = DiscoCacheKey(entity, node); | ||||
|     DiscoInfo? info; | ||||
|     Completer<Result<DiscoError, DiscoInfo>>? completer; | ||||
|     await _cacheLock.synchronized(() async { | ||||
|       // Check if we already know what the JID supports | ||||
|       if (_discoInfoCache.containsKey(cacheKey)) { | ||||
|         info = _discoInfoCache[cacheKey]; | ||||
|       } else { | ||||
|         // Is a request running? | ||||
|         if (_runningInfoQueries.containsKey(cacheKey)) { | ||||
|           completer = Completer(); | ||||
|           _runningInfoQueries[cacheKey]!.add(completer!); | ||||
|         } else { | ||||
|           _runningInfoQueries[cacheKey] = List.from(<Completer<DiscoInfo?>>[]); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (info != null) { | ||||
|       return Result<DiscoError, DiscoInfo>(info); | ||||
|     } else if (completer != null) { | ||||
|       return completer!.future; | ||||
|     } | ||||
| 
 | ||||
|     final stanza = await getAttributes().sendStanza( | ||||
|       buildDiscoInfoQueryStanza(entity, node), | ||||
|     ); | ||||
|     final query = stanza.firstTag('query'); | ||||
|     if (query == null) { | ||||
|       final result = Result<DiscoError, DiscoInfo>(InvalidResponseDiscoError()); | ||||
|       await _exitDiscoInfoCriticalSection(cacheKey, result); | ||||
|       return result; | ||||
|     } | ||||
| 
 | ||||
|     final error = stanza.firstTag('error'); | ||||
|     if (error != null && stanza.attributes['type'] == 'error') { | ||||
|       final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError()); | ||||
|       await _exitDiscoInfoCriticalSection(cacheKey, result); | ||||
|       return result; | ||||
|     } | ||||
|      | ||||
|     final features = List<String>.empty(growable: true); | ||||
|     final identities = List<Identity>.empty(growable: true); | ||||
| 
 | ||||
|     for (final element in query.children) { | ||||
|       if (element.tag == 'feature') { | ||||
|         features.add(element.attributes['var']! as String); | ||||
|       } else if (element.tag == 'identity') { | ||||
|         identities.add(Identity( | ||||
|           category: element.attributes['category']! as String, | ||||
|           type: element.attributes['type']! as String, | ||||
|           name: element.attributes['name'] as String?, | ||||
|         ),); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     final result = Result<DiscoError, DiscoInfo>( | ||||
|       DiscoInfo( | ||||
|         features, | ||||
|         identities, | ||||
|         query.findTags('x', xmlns: dataFormsXmlns).map(parseDataForm).toList(), | ||||
|         JID.fromString(stanza.attributes['from']! as String), | ||||
|       ), | ||||
|     ); | ||||
|     await _exitDiscoInfoCriticalSection(cacheKey, result); | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   /// Sends a disco items query to the (full) jid [entity], optionally with node=[node]. | ||||
|   Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, { String? node }) async { | ||||
|     final stanza = await getAttributes() | ||||
|       .sendStanza(buildDiscoItemsQueryStanza(entity, node: node)) as Stanza; | ||||
| 
 | ||||
|     final query = stanza.firstTag('query'); | ||||
|     if (query == null) return Result(InvalidResponseDiscoError()); | ||||
| 
 | ||||
|     final error = stanza.firstTag('error'); | ||||
|     if (error != null && stanza.type == 'error') { | ||||
|       //print("Disco Items error: " + error.toXml()); | ||||
|       return Result(ErrorResponseDiscoError()); | ||||
|     } | ||||
| 
 | ||||
|     final items = query.findTags('item').map((node) => DiscoItem( | ||||
|       jid: node.attributes['jid']! as String, | ||||
|       node: node.attributes['node'] as String?, | ||||
|       name: node.attributes['name'] as String?, | ||||
|     ),).toList(); | ||||
| 
 | ||||
|     return Result(items); | ||||
|   } | ||||
| 
 | ||||
|   /// Queries information about a jid based on its node and capability hash. | ||||
|   Future<Result<DiscoError, DiscoInfo>> discoInfoCapHashQuery(String jid, String node, String ver) async { | ||||
|     return discoInfoQuery(jid, node: '$node#$ver'); | ||||
|   } | ||||
| 
 | ||||
|   Future<Result<DiscoError, List<DiscoInfo>>> performDiscoSweep() async { | ||||
|     final attrs = getAttributes(); | ||||
|     final serverJid = attrs.getConnectionSettings().jid.domain; | ||||
|     final infoResults = List<DiscoInfo>.empty(growable: true); | ||||
|     final result = await discoInfoQuery(serverJid); | ||||
|     if (result.isType<DiscoInfo>()) { | ||||
|       final info = result.get<DiscoInfo>(); | ||||
|       logger.finest('Discovered supported server features: ${info.features}'); | ||||
|       infoResults.add(info); | ||||
| 
 | ||||
|       attrs.sendEvent(ServerItemDiscoEvent(info)); | ||||
|       attrs.sendEvent(ServerDiscoDoneEvent()); | ||||
|     } else { | ||||
|       logger.warning('Failed to discover server features'); | ||||
|       return Result(UnknownDiscoError()); | ||||
|     } | ||||
| 
 | ||||
|     final response = await discoItemsQuery(serverJid); | ||||
|     if (response.isType<List<DiscoItem>>()) { | ||||
|       logger.finest('Discovered disco items form $serverJid'); | ||||
| 
 | ||||
|       // Query all items | ||||
|       final items = response.get<List<DiscoItem>>(); | ||||
|       for (final item in items) { | ||||
|         logger.finest('Querying info for ${item.jid}...'); | ||||
|         final itemInfoResult = await discoInfoQuery(item.jid); | ||||
|         if (itemInfoResult.isType<DiscoInfo>()) { | ||||
|           final itemInfo = itemInfoResult.get<DiscoInfo>(); | ||||
|           logger.finest('Received info for ${item.jid}'); | ||||
|           infoResults.add(itemInfo); | ||||
|           attrs.sendEvent(ServerItemDiscoEvent(itemInfo)); | ||||
|         } else { | ||||
|           logger.warning('Failed to discover info for ${item.jid}'); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       logger.warning('Failed to discover items of $serverJid'); | ||||
|     } | ||||
| 
 | ||||
|     return Result(infoResults); | ||||
|   } | ||||
| 
 | ||||
|   /// A wrapper function around discoInfoQuery: Returns true if the entity with JID | ||||
|   /// [entity] supports the disco feature [feature]. If not, returns false. | ||||
|   Future<bool> supportsFeature(JID entity, String feature) async { | ||||
|     final info = await discoInfoQuery(entity.toString()); | ||||
|     if (info.isType<DiscoError>()) return false; | ||||
| 
 | ||||
|     return info.get<DiscoInfo>().features.contains(feature); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										118
									
								
								moxxmpp/lib/src/xeps/xep_0054.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								moxxmpp/lib/src/xeps/xep_0054.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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'; | ||||
| 
 | ||||
| class VCardPhoto { | ||||
| 
 | ||||
|   const VCardPhoto({ this.binval }); | ||||
|   final String? binval; | ||||
| } | ||||
| 
 | ||||
| class VCard { | ||||
| 
 | ||||
|   const VCard({ this.nickname, this.url, this.photo }); | ||||
|   final String? nickname; | ||||
|   final String? url; | ||||
|   final VCardPhoto? photo; | ||||
| } | ||||
| 
 | ||||
| class VCardManager extends XmppManagerBase { | ||||
| 
 | ||||
|   VCardManager() : _lastHash = {}, super(); | ||||
|   final Map<String, String> _lastHash; | ||||
|    | ||||
|   @override | ||||
|   String getId() => vcardManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'vCardManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'presence', | ||||
|       tagName: 'x', | ||||
|       tagXmlns: vCardTempUpdate, | ||||
|       callback: _onPresence, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   /// In case we get the avatar hash some other way. | ||||
|   void setLastHash(String jid, String hash) { | ||||
|     _lastHash[jid] = hash; | ||||
|   } | ||||
|    | ||||
|   Future<StanzaHandlerData> _onPresence(Stanza presence, StanzaHandlerData state) async { | ||||
|     final x = presence.firstTag('x', xmlns: vCardTempUpdate)!; | ||||
|     final hash = x.firstTag('photo')!.innerText(); | ||||
| 
 | ||||
|     final from = JID.fromString(presence.from!).toBare().toString(); | ||||
|     final lastHash = _lastHash[from]; | ||||
|     if (lastHash != hash) { | ||||
|       _lastHash[from] = hash; | ||||
|       final vcard = await requestVCard(from); | ||||
| 
 | ||||
|       if (vcard != null) { | ||||
|         final binval = vcard.photo?.binval; | ||||
|         if (binval != null) { | ||||
|           getAttributes().sendEvent(AvatarUpdatedEvent(jid: from, base64: binval, hash: hash)); | ||||
|         } else { | ||||
|           logger.warning('No avatar data found'); | ||||
|         } | ||||
|       } else { | ||||
|         logger.warning('Failed to retrieve vCard for $from'); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
|    | ||||
|   VCardPhoto? _parseVCardPhoto(XMLNode? node) { | ||||
|     if (node == null) return null; | ||||
| 
 | ||||
|     return VCardPhoto( | ||||
|       binval: node.firstTag('BINVAL')?.innerText(), | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   VCard _parseVCard(XMLNode vcard) { | ||||
|     final nickname = vcard.firstTag('NICKNAME')?.innerText(); | ||||
|     final url = vcard.firstTag('URL')?.innerText(); | ||||
|      | ||||
|     return VCard( | ||||
|       url: url, | ||||
|       nickname: nickname, | ||||
|       photo: _parseVCardPhoto(vcard.firstTag('PHOTO')), | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   Future<VCard?> requestVCard(String jid) async { | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         to: jid, | ||||
|         type: 'get', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'vCard', | ||||
|             xmlns: vCardTempXmlns, | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (result.attributes['type'] != 'result') return null; | ||||
|     final vcard = result.firstTag('vCard', xmlns: vCardTempXmlns); | ||||
|     if (vcard == null) return null; | ||||
|      | ||||
|     return _parseVCard(vcard); | ||||
|   }  | ||||
| } | ||||
							
								
								
									
										15
									
								
								moxxmpp/lib/src/xeps/xep_0060/errors.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								moxxmpp/lib/src/xeps/xep_0060/errors.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| abstract class PubSubError {} | ||||
| 
 | ||||
| class UnknownPubSubError extends PubSubError {} | ||||
| 
 | ||||
| class PreconditionsNotMetError extends PubSubError {} | ||||
| 
 | ||||
| class MalformedResponseError extends PubSubError {} | ||||
| 
 | ||||
| class NoItemReturnedError extends PubSubError {} | ||||
| 
 | ||||
| /// Returned if we can guess that the server, by which I mean ejabberd, rejected | ||||
| /// the publish due to not liking that we set "max_items" to "max". | ||||
| /// NOTE: This workaround is required due to https://github.com/processone/ejabberd/issues/3044 | ||||
| // TODO(Unknown): Remove once ejabberd fixes it | ||||
| class EjabberdMaxItemsError extends PubSubError {} | ||||
							
								
								
									
										25
									
								
								moxxmpp/lib/src/xeps/xep_0060/helpers.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								moxxmpp/lib/src/xeps/xep_0060/helpers.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0060/errors.dart'; | ||||
| 
 | ||||
| PubSubError getPubSubError(XMLNode stanza) { | ||||
|   final error = stanza.firstTag('error'); | ||||
|   if (error != null) { | ||||
|     final conflict = error.firstTag('conflict'); | ||||
|     final preconditions = error.firstTag('precondition-not-met'); | ||||
|     if (conflict != null && preconditions != null) { | ||||
|       return PreconditionsNotMetError(); | ||||
|     } | ||||
| 
 | ||||
|     final badRequest = error.firstTag('bad-request', xmlns: fullStanzaXmlns); | ||||
|     final text = error.firstTag('text', xmlns: fullStanzaXmlns); | ||||
|     if (error.attributes['type'] == 'modify' && | ||||
|         badRequest != null && | ||||
|         text != null && | ||||
|         (text.text ?? '').contains('max_items')) { | ||||
|       return EjabberdMaxItemsError(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   return UnknownPubSubError(); | ||||
| } | ||||
							
								
								
									
										545
									
								
								moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										545
									
								
								moxxmpp/lib/src/xeps/xep_0060/xep_0060.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,545 @@ | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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/types/resultv2.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0004.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_0060/errors.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0060/helpers.dart'; | ||||
| 
 | ||||
| class PubSubPublishOptions { | ||||
| 
 | ||||
|   const PubSubPublishOptions({ | ||||
|     this.accessModel, | ||||
|     this.maxItems, | ||||
|   }); | ||||
|   final String? accessModel; | ||||
|   final String? maxItems; | ||||
|    | ||||
|   XMLNode toXml() { | ||||
|     return DataForm( | ||||
|       type: 'submit', | ||||
|       instructions: [], | ||||
|       reported: [], | ||||
|       items: [], | ||||
|       fields: [ | ||||
|         const DataFormField( | ||||
|           options: [], | ||||
|           isRequired: false, | ||||
|           values: [ pubsubPublishOptionsXmlns ], | ||||
|           varAttr: 'FORM_TYPE', | ||||
|           type: 'hidden', | ||||
|         ), | ||||
|         ...accessModel != null ? [ | ||||
|             DataFormField( | ||||
|               options: [], | ||||
|               isRequired: false, | ||||
|               values: [ accessModel! ], | ||||
|               varAttr: 'pubsub#access_model', | ||||
|             ) | ||||
|           ] : [], | ||||
|         ...maxItems != null ? [ | ||||
|           DataFormField( | ||||
|             options: [], | ||||
|             isRequired: false, | ||||
|             values: [maxItems! ], | ||||
|             varAttr: 'pubsub#max_items', | ||||
|           ), | ||||
|         ] : [], | ||||
|       ], | ||||
|     ).toXml(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class PubSubItem { | ||||
| 
 | ||||
|   const PubSubItem({ required this.id, required this.node, required this.payload }); | ||||
|   final String id; | ||||
|   final String node; | ||||
|   final XMLNode payload; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => '$id: ${payload.toXml()}'; | ||||
| } | ||||
| 
 | ||||
| class PubSubManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getId() => pubsubManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'PubsubManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'event', | ||||
|       tagXmlns: pubsubEventXmlns, | ||||
|       callback: _onPubsubMessage, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
| 
 | ||||
|   Future<StanzaHandlerData> _onPubsubMessage(Stanza message, StanzaHandlerData state) async { | ||||
|     logger.finest('Received PubSub event'); | ||||
|     final event = message.firstTag('event', xmlns: pubsubEventXmlns)!; | ||||
|     final items = event.firstTag('items')!; | ||||
|     final item = items.firstTag('item')!; | ||||
| 
 | ||||
|     getAttributes().sendEvent(PubSubNotificationEvent( | ||||
|       item: PubSubItem( | ||||
|         id: item.attributes['id']! as String, | ||||
|         node: items.attributes['node']! as String, | ||||
|         payload: item.children[0], | ||||
|       ), | ||||
|       from: message.attributes['from']! as String, | ||||
|     ),); | ||||
|      | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
| 
 | ||||
|   Future<int> _getNodeItemCount(String jid, String node) async { | ||||
|     final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!; | ||||
|     final response = await dm.discoItemsQuery(jid, node: node); | ||||
|     var count = 0; | ||||
|     if (response.isType<DiscoError>()) { | ||||
|       logger.warning('_getNodeItemCount: disco#items query failed. Assuming no items.'); | ||||
|     } else { | ||||
|       count = response.get<List<DiscoItem>>().length; | ||||
|     } | ||||
| 
 | ||||
|     return count; | ||||
|   } | ||||
|    | ||||
|   Future<PubSubPublishOptions> _preprocessPublishOptions(String jid, String node, PubSubPublishOptions options) async { | ||||
|     if (options.maxItems != null) { | ||||
|       final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!; | ||||
|       final result = await dm.discoInfoQuery(jid); | ||||
|       if (result.isType<DiscoError>()) { | ||||
|         if (options.maxItems == 'max') { | ||||
|           logger.severe('disco#info query failed and options.maxItems is set to "max".'); | ||||
|           return options; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       final nodeMultiItemsSupported = result.isType<DiscoInfo>() && result.get<DiscoInfo>().features.contains(pubsubNodeConfigMultiItems); | ||||
|       final nodeMaxSupported = result.isType<DiscoInfo>() && result.get<DiscoInfo>().features.contains(pubsubNodeConfigMax); | ||||
|       if (options.maxItems != null && !nodeMultiItemsSupported) { | ||||
|         // TODO(PapaTutuWawa): Here, we need to admit defeat | ||||
|         logger.finest('PubSub host does not support multi-items!'); | ||||
| 
 | ||||
|         return PubSubPublishOptions( | ||||
|           accessModel: options.accessModel, | ||||
|         ); | ||||
|       } else if (options.maxItems == 'max' && !nodeMaxSupported) { | ||||
|         logger.finest('PubSub host does not support node-config-max. Working around it'); | ||||
|         final count = await _getNodeItemCount(jid, node) + 1; | ||||
| 
 | ||||
|         return PubSubPublishOptions( | ||||
|           accessModel: options.accessModel, | ||||
|           maxItems: '$count', | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return options; | ||||
|   } | ||||
|    | ||||
|   Future<Result<PubSubError, bool>> subscribe(String jid, String node) async { | ||||
|     final attrs = getAttributes(); | ||||
|     final result = await attrs.sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         to: jid, | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'pubsub', | ||||
|             xmlns: pubsubXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'subscribe', | ||||
|                 attributes: <String, String>{ | ||||
|                   'node': node, | ||||
|                   'jid': attrs.getFullJID().toBare().toString(), | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (result.attributes['type'] != 'result') return Result(UnknownPubSubError()); | ||||
| 
 | ||||
|     final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); | ||||
|     if (pubsub == null) return Result(UnknownPubSubError()); | ||||
| 
 | ||||
|     final subscription = pubsub.firstTag('subscription'); | ||||
|     if (subscription == null) return Result(UnknownPubSubError()); | ||||
| 
 | ||||
|     return Result(subscription.attributes['subscription'] == 'subscribed'); | ||||
|   } | ||||
| 
 | ||||
|   Future<Result<PubSubError, bool>> unsubscribe(String jid, String node) async { | ||||
|     final attrs = getAttributes(); | ||||
|     final result = await attrs.sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         to: jid, | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'pubsub', | ||||
|             xmlns: pubsubXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'unsubscribe', | ||||
|                 attributes: <String, String>{ | ||||
|                   'node': node, | ||||
|                   'jid': attrs.getFullJID().toBare().toString(), | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (result.attributes['type'] != 'result') return Result(UnknownPubSubError()); | ||||
| 
 | ||||
|     final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); | ||||
|     if (pubsub == null) return Result(UnknownPubSubError()); | ||||
| 
 | ||||
|     final subscription = pubsub.firstTag('subscription'); | ||||
|     if (subscription == null) return Result(UnknownPubSubError()); | ||||
| 
 | ||||
|     return Result(subscription.attributes['subscription'] == 'none'); | ||||
|   } | ||||
|    | ||||
|   /// Publish [payload] to the PubSub node [node] on JID [jid]. Returns true if it | ||||
|   /// was successful. False otherwise. | ||||
|   Future<Result<PubSubError, bool>> publish( | ||||
|     String jid, | ||||
|     String node, | ||||
|     XMLNode payload, { | ||||
|       String? id, | ||||
|       PubSubPublishOptions? options, | ||||
|     } | ||||
|   ) async { | ||||
|     return _publish( | ||||
|       jid, | ||||
|       node, | ||||
|       payload, | ||||
|       id: id, | ||||
|       options: options, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Future<Result<PubSubError, bool>> _publish( | ||||
|     String jid, | ||||
|     String node, | ||||
|     XMLNode payload, { | ||||
|       String? id, | ||||
|       PubSubPublishOptions? options, | ||||
|       // Should, if publishing fails, try to reconfigure and publish again? | ||||
|       bool tryConfigureAndPublish = true, | ||||
|     } | ||||
|   ) async { | ||||
|     PubSubPublishOptions? pubOptions; | ||||
|     if (options != null) { | ||||
|       pubOptions = await _preprocessPublishOptions(jid, node, options); | ||||
|     } | ||||
| 
 | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         to: jid, | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'pubsub', | ||||
|             xmlns: pubsubXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'publish', | ||||
|                 attributes: <String, String>{ 'node': node }, | ||||
|                 children: [ | ||||
|                   XMLNode( | ||||
|                     tag: 'item', | ||||
|                     attributes: id != null ? <String, String>{ 'id': id } : <String, String>{}, | ||||
|                     children: [ payload ], | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|               ...options != null ? [ | ||||
|                 XMLNode( | ||||
|                   tag: 'publish-options', | ||||
|                   children: [options.toXml()], | ||||
|                 ),  | ||||
|               ] : [], | ||||
|             ], | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     if (result.attributes['type'] != 'result') { | ||||
|       final error = getPubSubError(result); | ||||
| 
 | ||||
|       // If preconditions are not met, configure the node | ||||
|       if (error is PreconditionsNotMetError && tryConfigureAndPublish) { | ||||
|         final configureResult = await configure(jid, node, pubOptions!); | ||||
|         if (configureResult.isType<PubSubError>()) { | ||||
|           return Result(configureResult.get<PubSubError>()); | ||||
|         } | ||||
| 
 | ||||
|         final publishResult = await _publish( | ||||
|           jid, | ||||
|           node, | ||||
|           payload, | ||||
|           id: id, | ||||
|           options: options, | ||||
|           tryConfigureAndPublish: false, | ||||
|         ); | ||||
|         if (publishResult.isType<PubSubError>()) return publishResult; | ||||
|       } else if (error is EjabberdMaxItemsError && tryConfigureAndPublish && options != null) { | ||||
|         // TODO(Unknown): Remove once ejabberd fixes the bug. See errors.dart for more info. | ||||
|         logger.warning('Publish failed due to the server rejecting the usage of "max" for "max_items" in publish options. Configuring...'); | ||||
|         final count = await _getNodeItemCount(jid, node) + 1; | ||||
|         return publish( | ||||
|           jid, | ||||
|           node, | ||||
|           payload, | ||||
|           id: id, | ||||
|           options: PubSubPublishOptions( | ||||
|             accessModel: options.accessModel, | ||||
|             maxItems: '$count', | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         return Result(error); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     final pubsubElement = result.firstTag('pubsub', xmlns: pubsubXmlns); | ||||
|     if (pubsubElement == null) return Result(MalformedResponseError()); | ||||
| 
 | ||||
|     final publishElement = pubsubElement.firstTag('publish'); | ||||
|     if (publishElement == null) return Result(MalformedResponseError()); | ||||
| 
 | ||||
|     final item = publishElement.firstTag('item'); | ||||
|     if (item == null) return Result(MalformedResponseError()); | ||||
| 
 | ||||
|     if (id != null) return Result(item.attributes['id'] == id); | ||||
| 
 | ||||
|     return const Result(true); | ||||
|   } | ||||
|    | ||||
|   Future<Result<PubSubError, List<PubSubItem>>> getItems(String jid, String node) async { | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'get', | ||||
|         to: jid, | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'pubsub', | ||||
|             xmlns: pubsubXmlns, | ||||
|             children: [ | ||||
|               XMLNode(tag: 'items', attributes: <String, String>{ 'node': node }), | ||||
|             ], | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (result.attributes['type'] != 'result') return Result(getPubSubError(result)); | ||||
| 
 | ||||
|     final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); | ||||
|     if (pubsub == null) return Result(getPubSubError(result)); | ||||
| 
 | ||||
|     final items = pubsub | ||||
|       .firstTag('items')! | ||||
|       .children.map((item) { | ||||
|         return PubSubItem( | ||||
|           id: item.attributes['id']! as String, | ||||
|           payload: item.children[0], | ||||
|           node: node, | ||||
|         ); | ||||
|       }) | ||||
|       .toList(); | ||||
| 
 | ||||
|     return Result(items); | ||||
|   } | ||||
| 
 | ||||
|   Future<Result<PubSubError, PubSubItem>> getItem(String jid, String node, String id) async { | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'get', | ||||
|         to: jid, | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'pubsub', | ||||
|             xmlns: pubsubXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'items', | ||||
|                 attributes: <String, String>{ 'node': node }, | ||||
|                 children: [ | ||||
|                   XMLNode( | ||||
|                     tag: 'item', | ||||
|                     attributes: <String, String>{ 'id': id }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (result.attributes['type'] != 'result') return Result(getPubSubError(result)); | ||||
| 
 | ||||
|     final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); | ||||
|     if (pubsub == null) return Result(getPubSubError(result)); | ||||
| 
 | ||||
|     final itemElement = pubsub.firstTag('items')?.firstTag('item'); | ||||
|     if (itemElement == null) return Result(NoItemReturnedError()); | ||||
| 
 | ||||
|     final item = PubSubItem( | ||||
|       id: itemElement.attributes['id']! as String, | ||||
|       payload: itemElement.children[0], | ||||
|       node: node, | ||||
|     ); | ||||
| 
 | ||||
|     return Result(item); | ||||
|   } | ||||
| 
 | ||||
|   Future<Result<PubSubError, bool>> configure(String jid, String node, PubSubPublishOptions options) async { | ||||
|     final attrs = getAttributes(); | ||||
| 
 | ||||
|     // Request the form | ||||
|     final form = await attrs.sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'get', | ||||
|         to: jid, | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'pubsub', | ||||
|             xmlns: pubsubOwnerXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'configure', | ||||
|                 attributes: <String, String>{ | ||||
|                   'node': node, | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     if (form.attributes['type'] != 'result') return Result(getPubSubError(form)); | ||||
| 
 | ||||
|     final submit = await attrs.sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         to: jid, | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'pubsub', | ||||
|             xmlns: pubsubOwnerXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'configure', | ||||
|                 attributes: <String, String>{ | ||||
|                   'node': node, | ||||
|                 }, | ||||
|                 children: [ | ||||
|                   options.toXml(), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     if (submit.attributes['type'] != 'result') return Result(getPubSubError(form)); | ||||
| 
 | ||||
|     return const Result(true); | ||||
|   } | ||||
| 
 | ||||
|   Future<Result<PubSubError, bool>> delete(JID host, String node) async { | ||||
|     final request = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         to: host.toString(), | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'pubsub', | ||||
|             xmlns: pubsubOwnerXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'delete', | ||||
|                 attributes: <String, String>{ | ||||
|                   'node': node, | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ) as Stanza; | ||||
| 
 | ||||
|     if (request.type != 'result') { | ||||
|       // TODO(Unknown): Be more specific | ||||
|       return Result(UnknownPubSubError()); | ||||
|     } | ||||
| 
 | ||||
|     return const Result(true); | ||||
|   } | ||||
| 
 | ||||
|   Future<Result<PubSubError, bool>> retract(JID host, String node, String itemId) async { | ||||
|     final request = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         to: host.toString(), | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'pubsub', | ||||
|             xmlns: pubsubXmlns, | ||||
|             children: [ | ||||
|               XMLNode( | ||||
|                 tag: 'retract', | ||||
|                 attributes: <String, String>{ | ||||
|                   'node': node, | ||||
|                 }, | ||||
|                 children: [ | ||||
|                   XMLNode( | ||||
|                     tag: 'item', | ||||
|                     attributes: <String, String>{ | ||||
|                       'id': itemId, | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ) as Stanza; | ||||
| 
 | ||||
|     if (request.type != 'result') { | ||||
|       // TODO(Unknown): Be more specific | ||||
|       return Result(UnknownPubSubError()); | ||||
|     } | ||||
| 
 | ||||
|     return const Result(true); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										71
									
								
								moxxmpp/lib/src/xeps/xep_0066.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								moxxmpp/lib/src/xeps/xep_0066.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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'; | ||||
| 
 | ||||
| /// A data class representing the jabber:x:oob tag. | ||||
| class OOBData { | ||||
| 
 | ||||
|   const OOBData({ this.url, this.desc }); | ||||
|   final String? url; | ||||
|   final String? desc; | ||||
| } | ||||
| 
 | ||||
| XMLNode constructOOBNode(OOBData data) { | ||||
|   final children = List<XMLNode>.empty(growable: true); | ||||
| 
 | ||||
|   if (data.url != null) { | ||||
|     children.add(XMLNode(tag: 'url', text: data.url)); | ||||
|   } | ||||
|   if (data.desc != null) { | ||||
|     children.add(XMLNode(tag: 'desc', text: data.desc)); | ||||
|   } | ||||
|    | ||||
|   return XMLNode.xmlns( | ||||
|     tag: 'x', | ||||
|     xmlns: oobDataXmlns, | ||||
|     children: children, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class OOBManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getName() => 'OOBName'; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => oobManager; | ||||
| 
 | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [ oobDataXmlns ]; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'x', | ||||
|       tagXmlns: oobDataXmlns, | ||||
|       callback: _onMessage, | ||||
|       // Before the message manager | ||||
|       priority: -99, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onMessage(Stanza message, StanzaHandlerData state) async { | ||||
|     final x = message.firstTag('x', xmlns: oobDataXmlns)!; | ||||
|     final url = x.firstTag('url'); | ||||
|     final desc = x.firstTag('desc'); | ||||
| 
 | ||||
|     return state.copyWith( | ||||
|       oob: OOBData( | ||||
|         url: url?.innerText(), | ||||
|         desc: desc?.innerText(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										173
									
								
								moxxmpp/lib/src/xeps/xep_0084.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								moxxmpp/lib/src/xeps/xep_0084.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,173 @@ | ||||
| import 'package:moxxmpp/src/events.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/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_0060/errors.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; | ||||
| 
 | ||||
| class UserAvatar { | ||||
| 
 | ||||
|   const UserAvatar({ required this.base64, required this.hash }); | ||||
|   final String base64; | ||||
|   final String hash; | ||||
| } | ||||
| 
 | ||||
| class UserAvatarMetadata { | ||||
| 
 | ||||
|   const UserAvatarMetadata( | ||||
|     this.id, | ||||
|     this.length, | ||||
|     this.width, | ||||
|     this.height, | ||||
|     this.mime, | ||||
|   ); | ||||
|   /// The amount of bytes in the file | ||||
|   final int length; | ||||
|   /// The identifier of the avatar | ||||
|   final String id; | ||||
|   /// Image proportions | ||||
|   final int width; | ||||
|   final int height; | ||||
|   /// The MIME type of the avatar | ||||
|   final String mime; | ||||
| } | ||||
| 
 | ||||
| /// NOTE: This class requires a PubSubManager | ||||
| class UserAvatarManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getId() => userAvatarManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'UserAvatarManager'; | ||||
| 
 | ||||
|   PubSubManager _getPubSubManager() => getAttributes().getManagerById(pubsubManager)! as PubSubManager; | ||||
|    | ||||
|   @override | ||||
|   Future<void> onXmppEvent(XmppEvent event) async { | ||||
|     if (event is PubSubNotificationEvent) { | ||||
|       getAttributes().sendEvent( | ||||
|         AvatarUpdatedEvent( | ||||
|           jid: event.from, | ||||
|           base64: event.item.payload.innerText(), | ||||
|           hash: event.item.id, | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // TODO(PapaTutuWawa): Check for PEP support | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   /// Requests the avatar from [jid]. Returns the avatar data if the request was | ||||
|   /// successful. Null otherwise | ||||
|   // TODO(Unknown): Migrate to Resultsv2 | ||||
|   Future<UserAvatar?> getUserAvatar(String jid) async { | ||||
|     final pubsub = _getPubSubManager(); | ||||
|     final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns); | ||||
|     if (resultsRaw.isType<PubSubError>()) return null; | ||||
| 
 | ||||
|     final results = resultsRaw.get<List<PubSubItem>>(); | ||||
|     if (results.isEmpty) return null; | ||||
| 
 | ||||
|     final item = results[0]; | ||||
|     return UserAvatar( | ||||
|       base64: item.payload.innerText(), | ||||
|       hash: item.id, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Publish the avatar data, [base64], on the pubsub node using [hash] as | ||||
|   /// the item id. [hash] must be the SHA-1 hash of the image data, while | ||||
|   /// [base64] must be the base64-encoded version of the image data. | ||||
|   // TODO(Unknown): Migrate to Resultsv2 | ||||
|   Future<bool> publishUserAvatar(String base64, String hash, bool public) async { | ||||
|     final pubsub = _getPubSubManager(); | ||||
|     final result = await pubsub.publish( | ||||
|       getAttributes().getFullJID().toBare().toString(), | ||||
|       userAvatarDataXmlns, | ||||
|       XMLNode.xmlns( | ||||
|         tag: 'data', | ||||
|         xmlns: userAvatarDataXmlns, | ||||
|         text: base64, | ||||
|       ), | ||||
|       id: hash, | ||||
|       options: PubSubPublishOptions( | ||||
|         accessModel: public ? 'open' : 'roster', | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     return !result.isType<PubSubError>(); | ||||
|   } | ||||
| 
 | ||||
|   /// Publish avatar metadata [metadata] to the User Avatar's metadata node. If [public] | ||||
|   /// is true, then the node will be set to an 'open' access model. If [public] is false, | ||||
|   /// then the node will be set to an 'roster' access model. | ||||
|   // TODO(Unknown): Migrate to Resultsv2 | ||||
|   Future<bool> publishUserAvatarMetadata(UserAvatarMetadata metadata, bool public) async { | ||||
|     final pubsub = _getPubSubManager(); | ||||
|     final result = await pubsub.publish( | ||||
|       getAttributes().getFullJID().toBare().toString(), | ||||
|       userAvatarMetadataXmlns, | ||||
|       XMLNode.xmlns( | ||||
|         tag: 'metadata', | ||||
|         xmlns: userAvatarMetadataXmlns, | ||||
|         children: [ | ||||
|           XMLNode( | ||||
|             tag: 'info', | ||||
|             attributes: <String, String>{ | ||||
|               'bytes': metadata.length.toString(), | ||||
|               'height': metadata.height.toString(), | ||||
|               'width': metadata.width.toString(), | ||||
|               'type': metadata.mime, | ||||
|               'id': metadata.id, | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       id: metadata.id, | ||||
|       options: PubSubPublishOptions( | ||||
|         accessModel: public ? 'open' : 'roster', | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     return result.isType<PubSubError>(); | ||||
|   } | ||||
|    | ||||
|   /// Subscribe the data and metadata node of [jid]. | ||||
|   // TODO(Unknown): Migrate to Resultsv2 | ||||
|   Future<bool> subscribe(String jid) async { | ||||
|     await _getPubSubManager().subscribe(jid, userAvatarDataXmlns); | ||||
|     await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); | ||||
| 
 | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   /// Unsubscribe the data and metadata node of [jid]. | ||||
|   // TODO(Unknown): Migrate to Resultsv2 | ||||
|   Future<bool> unsubscribe(String jid) async { | ||||
|     await _getPubSubManager().unsubscribe(jid, userAvatarDataXmlns); | ||||
|     await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); | ||||
| 
 | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns the PubSub Id of an avatar after doing a disco#items query. | ||||
|   /// Note that this assumes that there is only one (1) item published on | ||||
|   /// the node. | ||||
|   // TODO(Unknown): Migrate to Resultsv2 | ||||
|   Future<String?> getAvatarId(String jid) async { | ||||
|     final disco = getAttributes().getManagerById(discoManager)! as DiscoManager; | ||||
|     final response = await disco.discoItemsQuery(jid, node: userAvatarDataXmlns); | ||||
|     if (response.isType<DiscoError>()) return null; | ||||
| 
 | ||||
|     final items = response.get<List<DiscoItem>>(); | ||||
|     if (items.isEmpty) return null; | ||||
| 
 | ||||
|     return items.first.name; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										111
									
								
								moxxmpp/lib/src/xeps/xep_0085.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								moxxmpp/lib/src/xeps/xep_0085.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,111 @@ | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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'; | ||||
| 
 | ||||
| enum ChatState { | ||||
|   active, | ||||
|   composing, | ||||
|   paused, | ||||
|   inactive, | ||||
|   gone | ||||
| } | ||||
| 
 | ||||
| ChatState chatStateFromString(String raw) { | ||||
|   switch(raw) { | ||||
|     case 'active': { | ||||
|       return ChatState.active; | ||||
|     } | ||||
|     case 'composing': { | ||||
|       return ChatState.composing; | ||||
|     }  | ||||
|     case 'paused': { | ||||
|       return ChatState.paused; | ||||
|     } | ||||
|     case 'inactive': { | ||||
|       return ChatState.inactive; | ||||
|     } | ||||
|     case 'gone': { | ||||
|       return ChatState.gone; | ||||
|     } | ||||
|     default: { | ||||
|       return ChatState.gone; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| String chatStateToString(ChatState state) => state.toString().split('.').last; | ||||
| 
 | ||||
| class ChatStateManager extends XmppManagerBase { | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [ chatStateXmlns ]; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'ChatStateManager'; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => chatStateManager; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagXmlns: chatStateXmlns, | ||||
|       callback: _onChatStateReceived, | ||||
|       // Before the message handler | ||||
|       priority: -99, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onChatStateReceived(Stanza message, StanzaHandlerData state) async { | ||||
|     final element = state.stanza.firstTagByXmlns(chatStateXmlns)!; | ||||
|     ChatState? chatState; | ||||
| 
 | ||||
|     switch (element.tag) { | ||||
|       case 'active': { | ||||
|         chatState = ChatState.active; | ||||
|       } | ||||
|       break; | ||||
|       case 'composing': { | ||||
|         chatState = ChatState.composing; | ||||
|       } | ||||
|       break; | ||||
|       case 'paused': { | ||||
|         chatState = ChatState.paused; | ||||
|       } | ||||
|       break; | ||||
|       case 'inactive': { | ||||
|         chatState = ChatState.inactive; | ||||
|       } | ||||
|       break; | ||||
|       case 'gone': { | ||||
|         chatState = ChatState.gone; | ||||
|       } | ||||
|       break; | ||||
|       default: { | ||||
|         logger.warning("Received invalid chat state '${element.tag}'"); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return state.copyWith(chatState: chatState); | ||||
|   } | ||||
| 
 | ||||
|   /// Send a chat state notification to [to]. You can specify the type attribute | ||||
|   /// of the message with [messageType]. | ||||
|   void sendChatState(ChatState state, String to, { String messageType = 'chat' }) { | ||||
|     final tagName = state.toString().split('.').last; | ||||
| 
 | ||||
|     getAttributes().sendStanza( | ||||
|       Stanza.message( | ||||
|         to: to, | ||||
|         type: messageType, | ||||
|         children: [ XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns) ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										59
									
								
								moxxmpp/lib/src/xeps/xep_0115.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								moxxmpp/lib/src/xeps/xep_0115.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| import 'dart:convert'; | ||||
| import 'package:cryptography/cryptography.dart'; | ||||
| import 'package:moxxmpp/src/rfcs/rfc_4790.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; | ||||
| 
 | ||||
| class CapabilityHashInfo { | ||||
| 
 | ||||
|   const CapabilityHashInfo(this.ver, this.node, this.hash); | ||||
|   final String ver; | ||||
|   final String node; | ||||
|   final String hash; | ||||
| } | ||||
| 
 | ||||
| /// Calculates the Entitiy Capability hash according to XEP-0115 based on the | ||||
| /// disco information. | ||||
| Future<String> calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm) async { | ||||
|   final buffer = StringBuffer(); | ||||
|   final identitiesSorted = info.identities | ||||
|     .map((Identity i) => '${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}') | ||||
|     .toList(); | ||||
|   // ignore: cascade_invocations | ||||
|   identitiesSorted.sort(ioctetSortComparator); | ||||
|   buffer.write('${identitiesSorted.join("<")}<'); | ||||
| 
 | ||||
|   final featuresSorted = List<String>.from(info.features) | ||||
|     ..sort(ioctetSortComparator); | ||||
|   buffer.write('${featuresSorted.join("<")}<'); | ||||
| 
 | ||||
|   if (info.extendedInfo.isNotEmpty) { | ||||
|     final sortedExt = info.extendedInfo | ||||
|       ..sort((a, b) => ioctetSortComparator( | ||||
|         a.getFieldByVar('FORM_TYPE')!.values.first, | ||||
|         b.getFieldByVar('FORM_TYPE')!.values.first, | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     for (final ext in sortedExt) { | ||||
|       buffer.write('${ext.getFieldByVar("FORM_TYPE")!.values.first}<'); | ||||
| 
 | ||||
|       final sortedFields = ext.fields..sort((a, b) => ioctetSortComparator( | ||||
|           a.varAttr!, | ||||
|           b.varAttr!, | ||||
|         ), | ||||
|       ); | ||||
| 
 | ||||
|       for (final field in sortedFields) { | ||||
|         if (field.varAttr == 'FORM_TYPE') continue; | ||||
| 
 | ||||
|         buffer.write('${field.varAttr!}<'); | ||||
|         final sortedValues = field.values..sort(ioctetSortComparator); | ||||
|         for (final value in sortedValues) { | ||||
|           buffer.write('$value<'); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   return base64.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes); | ||||
| } | ||||
							
								
								
									
										81
									
								
								moxxmpp/lib/src/xeps/xep_0184.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								moxxmpp/lib/src/xeps/xep_0184.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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'; | ||||
| 
 | ||||
| XMLNode makeMessageDeliveryRequest() { | ||||
|   return XMLNode.xmlns( | ||||
|     tag: 'request', | ||||
|     xmlns: deliveryXmlns, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| XMLNode makeMessageDeliveryResponse(String id) { | ||||
|   return XMLNode.xmlns( | ||||
|     tag: 'received', | ||||
|     xmlns: deliveryXmlns, | ||||
|     attributes: { 'id': id }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class MessageDeliveryReceiptManager extends XmppManagerBase { | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [ deliveryXmlns ]; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'MessageDeliveryReceiptManager'; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => messageDeliveryReceiptManager; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'received', | ||||
|       tagXmlns: deliveryXmlns, | ||||
|       callback: _onDeliveryReceiptReceived, | ||||
|       // Before the message handler | ||||
|       priority: -99, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'request', | ||||
|       tagXmlns: deliveryXmlns, | ||||
|       callback: _onDeliveryRequestReceived, | ||||
|       // Before the message handler | ||||
|       priority: -99, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onDeliveryRequestReceived(Stanza message, StanzaHandlerData state) async { | ||||
|     return state.copyWith(deliveryReceiptRequested: true); | ||||
|   } | ||||
|    | ||||
|   Future<StanzaHandlerData> _onDeliveryReceiptReceived(Stanza message, StanzaHandlerData state) async { | ||||
|     final received = message.firstTag('received', xmlns: deliveryXmlns)!; | ||||
|     for (final item in message.children) { | ||||
|       if (!['origin-id', 'stanza-id', 'delay', 'store', 'received'].contains(item.tag)) { | ||||
|         logger.info("Won't handle stanza as delivery receipt because we found an '${item.tag}' element"); | ||||
| 
 | ||||
|         return state.copyWith(done: true); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     getAttributes().sendEvent( | ||||
|       DeliveryReceiptReceivedEvent( | ||||
|         from: JID.fromString(message.attributes['from']! as String), | ||||
|         id: received.attributes['id']! as String, | ||||
|       ), | ||||
|     ); | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										170
									
								
								moxxmpp/lib/src/xeps/xep_0191.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								moxxmpp/lib/src/xeps/xep_0191.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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/xep_0030.dart'; | ||||
| 
 | ||||
| class BlockingManager extends XmppManagerBase { | ||||
|   BlockingManager() : _supported = false, _gotSupported = false, super(); | ||||
| 
 | ||||
|   bool _supported; | ||||
|   bool _gotSupported; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => blockingManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'BlockingManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'iq', | ||||
|       tagName: 'unblock', | ||||
|       tagXmlns: blockingXmlns, | ||||
|       callback: _unblockPush, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'iq', | ||||
|       tagName: 'block', | ||||
|       tagXmlns: blockingXmlns, | ||||
|       callback: _blockPush, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async { | ||||
|     if (_gotSupported) return _supported; | ||||
| 
 | ||||
|     // Query the server | ||||
|     final disco = getAttributes().getManagerById<DiscoManager>(discoManager)!; | ||||
|     _supported = await disco.supportsFeature( | ||||
|       getAttributes().getConnectionSettings().jid.toBare(), | ||||
|       blockingXmlns, | ||||
|     ); | ||||
|     _gotSupported = true; | ||||
|     return _supported; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> onXmppEvent(XmppEvent event) async { | ||||
|     if (event is StreamResumeFailedEvent) { | ||||
|       _gotSupported = false; | ||||
|       _supported = false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   Future<StanzaHandlerData> _blockPush(Stanza iq, StanzaHandlerData state) async { | ||||
|     final block = iq.firstTag('block', xmlns: blockingXmlns)!; | ||||
| 
 | ||||
|     getAttributes().sendEvent( | ||||
|       BlocklistBlockPushEvent( | ||||
|         items: block.findTags('item').map((i) => i.attributes['jid']! as String).toList(), | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
| 
 | ||||
|   Future<StanzaHandlerData> _unblockPush(Stanza iq, StanzaHandlerData state) async { | ||||
|     final unblock = iq.firstTag('unblock', xmlns: blockingXmlns)!; | ||||
|     final items = unblock.findTags('item'); | ||||
| 
 | ||||
|     if (items.isNotEmpty) { | ||||
|       getAttributes().sendEvent( | ||||
|         BlocklistUnblockPushEvent( | ||||
|           items: items.map((i) => i.attributes['jid']! as String).toList(), | ||||
|         ), | ||||
|       ); | ||||
|     } else { | ||||
|       getAttributes().sendEvent( | ||||
|         BlocklistUnblockAllPushEvent(), | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
|    | ||||
|   Future<bool> block(List<String> items) async { | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'block', | ||||
|             xmlns: blockingXmlns, | ||||
|             children: items | ||||
|               .map((item) { | ||||
|                 return XMLNode( | ||||
|                   tag: 'item', | ||||
|                   attributes: <String, String>{ 'jid': item }, | ||||
|                 ); | ||||
|               }) | ||||
|               .toList(), | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     return result.attributes['type'] == 'result'; | ||||
|   } | ||||
| 
 | ||||
|   Future<bool> unblockAll() async { | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'unblock', | ||||
|             xmlns: blockingXmlns, | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     return result.attributes['type'] == 'result'; | ||||
|   } | ||||
|    | ||||
|   Future<bool> unblock(List<String> items) async { | ||||
|     assert(items.isNotEmpty, 'The list of items to unblock must be non-empty'); | ||||
| 
 | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'unblock', | ||||
|             xmlns: blockingXmlns, | ||||
|             children: items.map((item) => XMLNode( | ||||
|                 tag: 'item', | ||||
|                 attributes: <String, String>{ 'jid': item }, | ||||
|             ),).toList(), | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     return result.attributes['type'] == 'result'; | ||||
|   } | ||||
| 
 | ||||
|   Future<List<String>> getBlocklist() async { | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'get', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'blocklist', | ||||
|             xmlns: blockingXmlns, | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     final blocklist = result.firstTag('blocklist', xmlns: blockingXmlns)!; | ||||
|     return blocklist.findTags('item').map((item) => item.attributes['jid']! as String).toList(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										156
									
								
								moxxmpp/lib/src/xeps/xep_0198/negotiator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								moxxmpp/lib/src/xeps/xep_0198/negotiator.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,156 @@ | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0198/state.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0352.dart'; | ||||
| 
 | ||||
| enum _StreamManagementNegotiatorState { | ||||
|   // We have not done anything yet | ||||
|   ready, | ||||
|   // The SM resume has been requested | ||||
|   resumeRequested, | ||||
|   // The SM enablement has been requested | ||||
|   enableRequested, | ||||
| } | ||||
| 
 | ||||
| /// NOTE: The stream management negotiator requires that loadState has been called on the | ||||
| ///       StreamManagementManager at least once before connecting, if stream resumption | ||||
| ///       is wanted. | ||||
| class StreamManagementNegotiator extends XmppFeatureNegotiatorBase { | ||||
|    | ||||
|   StreamManagementNegotiator() | ||||
|     : _state = _StreamManagementNegotiatorState.ready, | ||||
|       _supported = false, | ||||
|       _resumeFailed = false, | ||||
|       _isResumed = false, | ||||
|       _log = Logger('StreamManagementNegotiator'), | ||||
|       super(10, false, smXmlns, streamManagementNegotiator); | ||||
|   _StreamManagementNegotiatorState _state; | ||||
|   bool _resumeFailed; | ||||
|   bool _isResumed; | ||||
|   | ||||
|   final Logger _log; | ||||
| 
 | ||||
|   /// True if Stream Management is supported on this stream. | ||||
|   bool _supported; | ||||
|   bool get isSupported => _supported; | ||||
| 
 | ||||
|   /// True if the current stream is resumed. False if not. | ||||
|   bool get isResumed => _isResumed; | ||||
|    | ||||
|   @override | ||||
|   bool matchesFeature(List<XMLNode> features) { | ||||
|     final sm = attributes.getManagerById<StreamManagementManager>(smManager)!; | ||||
| 
 | ||||
|     if (sm.state.streamResumptionId != null && !_resumeFailed) { | ||||
|       // We could do Stream resumption | ||||
|       return super.matchesFeature(features) && attributes.isAuthenticated(); | ||||
|     } else { | ||||
|       // We cannot do a stream resumption | ||||
|       final br = attributes.getNegotiatorById(resourceBindingNegotiator); | ||||
|       return super.matchesFeature(features) && br?.state == NegotiatorState.done && attributes.isAuthenticated(); | ||||
|     } | ||||
|   } | ||||
|        | ||||
|   @override | ||||
|   Future<void> negotiate(XMLNode nonza) async { | ||||
|     // negotiate is only called when we matched the stream feature, so we know | ||||
|     // that the server advertises it. | ||||
|     _supported = true; | ||||
| 
 | ||||
|     switch (_state) { | ||||
|       case _StreamManagementNegotiatorState.ready: | ||||
|         final sm = attributes.getManagerById<StreamManagementManager>(smManager)!; | ||||
|         final srid = sm.state.streamResumptionId; | ||||
|         final h = sm.state.s2c; | ||||
| 
 | ||||
|         // Attempt stream resumption first | ||||
|         if (srid != null) { | ||||
|           _log.finest('Found stream resumption Id. Attempting to perform stream resumption'); | ||||
|           _state = _StreamManagementNegotiatorState.resumeRequested; | ||||
|           attributes.sendNonza(StreamManagementResumeNonza(srid, h)); | ||||
|         } else { | ||||
|           _log.finest('Attempting to enable stream management'); | ||||
|           _state = _StreamManagementNegotiatorState.enableRequested; | ||||
|           attributes.sendNonza(StreamManagementEnableNonza()); | ||||
|         } | ||||
|         break; | ||||
|         case _StreamManagementNegotiatorState.resumeRequested: | ||||
|           if (nonza.tag == 'resumed') { | ||||
|             _log.finest('Stream Management resumption successful'); | ||||
| 
 | ||||
|             assert(attributes.getFullJID().resource != '', 'Resume only works when we already have a resource bound and know about it'); | ||||
| 
 | ||||
|             final csi = attributes.getManagerById(csiManager) as CSIManager?; | ||||
|             if (csi != null) { | ||||
|               csi.restoreCSIState(); | ||||
|             } | ||||
| 
 | ||||
|             final h = int.parse(nonza.attributes['h']! as String); | ||||
|             await attributes.sendEvent(StreamResumedEvent(h: h)); | ||||
| 
 | ||||
|             _resumeFailed = false; | ||||
|             _isResumed = true; | ||||
|             state = NegotiatorState.skipRest; | ||||
|           } else { | ||||
|             // We assume it is <failed /> | ||||
|             _log.info('Stream resumption failed. Expected <resumed />, got ${nonza.tag}, Proceeding with new stream...'); | ||||
|             await attributes.sendEvent(StreamResumeFailedEvent()); | ||||
|             final sm = attributes.getManagerById<StreamManagementManager>(smManager)!; | ||||
| 
 | ||||
|             // We have to do this because we otherwise get a stanza stuck in the queue, | ||||
|             // thus spamming the server on every <a /> nonza we receive. | ||||
|             // ignore: cascade_invocations | ||||
|             await sm.setState(StreamManagementState(0, 0)); | ||||
|             await sm.commitState(); | ||||
| 
 | ||||
|             _resumeFailed = true; | ||||
|             _isResumed = false; | ||||
|             _state = _StreamManagementNegotiatorState.ready; | ||||
|             state = NegotiatorState.retryLater; | ||||
|           } | ||||
|         break; | ||||
|       case _StreamManagementNegotiatorState.enableRequested: | ||||
|         if (nonza.tag == 'enabled') { | ||||
|           _log.finest('Stream Management enabled'); | ||||
| 
 | ||||
|           final id = nonza.attributes['id'] as String?; | ||||
|           if (id != null && ['true', '1'].contains(nonza.attributes['resume'])) { | ||||
|             _log.info('Stream Resumption available'); | ||||
|           } | ||||
| 
 | ||||
|           await attributes.sendEvent( | ||||
|             StreamManagementEnabledEvent( | ||||
|               resource: attributes.getFullJID().resource, | ||||
|               id: id, | ||||
|               location: nonza.attributes['location'] as String?, | ||||
|             ), | ||||
|           ); | ||||
| 
 | ||||
|           state = NegotiatorState.done; | ||||
|         } else { | ||||
|           // We assume a <failed /> | ||||
|           _log.warning('Stream Management enablement failed'); | ||||
|           state = NegotiatorState.done; | ||||
|         } | ||||
| 
 | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void reset() { | ||||
|     _state = _StreamManagementNegotiatorState.ready; | ||||
|     _supported = false; | ||||
|     _resumeFailed = false; | ||||
|     _isResumed = false; | ||||
| 
 | ||||
|     super.reset(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										42
									
								
								moxxmpp/lib/src/xeps/xep_0198/nonzas.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								moxxmpp/lib/src/xeps/xep_0198/nonzas.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| class StreamManagementEnableNonza extends XMLNode { | ||||
|   StreamManagementEnableNonza() : super( | ||||
|     tag: 'enable', | ||||
|     attributes: <String, String>{ | ||||
|       'xmlns': smXmlns, | ||||
|       'resume': 'true' | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class StreamManagementResumeNonza extends XMLNode { | ||||
|   StreamManagementResumeNonza(String id, int h) : super( | ||||
|     tag: 'resume', | ||||
|     attributes: <String, String>{ | ||||
|       'xmlns': smXmlns, | ||||
|       'previd': id, | ||||
|       'h': h.toString() | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class StreamManagementAckNonza extends XMLNode { | ||||
|   StreamManagementAckNonza(int h) : super( | ||||
|     tag: 'a', | ||||
|     attributes: <String, String>{ | ||||
|       'xmlns': smXmlns, | ||||
|       'h': h.toString() | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class StreamManagementRequestNonza extends XMLNode { | ||||
|   StreamManagementRequestNonza() : super( | ||||
|     tag: 'r', | ||||
|     attributes: <String, String>{ | ||||
|       'xmlns': smXmlns, | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										19
									
								
								moxxmpp/lib/src/xeps/xep_0198/state.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								moxxmpp/lib/src/xeps/xep_0198/state.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| 
 | ||||
| part 'state.freezed.dart'; | ||||
| part 'state.g.dart'; | ||||
| 
 | ||||
| @freezed | ||||
| class StreamManagementState with _$StreamManagementState { | ||||
|   factory StreamManagementState( | ||||
|     int c2s, | ||||
|     int s2c, | ||||
|     { | ||||
|       String? streamResumptionLocation, | ||||
|       String? streamResumptionId, | ||||
|     } | ||||
|   ) = _StreamManagementState; | ||||
| 
 | ||||
|   // JSON | ||||
|   factory StreamManagementState.fromJson(Map<String, dynamic> json) => _$StreamManagementStateFromJson(json); | ||||
| } | ||||
							
								
								
									
										217
									
								
								moxxmpp/lib/src/xeps/xep_0198/state.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								moxxmpp/lib/src/xeps/xep_0198/state.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,217 @@ | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target | ||||
| 
 | ||||
| part of 'state.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| T _$identity<T>(T value) => value; | ||||
| 
 | ||||
| final _privateConstructorUsedError = UnsupportedError( | ||||
|     'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); | ||||
| 
 | ||||
| StreamManagementState _$StreamManagementStateFromJson( | ||||
|     Map<String, dynamic> json) { | ||||
|   return _StreamManagementState.fromJson(json); | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| mixin _$StreamManagementState { | ||||
|   int get c2s => throw _privateConstructorUsedError; | ||||
|   int get s2c => throw _privateConstructorUsedError; | ||||
|   String? get streamResumptionLocation => throw _privateConstructorUsedError; | ||||
|   String? get streamResumptionId => throw _privateConstructorUsedError; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|   @JsonKey(ignore: true) | ||||
|   $StreamManagementStateCopyWith<StreamManagementState> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| abstract class $StreamManagementStateCopyWith<$Res> { | ||||
|   factory $StreamManagementStateCopyWith(StreamManagementState value, | ||||
|           $Res Function(StreamManagementState) then) = | ||||
|       _$StreamManagementStateCopyWithImpl<$Res>; | ||||
|   $Res call( | ||||
|       {int c2s, | ||||
|       int s2c, | ||||
|       String? streamResumptionLocation, | ||||
|       String? streamResumptionId}); | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| class _$StreamManagementStateCopyWithImpl<$Res> | ||||
|     implements $StreamManagementStateCopyWith<$Res> { | ||||
|   _$StreamManagementStateCopyWithImpl(this._value, this._then); | ||||
| 
 | ||||
|   final StreamManagementState _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function(StreamManagementState) _then; | ||||
| 
 | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? c2s = freezed, | ||||
|     Object? s2c = freezed, | ||||
|     Object? streamResumptionLocation = freezed, | ||||
|     Object? streamResumptionId = freezed, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       c2s: c2s == freezed | ||||
|           ? _value.c2s | ||||
|           : c2s // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       s2c: s2c == freezed | ||||
|           ? _value.s2c | ||||
|           : s2c // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       streamResumptionLocation: streamResumptionLocation == freezed | ||||
|           ? _value.streamResumptionLocation | ||||
|           : streamResumptionLocation // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       streamResumptionId: streamResumptionId == freezed | ||||
|           ? _value.streamResumptionId | ||||
|           : streamResumptionId // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| abstract class _$$_StreamManagementStateCopyWith<$Res> | ||||
|     implements $StreamManagementStateCopyWith<$Res> { | ||||
|   factory _$$_StreamManagementStateCopyWith(_$_StreamManagementState value, | ||||
|           $Res Function(_$_StreamManagementState) then) = | ||||
|       __$$_StreamManagementStateCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   $Res call( | ||||
|       {int c2s, | ||||
|       int s2c, | ||||
|       String? streamResumptionLocation, | ||||
|       String? streamResumptionId}); | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| class __$$_StreamManagementStateCopyWithImpl<$Res> | ||||
|     extends _$StreamManagementStateCopyWithImpl<$Res> | ||||
|     implements _$$_StreamManagementStateCopyWith<$Res> { | ||||
|   __$$_StreamManagementStateCopyWithImpl(_$_StreamManagementState _value, | ||||
|       $Res Function(_$_StreamManagementState) _then) | ||||
|       : super(_value, (v) => _then(v as _$_StreamManagementState)); | ||||
| 
 | ||||
|   @override | ||||
|   _$_StreamManagementState get _value => | ||||
|       super._value as _$_StreamManagementState; | ||||
| 
 | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? c2s = freezed, | ||||
|     Object? s2c = freezed, | ||||
|     Object? streamResumptionLocation = freezed, | ||||
|     Object? streamResumptionId = freezed, | ||||
|   }) { | ||||
|     return _then(_$_StreamManagementState( | ||||
|       c2s == freezed | ||||
|           ? _value.c2s | ||||
|           : c2s // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       s2c == freezed | ||||
|           ? _value.s2c | ||||
|           : s2c // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       streamResumptionLocation: streamResumptionLocation == freezed | ||||
|           ? _value.streamResumptionLocation | ||||
|           : streamResumptionLocation // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|       streamResumptionId: streamResumptionId == freezed | ||||
|           ? _value.streamResumptionId | ||||
|           : streamResumptionId // ignore: cast_nullable_to_non_nullable | ||||
|               as String?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$_StreamManagementState implements _StreamManagementState { | ||||
|   _$_StreamManagementState(this.c2s, this.s2c, | ||||
|       {this.streamResumptionLocation, this.streamResumptionId}); | ||||
| 
 | ||||
|   factory _$_StreamManagementState.fromJson(Map<String, dynamic> json) => | ||||
|       _$$_StreamManagementStateFromJson(json); | ||||
| 
 | ||||
|   @override | ||||
|   final int c2s; | ||||
|   @override | ||||
|   final int s2c; | ||||
|   @override | ||||
|   final String? streamResumptionLocation; | ||||
|   @override | ||||
|   final String? streamResumptionId; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'StreamManagementState(c2s: $c2s, s2c: $s2c, streamResumptionLocation: $streamResumptionLocation, streamResumptionId: $streamResumptionId)'; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(dynamic other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$_StreamManagementState && | ||||
|             const DeepCollectionEquality().equals(other.c2s, c2s) && | ||||
|             const DeepCollectionEquality().equals(other.s2c, s2c) && | ||||
|             const DeepCollectionEquality().equals( | ||||
|                 other.streamResumptionLocation, streamResumptionLocation) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other.streamResumptionId, streamResumptionId)); | ||||
|   } | ||||
| 
 | ||||
|   @JsonKey(ignore: true) | ||||
|   @override | ||||
|   int get hashCode => Object.hash( | ||||
|       runtimeType, | ||||
|       const DeepCollectionEquality().hash(c2s), | ||||
|       const DeepCollectionEquality().hash(s2c), | ||||
|       const DeepCollectionEquality().hash(streamResumptionLocation), | ||||
|       const DeepCollectionEquality().hash(streamResumptionId)); | ||||
| 
 | ||||
|   @JsonKey(ignore: true) | ||||
|   @override | ||||
|   _$$_StreamManagementStateCopyWith<_$_StreamManagementState> get copyWith => | ||||
|       __$$_StreamManagementStateCopyWithImpl<_$_StreamManagementState>( | ||||
|           this, _$identity); | ||||
| 
 | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$_StreamManagementStateToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| abstract class _StreamManagementState implements StreamManagementState { | ||||
|   factory _StreamManagementState(final int c2s, final int s2c, | ||||
|       {final String? streamResumptionLocation, | ||||
|       final String? streamResumptionId}) = _$_StreamManagementState; | ||||
| 
 | ||||
|   factory _StreamManagementState.fromJson(Map<String, dynamic> json) = | ||||
|       _$_StreamManagementState.fromJson; | ||||
| 
 | ||||
|   @override | ||||
|   int get c2s; | ||||
|   @override | ||||
|   int get s2c; | ||||
|   @override | ||||
|   String? get streamResumptionLocation; | ||||
|   @override | ||||
|   String? get streamResumptionId; | ||||
|   @override | ||||
|   @JsonKey(ignore: true) | ||||
|   _$$_StreamManagementStateCopyWith<_$_StreamManagementState> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
							
								
								
									
										25
									
								
								moxxmpp/lib/src/xeps/xep_0198/state.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								moxxmpp/lib/src/xeps/xep_0198/state.g.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| 
 | ||||
| part of 'state.dart'; | ||||
| 
 | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
| 
 | ||||
| _$_StreamManagementState _$$_StreamManagementStateFromJson( | ||||
|         Map<String, dynamic> json) => | ||||
|     _$_StreamManagementState( | ||||
|       json['c2s'] as int, | ||||
|       json['s2c'] as int, | ||||
|       streamResumptionLocation: json['streamResumptionLocation'] as String?, | ||||
|       streamResumptionId: json['streamResumptionId'] as String?, | ||||
|     ); | ||||
| 
 | ||||
| Map<String, dynamic> _$$_StreamManagementStateToJson( | ||||
|         _$_StreamManagementState instance) => | ||||
|     <String, dynamic>{ | ||||
|       'c2s': instance.c2s, | ||||
|       's2c': instance.s2c, | ||||
|       'streamResumptionLocation': instance.streamResumptionLocation, | ||||
|       'streamResumptionId': instance.streamResumptionId, | ||||
|     }; | ||||
							
								
								
									
										393
									
								
								moxxmpp/lib/src/xeps/xep_0198/xep_0198.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										393
									
								
								moxxmpp/lib/src/xeps/xep_0198/xep_0198.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,393 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:math'; | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:moxxmpp/src/connection.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0198/state.dart'; | ||||
| import 'package:synchronized/synchronized.dart'; | ||||
| 
 | ||||
| const xmlUintMax = 4294967296; // 2**32 | ||||
| 
 | ||||
| typedef StanzaAckedCallback = bool Function(Stanza stanza); | ||||
| 
 | ||||
| class StreamManagementManager extends XmppManagerBase { | ||||
| 
 | ||||
|   StreamManagementManager({ | ||||
|     this.ackTimeout = const Duration(seconds: 30), | ||||
|   }) | ||||
|   : _state = StreamManagementState(0, 0), | ||||
|     _unackedStanzas = {}, | ||||
|     _stateLock = Lock(), | ||||
|     _streamManagementEnabled = false, | ||||
|     _lastAckTimestamp = -1, | ||||
|     _pendingAcks = 0, | ||||
|     _streamResumed = false, | ||||
|     _ackLock = Lock(); | ||||
|   /// The queue of stanzas that are not (yet) acked | ||||
|   final Map<int, Stanza> _unackedStanzas; | ||||
|   /// Commitable state of the StreamManagementManager | ||||
|   StreamManagementState _state; | ||||
|   /// Mutex lock for _state | ||||
|   final Lock _stateLock; | ||||
|   /// If the have enabled SM on the stream yet | ||||
|   bool _streamManagementEnabled; | ||||
|   /// If the current stream has been resumed; | ||||
|   bool _streamResumed; | ||||
|   /// The time in which the response to an ack is still valid. Counts as a timeout | ||||
|   /// otherwise | ||||
|   @internal | ||||
|   final Duration ackTimeout; | ||||
|   /// The time at which the last ack has been sent | ||||
|   int _lastAckTimestamp; | ||||
|   /// The timer to see if we timed the connection out | ||||
|   Timer? _ackTimer; | ||||
|   /// Counts how many acks we're waiting for | ||||
|   int _pendingAcks; | ||||
|   /// Lock for both [_lastAckTimestamp] and [_pendingAcks]. | ||||
|   final Lock _ackLock; | ||||
| 
 | ||||
|   /// Functions for testing | ||||
|   @visibleForTesting | ||||
|   Map<int, Stanza> getUnackedStanzas() => _unackedStanzas; | ||||
| 
 | ||||
|   @visibleForTesting | ||||
|   Future<int> getPendingAcks() async { | ||||
|     var acks = 0; | ||||
| 
 | ||||
|     await _ackLock.synchronized(() async { | ||||
|       acks = _pendingAcks; | ||||
|     }); | ||||
| 
 | ||||
|     return acks; | ||||
|   } | ||||
| 
 | ||||
|   /// Called when a stanza has been acked to decide whether we should trigger a | ||||
|   /// StanzaAckedEvent. | ||||
|   /// | ||||
|   /// Return true when the stanza should trigger this event. Return false if not. | ||||
|   @visibleForOverriding | ||||
|   bool shouldTriggerAckedEvent(Stanza stanza) { | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   Future<bool> isSupported() async { | ||||
|     return getAttributes().getNegotiatorById<StreamManagementNegotiator>(streamManagementNegotiator)!.isSupported; | ||||
|   } | ||||
|    | ||||
|   /// Returns the amount of stanzas waiting to get acked | ||||
|   int getUnackedStanzaCount() => _unackedStanzas.length; | ||||
| 
 | ||||
|   /// May be overwritten by a subclass. Should save [state] so that it can be loaded again | ||||
|   /// with [this.loadState]. | ||||
|   Future<void> commitState() async {} | ||||
|   Future<void> loadState() async {} | ||||
| 
 | ||||
|   Future<void> setState(StreamManagementState state) async { | ||||
|     await _stateLock.synchronized(() async { | ||||
|       _state = state; | ||||
|       await commitState(); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /// Resets the state such that a resumption is no longer possible without creating | ||||
|   /// a new session. Primarily useful for clearing the state after disconnecting | ||||
|   Future<void> resetState() async { | ||||
|     await setState( | ||||
|       _state.copyWith( | ||||
|         c2s: 0, | ||||
|         s2c: 0, | ||||
|         streamResumptionLocation: null, | ||||
|         streamResumptionId: null, | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     await _ackLock.synchronized(() async { | ||||
|       _pendingAcks = 0; | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   StreamManagementState get state => _state; | ||||
| 
 | ||||
|   bool get streamResumed => _streamResumed; | ||||
|    | ||||
|   @override | ||||
|   String getId() => smManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'StreamManagementManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<NonzaHandler> getNonzaHandlers() => [ | ||||
|     NonzaHandler( | ||||
|       nonzaTag: 'r', | ||||
|       nonzaXmlns: smXmlns, | ||||
|       callback: _handleAckRequest, | ||||
|     ), | ||||
|     NonzaHandler( | ||||
|       nonzaTag: 'a', | ||||
|       nonzaXmlns: smXmlns, | ||||
|       callback: _handleAckResponse, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       callback: _onServerStanzaReceived, | ||||
|       priority: 9999, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getOutgoingPostStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       callback: _onClientStanzaSent, | ||||
|     ) | ||||
|   ]; | ||||
|    | ||||
|   @override | ||||
|   Future<void> onXmppEvent(XmppEvent event) async { | ||||
|     if (event is StreamResumedEvent) { | ||||
|       _enableStreamManagement(); | ||||
| 
 | ||||
|       await _ackLock.synchronized(() async { | ||||
|         _pendingAcks = 0; | ||||
|       }); | ||||
| 
 | ||||
|       await onStreamResumed(event.h); | ||||
|     } else if (event is StreamManagementEnabledEvent) { | ||||
|       _enableStreamManagement(); | ||||
| 
 | ||||
|       await _ackLock.synchronized(() async { | ||||
|         _pendingAcks = 0; | ||||
|       }); | ||||
| 
 | ||||
|       await setState( | ||||
|         StreamManagementState( | ||||
|           0, | ||||
|           0, | ||||
|           streamResumptionId: event.id, | ||||
|           streamResumptionLocation: event.location, | ||||
|         ), | ||||
|       ); | ||||
|     } else if (event is ConnectingEvent) { | ||||
|       _disableStreamManagement(); | ||||
|       _streamResumed = false; | ||||
|     } else if (event is ConnectionStateChangedEvent) { | ||||
|       if (event.state == XmppConnectionState.connected) { | ||||
|         // Push out all pending stanzas | ||||
|         await onStreamResumed(0); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Starts the timer to detect timeouts based on ack responses, if the timer | ||||
|   /// is not already running. | ||||
|   void _startAckTimer() { | ||||
|     if (_ackTimer != null) return; | ||||
| 
 | ||||
|     logger.fine('Starting ack timer'); | ||||
|     _ackTimer = Timer.periodic( | ||||
|       ackTimeout, | ||||
|       _ackTimerCallback, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Stops the timer, if it is running. | ||||
|   void _stopAckTimer() { | ||||
|     if (_ackTimer == null) return; | ||||
| 
 | ||||
|     logger.fine('Stopping ack timer'); | ||||
|     _ackTimer!.cancel(); | ||||
|     _ackTimer = null; | ||||
|   } | ||||
| 
 | ||||
|   /// Timer callback that checks if all acks have been answered. If not and the last | ||||
|   /// response has been more that [ackTimeout] in the past, declare the session dead. | ||||
|   void _ackTimerCallback(Timer timer) { | ||||
|     _ackLock.synchronized(() async { | ||||
|       final now = DateTime.now().millisecondsSinceEpoch; | ||||
| 
 | ||||
|       if (now - _lastAckTimestamp >= ackTimeout.inMilliseconds && _pendingAcks > 0) { | ||||
|         _stopAckTimer(); | ||||
|         await getAttributes().getConnection().reconnectionPolicy.onFailure(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /// Wrapper around sending an <r /> nonza that starts the ack timeout timer. | ||||
|   Future<void> _sendAckRequest() async { | ||||
|     logger.fine('_sendAckRequest: Waiting to acquire lock...'); | ||||
|     await _ackLock.synchronized(() async { | ||||
|       logger.fine('_sendAckRequest: Done...'); | ||||
|       final now = DateTime.now().millisecondsSinceEpoch; | ||||
| 
 | ||||
|       _lastAckTimestamp = now; | ||||
|       _pendingAcks++; | ||||
|       _startAckTimer(); | ||||
| 
 | ||||
|       logger.fine('_pendingAcks is now at $_pendingAcks'); | ||||
|        | ||||
|       getAttributes().sendNonza(StreamManagementRequestNonza()); | ||||
|        | ||||
|       logger.fine('_sendAckRequest: Releasing lock...'); | ||||
|     });  | ||||
|   } | ||||
|    | ||||
|   /// Resets the enablement of stream management, but __NOT__ the internal state. | ||||
|   /// This is to prevent ack requests being sent before we resume or re-enable | ||||
|   /// stream management. | ||||
|   void _disableStreamManagement() { | ||||
|     _streamManagementEnabled = false; | ||||
|     logger.finest('Stream Management disabled'); | ||||
|   } | ||||
|    | ||||
|   /// Enables support for XEP-0198 stream management | ||||
|   void _enableStreamManagement() { | ||||
|     _streamManagementEnabled = true; | ||||
|     logger.finest('Stream Management enabled'); | ||||
|   } | ||||
|    | ||||
|   /// Returns whether XEP-0198 stream management is enabled | ||||
|   bool isStreamManagementEnabled() => _streamManagementEnabled; | ||||
| 
 | ||||
|   /// To be called when receiving a <a /> nonza. | ||||
|   Future<bool> _handleAckRequest(XMLNode nonza) async { | ||||
|     final attrs = getAttributes(); | ||||
|     logger.finest('Sending ack response'); | ||||
|     await _stateLock.synchronized(() async { | ||||
|       attrs.sendNonza(StreamManagementAckNonza(_state.s2c)); | ||||
|     }); | ||||
| 
 | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   /// Called when we receive an <a /> nonza from the server. | ||||
|   /// This is a response to the question "How many of my stanzas have you handled". | ||||
|   Future<bool> _handleAckResponse(XMLNode nonza) async { | ||||
|     final h = int.parse(nonza.attributes['h']! as String); | ||||
| 
 | ||||
|     await _ackLock.synchronized(() async { | ||||
|       await _stateLock.synchronized(() async { | ||||
|         if (_pendingAcks > 0) { | ||||
|           // Prevent diff from becoming negative | ||||
|           final diff = max(_state.c2s - h, 0); | ||||
|           _pendingAcks = diff; | ||||
|         } else { | ||||
|           _stopAckTimer(); | ||||
|         } | ||||
| 
 | ||||
|         logger.fine('_pendingAcks is now at $_pendingAcks'); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Return early if we acked nothing. | ||||
|     // Taken from slixmpp's stream management code | ||||
|     logger.fine('_handleAckResponse: Waiting to aquire lock...'); | ||||
|     await _stateLock.synchronized(() async { | ||||
|         logger.fine('_handleAckResponse: Done...'); | ||||
|         if (h == _state.c2s && _unackedStanzas.isEmpty) { | ||||
|           logger.fine('_handleAckResponse: Releasing lock...'); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         final attrs = getAttributes(); | ||||
|         final sequences = _unackedStanzas.keys.toList()..sort(); | ||||
|         for (final height in sequences) { | ||||
|           // Do nothing if the ack does not concern this stanza | ||||
|           if (height > h) continue; | ||||
| 
 | ||||
|           final stanza = _unackedStanzas[height]!; | ||||
|           _unackedStanzas.remove(height); | ||||
| 
 | ||||
|           // Create a StanzaAckedEvent if the stanza is correct | ||||
|           if (shouldTriggerAckedEvent(stanza)) { | ||||
|             attrs.sendEvent(StanzaAckedEvent(stanza)); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (h > _state.c2s) { | ||||
|           logger.info('C2S height jumped from ${_state.c2s} (local) to $h (remote).'); | ||||
|           // ignore: cascade_invocations | ||||
|           logger.info('Proceeding with $h as local C2S counter.'); | ||||
| 
 | ||||
|           _state = _state.copyWith(c2s: h); | ||||
|           await commitState(); | ||||
|         } | ||||
| 
 | ||||
|         logger.fine('_handleAckResponse: Releasing lock...'); | ||||
|     }); | ||||
| 
 | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   // Just a helper function to not increment the counters above xmlUintMax | ||||
|   Future<void> _incrementC2S() async { | ||||
|     logger.fine('_incrementC2S: Waiting to aquire lock...'); | ||||
|     await _stateLock.synchronized(() async { | ||||
|         logger.fine('_incrementC2S: Done'); | ||||
|         _state = _state.copyWith(c2s: _state.c2s + 1 % xmlUintMax); | ||||
|         await commitState(); | ||||
|         logger.fine('_incrementC2S: Releasing lock...'); | ||||
|     }); | ||||
|   } | ||||
|   Future<void> _incrementS2C() async { | ||||
|     logger.fine('_incrementS2C: Waiting to aquire lock...'); | ||||
|     await _stateLock.synchronized(() async { | ||||
|         logger.fine('_incrementS2C: Done'); | ||||
|         _state = _state.copyWith(s2c: _state.s2c + 1 % xmlUintMax); | ||||
|         await commitState(); | ||||
|         logger.fine('_incrementS2C: Releasing lock...'); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /// Called whenever we receive a stanza from the server. | ||||
|   Future<StanzaHandlerData> _onServerStanzaReceived(Stanza stanza, StanzaHandlerData state) async { | ||||
|     await _incrementS2C(); | ||||
|     return state; | ||||
|   } | ||||
| 
 | ||||
|   /// Called whenever we send a stanza. | ||||
|   Future<StanzaHandlerData> _onClientStanzaSent(Stanza stanza, StanzaHandlerData state) async { | ||||
|     await _incrementC2S(); | ||||
|     _unackedStanzas[_state.c2s] = stanza; | ||||
|      | ||||
|     if (isStreamManagementEnabled()) { | ||||
|       await _sendAckRequest(); | ||||
|     } | ||||
| 
 | ||||
|     return state; | ||||
|   } | ||||
| 
 | ||||
|   /// To be called when the stream has been resumed | ||||
|   @visibleForTesting | ||||
|   Future<void> onStreamResumed(int h) async { | ||||
|     _streamResumed = true; | ||||
|     await _handleAckResponse(StreamManagementAckNonza(h)); | ||||
| 
 | ||||
|     final stanzas = _unackedStanzas.values.toList(); | ||||
|     _unackedStanzas.clear(); | ||||
|      | ||||
|     // Retransmit the rest of the queue | ||||
|     final attrs = getAttributes(); | ||||
|     for (final stanza in stanzas) { | ||||
|       await attrs.sendStanza(stanza, awaitable: false); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Pings the connection open by send an ack request | ||||
|   void sendAckRequestPing() { | ||||
|     _sendAckRequest(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										48
									
								
								moxxmpp/lib/src/xeps/xep_0203.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								moxxmpp/lib/src/xeps/xep_0203.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| 
 | ||||
| @immutable | ||||
| class DelayedDelivery { | ||||
| 
 | ||||
|   const DelayedDelivery(this.from, this.timestamp); | ||||
|   final DateTime timestamp; | ||||
|   final String from; | ||||
| } | ||||
| 
 | ||||
| class DelayedDeliveryManager extends XmppManagerBase { | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => delayedDeliveryManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'DelayedDeliveryManager'; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       callback: _onIncomingMessage, | ||||
|       priority: 200, | ||||
|     ), | ||||
|   ]; | ||||
| 
 | ||||
|   Future<StanzaHandlerData> _onIncomingMessage(Stanza stanza, StanzaHandlerData state) async { | ||||
|     final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns); | ||||
|     if (delay == null) return state; | ||||
| 
 | ||||
|     return state.copyWith( | ||||
|       delayedDelivery: DelayedDelivery( | ||||
|         delay.attributes['from']! as String, | ||||
|         DateTime.parse(delay.attributes['stamp']! as String), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										170
									
								
								moxxmpp/lib/src/xeps/xep_0280.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								moxxmpp/lib/src/xeps/xep_0280.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:moxxmpp/src/connection.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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/xep_0030.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0297.dart'; | ||||
| 
 | ||||
| class CarbonsManager extends XmppManagerBase { | ||||
| 
 | ||||
|   CarbonsManager() : _isEnabled = false, _supported = false, _gotSupported = false, super(); | ||||
|   bool _isEnabled; | ||||
|   bool _supported; | ||||
|   bool _gotSupported; | ||||
|    | ||||
|   @override | ||||
|   String getId() => carbonsManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'CarbonsManager'; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'received', | ||||
|       tagXmlns: carbonsXmlns, | ||||
|       callback: _onMessageReceived, | ||||
|       // Before all managers the message manager depends on | ||||
|       priority: -98, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'sent', | ||||
|       tagXmlns: carbonsXmlns, | ||||
|       callback: _onMessageSent, | ||||
|       // Before all managers the message manager depends on | ||||
|       priority: -98, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async { | ||||
|     if (_gotSupported) return _supported; | ||||
| 
 | ||||
|     // Query the server | ||||
|     final disco = getAttributes().getManagerById<DiscoManager>(discoManager)!; | ||||
|     _supported = await disco.supportsFeature( | ||||
|       getAttributes().getConnectionSettings().jid.toBare(), | ||||
|       carbonsXmlns, | ||||
|     ); | ||||
|     _gotSupported = true; | ||||
|     return _supported; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> onXmppEvent(XmppEvent event) async { | ||||
|     if (event is ServerDiscoDoneEvent && !_isEnabled) { | ||||
|       final attrs = getAttributes(); | ||||
| 
 | ||||
|       if (attrs.isFeatureSupported(carbonsXmlns)) { | ||||
|         logger.finest('Message carbons supported. Enabling...'); | ||||
|         await enableCarbons(); | ||||
|         logger.finest('Message carbons enabled'); | ||||
|       } else { | ||||
|         logger.info('Message carbons not supported.'); | ||||
|       } | ||||
|     } else if (event is StreamResumeFailedEvent) { | ||||
|       _gotSupported = false; | ||||
|       _supported = false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   Future<StanzaHandlerData> _onMessageReceived(Stanza message, StanzaHandlerData state) async { | ||||
|     final from = JID.fromString(message.attributes['from']! as String); | ||||
|     final received = message.firstTag('received', xmlns: carbonsXmlns)!; | ||||
|     if (!isCarbonValid(from)) return state.copyWith(done: true); | ||||
| 
 | ||||
|     final forwarded = received.firstTag('forwarded', xmlns: forwardedXmlns)!; | ||||
|     final carbon = unpackForwarded(forwarded); | ||||
| 
 | ||||
|     return state.copyWith( | ||||
|       isCarbon: true, | ||||
|       stanza: carbon, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Future<StanzaHandlerData> _onMessageSent(Stanza message, StanzaHandlerData state) async { | ||||
|     final from = JID.fromString(message.attributes['from']! as String); | ||||
|     final sent = message.firstTag('sent', xmlns: carbonsXmlns)!; | ||||
|     if (!isCarbonValid(from)) return state.copyWith(done: true); | ||||
| 
 | ||||
|     final forwarded = sent.firstTag('forwarded', xmlns: forwardedXmlns)!; | ||||
|     final carbon = unpackForwarded(forwarded); | ||||
| 
 | ||||
|     return state.copyWith( | ||||
|       isCarbon: true, | ||||
|       stanza: carbon, | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   Future<bool> enableCarbons() async { | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'enable', | ||||
|             xmlns: carbonsXmlns, | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|       addFrom: StanzaFromType.full, | ||||
|       addId: true, | ||||
|     ); | ||||
| 
 | ||||
|     if (result.attributes['type'] != 'result') { | ||||
|       logger.warning('Failed to enable message carbons'); | ||||
| 
 | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     logger.fine('Successfully enabled message carbons'); | ||||
| 
 | ||||
|     _isEnabled = true; | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   Future<bool> disableCarbons() async { | ||||
|     final result = await getAttributes().sendStanza( | ||||
|       Stanza.iq( | ||||
|         type: 'set', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'disable', | ||||
|             xmlns: carbonsXmlns, | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|       addFrom: StanzaFromType.full, | ||||
|       addId: true, | ||||
|     ); | ||||
| 
 | ||||
|     if (result.attributes['type'] != 'result') { | ||||
|       logger.warning('Failed to disable message carbons'); | ||||
| 
 | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     logger.fine('Successfully disabled message carbons'); | ||||
|      | ||||
|     _isEnabled = false; | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   @visibleForTesting | ||||
|   void forceEnable() { | ||||
|     _isEnabled = true; | ||||
|   } | ||||
|    | ||||
|   bool isCarbonValid(JID senderJid) { | ||||
|     return _isEnabled && senderJid == getAttributes().getConnectionSettings().jid.toBare(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										21
									
								
								moxxmpp/lib/src/xeps/xep_0297.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								moxxmpp/lib/src/xeps/xep_0297.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| /// Extracts the message stanza from the <forwarded /> node. | ||||
| Stanza unpackForwarded(XMLNode forwarded) { | ||||
|   assert(forwarded.attributes['xmlns'] == forwardedXmlns, 'Invalid element xmlns'); | ||||
|   assert(forwarded.tag == 'forwarded', 'Invalid element name'); | ||||
| 
 | ||||
|   // NOTE: We only use this XEP (for now) in the context of Message Carbons | ||||
|   final stanza = forwarded.firstTag('message', xmlns: stanzaXmlns)!; | ||||
|   return Stanza( | ||||
|     to: stanza.attributes['to']! as String, | ||||
|     from: stanza.attributes['from']! as String, | ||||
|     type: stanza.attributes['type']! as String, | ||||
|     id: stanza.attributes['id']! as String, | ||||
|     tag: stanza.tag, | ||||
|     attributes: stanza.attributes as Map<String, String>, | ||||
|     children: stanza.children, | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										104
									
								
								moxxmpp/lib/src/xeps/xep_0300.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								moxxmpp/lib/src/xeps/xep_0300.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,104 @@ | ||||
| import 'package:cryptography/cryptography.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/stringxml.dart'; | ||||
| 
 | ||||
| XMLNode constructHashElement(String algo, String base64Hash) { | ||||
|   return XMLNode.xmlns( | ||||
|     tag: 'hash', | ||||
|     xmlns: hashXmlns, | ||||
|     attributes: { 'algo': algo }, | ||||
|     text: base64Hash, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| enum HashFunction { | ||||
|   sha256, | ||||
|   sha512, | ||||
|   sha3_256, | ||||
|   sha3_512, | ||||
|   blake2b256, | ||||
|   blake2b512, | ||||
| } | ||||
| 
 | ||||
| extension HashNameToEnumExtension on HashFunction { | ||||
|   String toName() { | ||||
|     switch (this) { | ||||
|       case HashFunction.sha256: | ||||
|         return hashSha256; | ||||
|       case HashFunction.sha512: | ||||
|         return hashSha512; | ||||
|       case HashFunction.sha3_256: | ||||
|         return hashSha3512; | ||||
|       case HashFunction.sha3_512: | ||||
|         return hashSha3512; | ||||
|       case HashFunction.blake2b256: | ||||
|         return hashBlake2b256; | ||||
|       case HashFunction.blake2b512: | ||||
|         return hashBlake2b512; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| HashFunction hashFunctionFromName(String name) { | ||||
|   switch (name) { | ||||
|     case hashSha256: | ||||
|       return HashFunction.sha256; | ||||
|     case hashSha512: | ||||
|       return HashFunction.sha512; | ||||
|     case hashSha3256: | ||||
|       return HashFunction.sha3_256; | ||||
|     case hashSha3512: | ||||
|       return HashFunction.sha3_512; | ||||
|     case hashBlake2b256: | ||||
|       return HashFunction.blake2b256; | ||||
|     case hashBlake2b512: | ||||
|       return HashFunction.blake2b512; | ||||
|   } | ||||
| 
 | ||||
|   throw Exception(); | ||||
| } | ||||
| 
 | ||||
| class CryptographicHashManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getId() => cryptographicHashManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'CryptographicHashManager'; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
| 
 | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [ | ||||
|     '$hashFunctionNameBaseXmlns:$hashSha256', | ||||
|     '$hashFunctionNameBaseXmlns:$hashSha512', | ||||
|     //'$hashFunctionNameBaseXmlns:$hashSha3256', | ||||
|     //'$hashFunctionNameBaseXmlns:$hashSha3512', | ||||
|     //'$hashFunctionNameBaseXmlns:$hashBlake2b256', | ||||
|     '$hashFunctionNameBaseXmlns:$hashBlake2b512', | ||||
|   ]; | ||||
| 
 | ||||
|   static Future<List<int>> hashFromData(List<int> data, HashFunction function) async { | ||||
|     // TODO(PapaTutuWawa): Implemen the others as well | ||||
|     HashAlgorithm algo; | ||||
|     switch (function) { | ||||
|       case HashFunction.sha256: | ||||
|         algo = Sha256(); | ||||
|         break; | ||||
|       case HashFunction.sha512: | ||||
|         algo = Sha512(); | ||||
|         break; | ||||
|       case HashFunction.blake2b512: | ||||
|         algo = Blake2b(); | ||||
|         break; | ||||
|       // ignore: no_default_cases | ||||
|       default: | ||||
|         throw Exception(); | ||||
|     } | ||||
| 
 | ||||
|     final digest = await algo.hash(data); | ||||
|     return digest.bytes; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										69
									
								
								moxxmpp/lib/src/xeps/xep_0333.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								moxxmpp/lib/src/xeps/xep_0333.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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'; | ||||
| 
 | ||||
| XMLNode makeChatMarkerMarkable() { | ||||
|   return XMLNode.xmlns( | ||||
|     tag: 'markable', | ||||
|     xmlns: chatMarkersXmlns, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| XMLNode makeChatMarker(String tag, String id) { | ||||
|   assert(['received', 'displayed', 'acknowledged'].contains(tag), 'Invalid chat marker'); | ||||
|   return XMLNode.xmlns( | ||||
|     tag: tag, | ||||
|     xmlns: chatMarkersXmlns, | ||||
|     attributes: { 'id': id }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class ChatMarkerManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getName() => 'ChatMarkerManager'; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => chatMarkerManager; | ||||
| 
 | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [ chatMarkersXmlns ]; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagXmlns: chatMarkersXmlns, | ||||
|       callback: _onMessage, | ||||
|       // Before the message handler | ||||
|       priority: -99, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onMessage(Stanza message, StanzaHandlerData state) async { | ||||
|     final marker = message.firstTagByXmlns(chatMarkersXmlns)!; | ||||
| 
 | ||||
|     // Handle the <markable /> explicitly | ||||
|     if (marker.tag == 'markable') return state.copyWith(isMarkable: true); | ||||
|      | ||||
|     if (!['received', 'displayed', 'acknowledged'].contains(marker.tag)) { | ||||
|       logger.warning("Unknown message marker '${marker.tag}' found."); | ||||
|     } else { | ||||
|       getAttributes().sendEvent(ChatMarkerEvent( | ||||
|           from: JID.fromString(message.from!), | ||||
|           type: marker.tag, | ||||
|           id: marker.attributes['id']! as String, | ||||
|       ),); | ||||
|     } | ||||
| 
 | ||||
|     return state.copyWith(done: true); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										36
									
								
								moxxmpp/lib/src/xeps/xep_0334.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								moxxmpp/lib/src/xeps/xep_0334.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| enum MessageProcessingHint { | ||||
|   noPermanentStore, | ||||
|   noStore, | ||||
|   noCopies, | ||||
|   store, | ||||
| } | ||||
| 
 | ||||
| /// NOTE: We do not define a function for turning a Message Processing Hint element into | ||||
| ///       an enum value since the elements do not concern us as a client. | ||||
| extension XmlExtension on MessageProcessingHint { | ||||
|   XMLNode toXml() { | ||||
|     String tag; | ||||
|     switch (this) { | ||||
|       case MessageProcessingHint.noPermanentStore: | ||||
|         tag = 'no-permanent-store'; | ||||
|         break; | ||||
|       case MessageProcessingHint.noStore: | ||||
|         tag = 'no-store'; | ||||
|         break; | ||||
|       case MessageProcessingHint.noCopies: | ||||
|         tag = 'no-copy'; | ||||
|         break; | ||||
|       case MessageProcessingHint.store: | ||||
|         tag = 'store'; | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     return XMLNode.xmlns( | ||||
|       tag: tag, | ||||
|       xmlns: messageProcessingHintsXmlns, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										96
									
								
								moxxmpp/lib/src/xeps/xep_0352.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								moxxmpp/lib/src/xeps/xep_0352.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/negotiators/negotiator.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| class CSIActiveNonza extends XMLNode { | ||||
|   CSIActiveNonza() : super( | ||||
|     tag: 'active', | ||||
|     attributes: <String, String>{ | ||||
|       'xmlns': csiXmlns | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class CSIInactiveNonza extends XMLNode { | ||||
|   CSIInactiveNonza() : super( | ||||
|     tag: 'inactive', | ||||
|     attributes: <String, String>{ | ||||
|       'xmlns': csiXmlns | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| /// A Stub negotiator that is just for "intercepting" the stream feature. | ||||
| class CSINegotiator extends XmppFeatureNegotiatorBase { | ||||
|   CSINegotiator() : _supported = false, super(11, false, csiXmlns, csiNegotiator); | ||||
| 
 | ||||
|   /// True if CSI is supported. False otherwise. | ||||
|   bool _supported; | ||||
|   bool get isSupported => _supported; | ||||
|    | ||||
|   @override | ||||
|   Future<void> negotiate(XMLNode nonza) async { | ||||
|     // negotiate is only called when the negotiator matched, meaning the server | ||||
|     // advertises CSI. | ||||
|     _supported = true; | ||||
|     state = NegotiatorState.done; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   void reset() { | ||||
|     _supported = false; | ||||
| 
 | ||||
|     super.reset(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// The manager requires a CSINegotiator to be registered as a feature negotiator. | ||||
| class CSIManager extends XmppManagerBase { | ||||
| 
 | ||||
|   CSIManager() : _isActive = true, super(); | ||||
|   bool _isActive;  | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => csiManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'CSIManager'; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async { | ||||
|     return getAttributes().getNegotiatorById<CSINegotiator>(csiNegotiator)!.isSupported; | ||||
|   } | ||||
|    | ||||
|   /// To be called after a stream has been resumed as CSI does not | ||||
|   /// survive a stream resumption. | ||||
|   void restoreCSIState() { | ||||
|     if (_isActive) { | ||||
|       setActive(); | ||||
|     } else { | ||||
|       setInactive(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /// Tells the server to top optimizing traffic | ||||
|   Future<void> setActive() async { | ||||
|     _isActive = true; | ||||
| 
 | ||||
|     final attrs = getAttributes(); | ||||
|     if (await isSupported()) { | ||||
|       attrs.sendNonza(CSIActiveNonza()); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Tells the server to optimize traffic following XEP-0352 | ||||
|   Future<void> setInactive() async { | ||||
|     _isActive = false; | ||||
| 
 | ||||
|     final attrs = getAttributes(); | ||||
|     if (await isSupported()) { | ||||
|       attrs.sendNonza(CSIInactiveNonza()); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										96
									
								
								moxxmpp/lib/src/xeps/xep_0359.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								moxxmpp/lib/src/xeps/xep_0359.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,96 @@ | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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/types.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; | ||||
| 
 | ||||
| /// Represents data provided by XEP-0359. | ||||
| /// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of | ||||
| ///       the message stanza. | ||||
| class StableStanzaId { | ||||
| 
 | ||||
|   const StableStanzaId({ this.originId, this.stanzaId, this.stanzaIdBy }); | ||||
|   final String? originId; | ||||
|   final String? stanzaId; | ||||
|   final String? stanzaIdBy; | ||||
| } | ||||
| 
 | ||||
| XMLNode makeOriginIdElement(String id) { | ||||
|   return XMLNode.xmlns( | ||||
|     tag: 'origin-id', | ||||
|     xmlns: stableIdXmlns, | ||||
|     attributes: { 'id': id }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class StableIdManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getName() => 'StableIdManager'; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => stableIdManager; | ||||
| 
 | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [ stableIdXmlns ]; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       callback: _onMessage, | ||||
|       // Before the MessageManager | ||||
|       priority: -99, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onMessage(Stanza message, StanzaHandlerData state) async { | ||||
|     final from = JID.fromString(message.attributes['from']! as String); | ||||
|     String? originId; | ||||
|     String? stanzaId; | ||||
|     String? stanzaIdBy; | ||||
|     final originIdTag = message.firstTag('origin-id', xmlns: stableIdXmlns); | ||||
|     final stanzaIdTag = message.firstTag('stanza-id', xmlns: stableIdXmlns); | ||||
|     if (originIdTag != null || stanzaIdTag != null) { | ||||
|       logger.finest('Found Unique and Stable Stanza Id tag'); | ||||
|       final attrs = getAttributes(); | ||||
|       final disco = attrs.getManagerById<DiscoManager>(discoManager)!; | ||||
|       final result = await disco.discoInfoQuery(from.toString()); | ||||
|       if (result.isType<DiscoInfo>()) { | ||||
|         final info = result.get<DiscoInfo>(); | ||||
|         logger.finest('Got info for ${from.toString()}'); | ||||
|         if (info.features.contains(stableIdXmlns)) { | ||||
|           logger.finest('${from.toString()} supports $stableIdXmlns.'); | ||||
| 
 | ||||
|           if (originIdTag != null) { | ||||
|             originId = originIdTag.attributes['id']! as String; | ||||
|           } | ||||
| 
 | ||||
|           if (stanzaIdTag != null) { | ||||
|             stanzaId = stanzaIdTag.attributes['id']! as String; | ||||
|             stanzaIdBy = stanzaIdTag.attributes['by']! as String; | ||||
|           } | ||||
|         } else { | ||||
|           logger.finest('${from.toString()} does not support $stableIdXmlns. Ignoring... '); | ||||
|         } | ||||
|       } else { | ||||
|         logger.finest('Failed to find out if ${from.toString()} supports $stableIdXmlns. Ignoring... '); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return state.copyWith( | ||||
|       stableId: StableStanzaId( | ||||
|         originId: originId, | ||||
|         stanzaId: stanzaId, | ||||
|         stanzaIdBy: stanzaIdBy, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										180
									
								
								moxxmpp/lib/src/xeps/xep_0363.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								moxxmpp/lib/src/xeps/xep_0363.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,180 @@ | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:moxlib/moxlib.dart'; | ||||
| import 'package:moxxmpp/src/events.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/types/error.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'; | ||||
| 
 | ||||
| const errorNoUploadServer = 1; | ||||
| const errorFileTooBig = 2; | ||||
| const errorGeneric = 3; | ||||
| 
 | ||||
| const allowedHTTPHeaders = [ 'authorization', 'cookie', 'expires' ]; | ||||
| 
 | ||||
| class HttpFileUploadSlot { | ||||
| 
 | ||||
|   const HttpFileUploadSlot(this.putUrl, this.getUrl, this.headers); | ||||
|   final String putUrl; | ||||
|   final String getUrl; | ||||
|   final Map<String, String> headers; | ||||
| } | ||||
| 
 | ||||
| /// Strips out all newlines from [value]. | ||||
| String _stripNewlinesFromString(String value) { | ||||
|   return value.replaceAll('\n', '').replaceAll('\r', ''); | ||||
| } | ||||
| 
 | ||||
| /// Prepares a list of headers by removing newlines from header names and values | ||||
| /// and also removes any headers that are not allowed by the XEP. | ||||
| @visibleForTesting | ||||
| Map<String, String> prepareHeaders(Map<String, String> headers) { | ||||
|   return headers.map((key, value) { | ||||
|       return MapEntry( | ||||
|         _stripNewlinesFromString(key), | ||||
|         _stripNewlinesFromString(value), | ||||
|       ); | ||||
|   }) | ||||
|   ..removeWhere((key, _) => !allowedHTTPHeaders.contains(key.toLowerCase())); | ||||
| } | ||||
| 
 | ||||
| class HttpFileUploadManager extends XmppManagerBase { | ||||
| 
 | ||||
|   HttpFileUploadManager() : _gotSupported = false, _supported = false, super(); | ||||
|   JID? _entityJid; | ||||
|   int? _maxUploadSize; | ||||
|   bool _gotSupported; | ||||
|   bool _supported; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => httpFileUploadManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'HttpFileUploadManager'; | ||||
| 
 | ||||
|   /// Returns whether the entity provided an identity that tells us that we can ask it | ||||
|   /// for an HTTP upload slot. | ||||
|   bool _containsFileUploadIdentity(DiscoInfo info) { | ||||
|     return listContains(info.identities, (Identity id) => id.category == 'store' && id.type == 'file'); | ||||
|   } | ||||
| 
 | ||||
|   /// Extract the maximum filesize in octets from the disco response. Returns null | ||||
|   /// if none was specified. | ||||
|   int? _getMaxFileSize(DiscoInfo info) { | ||||
|     for (final form in info.extendedInfo) { | ||||
|       for (final field in form.fields) { | ||||
|         if (field.varAttr == 'max-file-size') { | ||||
|           return int.parse(field.values.first); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> onXmppEvent(XmppEvent event) async { | ||||
|     if (event is StreamResumeFailedEvent) { | ||||
|       _gotSupported = false; | ||||
|       _supported = false; | ||||
|       _entityJid = null; | ||||
|       _maxUploadSize = null; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   @override | ||||
|   Future<bool> isSupported() async { | ||||
|     if (_gotSupported) return _supported; | ||||
|      | ||||
|     final result = await getAttributes().getManagerById<DiscoManager>(discoManager)!.performDiscoSweep(); | ||||
|     if (result.isType<DiscoError>()) { | ||||
|       _gotSupported = false; | ||||
|       _supported = false; | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     final infos = result.get<List<DiscoInfo>>(); | ||||
|     _gotSupported = true; | ||||
|     for (final info in infos) { | ||||
|       if (_containsFileUploadIdentity(info) && info.features.contains(httpFileUploadXmlns)) { | ||||
|          logger.info('Discovered HTTP File Upload for ${info.jid}'); | ||||
| 
 | ||||
|         _entityJid = info.jid; | ||||
|         _maxUploadSize = _getMaxFileSize(info); | ||||
|         _supported = true; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return _supported; | ||||
|   } | ||||
| 
 | ||||
|   /// Request a slot to upload a file to. [filename] is the file's name and [filesize] is | ||||
|   /// the file's size in octets. [contentType] is optional and refers to the file's | ||||
|   /// Mime type. | ||||
|   /// Returns an [HttpFileUploadSlot] if the request was successful; null otherwise. | ||||
|   Future<MayFail<HttpFileUploadSlot>> requestUploadSlot(String filename, int filesize, { String? contentType }) async { | ||||
|     if (!(await isSupported())) return MayFail.failure(errorNoUploadServer); | ||||
| 
 | ||||
|     if (_entityJid == null) { | ||||
|       logger.warning('Attempted to request HTTP File Upload slot but no entity is known to send this request to.'); | ||||
|       return MayFail.failure(errorNoUploadServer); | ||||
|     } | ||||
| 
 | ||||
|     if (_maxUploadSize != null && filesize > _maxUploadSize!) { | ||||
|       logger.warning('Attempted to request HTTP File Upload slot for a file that exceeds the filesize limit'); | ||||
|       return MayFail.failure(errorFileTooBig); | ||||
|     } | ||||
|      | ||||
|     final attrs = getAttributes(); | ||||
|     final response = await attrs.sendStanza( | ||||
|       Stanza.iq( | ||||
|         to: _entityJid.toString(), | ||||
|         type: 'get', | ||||
|         children: [ | ||||
|           XMLNode.xmlns( | ||||
|             tag: 'request', | ||||
|             xmlns: httpFileUploadXmlns, | ||||
|             attributes: { | ||||
|               'filename': filename, | ||||
|               'size': filesize.toString(), | ||||
|               ...contentType != null ? { 'content-type': contentType } : {} | ||||
|             }, | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (response.attributes['type']! != 'result') { | ||||
|       logger.severe('Failed to request HTTP File Upload slot.'); | ||||
|       // TODO(Unknown): Be more precise | ||||
|       return MayFail.failure(errorGeneric); | ||||
|     } | ||||
| 
 | ||||
|     final slot = response.firstTag('slot', xmlns: httpFileUploadXmlns)!; | ||||
|     final putUrl = slot.firstTag('put')!.attributes['url']! as String; | ||||
|     final getUrl = slot.firstTag('get')!.attributes['url']! as String; | ||||
|     final headers = Map<String, String>.fromEntries( | ||||
|       slot.findTags('header').map((tag) { | ||||
|         return MapEntry( | ||||
|           tag.attributes['name']! as String, | ||||
|           tag.innerText(), | ||||
|         ); | ||||
|       }), | ||||
|     ); | ||||
| 
 | ||||
|     return MayFail.success( | ||||
|       HttpFileUploadSlot( | ||||
|         putUrl, | ||||
|         getUrl, | ||||
|         prepareHeaders(headers), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										91
									
								
								moxxmpp/lib/src/xeps/xep_0380.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								moxxmpp/lib/src/xeps/xep_0380.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,91 @@ | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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'; | ||||
| 
 | ||||
| enum ExplicitEncryptionType { | ||||
|   otr, | ||||
|   legacyOpenPGP, | ||||
|   openPGP, | ||||
|   omemo, | ||||
|   omemo1, | ||||
|   omemo2, | ||||
|   unknown, | ||||
| } | ||||
| 
 | ||||
| String _explicitEncryptionTypeToString(ExplicitEncryptionType type) { | ||||
|   switch (type) { | ||||
|     case ExplicitEncryptionType.otr: return emeOtr; | ||||
|     case ExplicitEncryptionType.legacyOpenPGP: return emeLegacyOpenPGP; | ||||
|     case ExplicitEncryptionType.openPGP: return emeOpenPGP; | ||||
|     case ExplicitEncryptionType.omemo: return emeOmemo; | ||||
|     case ExplicitEncryptionType.omemo1: return emeOmemo1; | ||||
|     case ExplicitEncryptionType.omemo2: return emeOmemo2; | ||||
|     case ExplicitEncryptionType.unknown: return ''; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| ExplicitEncryptionType _explicitEncryptionTypeFromString(String str) { | ||||
|   switch (str) { | ||||
|     case emeOtr: return ExplicitEncryptionType.otr; | ||||
|     case emeLegacyOpenPGP: return ExplicitEncryptionType.legacyOpenPGP; | ||||
|     case emeOpenPGP: return ExplicitEncryptionType.openPGP; | ||||
|     case emeOmemo: return ExplicitEncryptionType.omemo; | ||||
|     case emeOmemo1: return ExplicitEncryptionType.omemo1; | ||||
|     case emeOmemo2: return ExplicitEncryptionType.omemo2; | ||||
|     default: return ExplicitEncryptionType.unknown; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// Create an <encryption /> element with [type] indicating which type of encryption was | ||||
| /// used. | ||||
| XMLNode buildEmeElement(ExplicitEncryptionType type) { | ||||
|   return XMLNode.xmlns( | ||||
|     tag: 'encryption', | ||||
|     xmlns: emeXmlns, | ||||
|     attributes: <String, String>{ | ||||
|       'namespace': _explicitEncryptionTypeToString(type), | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class EmeManager extends XmppManagerBase { | ||||
| 
 | ||||
|   EmeManager() : super(); | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => emeManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'EmeManager'; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
| 
 | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [emeXmlns]; | ||||
|    | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       tagName: 'encryption', | ||||
|       tagXmlns: emeXmlns, | ||||
|       callback: _onStanzaReceived, | ||||
|       // Before the message handler | ||||
|       priority: -99, | ||||
|     ), | ||||
|   ]; | ||||
| 
 | ||||
|   Future<StanzaHandlerData> _onStanzaReceived(Stanza message, StanzaHandlerData state) async { | ||||
|     final encryption = message.firstTag('encryption', xmlns: emeXmlns)!; | ||||
| 
 | ||||
|     return state.copyWith( | ||||
|       encryptionType: _explicitEncryptionTypeFromString( | ||||
|         encryption.attributes['namespace']! as String, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								moxxmpp/lib/src/xeps/xep_0384/crypto.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								moxxmpp/lib/src/xeps/xep_0384/crypto.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| 
 | ||||
| /// Checks the OMEMO affix elements. [envelope] refers to the  <envelope /> element we get | ||||
| /// after decrypting the payload. [sender] refers to the "to" attribute of the stanza. | ||||
| /// [ourJid] is our current full Jid. | ||||
| /// | ||||
| /// Returns true if the affix elements are all valid and as expected. Returns false if not. | ||||
| bool checkAffixElements(XMLNode envelope, String sender, JID ourJid) { | ||||
|   final from = envelope.firstTag('from')?.attributes['jid'] as String?; | ||||
|   if (from == null) return false; | ||||
|   final encSender = JID.fromString(from); | ||||
| 
 | ||||
|   final to = envelope.firstTag('to')?.attributes['jid'] as String?; | ||||
|   if (to == null) return false; | ||||
|   final encReceiver = JID.fromString(to); | ||||
| 
 | ||||
|   return encSender.toBare().toString() == JID.fromString(sender).toBare().toString() && | ||||
|     encReceiver.toBare().toString() == ourJid.toBare().toString(); | ||||
| } | ||||
							
								
								
									
										9
									
								
								moxxmpp/lib/src/xeps/xep_0384/errors.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								moxxmpp/lib/src/xeps/xep_0384/errors.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| abstract class OmemoError {} | ||||
| 
 | ||||
| class UnknownOmemoError extends OmemoError {} | ||||
| 
 | ||||
| class InvalidAffixElementsException with Exception {} | ||||
| 
 | ||||
| class OmemoNotSupportedForContactException extends OmemoError {} | ||||
| 
 | ||||
| class EncryptionFailedException with Exception {} | ||||
							
								
								
									
										82
									
								
								moxxmpp/lib/src/xeps/xep_0384/helpers.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								moxxmpp/lib/src/xeps/xep_0384/helpers.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | ||||
| import 'dart:math'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:omemo_dart/omemo_dart.dart'; | ||||
| import 'package:random_string/random_string.dart'; | ||||
| 
 | ||||
| /// Generate a random alpha-numeric string with a random length between 0 and 200 in | ||||
| /// accordance to XEP-0420's rpad affix element. | ||||
| String generateRpad() { | ||||
|   final random = Random.secure(); | ||||
|   final length = random.nextInt(200); | ||||
|   return randomAlphaNumeric(length, provider: CoreRandomProvider.from(random)); | ||||
| } | ||||
| 
 | ||||
| /// Convert the XML representation of an OMEMO bundle into an OmemoBundle object. | ||||
| /// [jid] refers to the JID the bundle belongs to. [id] refers to the bundle's device | ||||
| /// identifier. [bundle] refers to the <bundle /> element. | ||||
| /// | ||||
| /// Returns the OmemoBundle. | ||||
| OmemoBundle bundleFromXML(JID jid, int id, XMLNode bundle) { | ||||
|   assert(bundle.attributes['xmlns'] == omemoXmlns, 'Invalid xmlns'); | ||||
| 
 | ||||
|   final spk = bundle.firstTag('spk')!; | ||||
|   final prekeys = <int, String>{}; | ||||
|   for (final pk in bundle.firstTag('prekeys')!.findTags('pk')) { | ||||
|     prekeys[int.parse(pk.attributes['id']! as String)] = pk.innerText(); | ||||
|   } | ||||
| 
 | ||||
|   return OmemoBundle( | ||||
|     jid.toBare().toString(), | ||||
|     id, | ||||
|     spk.innerText(), | ||||
|     int.parse(spk.attributes['id']! as String), | ||||
|     bundle.firstTag('spks')!.innerText(), | ||||
|     bundle.firstTag('ik')!.innerText(), | ||||
|     prekeys, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| /// Converts an OmemoBundle [bundle] into its XML representation. | ||||
| /// | ||||
| /// Returns the XML element. | ||||
| XMLNode bundleToXML(OmemoBundle bundle) { | ||||
|   final prekeys = List<XMLNode>.empty(growable: true); | ||||
|   for (final pk in bundle.opksEncoded.entries) { | ||||
|     prekeys.add( | ||||
|       XMLNode( | ||||
|         tag: 'pk', attributes: <String, String>{ | ||||
|           'id': '${pk.key}', | ||||
|         }, | ||||
|         text: pk.value, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return XMLNode.xmlns( | ||||
|     tag: 'bundle', | ||||
|     xmlns: omemoXmlns, | ||||
|     children: [ | ||||
|       XMLNode( | ||||
|         tag: 'spk', | ||||
|         attributes: <String, String>{ | ||||
|           'id': '${bundle.spkId}', | ||||
|         }, | ||||
|         text: bundle.spkEncoded, | ||||
|       ), | ||||
|       XMLNode( | ||||
|         tag: 'spks', | ||||
|         text: bundle.spkSignatureEncoded, | ||||
|       ), | ||||
|       XMLNode( | ||||
|         tag: 'ik', | ||||
|         text: bundle.ikEncoded, | ||||
|       ), | ||||
|       XMLNode( | ||||
|         tag: 'prekeys', | ||||
|         children: prekeys, | ||||
|       ), | ||||
|     ], | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										7
									
								
								moxxmpp/lib/src/xeps/xep_0384/types.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								moxxmpp/lib/src/xeps/xep_0384/types.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| /// A simple wrapper class for defining elements that should not be encrypted. | ||||
| class DoNotEncrypt { | ||||
| 
 | ||||
|   const DoNotEncrypt(this.tag, this.xmlns); | ||||
|   final String tag; | ||||
|   final String xmlns; | ||||
| } | ||||
							
								
								
									
										953
									
								
								moxxmpp/lib/src/xeps/xep_0384/xep_0384.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										953
									
								
								moxxmpp/lib/src/xeps/xep_0384/xep_0384.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,953 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:collection'; | ||||
| import 'dart:convert'; | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:moxlib/moxlib.dart'; | ||||
| import 'package:moxxmpp/src/events.dart'; | ||||
| import 'package:moxxmpp/src/jid.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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/types/resultv2.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_0060/errors.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0334.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0380.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0384/crypto.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0384/errors.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0384/helpers.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0384/types.dart'; | ||||
| import 'package:omemo_dart/omemo_dart.dart'; | ||||
| import 'package:synchronized/synchronized.dart'; | ||||
| 
 | ||||
| const _doNotEncryptList = [ | ||||
|   // XEP-0033 | ||||
|   DoNotEncrypt('addresses', extendedAddressingXmlns), | ||||
|   // XEP-0060 | ||||
|   DoNotEncrypt('pubsub', pubsubXmlns), | ||||
|   DoNotEncrypt('pubsub', pubsubOwnerXmlns), | ||||
|   // XEP-0334 | ||||
|   DoNotEncrypt('no-permanent-store', messageProcessingHintsXmlns), | ||||
|   DoNotEncrypt('no-store', messageProcessingHintsXmlns), | ||||
|   DoNotEncrypt('no-copy', messageProcessingHintsXmlns), | ||||
|   DoNotEncrypt('store', messageProcessingHintsXmlns), | ||||
|   // XEP-0359 | ||||
|   DoNotEncrypt('origin-id', stableIdXmlns), | ||||
|   DoNotEncrypt('stanza-id', stableIdXmlns), | ||||
| ]; | ||||
| 
 | ||||
| abstract class OmemoManager extends XmppManagerBase { | ||||
| 
 | ||||
|   OmemoManager() : _handlerLock = Lock(), _handlerFutures = {}, super(); | ||||
| 
 | ||||
|   final Lock _handlerLock; | ||||
|   final Map<JID, Queue<Completer<void>>> _handlerFutures; | ||||
| 
 | ||||
|   final Map<JID, List<int>> _deviceMap = {}; | ||||
| 
 | ||||
|   // Mapping whether we already tried to subscribe to the JID's devices node | ||||
|   final Map<JID, bool> _subscriptionMap = {}; | ||||
|    | ||||
|   @override | ||||
|   String getId() => omemoManager; | ||||
| 
 | ||||
|   @override | ||||
|   String getName() => 'OmemoManager'; | ||||
| 
 | ||||
|   // TODO(Unknown): Technically, this is not always true | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'iq', | ||||
|       tagXmlns: omemoXmlns, | ||||
|       tagName: 'encrypted', | ||||
|       callback: _onIncomingStanza, | ||||
|       priority: 9999, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'presence', | ||||
|       tagXmlns: omemoXmlns, | ||||
|       tagName: 'encrypted', | ||||
|       callback: _onIncomingStanza, | ||||
|       priority: 9999, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagXmlns: omemoXmlns, | ||||
|       tagName: 'encrypted', | ||||
|       callback: _onIncomingStanza, | ||||
|       priority: -98, | ||||
|     ), | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getOutgoingPreStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'iq', | ||||
|       callback: _onOutgoingStanza, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'presence', | ||||
|       callback: _onOutgoingStanza, | ||||
|     ), | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       callback: _onOutgoingStanza, | ||||
|       priority: 100, | ||||
|     ), | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> onXmppEvent(XmppEvent event) async { | ||||
|     if (event is PubSubNotificationEvent) { | ||||
|       if (event.item.node != omemoDevicesXmlns) return; | ||||
| 
 | ||||
|       logger.finest('Received PubSub device notification for ${event.from}'); | ||||
|       final ownJid = getAttributes().getFullJID().toBare().toString(); | ||||
|       final jid = JID.fromString(event.from).toBare(); | ||||
|       final ids = event.item.payload.children | ||||
|         .map((child) => int.parse(child.attributes['id']! as String)) | ||||
|         .toList(); | ||||
| 
 | ||||
|       if (event.from == ownJid) { | ||||
|         // Another client published to our device list node | ||||
|         if (!ids.contains(await _getDeviceId())) { | ||||
|           // Attempt to publish again | ||||
|           unawaited(publishBundle(await _getDeviceBundle())); | ||||
|         } | ||||
|       } else { | ||||
|         // Someone published to their device list node | ||||
|         logger.finest('Got devices $ids'); | ||||
|         _deviceMap[jid] = ids; | ||||
|       } | ||||
| 
 | ||||
|       // Generate an event | ||||
|       getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids)); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   @visibleForOverriding | ||||
|   Future<OmemoSessionManager> getSessionManager(); | ||||
| 
 | ||||
|   /// Wrapper around using getSessionManager and then calling encryptToJids on it. | ||||
|   Future<EncryptionResult> _encryptToJids(List<String> jids, String? plaintext, { List<OmemoBundle>? newSessions }) async { | ||||
|     final session = await getSessionManager(); | ||||
|     return session.encryptToJids(jids, plaintext, newSessions: newSessions); | ||||
|   } | ||||
| 
 | ||||
|   /// Wrapper around using getSessionManager and then calling encryptToJids on it. | ||||
|   Future<String?> _decryptMessage(List<int>? ciphertext, String senderJid, int senderDeviceId, List<EncryptedKey> keys, int sendTimestamp) async { | ||||
|     final session = await getSessionManager(); | ||||
|     return session.decryptMessage( | ||||
|       ciphertext, | ||||
|       senderJid, | ||||
|       senderDeviceId, | ||||
|       keys, | ||||
|       sendTimestamp, | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   /// Wrapper around using getSessionManager and then calling getDeviceId on it. | ||||
|   Future<int> _getDeviceId() async { | ||||
|     final session = await getSessionManager(); | ||||
|     return session.getDeviceId(); | ||||
|   } | ||||
| 
 | ||||
|   /// Wrapper around using getSessionManager and then calling getDeviceId on it. | ||||
|   Future<OmemoBundle> _getDeviceBundle() async { | ||||
|     final session = await getSessionManager(); | ||||
|     return session.getDeviceBundle(); | ||||
|   } | ||||
| 
 | ||||
|   /// Wrapper around using getSessionManager and then calling isRatchetAcknowledged on it. | ||||
|   Future<bool> _isRatchetAcknowledged(String jid, int deviceId) async { | ||||
|     final session = await getSessionManager(); | ||||
|     return session.isRatchetAcknowledged(jid, deviceId); | ||||
|   } | ||||
| 
 | ||||
|   /// Wrapper around checking if [jid] appears in the session manager's device map. | ||||
|   Future<bool> _hasSessionWith(String jid) async { | ||||
|     final session = await getSessionManager(); | ||||
|     final deviceMap = await session.getDeviceMap(); | ||||
|     return deviceMap.containsKey(jid); | ||||
|   } | ||||
|    | ||||
|   /// Determines what child elements of a stanza should be encrypted. If shouldEncrypt | ||||
|   /// returns true for [element], then [element] will be encrypted. If shouldEncrypt | ||||
|   /// returns false, then [element] won't be encrypted. | ||||
|   /// | ||||
|   /// The default implementation ignores all elements that are mentioned in XEP-0420, i.e.: | ||||
|   /// - XEP-0033 elements (<addresses />) | ||||
|   /// - XEP-0334 elements (<store/>, <no-copy/>, <no-store/>, <no-permanent-store/>) | ||||
|   /// - XEP-0359 elements (<origin-id />, <stanza-id />) | ||||
|   @visibleForOverriding | ||||
|   bool shouldEncryptElement(XMLNode element) { | ||||
|     for (final ignore in _doNotEncryptList) { | ||||
|       final xmlns = element.attributes['xmlns'] ?? ''; | ||||
|       if (element.tag == ignore.tag && xmlns == ignore.xmlns) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /// Encrypt [children] using OMEMO. This either produces an <encrypted /> element with | ||||
|   /// an attached payload, if [children] is not null, or an empty OMEMO message if | ||||
|   /// [children] is null. This function takes care of creating the affix elements as | ||||
|   /// specified by both XEP-0420 and XEP-0384. | ||||
|   /// [jids] is the list of JIDs the payload should be encrypted for. | ||||
|   Future<XMLNode> _encryptChildren(List<XMLNode>? children, List<String> jids, String toJid, List<OmemoBundle> newSessions) async { | ||||
|     XMLNode? payload; | ||||
|     if (children != null) { | ||||
|       payload = XMLNode.xmlns( | ||||
|         tag: 'envelope', | ||||
|         xmlns: sceXmlns, | ||||
|         children: [ | ||||
|           XMLNode( | ||||
|             tag: 'content', | ||||
|             children: children, | ||||
|           ), | ||||
| 
 | ||||
|           XMLNode( | ||||
|             tag: 'rpad', | ||||
|             text: generateRpad(), | ||||
|           ), | ||||
|           XMLNode( | ||||
|             tag: 'to', | ||||
|             attributes: <String, String>{ | ||||
|               'jid': toJid, | ||||
|             }, | ||||
|           ), | ||||
|           XMLNode( | ||||
|             tag: 'from', | ||||
|             attributes: <String, String>{ | ||||
|               'jid': getAttributes().getFullJID().toString(), | ||||
|             }, | ||||
|           ), | ||||
|           /* | ||||
|           XMLNode( | ||||
|             tag: 'time', | ||||
|             // TODO(Unknown): Implement | ||||
|             attributes: <String, String>{ | ||||
|               'stamp': '', | ||||
|             }, | ||||
|           ), | ||||
|           */ | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     final encryptedEnvelope = await _encryptToJids( | ||||
|       jids, | ||||
|       payload?.toXml(), | ||||
|       newSessions: newSessions, | ||||
|     ); | ||||
| 
 | ||||
|     final keyElements = <String, List<XMLNode>>{}; | ||||
|     for (final key in encryptedEnvelope.encryptedKeys) { | ||||
|       final keyElement = XMLNode( | ||||
|         tag: 'key', | ||||
|         attributes: <String, String>{ | ||||
|           'rid': '${key.rid}', | ||||
|           'kex': key.kex ? 'true' : 'false', | ||||
|         }, | ||||
|         text: key.value, | ||||
|       ); | ||||
| 
 | ||||
|       if (keyElements.containsKey(key.jid)) { | ||||
|         keyElements[key.jid]!.add(keyElement); | ||||
|       } else { | ||||
|         keyElements[key.jid] = [keyElement]; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     final keysElements = keyElements.entries.map((entry) { | ||||
|       return XMLNode( | ||||
|         tag: 'keys', | ||||
|         attributes: <String, String>{ | ||||
|           'jid': entry.key, | ||||
|         }, | ||||
|         children: entry.value, | ||||
|       ); | ||||
|     }).toList(); | ||||
| 
 | ||||
|     var payloadElement = <XMLNode>[]; | ||||
|     if (payload != null) { | ||||
|       payloadElement = [ | ||||
|         XMLNode( | ||||
|           tag: 'payload', | ||||
|           text: base64.encode(encryptedEnvelope.ciphertext!), | ||||
|         ), | ||||
|       ]; | ||||
|     } | ||||
|      | ||||
|     return XMLNode.xmlns( | ||||
|       tag: 'encrypted', | ||||
|       xmlns: omemoXmlns, | ||||
|       children: [ | ||||
|         ...payloadElement, | ||||
|         XMLNode( | ||||
|           tag: 'header', | ||||
|           attributes: <String, String>{ | ||||
|             'sid': (await _getDeviceId()).toString(), | ||||
|           }, | ||||
|           children: keysElements, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// A logging wrapper around acking the ratchet with [jid] with identifier [deviceId]. | ||||
|   Future<void> _ackRatchet(String jid, int deviceId) async { | ||||
|     logger.finest('Acking ratchet $jid:$deviceId'); | ||||
|     final session = await getSessionManager(); | ||||
|     await session.ratchetAcknowledged(jid, deviceId); | ||||
|   } | ||||
| 
 | ||||
|   /// Figure out if new sessions need to be built. [toJid] is the JID of the entity we | ||||
|   /// want to send a message to. [children] refers to the unencrypted children of the | ||||
|   /// message. They are required to be passed because shouldIgnoreUnackedRatchets is | ||||
|   /// called here. | ||||
|   /// | ||||
|   /// Either returns a list of bundles we "need" to build a session with or an OmemoError. | ||||
|   Future<Result<OmemoError, List<OmemoBundle>>> _findNewSessions(JID toJid, List<XMLNode> children) async { | ||||
|     final ownJid = getAttributes().getFullJID().toBare(); | ||||
|     final session = await getSessionManager(); | ||||
|     final ownId = await session.getDeviceId(); | ||||
| 
 | ||||
|     // Ignore our own device if it is the only published device on our devices node | ||||
|     if (toJid.toBare() == ownJid) { | ||||
|       final deviceList = await getDeviceList(ownJid); | ||||
|       if (deviceList.isType<List<int>>()) { | ||||
|         final devices = deviceList.get<List<int>>(); | ||||
|         if (devices.length == 1 && devices.first == ownId) { | ||||
|           return const Result(<OmemoBundle>[]); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     final newSessions = List<OmemoBundle>.empty(growable: true); | ||||
|     final sessionAvailable = await _hasSessionWith(toJid.toString());    | ||||
|     if (!sessionAvailable) { | ||||
|       logger.finest('No session for $toJid. Retrieving bundles to build a new session.'); | ||||
|       final result = await retrieveDeviceBundles(toJid); | ||||
|       if (result.isType<List<OmemoBundle>>()) { | ||||
|         final bundles = result.get<List<OmemoBundle>>(); | ||||
| 
 | ||||
|         if (ownJid == toJid) { | ||||
|           logger.finest('Requesting bundles for own JID. Ignoring current device'); | ||||
|           newSessions.addAll(bundles.where((bundle) => bundle.id != ownId)); | ||||
|         } else { | ||||
|           newSessions.addAll(bundles); | ||||
|         } | ||||
|       } else { | ||||
|         logger.warning('Failed to retrieve device bundles for $toJid'); | ||||
|         return Result(OmemoNotSupportedForContactException()); | ||||
|       } | ||||
| 
 | ||||
|       if (!_subscriptionMap.containsKey(toJid)) { | ||||
|         await subscribeToDeviceList(toJid); | ||||
|       } | ||||
|     } else { | ||||
|       final toBare = toJid.toBare(); | ||||
|       final ratchetSessions = (await session.getDeviceMap())[toBare.toString()]!; | ||||
|       final deviceMapRaw = await getDeviceList(toBare); | ||||
|       if (!_subscriptionMap.containsKey(toBare)) { | ||||
|         unawaited(subscribeToDeviceList(toBare)); | ||||
|       } | ||||
| 
 | ||||
|       if (deviceMapRaw.isType<OmemoError>()) { | ||||
|         logger.warning('Failed to get device list'); | ||||
|         return Result(UnknownOmemoError()); | ||||
|       } | ||||
| 
 | ||||
|       final deviceList = deviceMapRaw.get<List<int>>(); | ||||
|       for (final id in deviceList) { | ||||
|         // We already have a session with that device | ||||
|         if (ratchetSessions.contains(id)) continue; | ||||
| 
 | ||||
|         // Ignore requests for our own device. | ||||
|         if (toJid == ownJid && id == ownId) { | ||||
|           logger.finest('Attempted to request bundle for our own device $id, which is the current device. Skipping request...'); | ||||
|           continue; | ||||
|         } | ||||
| 
 | ||||
|         logger.finest('Retrieving bundle for $toJid:$id'); | ||||
|         final bundle = await retrieveDeviceBundle(toJid, id); | ||||
|         if (bundle.isType<OmemoBundle>()) { | ||||
|           newSessions.add(bundle.get<OmemoBundle>()); | ||||
|         } else { | ||||
|           logger.warning('Failed to retrieve bundle for $toJid:$id'); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return Result(newSessions); | ||||
|   } | ||||
| 
 | ||||
|   /// Sends an empty Omemo message to [toJid]. | ||||
|   /// | ||||
|   /// If [findNewSessions] is true, then | ||||
|   /// new devices will be looked for first before sending the message. This means that | ||||
|   /// the new sessions will be included in the empty Omemo message. If false, then no | ||||
|   /// new sessions will be looked for before encrypting. | ||||
|   /// | ||||
|   /// [calledFromCriticalSection] MUST NOT be used from outside the manager. If true, then | ||||
|   /// sendEmptyMessage will not attempt to enter the critical section guarding the | ||||
|   /// encryption and decryption. If false, then the critical section will be entered before | ||||
|   /// encryption and left after sending the message. | ||||
|   Future<void> sendEmptyMessage(JID toJid, { | ||||
|     bool findNewSessions = false, | ||||
|     @protected | ||||
|     bool calledFromCriticalSection = false, | ||||
|   }) async { | ||||
|     if (!calledFromCriticalSection) { | ||||
|       final completer = await _handlerEntry(toJid); | ||||
|       if (completer != null) { | ||||
|         await completer.future; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     var newSessions = <OmemoBundle>[]; | ||||
|     if (findNewSessions) { | ||||
|       final result = await _findNewSessions(toJid, <XMLNode>[]); | ||||
|       if (!result.isType<OmemoError>()) newSessions = result.get<List<OmemoBundle>>(); | ||||
|     } | ||||
| 
 | ||||
|     final empty = await _encryptChildren( | ||||
|       null, | ||||
|       [toJid.toString()], | ||||
|       toJid.toString(), | ||||
|       newSessions, | ||||
|     ); | ||||
| 
 | ||||
|     await getAttributes().sendStanza( | ||||
|       Stanza.message( | ||||
|         to: toJid.toString(), | ||||
|         type: 'chat', | ||||
|         children: [ | ||||
|           empty, | ||||
| 
 | ||||
|           // Add a storage hint in case this is a message | ||||
|           // Taken from the example at | ||||
|           // https://xmpp.org/extensions/xep-0384.html#message-structure-description. | ||||
|           MessageProcessingHint.store.toXml(), | ||||
|         ], | ||||
|       ), | ||||
|       awaitable: false, | ||||
|       encrypted: true, | ||||
|     ); | ||||
| 
 | ||||
|     if (!calledFromCriticalSection) { | ||||
|       await _handlerExit(toJid); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   Future<StanzaHandlerData> _onOutgoingStanza(Stanza stanza, StanzaHandlerData state) async { | ||||
|     if (state.encrypted) { | ||||
|       logger.finest('Not encrypting since state.encrypted is true'); | ||||
|       return state; | ||||
|     } | ||||
| 
 | ||||
|     if (stanza.to == null) { | ||||
|       // We cannot encrypt in this case. | ||||
|       return state; | ||||
|     } | ||||
| 
 | ||||
|     final toJid = JID.fromString(stanza.to!).toBare(); | ||||
|     if (!(await shouldEncryptStanza(toJid, stanza))) { | ||||
|       logger.finest('shouldEncryptStanza returned false for message to $toJid. Not encrypting.'); | ||||
|       return state; | ||||
|     } else { | ||||
|       logger.finest('shouldEncryptStanza returned true for message to $toJid.'); | ||||
|     } | ||||
|      | ||||
|     final completer = await _handlerEntry(toJid); | ||||
|     if (completer != null) { | ||||
|       await completer.future; | ||||
|     } | ||||
| 
 | ||||
|     final newSessions = List<OmemoBundle>.empty(growable: true); | ||||
|     // Try to find new sessions for [toJid]. | ||||
|     final resultToJid = await _findNewSessions(toJid, stanza.children); | ||||
|     if (resultToJid.isType<List<OmemoBundle>>()) { | ||||
|       newSessions.addAll(resultToJid.get<List<OmemoBundle>>()); | ||||
|     } else { | ||||
|       if (resultToJid.isType<OmemoNotSupportedForContactException>()) { | ||||
|         await _handlerExit(toJid); | ||||
|         return state.copyWith( | ||||
|           cancel: true, | ||||
|           cancelReason: resultToJid.get<OmemoNotSupportedForContactException>(), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Try to find new sessions for our own Jid. | ||||
|     final ownJid = getAttributes().getFullJID().toBare(); | ||||
|     final resultOwnJid = await _findNewSessions(ownJid, stanza.children); | ||||
|     if (resultOwnJid.isType<List<OmemoBundle>>()) { | ||||
|       newSessions.addAll(resultOwnJid.get<List<OmemoBundle>>()); | ||||
|     } | ||||
|      | ||||
|     final toEncrypt = List<XMLNode>.empty(growable: true); | ||||
|     final children = List<XMLNode>.empty(growable: true); | ||||
|     for (final child in stanza.children) { | ||||
|       if (!shouldEncryptElement(child)) { | ||||
|         children.add(child); | ||||
|       } else { | ||||
|         toEncrypt.add(child); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     final jidsToEncryptFor = <String>[JID.fromString(stanza.to!).toBare().toString()]; | ||||
|     // Prevent encrypting to self if there is only one device (ours). | ||||
|     if (await _hasSessionWith(ownJid.toString())) { | ||||
|       jidsToEncryptFor.add(ownJid.toString()); | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       logger.finest('Encrypting stanza'); | ||||
|       final encrypted = await _encryptChildren( | ||||
|         toEncrypt, | ||||
|         jidsToEncryptFor, | ||||
|         stanza.to!, | ||||
|         newSessions, | ||||
|       ); | ||||
|       logger.finest('Encryption done'); | ||||
| 
 | ||||
|       children.add(encrypted); | ||||
| 
 | ||||
|       // Only add EME when sending a message | ||||
|       if (stanza.tag == 'message') { | ||||
|         children.add(buildEmeElement(ExplicitEncryptionType.omemo2)); | ||||
|       } | ||||
| 
 | ||||
|       // Add a storage hint in case this is a message | ||||
|       // Taken from the example at | ||||
|       // https://xmpp.org/extensions/xep-0384.html#message-structure-description. | ||||
|       if (stanza.tag == 'message') { | ||||
|         children.add(MessageProcessingHint.store.toXml()); | ||||
|       } | ||||
|        | ||||
|       await _handlerExit(toJid); | ||||
|       return state.copyWith( | ||||
|         stanza: state.stanza.copyWith( | ||||
|           children: children, | ||||
|         ), | ||||
|         encrypted: true, | ||||
|       ); | ||||
|     } catch (ex) { | ||||
|       logger.severe('Encryption failed! $ex'); | ||||
|       await _handlerExit(toJid); | ||||
|       return state.copyWith( | ||||
|         cancel: true, | ||||
|         cancelReason: EncryptionFailedException(), | ||||
|         other: { | ||||
|           ...state.other, | ||||
|           'encryption_error': ex, | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// This function returns true if the encryption scheme should ignore unacked ratchets | ||||
|   /// and don't try to build a new ratchet even though there are unacked ones. | ||||
|   /// The current logic is that chat states with no body ignore the "ack" state of the | ||||
|   /// ratchets. | ||||
|   /// | ||||
|   /// This function may be overriden. By default, the ack status of the ratchet is ignored | ||||
|   /// if we're sending a message containing chatstates or chat markers and the message does | ||||
|   /// not contain a <body /> element. | ||||
|   @visibleForOverriding | ||||
|   bool shouldIgnoreUnackedRatchets(List<XMLNode> children) { | ||||
|     return listContains( | ||||
|       children, | ||||
|       (XMLNode child) { | ||||
|         return child.attributes['xmlns'] == chatStateXmlns || child.attributes['xmlns'] == chatMarkersXmlns; | ||||
|       }, | ||||
|     ) && !listContains( | ||||
|       children, | ||||
|       (XMLNode child) => child.tag == 'body', | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// This function is called whenever a message is to be encrypted. If it returns true, | ||||
|   /// then the message will be encrypted. If it returns false, the message won't be | ||||
|   /// encrypted. | ||||
|   @visibleForOverriding | ||||
|   Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza); | ||||
|    | ||||
|   /// Wrapper function that attempts to enter the encryption/decryption critical section. | ||||
|   /// In case the critical section could be entered, null is returned. If not, then a | ||||
|   /// Completer is returned whose future will resolve once the critical section can be | ||||
|   /// entered. | ||||
|   Future<Completer<void>?> _handlerEntry(JID fromJid) async { | ||||
|     return _handlerLock.synchronized(() { | ||||
|       if (_handlerFutures.containsKey(fromJid)) { | ||||
|         final c = Completer<void>(); | ||||
|         _handlerFutures[fromJid]!.addLast(c); | ||||
|         return c; | ||||
|       } | ||||
| 
 | ||||
|       _handlerFutures[fromJid] = Queue(); | ||||
|       return null; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /// Wrapper function that exits the critical section. | ||||
|   Future<void> _handlerExit(JID fromJid) async { | ||||
|     await _handlerLock.synchronized(() { | ||||
|       if (_handlerFutures.containsKey(fromJid)) { | ||||
|         if (_handlerFutures[fromJid]!.isEmpty) { | ||||
|           _handlerFutures.remove(fromJid); | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         _handlerFutures[fromJid]!.removeFirst().complete(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   Future<StanzaHandlerData> _onIncomingStanza(Stanza stanza, StanzaHandlerData state) async { | ||||
|     final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns); | ||||
|     if (encrypted == null) return state; | ||||
|     if (stanza.from == null) return state; | ||||
| 
 | ||||
|     final fromJid = JID.fromString(stanza.from!).toBare(); | ||||
|     final completer = await _handlerEntry(fromJid); | ||||
|     if (completer != null) { | ||||
|       await completer.future; | ||||
|     } | ||||
|      | ||||
|     final header = encrypted.firstTag('header')!; | ||||
|     final payloadElement = encrypted.firstTag('payload'); | ||||
|     final keys = List<EncryptedKey>.empty(growable: true); | ||||
|     for (final keysElement in header.findTags('keys')) { | ||||
|       final jid = keysElement.attributes['jid']! as String; | ||||
|       for (final key in keysElement.findTags('key')) { | ||||
|         keys.add( | ||||
|           EncryptedKey( | ||||
|             jid, | ||||
|             int.parse(key.attributes['rid']! as String), | ||||
|             key.innerText(), | ||||
|             key.attributes['kex'] == 'true', | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     final ourJid = getAttributes().getFullJID(); | ||||
|     final sid = int.parse(header.attributes['sid']! as String); | ||||
| 
 | ||||
|     // Ensure that if we receive a message from a device that we don't know about, we | ||||
|     // ensure that _deviceMap is up-to-date. | ||||
|     final devices = _deviceMap[fromJid] ?? <int>[]; | ||||
|     if (!devices.contains(sid)) { | ||||
|       await getDeviceList(fromJid); | ||||
|     } | ||||
| 
 | ||||
|     String? decrypted; | ||||
|     try { | ||||
|       decrypted = await _decryptMessage( | ||||
|         payloadElement != null ? base64.decode(payloadElement.innerText()) : null, | ||||
|         fromJid.toString(), | ||||
|         sid, | ||||
|         keys, | ||||
|         state.delayedDelivery?.timestamp.millisecondsSinceEpoch ?? DateTime.now().millisecondsSinceEpoch, | ||||
|       ); | ||||
|     } catch (ex) { | ||||
|       logger.warning('Error occurred during message decryption: $ex'); | ||||
| 
 | ||||
|       await _handlerExit(fromJid); | ||||
|       return state.copyWith( | ||||
|         other: { | ||||
|           ...state.other, | ||||
|           'encryption_error': ex, | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|      | ||||
|     final isAcked = await _isRatchetAcknowledged(fromJid.toString(), sid); | ||||
|     if (!isAcked) { | ||||
|       // Unacked ratchet decrypted this message | ||||
|       if (decrypted != null) { | ||||
|         // The message is not empty, i.e. contains content | ||||
|         logger.finest('Received non-empty OMEMO encrypted message for unacked ratchet. Acking with empty OMEMO message.'); | ||||
| 
 | ||||
|         await _ackRatchet(fromJid.toString(), sid); | ||||
|         await sendEmptyMessage(fromJid, calledFromCriticalSection: true); | ||||
| 
 | ||||
|         final envelope = XMLNode.fromString(decrypted); | ||||
|         final children = stanza.children.where( | ||||
|           (child) => child.tag != 'encrypted' || child.attributes['xmlns'] != omemoXmlns, | ||||
|         ).toList() | ||||
|           ..addAll(envelope.firstTag('content')!.children); | ||||
| 
 | ||||
|         final other = Map<String, dynamic>.from(state.other); | ||||
|         if (!checkAffixElements(envelope, stanza.from!, ourJid)) { | ||||
|           other['encryption_error'] = InvalidAffixElementsException(); | ||||
|         } | ||||
| 
 | ||||
|         await _handlerExit(fromJid); | ||||
|         return state.copyWith( | ||||
|           encrypted: true, | ||||
|           stanza: Stanza( | ||||
|             to: stanza.to, | ||||
|             from: stanza.from, | ||||
|             id: stanza.id, | ||||
|             type: stanza.type, | ||||
|             children: children, | ||||
|             tag: stanza.tag, | ||||
|             attributes: Map<String, String>.from(stanza.attributes), | ||||
|           ), | ||||
|           other: other, | ||||
|         ); | ||||
|       } else { | ||||
|         logger.info('Received empty OMEMO message for unacked ratchet. Marking $fromJid:$sid as acked'); | ||||
|         await _ackRatchet(fromJid.toString(), sid); | ||||
| 
 | ||||
|         final ownId = await (await getSessionManager()).getDeviceId(); | ||||
|         final kex = keys.any((key) => key.kex && key.rid == ownId); | ||||
|         if (kex) { | ||||
|           logger.info('Empty OMEMO message contained a kex. Answering.'); | ||||
|           await sendEmptyMessage(fromJid, calledFromCriticalSection: true); | ||||
|         } | ||||
| 
 | ||||
|         await _handlerExit(fromJid); | ||||
|         return state; | ||||
|       } | ||||
|     } else { | ||||
|       // The ratchet that decrypted the message was acked | ||||
|       if (decrypted != null) { | ||||
|         final envelope = XMLNode.fromString(decrypted); | ||||
| 
 | ||||
|         final children = stanza.children.where( | ||||
|           (child) => child.tag != 'encrypted' || child.attributes['xmlns'] != omemoXmlns, | ||||
|         ).toList() | ||||
|           ..addAll(envelope.firstTag('content')!.children); | ||||
| 
 | ||||
|         final other = Map<String, dynamic>.from(state.other); | ||||
|         if (!checkAffixElements(envelope, stanza.from!, ourJid)) { | ||||
|           other['encryption_error'] = InvalidAffixElementsException(); | ||||
|         } | ||||
|          | ||||
|         await _handlerExit(fromJid); | ||||
|         return state.copyWith( | ||||
|           encrypted: true, | ||||
|           stanza: Stanza( | ||||
|             to: stanza.to, | ||||
|             from: stanza.from, | ||||
|             id: stanza.id, | ||||
|             type: stanza.type, | ||||
|             children: children, | ||||
|             tag: stanza.tag, | ||||
|             attributes: Map<String, String>.from(stanza.attributes), | ||||
|           ), | ||||
|           other: other, | ||||
|         ); | ||||
|       } else { | ||||
|         logger.info('Received empty OMEMO message on acked ratchet. Doing nothing'); | ||||
|         await _handlerExit(fromJid); | ||||
|         return state; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Convenience function that attempts to retrieve the raw XML payload from the | ||||
|   /// device list PubSub node. | ||||
|   /// | ||||
|   /// On success, returns the XML data. On failure, returns an OmemoError. | ||||
|   Future<Result<OmemoError, XMLNode>> _retrieveDeviceListPayload(JID jid) async { | ||||
|     final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; | ||||
|     final result = await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns); | ||||
|     if (result.isType<PubSubError>()) return Result(UnknownOmemoError()); | ||||
|     return Result(result.get<List<PubSubItem>>().first.payload); | ||||
|   } | ||||
|    | ||||
|   /// Retrieves the OMEMO device list from [jid]. | ||||
|   Future<Result<OmemoError, List<int>>> getDeviceList(JID jid) async { | ||||
|     if (_deviceMap.containsKey(jid)) return Result(_deviceMap[jid]); | ||||
| 
 | ||||
|     final itemsRaw = await _retrieveDeviceListPayload(jid); | ||||
|     if (itemsRaw.isType<OmemoError>()) return Result(UnknownOmemoError()); | ||||
| 
 | ||||
|     final ids = itemsRaw.get<XMLNode>().children | ||||
|       .map((child) => int.parse(child.attributes['id']! as String)) | ||||
|       .toList(); | ||||
|     _deviceMap[jid] = ids; | ||||
|     return Result(ids); | ||||
|   } | ||||
| 
 | ||||
|   /// Retrieve all device bundles for the JID [jid]. | ||||
|   /// | ||||
|   /// On success, returns a list of devices. On failure, returns am OmemoError. | ||||
|   Future<Result<OmemoError, List<OmemoBundle>>> retrieveDeviceBundles(JID jid) async { | ||||
|     // TODO(Unknown): Should we query the device list first? | ||||
|     final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; | ||||
|     final bundlesRaw = await pm.getItems(jid.toString(), omemoBundlesXmlns); | ||||
|     if (bundlesRaw.isType<PubSubError>()) return Result(UnknownOmemoError()); | ||||
| 
 | ||||
|     final bundles = bundlesRaw.get<List<PubSubItem>>().map( | ||||
|       (bundle) => bundleFromXML(jid, int.parse(bundle.id), bundle.payload), | ||||
|     ).toList(); | ||||
| 
 | ||||
|     return Result(bundles); | ||||
|   } | ||||
|    | ||||
|   /// Retrieves a bundle from entity [jid] with the device id [deviceId]. | ||||
|   /// | ||||
|   /// On success, returns the device bundle. On failure, returns an OmemoError. | ||||
|   Future<Result<OmemoError, OmemoBundle>> retrieveDeviceBundle(JID jid, int deviceId) async { | ||||
|     final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; | ||||
|     final bareJid = jid.toBare().toString(); | ||||
|     final item = await pm.getItem(bareJid, omemoBundlesXmlns, '$deviceId'); | ||||
|     if (item.isType<PubSubError>()) return Result(UnknownOmemoError()); | ||||
| 
 | ||||
|     return Result(bundleFromXML(jid, deviceId, item.get<PubSubItem>().payload)); | ||||
|   } | ||||
| 
 | ||||
|   /// Attempts to publish a device bundle to the device list and device bundle PubSub | ||||
|   /// nodes. | ||||
|   /// | ||||
|   /// On success, returns true. On failure, returns an OmemoError. | ||||
|   Future<Result<OmemoError, bool>> publishBundle(OmemoBundle bundle) async { | ||||
|     final attrs = getAttributes(); | ||||
|     final pm = attrs.getManagerById<PubSubManager>(pubsubManager)!; | ||||
|     final bareJid = attrs.getFullJID().toBare(); | ||||
| 
 | ||||
|     XMLNode? deviceList; | ||||
|     final deviceListRaw = await _retrieveDeviceListPayload(bareJid); | ||||
|     if (!deviceListRaw.isType<OmemoError>()) { | ||||
|       deviceList = deviceListRaw.get<XMLNode>(); | ||||
|     } | ||||
| 
 | ||||
|     deviceList ??= XMLNode.xmlns( | ||||
|       tag: 'devices', | ||||
|       xmlns: omemoDevicesXmlns, | ||||
|     ); | ||||
| 
 | ||||
|     final ids = deviceList.children | ||||
|       .map((child) => int.parse(child.attributes['id']! as String)); | ||||
|        | ||||
|     if (!ids.contains(bundle.id)) { | ||||
|       // Only update the device list if the device Id is not there | ||||
|       final newDeviceList = XMLNode.xmlns( | ||||
|         tag: 'devices', | ||||
|         xmlns: omemoDevicesXmlns, | ||||
|         children: [ | ||||
|           ...deviceList.children, | ||||
|           XMLNode( | ||||
|             tag: 'device', | ||||
|             attributes: <String, String>{ | ||||
|               'id': '${bundle.id}', | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
|        | ||||
|       final deviceListPublish = await pm.publish( | ||||
|         bareJid.toString(), | ||||
|         omemoDevicesXmlns, | ||||
|         newDeviceList, | ||||
|         id: 'current', | ||||
|         options: const PubSubPublishOptions( | ||||
|           accessModel: 'open', | ||||
|         ), | ||||
|       ); | ||||
|       if (deviceListPublish.isType<PubSubError>()) return const Result(false); | ||||
|     }     | ||||
| 
 | ||||
|     final deviceBundlePublish = await pm.publish( | ||||
|       bareJid.toString(), | ||||
|       omemoBundlesXmlns, | ||||
|       bundleToXML(bundle), | ||||
|       id: '${bundle.id}', | ||||
|       options: const PubSubPublishOptions( | ||||
|         accessModel: 'open', | ||||
|         maxItems: 'max', | ||||
|       ), | ||||
|     ); | ||||
|      | ||||
|     return Result(deviceBundlePublish.isType<PubSubError>()); | ||||
|   } | ||||
| 
 | ||||
|   /// Subscribes to the device list PubSub node of [jid]. | ||||
|   Future<void> subscribeToDeviceList(JID jid) async { | ||||
|     final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; | ||||
|     final result = await pm.subscribe(jid.toString(), omemoDevicesXmlns); | ||||
| 
 | ||||
|     if (!result.isType<PubSubError>()) { | ||||
|       _subscriptionMap[jid] = true; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Attempts to find out if [jid] supports omemo:2. | ||||
|   /// | ||||
|   /// On success, returns whether [jid] has published a device list and device bundles. | ||||
|   /// On failure, returns an OmemoError. | ||||
|   Future<Result<OmemoError, bool>> supportsOmemo(JID jid) async { | ||||
|     final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!; | ||||
|     final items = await dm.discoItemsQuery(jid.toBare().toString()); | ||||
| 
 | ||||
|     if (items.isType<DiscoError>()) return Result(UnknownOmemoError()); | ||||
| 
 | ||||
|     final nodes = items.get<List<DiscoItem>>(); | ||||
|     final result = nodes.any((item) => item.node == omemoDevicesXmlns) && nodes.any((item) => item.node == omemoBundlesXmlns); | ||||
|     return Result(result); | ||||
|   } | ||||
| 
 | ||||
|   /// Attempts to delete a device with device id [deviceId] from the device bundles node | ||||
|   /// and then the device list node. This allows a device that was accidentally removed | ||||
|   /// to republish without any race conditions. | ||||
|   /// Note that this does not delete a possibly existent ratchet session. | ||||
|   /// | ||||
|   /// On success, returns true. On failure, returns an OmemoError. | ||||
|   Future<Result<OmemoError, bool>> deleteDevice(int deviceId) async { | ||||
|     final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; | ||||
|     final jid = getAttributes().getFullJID().toBare(); | ||||
| 
 | ||||
|     final bundleResult = await pm.retract(jid, omemoBundlesXmlns, '$deviceId'); | ||||
|     if (bundleResult.isType<PubSubError>()) { | ||||
|       // TODO(Unknown): Be more specific | ||||
|       return Result(UnknownOmemoError()); | ||||
|     } | ||||
| 
 | ||||
|     final deviceListResult = await _retrieveDeviceListPayload(jid); | ||||
|     if (deviceListResult.isType<OmemoError>()) { | ||||
|       return Result(bundleResult.get<OmemoError>()); | ||||
|     } | ||||
| 
 | ||||
|     final payload = deviceListResult.get<XMLNode>(); | ||||
|     final newPayload = XMLNode.xmlns( | ||||
|       tag: 'devices', | ||||
|       xmlns: omemoDevicesXmlns, | ||||
|       children: payload.children | ||||
|         .where((child) => child.attributes['id'] != '$deviceId') | ||||
|         .toList(), | ||||
|     ); | ||||
|     final publishResult = await pm.publish( | ||||
|       jid.toString(), | ||||
|       omemoDevicesXmlns, | ||||
|       newPayload, | ||||
|       id: 'current', | ||||
|       options: const PubSubPublishOptions( | ||||
|         accessModel: 'open', | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     if (publishResult.isType<PubSubError>()) return Result(UnknownOmemoError()); | ||||
| 
 | ||||
|     return const Result(true); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										99
									
								
								moxxmpp/lib/src/xeps/xep_0385.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								moxxmpp/lib/src/xeps/xep_0385.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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/staging/extensible_file_thumbnails.dart'; | ||||
| 
 | ||||
| class StatelessMediaSharingData { | ||||
| 
 | ||||
|   const StatelessMediaSharingData({ required this.mediaType, required this.size, required this.description, required this.hashes, required this.url, required this.thumbnails }); | ||||
|   final String mediaType; | ||||
|   final int size; | ||||
|   final String description; | ||||
|   final Map<String, String> hashes; // algo -> hash value | ||||
|   final List<Thumbnail> thumbnails; | ||||
|    | ||||
|   final String url; | ||||
| } | ||||
| 
 | ||||
| StatelessMediaSharingData parseSIMSElement(XMLNode node) { | ||||
|   assert(node.attributes['xmlns'] == simsXmlns, 'Invalid element xmlns'); | ||||
|   assert(node.tag == 'media-sharing', 'Invalid element name'); | ||||
| 
 | ||||
|   final file = node.firstTag('file', xmlns: jingleFileTransferXmlns)!; | ||||
|   final hashes = <String, String>{}; | ||||
|   for (final i in file.findTags('hash', xmlns: hashXmlns)) { | ||||
|     hashes[i.attributes['algo']! as String] = i.innerText(); | ||||
|   } | ||||
| 
 | ||||
|   var url = ''; | ||||
|   final references = file.firstTag('sources')!.findTags('reference', xmlns: referenceXmlns); | ||||
|   for (final i in references) { | ||||
|     if (i.attributes['type'] != 'data') continue; | ||||
| 
 | ||||
|     final uri = i.attributes['uri']! as String; | ||||
|     if (!uri.startsWith('https://')) continue; | ||||
| 
 | ||||
|     url = uri; | ||||
|     break; | ||||
|   } | ||||
| 
 | ||||
|   final thumbnails = List<Thumbnail>.empty(growable: true); | ||||
|   for (final child in file.children) { | ||||
|     // TODO(Unknown): Handle other thumbnails | ||||
|     if (child.tag == 'file-thumbnail' && child.attributes['xmlns'] == fileThumbnailsXmlns) { | ||||
|       final thumb = parseFileThumbnailElement(child); | ||||
|       if (thumb != null) { | ||||
|         thumbnails.add(thumb); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   return StatelessMediaSharingData( | ||||
|     mediaType: file.firstTag('media-type')!.innerText(), | ||||
|     size: int.parse(file.firstTag('size')!.innerText()), | ||||
|     description: file.firstTag('description')!.innerText(), | ||||
|     url: url, | ||||
|     hashes: hashes, | ||||
|     thumbnails: thumbnails, | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| class SIMSManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getName() => 'SIMSManager'; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => simsManager; | ||||
| 
 | ||||
|   @override | ||||
|   List<String> getDiscoFeatures() => [ simsXmlns ]; | ||||
|    | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       callback: _onMessage, | ||||
|       tagName: 'reference', | ||||
|       tagXmlns: referenceXmlns, | ||||
|       // Before the message handler | ||||
|       priority: -99, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onMessage(Stanza message, StanzaHandlerData state) async { | ||||
|     final references = message.findTags('reference', xmlns: referenceXmlns); | ||||
|     for (final ref in references) { | ||||
|       final sims = ref.firstTag('media-sharing', xmlns: simsXmlns); | ||||
|       if (sims != null) return state.copyWith(sims: parseSIMSElement(sims)); | ||||
|     } | ||||
| 
 | ||||
|     return state; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										32
									
								
								moxxmpp/lib/src/xeps/xep_0414.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								moxxmpp/lib/src/xeps/xep_0414.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| import 'package:cryptography/cryptography.dart'; | ||||
| 
 | ||||
| class InvalidHashAlgorithmException implements Exception { | ||||
| 
 | ||||
|   InvalidHashAlgorithmException(this.name); | ||||
|   final String name; | ||||
| 
 | ||||
|   String errMsg() => 'Invalid hash algorithm: $name'; | ||||
| } | ||||
| 
 | ||||
| /// Returns the hash algorithm specified by its name, according to XEP-0414. | ||||
| HashAlgorithm? getHashByName(String name) { | ||||
|   switch (name) { | ||||
|     case 'sha-1': return Sha1(); | ||||
|     case 'sha-256': return Sha256(); | ||||
|     case 'sha-512': return Sha512(); | ||||
|     // NOTE: cryptography provides an implementation of blake2b, however, | ||||
|     //       I have no idea what it's output length is and you cannot set | ||||
|     //       one. => New dependency | ||||
|     // TODO(Unknown): Implement | ||||
|     //case "blake2b-256": ; | ||||
|     // hashLengthInBytes == 64 => 512? | ||||
|     case 'blake2b-512': Blake2b(); | ||||
|     // NOTE: cryptography does not provide SHA3 hashes => New dependency | ||||
|     // TODO(Unknown): Implement | ||||
|     //case "sha3-256": ; | ||||
|     // TODO(Unknown): Implement | ||||
|     //case "sha3-512": ; | ||||
|   } | ||||
| 
 | ||||
|   throw InvalidHashAlgorithmException(name); | ||||
| } | ||||
							
								
								
									
										108
									
								
								moxxmpp/lib/src/xeps/xep_0446.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								moxxmpp/lib/src/xeps/xep_0446.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0300.dart'; | ||||
| 
 | ||||
| class FileMetadataData { | ||||
| 
 | ||||
|   const FileMetadataData({ | ||||
|     this.mediaType, | ||||
|     this.width, | ||||
|     this.height, | ||||
|     this.desc, | ||||
|     this.length, | ||||
|     this.name, | ||||
|     this.size, | ||||
|     required this.thumbnails, | ||||
|     Map<String, String>? hashes, | ||||
|   }) : hashes = hashes ?? const {}; | ||||
| 
 | ||||
|   /// Parse [node] as a FileMetadataData element. | ||||
|   factory FileMetadataData.fromXML(XMLNode node) { | ||||
|     assert(node.attributes['xmlns'] == fileMetadataXmlns, 'Invalid element xmlns'); | ||||
|     assert(node.tag == 'file', 'Invalid element anme'); | ||||
| 
 | ||||
|     final lengthElement = node.firstTag('length'); | ||||
|     final length = lengthElement != null ? int.parse(lengthElement.innerText()) : null; | ||||
|     final sizeElement = node.firstTag('size'); | ||||
|     final size = sizeElement != null ? int.parse(sizeElement.innerText()) : null; | ||||
| 
 | ||||
|     final hashes = <String, String>{}; | ||||
|     for (final e in node.findTags('hash')) { | ||||
|       hashes[e.attributes['algo']! as String] = e.innerText(); | ||||
|     } | ||||
| 
 | ||||
|     // Thumbnails | ||||
|     final thumbnails = List<Thumbnail>.empty(growable: true); | ||||
|     for (final i in node.findTags('file-thumbnail')) { | ||||
|       final thumbnail = parseFileThumbnailElement(i); | ||||
|       if (thumbnail != null) { | ||||
|         thumbnails.add(thumbnail); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Length and height | ||||
|     final widthString = node.firstTag('length'); | ||||
|     final heightString = node.firstTag('height'); | ||||
|     int? width; | ||||
|     int? height; | ||||
|     if (widthString != null) { | ||||
|       width = int.parse(widthString.innerText()); | ||||
|     } | ||||
|     if (heightString != null) { | ||||
|       height = int.parse(heightString.innerText()); | ||||
|     } | ||||
|      | ||||
|     return FileMetadataData( | ||||
|       mediaType: node.firstTag('media-type')?.innerText(), | ||||
|       width: width, | ||||
|       height: height, | ||||
|       desc: node.firstTag('desc')?.innerText(), | ||||
|       hashes: hashes, | ||||
|       length: length, | ||||
|       name: node.firstTag('name')?.innerText(), | ||||
|       size: size, | ||||
|       thumbnails: thumbnails, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   final String? mediaType; | ||||
|   final int? width; | ||||
|   final int? height; | ||||
|   final List<Thumbnail> thumbnails; | ||||
|   final String? desc; | ||||
|   final Map<String, String> hashes; | ||||
|   final int? length; | ||||
|   final String? name; | ||||
|   final int? size; | ||||
| 
 | ||||
|   XMLNode toXML() { | ||||
|     final node = XMLNode.xmlns( | ||||
|       tag: 'file', | ||||
|       xmlns: fileMetadataXmlns, | ||||
|       children: List.empty(growable: true), | ||||
|     ); | ||||
| 
 | ||||
|     if (mediaType != null) node.addChild(XMLNode(tag: 'media-type', text: mediaType)); | ||||
|     if (width != null) node.addChild(XMLNode(tag: 'width', text: '$width')); | ||||
|     if (height != null) node.addChild(XMLNode(tag: 'height', text: '$height')); | ||||
|     if (desc != null) node.addChild(XMLNode(tag: 'desc', text: desc)); | ||||
|     if (length != null) node.addChild(XMLNode(tag: 'length', text: length.toString())); | ||||
|     if (name != null) node.addChild(XMLNode(tag: 'name', text: name)); | ||||
|     if (size != null) node.addChild(XMLNode(tag: 'size', text: size.toString())); | ||||
| 
 | ||||
|     for (final hash in hashes.entries) { | ||||
|       node.addChild( | ||||
|         constructHashElement(hash.key, hash.value), | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     for (final thumbnail in thumbnails) { | ||||
|       node.addChild( | ||||
|         constructFileThumbnailElement(thumbnail), | ||||
|       ); | ||||
|     } | ||||
|      | ||||
|     return node; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										126
									
								
								moxxmpp/lib/src/xeps/xep_0447.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								moxxmpp/lib/src/xeps/xep_0447.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | ||||
| import 'package:moxlib/moxlib.dart'; | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.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_0446.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0448.dart'; | ||||
| 
 | ||||
| /// The base class for sources for StatelessFileSharing | ||||
| // ignore: one_member_abstracts | ||||
| abstract class StatelessFileSharingSource { | ||||
|   /// Turn the source into an XML element. | ||||
|   XMLNode toXml(); | ||||
| } | ||||
| 
 | ||||
| /// Implementation for url-data source elements. | ||||
| class StatelessFileSharingUrlSource extends StatelessFileSharingSource { | ||||
| 
 | ||||
|   StatelessFileSharingUrlSource(this.url); | ||||
| 
 | ||||
|   factory StatelessFileSharingUrlSource.fromXml(XMLNode element) { | ||||
|     assert(element.attributes['xmlns'] == urlDataXmlns, 'Element has the wrong xmlns'); | ||||
| 
 | ||||
|     return StatelessFileSharingUrlSource(element.attributes['target']! as String); | ||||
|   } | ||||
| 
 | ||||
|   final String url; | ||||
| 
 | ||||
|   @override | ||||
|   XMLNode toXml() { | ||||
|     return XMLNode.xmlns( | ||||
|       tag: 'url-data', | ||||
|       xmlns: urlDataXmlns, | ||||
|       attributes: <String, String>{ | ||||
|         'target': url, | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class StatelessFileSharingData { | ||||
| 
 | ||||
|   const StatelessFileSharingData(this.metadata, this.sources); | ||||
| 
 | ||||
|   /// Parse [node] as a StatelessFileSharingData element. | ||||
|   factory StatelessFileSharingData.fromXML(XMLNode node) { | ||||
|     assert(node.attributes['xmlns'] == sfsXmlns, 'Invalid element xmlns'); | ||||
|     assert(node.tag == 'file-sharing', 'Invalid element name'); | ||||
| 
 | ||||
|     final sources = List<StatelessFileSharingSource>.empty(growable: true); | ||||
|      | ||||
|     final sourcesElement = node.firstTag('sources')!; | ||||
|     for (final source in sourcesElement.children) { | ||||
|       if (source.attributes['xmlns'] == urlDataXmlns) { | ||||
|         sources.add(StatelessFileSharingUrlSource.fromXml(source)); | ||||
|       } else if (source.attributes['xmlns'] == sfsEncryptionXmlns) { | ||||
|         sources.add(StatelessFileSharingEncryptedSource.fromXml(source)); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return StatelessFileSharingData( | ||||
|       FileMetadataData.fromXML(node.firstTag('file')!), | ||||
|       sources, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   final FileMetadataData metadata; | ||||
|   final List<StatelessFileSharingSource> sources; | ||||
| 
 | ||||
|   XMLNode toXML() { | ||||
|     return XMLNode.xmlns( | ||||
|       tag: 'file-sharing', | ||||
|       xmlns: sfsXmlns, | ||||
|       children: [ | ||||
|         metadata.toXML(), | ||||
|         XMLNode( | ||||
|           tag: 'sources', | ||||
|           children: sources | ||||
|             .map((source) => source.toXml()) | ||||
|             .toList(), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   StatelessFileSharingUrlSource? getFirstUrlSource() { | ||||
|     return firstWhereOrNull( | ||||
|       sources, | ||||
|       (StatelessFileSharingSource source) => source is StatelessFileSharingUrlSource, | ||||
|     ) as StatelessFileSharingUrlSource?; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class SFSManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getName() => 'SFSManager'; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => sfsManager; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'file-sharing', | ||||
|       tagXmlns: sfsXmlns, | ||||
|       callback: _onMessage, | ||||
|       // Before the message handler | ||||
|       priority: -99, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onMessage(Stanza message, StanzaHandlerData state) async { | ||||
|     final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!; | ||||
| 
 | ||||
|     return state.copyWith( | ||||
|       sfs: StatelessFileSharingData.fromXML(sfs), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										103
									
								
								moxxmpp/lib/src/xeps/xep_0448.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								moxxmpp/lib/src/xeps/xep_0448.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,103 @@ | ||||
| import 'dart:convert'; | ||||
| import 'package:moxlib/moxlib.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stringxml.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0300.dart'; | ||||
| import 'package:moxxmpp/src/xeps/xep_0447.dart'; | ||||
| 
 | ||||
| enum SFSEncryptionType { | ||||
|   aes128GcmNoPadding, | ||||
|   aes256GcmNoPadding, | ||||
|   aes256CbcPkcs7, | ||||
| } | ||||
| 
 | ||||
| extension SFSEncryptionTypeNamespaceExtension on SFSEncryptionType { | ||||
|   String toNamespace() { | ||||
|     switch (this) { | ||||
|       case SFSEncryptionType.aes128GcmNoPadding: | ||||
|         return sfsEncryptionAes128GcmNoPaddingXmlns; | ||||
|       case SFSEncryptionType.aes256GcmNoPadding: | ||||
|         return sfsEncryptionAes256GcmNoPaddingXmlns; | ||||
|       case SFSEncryptionType.aes256CbcPkcs7: | ||||
|         return sfsEncryptionAes256CbcPkcs7Xmlns; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| SFSEncryptionType encryptionTypeFromNamespace(String xmlns) { | ||||
|   switch (xmlns) { | ||||
|     case sfsEncryptionAes128GcmNoPaddingXmlns: | ||||
|       return SFSEncryptionType.aes128GcmNoPadding; | ||||
|     case sfsEncryptionAes256GcmNoPaddingXmlns: | ||||
|       return SFSEncryptionType.aes256GcmNoPadding; | ||||
|     case sfsEncryptionAes256CbcPkcs7Xmlns: | ||||
|       return SFSEncryptionType.aes256CbcPkcs7; | ||||
|   } | ||||
| 
 | ||||
|   throw Exception(); | ||||
| } | ||||
| 
 | ||||
| class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource { | ||||
| 
 | ||||
|   StatelessFileSharingEncryptedSource(this.encryption, this.key, this.iv, this.hashes, this.source); | ||||
|   factory StatelessFileSharingEncryptedSource.fromXml(XMLNode element) { | ||||
|     assert(element.attributes['xmlns'] == sfsEncryptionXmlns, 'Element has invalid xmlns'); | ||||
| 
 | ||||
|     final key = base64Decode(element.firstTag('key')!.text!); | ||||
|     final iv = base64Decode(element.firstTag('iv')!.text!); | ||||
|     final sources = element.firstTag('sources', xmlns: sfsXmlns)!.children; | ||||
| 
 | ||||
|     // Find the first URL source | ||||
|     final source = firstWhereOrNull( | ||||
|       sources, | ||||
|       (XMLNode child) => child.tag == 'url-data' && child.attributes['xmlns'] == urlDataXmlns, | ||||
|     )!; | ||||
| 
 | ||||
|     // Find hashes | ||||
|     final hashes = <String, String>{}; | ||||
|     for (final hash in element.findTags('hash', xmlns: hashXmlns)) { | ||||
|       hashes[hash.attributes['algo']! as String] = hash.text!; | ||||
|     } | ||||
|      | ||||
|     return StatelessFileSharingEncryptedSource( | ||||
|       encryptionTypeFromNamespace(element.attributes['cipher']! as String), | ||||
|       key, | ||||
|       iv, | ||||
|       hashes, | ||||
|       StatelessFileSharingUrlSource.fromXml(source), | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   final List<int> key; | ||||
|   final List<int> iv; | ||||
|   final SFSEncryptionType encryption; | ||||
|   final Map<String, String> hashes; | ||||
|   final StatelessFileSharingUrlSource source; | ||||
| 
 | ||||
|   @override | ||||
|   XMLNode toXml() { | ||||
|     return XMLNode.xmlns( | ||||
|       tag: 'encrypted', | ||||
|       xmlns: sfsEncryptionXmlns, | ||||
|       attributes: <String, String>{ | ||||
|         'cipher': encryption.toNamespace(), | ||||
|       }, | ||||
|       children: [ | ||||
|         XMLNode( | ||||
|           tag: 'key', | ||||
|           text: base64Encode(key), | ||||
|         ), | ||||
|         XMLNode( | ||||
|           tag: 'iv', | ||||
|           text: base64Encode(iv), | ||||
|         ), | ||||
|         ...hashes.entries.map((hash) => constructHashElement(hash.key, hash.value)), | ||||
|         XMLNode.xmlns( | ||||
|           tag: 'sources', | ||||
|           xmlns: sfsXmlns, | ||||
|           children: [source.toXml()], | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										66
									
								
								moxxmpp/lib/src/xeps/xep_0461.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								moxxmpp/lib/src/xeps/xep_0461.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| import 'package:moxxmpp/src/managers/base.dart'; | ||||
| import 'package:moxxmpp/src/managers/data.dart'; | ||||
| import 'package:moxxmpp/src/managers/handlers.dart'; | ||||
| import 'package:moxxmpp/src/managers/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/namespaces.dart'; | ||||
| import 'package:moxxmpp/src/stanza.dart'; | ||||
| 
 | ||||
| class ReplyData { | ||||
| 
 | ||||
|   const ReplyData({ | ||||
|       required this.to, | ||||
|       required this.id, | ||||
|       this.start, | ||||
|       this.end, | ||||
|   }); | ||||
|   final String to; | ||||
|   final String id; | ||||
|   final int? start; | ||||
|   final int? end; | ||||
| } | ||||
| 
 | ||||
| class MessageRepliesManager extends XmppManagerBase { | ||||
|   @override | ||||
|   String getName() => 'MessageRepliesManager'; | ||||
| 
 | ||||
|   @override | ||||
|   String getId() => messageRepliesManager; | ||||
| 
 | ||||
|   @override | ||||
|   List<StanzaHandler> getIncomingStanzaHandlers() => [ | ||||
|     StanzaHandler( | ||||
|       stanzaTag: 'message', | ||||
|       tagName: 'reply', | ||||
|       tagXmlns: replyXmlns, | ||||
|       callback: _onMessage, | ||||
|       // Before the message handler | ||||
|       priority: -99, | ||||
|     ) | ||||
|   ]; | ||||
| 
 | ||||
|   @override | ||||
|   Future<bool> isSupported() async => true; | ||||
|    | ||||
|   Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async { | ||||
|     final reply = stanza.firstTag('reply', xmlns: replyXmlns)!; | ||||
|     final id = reply.attributes['id']! as String; | ||||
|     final to = reply.attributes['to']! as String; | ||||
|     int? start; | ||||
|     int? end; | ||||
| 
 | ||||
|     // TODO(Unknown): Maybe extend firstTag to also look for attributes | ||||
|     final fallback = stanza.firstTag('fallback', xmlns: fallbackXmlns); | ||||
|     if (fallback != null) { | ||||
|       final body = fallback.firstTag('body')!; | ||||
|       start = int.parse(body.attributes['start']! as String); | ||||
|       end = int.parse(body.attributes['end']! as String); | ||||
|     } | ||||
| 
 | ||||
|     return state.copyWith(reply: ReplyData( | ||||
|         id: id, | ||||
|         to: to, | ||||
|         start: start, | ||||
|         end: end, | ||||
|     ),); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										36
									
								
								moxxmpp/pubspec.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								moxxmpp/pubspec.yaml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| name: moxxmpp | ||||
| description: A pure-Dart XMPP library | ||||
| version: 0.1.0 | ||||
| homepage: https://codeberg.org/moxxy/moxxmpp | ||||
| 
 | ||||
| environment: | ||||
|   sdk: '>=2.18.0 <3.0.0' | ||||
| 
 | ||||
| dependencies: | ||||
|   cryptography: 2.0.5 | ||||
|   hex: 0.2.0 | ||||
|   logging: 1.0.2 | ||||
|   moxlib: | ||||
|    hosted: https://git.polynom.me/api/packages/Moxxy/pub | ||||
|    version: 0.1.5 | ||||
|   omemo_dart: | ||||
|    hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub | ||||
|    version: 0.3.1 | ||||
|   random_string: 2.3.1 | ||||
|   saslprep: 1.0.2 | ||||
|   uuid: 3.0.5 | ||||
|   xml: ^6.1.0 | ||||
| 
 | ||||
| dev_dependencies: | ||||
|   build_runner: ^2.1.11 | ||||
|   freezed: ^2.1.0+1 | ||||
|   json_serializable: ^6.3.1 | ||||
|   meta: ^1.7.0 | ||||
|   test: ^1.16.0 | ||||
|   very_good_analysis: ^3.0.1 | ||||
| 
 | ||||
| dependency_overrides: | ||||
|   omemo_dart: | ||||
|     git: | ||||
|       url: https://codeberg.org/PapaTutuWawa/omemo_dart.git | ||||
|       rev: c68471349ab1b347ec9ad54651265710842c50b7 | ||||
							
								
								
									
										16
									
								
								moxxmpp/test/moxxmpp_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								moxxmpp/test/moxxmpp_test.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| import 'package:moxxmpp/moxxmpp.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| void main() { | ||||
|   group('A group of tests', () { | ||||
|     final awesome = Awesome(); | ||||
| 
 | ||||
|     setUp(() { | ||||
|       // Additional setup goes here. | ||||
|     }); | ||||
| 
 | ||||
|     test('First Test', () { | ||||
|       expect(awesome.isAwesome, isTrue); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user