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