Merge branch 'master' into xep_0045

This commit is contained in:
ikjot-2605 2023-06-30 04:56:15 +00:00
commit 9e70e802ef
61 changed files with 769 additions and 573 deletions

View File

@ -30,24 +30,26 @@ class EchoMessageManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final body = stanza.firstTag('body'); final body = stanza.firstTag('body');
if (body == null) return state.copyWith(done: true); if (body == null) return state..done = true;
final bodyText = body.innerText(); final bodyText = body.innerText();
await getAttributes().sendStanza( await getAttributes().sendStanza(
Stanza.message( StanzaDetails(
to: stanza.from, Stanza.message(
children: [ to: stanza.from,
XMLNode( children: [
tag: 'body', XMLNode(
text: 'Hello, ${stanza.from}! You said "$bodyText"', tag: 'body',
), text: 'Hello, ${stanza.from}! You said "$bodyText"',
], ),
],
),
awaitable: false,
), ),
awaitable: false,
); );
return state.copyWith(done: true); return state..done = true;
} }
} }

View File

@ -0,0 +1,116 @@
import 'package:chalkdart/chalk.dart';
import 'package:cli_repl/cli_repl.dart';
import 'package:example_dart/arguments.dart';
import 'package:example_dart/socket.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:omemo_dart/omemo_dart.dart' as omemo;
void main(List<String> args) async {
// Set up logging
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print(
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
);
});
final parser = ArgumentParser()
..parser.addOption('to', help: 'The JID to send messages to');
final options = parser.handleArguments(args);
if (options == null) {
return;
}
// Connect
final jid = parser.jid;
final to = JID.fromString(options['to']! as String).toBare();
final connection = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
ExampleTCPSocketWrapper(parser.srvRecord),
)..connectionSettings = parser.connectionSettings;
// Generate OMEMO data
omemo.OmemoManager? oom;
final moxxmppOmemo = OmemoManager(
() async => oom!,
(toJid, _) async => toJid == to,
);
oom = omemo.OmemoManager(
await omemo.OmemoDevice.generateNewDevice(jid.toString(), opkAmount: 5),
omemo.BlindTrustBeforeVerificationTrustManager(),
moxxmppOmemo.sendEmptyMessageImpl,
moxxmppOmemo.fetchDeviceList,
moxxmppOmemo.fetchDeviceBundle,
moxxmppOmemo.subscribeToDeviceListImpl,
moxxmppOmemo.publishDeviceImpl,
);
final deviceId = await oom.getDeviceId();
Logger.root.info('Our device id: $deviceId');
// Register the managers and negotiators
await connection.registerManagers([
PresenceManager(),
DiscoManager([]),
PubSubManager(),
MessageManager(),
moxxmppOmemo,
]);
await connection.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StartTlsNegotiator(),
SaslScramNegotiator(10, '', '', ScramHashType.sha1),
]);
// Set up event handlers
connection.asBroadcastStream().listen((event) {
if (event is MessageEvent) {
Logger.root.info(event.id);
Logger.root.info(event.extensions.keys.toList());
final body = event.encryptionError != null
? chalk.red('Failed to decrypt message: ${event.encryptionError}')
: chalk.green(event.get<MessageBodyData>()?.body ?? '');
print('[${event.from.toString()}] $body');
}
});
// Connect
Logger.root.info('Connecting...');
final result =
await connection.connect(shouldReconnect: false, waitUntilLogin: true);
if (!result.isType<bool>()) {
Logger.root.severe('Authentication failed!');
return;
}
Logger.root.info('Connected.');
// Publish our bundle
Logger.root.info('Publishing bundle');
final device = await oom.getDevice();
final omemoResult = await moxxmppOmemo.publishBundle(await device.toBundle());
if (!omemoResult.isType<bool>()) {
Logger.root.severe(
'Failed to publish OMEMO bundle: ${omemoResult.get<OmemoError>()}');
return;
}
final repl = Repl(prompt: '> ');
await for (final line in repl.runAsync()) {
await connection
.getManagerById<MessageManager>(messageManager)!
.sendMessage(
to,
TypedMap<StanzaHandlerExtension>.fromList([
MessageBodyData(line),
]),
);
}
// Disconnect
await connection.disconnect();
}

View File

@ -0,0 +1,84 @@
import 'package:args/args.dart';
import 'package:chalkdart/chalk.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
extension StringToInt on String {
int toInt() => int.parse(this);
}
/// A wrapper around [ArgParser] for providing convenience functions and standard parameters
/// to the examples.
class ArgumentParser {
ArgumentParser() {
parser
..addOption('jid', help: 'The JID to connect as')
..addOption('password', help: 'The password to use for authenticating')
..addOption('host',
help:
'The host address to connect to (By default uses the domain part of the JID)')
..addOption('port', help: 'The port to connect to')
..addOption('xmpps-srv',
help:
'Inject a SRV record for _xmpps-client._tcp. Format: <priority>,<weight>,<target>,<port>')
..addFlag('help',
abbr: 'h',
negatable: false,
defaultsTo: false,
help: 'Show this help text');
}
/// The [ArgParser] that handles parsing the arguments.
final ArgParser parser = ArgParser();
/// The parsed options. Only valid after calling [handleArguments].
late ArgResults options;
ArgResults? handleArguments(List<String> args) {
options = parser.parse(args);
if (options['help']!) {
print(parser.usage);
return null;
}
if (options['jid'] == null) {
print(chalk.red('No JID specified'));
print(parser.usage);
return null;
}
if (options['password'] == null) {
print(chalk.red('No password specified'));
print(parser.usage);
return null;
}
return options;
}
/// The JID to connect as.
JID get jid => JID.fromString(options['jid']!).toBare();
/// Construct connection settings from the parsed options.
ConnectionSettings get connectionSettings => ConnectionSettings(
jid: jid,
password: options['password']!,
host: options['host'],
port: (options['port'] as String?)?.toInt(),
);
/// Construct an xmpps-client SRV record for injection, if specified.
MoxSrvRecord? get srvRecord {
if (options['xmpps-srv'] == null) {
return null;
}
final parts = options['xmpps-srv']!.split(',');
return MoxSrvRecord(
int.parse(parts[0]),
int.parse(parts[1]),
parts[2],
int.parse(parts[3]),
);
}
}

View File

@ -0,0 +1,22 @@
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
/// A simple socket for examples that allows injection of SRV records (since
/// we cannot use moxdns here).
class ExampleTCPSocketWrapper extends TCPSocketWrapper {
ExampleTCPSocketWrapper(this.srvRecord);
/// A potential SRV record to inject for testing.
final MoxSrvRecord? srvRecord;
@override
bool onBadCertificate(dynamic certificate, String domain) {
return true;
}
@override
Future<List<MoxSrvRecord>> srvQuery(String domain, bool dnssec) async {
return [
if (srvRecord != null) srvRecord!,
];
}
}

View File

@ -1,12 +1,14 @@
name: example_dart name: example_dart
description: A sample command-line application. description: A collection of samples for moxxmpp.
version: 1.0.0 version: 1.0.0
# homepage: https://www.example.com
environment: environment:
sdk: '>=2.18.0 <3.0.0' sdk: '>=2.18.0 <3.0.0'
dependencies: dependencies:
args: 2.4.1
chalkdart: 2.0.9
cli_repl: 0.2.3
logging: ^1.0.2 logging: ^1.0.2
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
@ -14,12 +16,19 @@ dependencies:
moxxmpp_socket_tcp: moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.3.1 version: 0.3.1
omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.5.1
dependency_overrides: dependency_overrides:
moxxmpp: moxxmpp:
path: ../packages/moxxmpp path: ../packages/moxxmpp
moxxmpp_socket_tcp: moxxmpp_socket_tcp:
path: ../packages/moxxmpp_socket_tcp path: ../packages/moxxmpp_socket_tcp
omemo_dart:
git:
url: https://github.com/PapaTutuWawa/omemo_dart.git
rev: 49c7e114e6cf80dcde55fbbd218bba3182045862
dev_dependencies: dev_dependencies:
lints: ^2.0.0 lints: ^2.0.0

View File

@ -0,0 +1,2 @@
prosodyctl --config ./prosody.cfg.lua register testuser1 localhost abc123
prosodyctl --config ./prosody.cfg.lua register testuser2 localhost abc123

View File

@ -16,6 +16,8 @@
- **BREAKING**: `MessageEvent` now makes use of `TypedMap`. - **BREAKING**: `MessageEvent` now makes use of `TypedMap`.
- **BREAKING**: Removed `PresenceReceivedEvent`. Use a manager registering handlers with priority greater than `[PresenceManager.presenceHandlerPriority]` instead. - **BREAKING**: Removed `PresenceReceivedEvent`. Use a manager registering handlers with priority greater than `[PresenceManager.presenceHandlerPriority]` instead.
- **BREAKING**: `ChatState.toString()` is now `ChatState.toName()` - **BREAKING**: `ChatState.toString()` is now `ChatState.toName()`
- **BREAKING**: Overriding `BaseOmemoManager` is no longer required. `OmemoManager` now takes callback methods instead.
- Removed `ErrorResponseDiscoError` from the possible XEP-0030 errors.
## 0.3.1 ## 0.3.1

View File

@ -38,7 +38,6 @@ export 'package:moxxmpp/src/settings.dart';
export 'package:moxxmpp/src/socket.dart'; export 'package:moxxmpp/src/socket.dart';
export 'package:moxxmpp/src/stanza.dart'; export 'package:moxxmpp/src/stanza.dart';
export 'package:moxxmpp/src/stringxml.dart'; export 'package:moxxmpp/src/stringxml.dart';
export 'package:moxxmpp/src/types/result.dart';
export 'package:moxxmpp/src/util/typed_map.dart'; export 'package:moxxmpp/src/util/typed_map.dart';
export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
export 'package:moxxmpp/src/xeps/staging/fast.dart'; export 'package:moxxmpp/src/xeps/staging/fast.dart';

View File

@ -25,11 +25,9 @@ import 'package:moxxmpp/src/settings.dart';
import 'package:moxxmpp/src/socket.dart'; import 'package:moxxmpp/src/socket.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/queue.dart'; import 'package:moxxmpp/src/util/queue.dart';
import 'package:moxxmpp/src/util/typed_map.dart'; import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:moxxmpp/src/xeps/xep_0352.dart'; import 'package:moxxmpp/src/xeps/xep_0352.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
@ -479,6 +477,7 @@ class XmppConnection {
newStanza, newStanza,
TypedMap(), TypedMap(),
encrypted: details.encrypted, encrypted: details.encrypted,
shouldEncrypt: details.shouldEncrypt,
forceEncryption: details.forceEncryption, forceEncryption: details.forceEncryption,
), ),
); );
@ -533,15 +532,14 @@ class XmppConnection {
// Run post-send handlers // Run post-send handlers
_log.fine('Running post stanza handlers..'); _log.fine('Running post stanza handlers..');
final extensions = TypedMap<StanzaHandlerExtension>()
..set(StreamManagementData(details.excludeFromStreamManagement));
await _runOutgoingPostStanzaHandlers( await _runOutgoingPostStanzaHandlers(
newStanza, newStanza,
initial: StanzaHandlerData( initial: StanzaHandlerData(
false, false,
false, false,
newStanza, newStanza,
extensions, details.postSendExtensions ?? TypedMap<StanzaHandlerExtension>(),
encrypted: data.encrypted,
), ),
); );
_log.fine('Done'); _log.fine('Done');
@ -736,6 +734,13 @@ class XmppConnection {
: ''; : '';
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}'); _log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
if (incomingPreHandlers.skip) {
_log.fine(
'Not processing stanza (${incomingPreHandlers.stanza.tag}, ${incomingPreHandlers.stanza.id}) due to skip=true.',
);
return;
}
final awaited = await _stanzaAwaiter.onData( final awaited = await _stanzaAwaiter.onData(
incomingPreHandlers.stanza, incomingPreHandlers.stanza,
connectionSettings.jid.toBare(), connectionSettings.jid.toBare(),
@ -753,6 +758,7 @@ class XmppConnection {
incomingPreHandlers.stanza, incomingPreHandlers.stanza,
incomingPreHandlers.extensions, incomingPreHandlers.extensions,
encrypted: incomingPreHandlers.encrypted, encrypted: incomingPreHandlers.encrypted,
encryptionError: incomingPreHandlers.encryptionError,
cancelReason: incomingPreHandlers.cancelReason, cancelReason: incomingPreHandlers.cancelReason,
), ),
); );

View File

@ -70,9 +70,9 @@ class MessageEvent extends XmppEvent {
MessageEvent( MessageEvent(
this.from, this.from,
this.to, this.to,
this.id,
this.encrypted, this.encrypted,
this.extensions, { this.extensions, {
this.id,
this.type, this.type,
this.error, this.error,
this.encryptionError, this.encryptionError,
@ -85,7 +85,7 @@ class MessageEvent extends XmppEvent {
final JID to; final JID to;
/// The id attribute of the message. /// The id attribute of the message.
final String id; final String? id;
/// The type attribute of the message. /// The type attribute of the message.
final String? type; final String? type;

View File

@ -47,7 +47,6 @@ abstract class XmppManagerBase {
final result = await dm!.discoInfoQuery( final result = await dm!.discoInfoQuery(
_managerAttributes.getConnectionSettings().jid.toDomain(), _managerAttributes.getConnectionSettings().jid.toDomain(),
shouldEncrypt: false,
); );
if (result.isType<DiscoError>()) { if (result.isType<DiscoError>()) {
return false; return false;

View File

@ -13,12 +13,20 @@ class StanzaHandlerData {
this.encryptionError, this.encryptionError,
this.encrypted = false, this.encrypted = false,
this.forceEncryption = false, this.forceEncryption = false,
this.shouldEncrypt = true,
this.skip = false,
}); });
/// Indicates to the runner that processing is now done. This means that all /// Indicates to the runner that processing is now done. This means that all
/// pre-processing is done and no other handlers should be consulted. /// pre-processing is done and no other handlers should be consulted.
bool done; bool done;
/// Only useful in combination with [done] = true: When [skip] is set to true and
/// this [StanzaHandlerData] object is returned from a IncomingPreStanzaHandler, then
/// moxxmpp will skip checking whether the stanza was awaited and will not run any actual
/// IncomingStanzaHandler callbacks.
bool skip;
/// Indicates to the runner that processing is to be cancelled and no further handlers /// Indicates to the runner that processing is to be cancelled and no further handlers
/// should run. The stanza also will not be sent. /// should run. The stanza also will not be sent.
bool cancel; bool cancel;
@ -33,7 +41,7 @@ class StanzaHandlerData {
/// absolutely necessary, e.g. with Message Carbons or OMEMO. /// absolutely necessary, e.g. with Message Carbons or OMEMO.
Stanza stanza; Stanza stanza;
/// Whether the stanza was received encrypted /// Whether the stanza is already encrypted
bool encrypted; bool encrypted;
// If true, forces the encryption manager to encrypt to the JID, even if it // If true, forces the encryption manager to encrypt to the JID, even if it
@ -42,6 +50,10 @@ class StanzaHandlerData {
// to the JID anyway. // to the JID anyway.
bool forceEncryption; bool forceEncryption;
/// Flag indicating whether a E2EE implementation should encrypt the stanza (true)
/// or not (false).
bool shouldEncrypt;
/// Additional data from other managers. /// Additional data from other managers.
final TypedMap<StanzaHandlerExtension> extensions; final TypedMap<StanzaHandlerExtension> extensions;
} }

View File

@ -1,4 +1,4 @@
import 'package:moxlib/moxlib.dart'; import 'package:collection/collection.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
@ -100,10 +100,10 @@ class StanzaHandler extends Handler {
matches &= firstTag?.xmlns == tagXmlns; matches &= firstTag?.xmlns == tagXmlns;
} }
} else if (tagXmlns != null) { } else if (tagXmlns != null) {
matches &= listContains( matches &= node.children.firstWhereOrNull(
node.children, (XMLNode node_) => node_.attributes['xmlns'] == tagXmlns,
(XMLNode node_) => node_.attributes['xmlns'] == tagXmlns, ) !=
); null;
} }
return matches; return matches;

View File

@ -73,16 +73,23 @@ class MessageManager extends XmppManagerBase {
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage( Future<StanzaHandlerData> _onMessage(
Stanza _, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final body = stanza.firstTag('body');
if (body != null) {
state.extensions.set(
MessageBodyData(body.innerText()),
);
}
getAttributes().sendEvent( getAttributes().sendEvent(
MessageEvent( MessageEvent(
JID.fromString(state.stanza.attributes['from']! as String), JID.fromString(state.stanza.attributes['from']! as String),
JID.fromString(state.stanza.attributes['to']! as String), JID.fromString(state.stanza.attributes['to']! as String),
state.stanza.attributes['id']! as String,
state.encrypted, state.encrypted,
state.extensions, state.extensions,
id: state.stanza.attributes['id'] as String?,
type: state.stanza.attributes['type'] as String?, type: state.stanza.attributes['type'] as String?,
error: StanzaError.fromStanza(state.stanza), error: StanzaError.fromStanza(state.stanza),
encryptionError: state.encryptionError, encryptionError: state.encryptionError,

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/connection.dart'; import 'package:moxxmpp/src/connection.dart';
@ -8,7 +9,6 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/settings.dart'; import 'package:moxxmpp/src/settings.dart';
import 'package:moxxmpp/src/socket.dart'; import 'package:moxxmpp/src/socket.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
/// The state a negotiator is currently in /// The state a negotiator is currently in
enum NegotiatorState { enum NegotiatorState {
@ -117,8 +117,7 @@ abstract class XmppFeatureNegotiatorBase {
/// Returns true if a feature in [features], which are the children of the /// Returns true if a feature in [features], which are the children of the
/// <stream:features /> nonza, can be negotiated. Otherwise, returns false. /// <stream:features /> nonza, can be negotiated. Otherwise, returns false.
bool matchesFeature(List<XMLNode> features) { bool matchesFeature(List<XMLNode> features) {
return firstWhereOrNull( return features.firstWhereOrNull(
features,
(XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns, (XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns,
) != ) !=
null; null;

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@ -10,7 +11,8 @@ import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
/// A function that will be called when presence, outside of subscription request /// A function that will be called when presence, outside of subscription request
/// management, will be sent. Useful for managers that want to add [XMLNode]s to said /// management, will be sent. Useful for managers that want to add [XMLNode]s to said
@ -156,7 +158,9 @@ class PresenceManager extends XmppManagerBase {
), ),
awaitable: false, awaitable: false,
bypassQueue: true, bypassQueue: true,
excludeFromStreamManagement: true, postSendExtensions: TypedMap<StanzaHandlerExtension>.fromList([
const StreamManagementData(true, null),
]),
), ),
); );
} }

View File

@ -1,10 +1,10 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';

View File

@ -1,4 +1,4 @@
import 'package:moxlib/moxlib.dart'; import 'package:collection/collection.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
@ -13,15 +13,13 @@ abstract class SaslNegotiator extends XmppFeatureNegotiatorBase {
@override @override
bool matchesFeature(List<XMLNode> features) { bool matchesFeature(List<XMLNode> features) {
// Is SASL advertised? // Is SASL advertised?
final mechanisms = firstWhereOrNull( final mechanisms = features.firstWhereOrNull(
features,
(XMLNode feature) => feature.attributes['xmlns'] == saslXmlns, (XMLNode feature) => feature.attributes['xmlns'] == saslXmlns,
); );
if (mechanisms == null) return false; if (mechanisms == null) return false;
// Is SASL PLAIN advertised? // Is SASL PLAIN advertised?
return firstWhereOrNull( return mechanisms.children.firstWhereOrNull(
mechanisms.children,
(XMLNode mechanism) => mechanism.text == mechanismName, (XMLNode mechanism) => mechanism.text == mechanismName,
) != ) !=
null; null;

View File

@ -1,12 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart'; import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart'; import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart'; import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart'; import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
import 'package:saslprep/saslprep.dart'; import 'package:saslprep/saslprep.dart';

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:math' show Random; import 'dart:math' show Random;
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
@ -10,7 +11,6 @@ import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart'; import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart'; import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart'; import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart'; import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
import 'package:random_string/random_string.dart'; import 'package:random_string/random_string.dart';

View File

@ -1,9 +1,9 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
enum _StartTlsState { ready, requested } enum _StartTlsState { ready, requested }

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/attributes.dart'; import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@ -14,7 +15,6 @@ import 'package:moxxmpp/src/roster/errors.dart';
import 'package:moxxmpp/src/roster/state.dart'; import 'package:moxxmpp/src/roster/state.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
@immutable @immutable
class XmppRosterItem { class XmppRosterItem {

View File

@ -1,5 +1,7 @@
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// A description of a stanza to send. /// A description of a stanza to send.
class StanzaDetails { class StanzaDetails {
@ -7,10 +9,11 @@ class StanzaDetails {
this.stanza, { this.stanza, {
this.addId = true, this.addId = true,
this.awaitable = true, this.awaitable = true,
this.shouldEncrypt = true,
this.encrypted = false, this.encrypted = false,
this.forceEncryption = false, this.forceEncryption = false,
this.bypassQueue = false, this.bypassQueue = false,
this.excludeFromStreamManagement = false, this.postSendExtensions,
}); });
/// The stanza to send. /// The stanza to send.
@ -22,9 +25,16 @@ class StanzaDetails {
/// Track the stanza to allow awaiting its response. /// Track the stanza to allow awaiting its response.
final bool awaitable; final bool awaitable;
final bool forceEncryption;
/// Flag indicating whether the stanza that is sent is already encrypted (true)
/// or not (false). This is only useful for E2EE implementations that have to
/// send heartbeats that must bypass themselves.
final bool encrypted; final bool encrypted;
final bool forceEncryption; /// Tells an E2EE implementation, if available, to encrypt the stanza (true) or
/// ignore the stanza (false).
final bool shouldEncrypt;
/// Bypasses being put into the queue. Useful for sending stanzas that must go out /// Bypasses being put into the queue. Useful for sending stanzas that must go out
/// now, where it's okay if it does not get sent. /// now, where it's okay if it does not get sent.
@ -34,31 +44,60 @@ class StanzaDetails {
/// This makes the Stream Management implementation, when available, ignore the stanza, /// This makes the Stream Management implementation, when available, ignore the stanza,
/// meaning that it gets counted but excluded from resending. /// meaning that it gets counted but excluded from resending.
/// This should never have to be set to true. /// This should never have to be set to true.
final bool excludeFromStreamManagement; final TypedMap<StanzaHandlerExtension>? postSendExtensions;
} }
/// A simple description of the <error /> element that may be inside a stanza /// A general error type for errors.
class StanzaError { abstract class StanzaError {
StanzaError(this.type, this.error); static StanzaError? fromXMLNode(XMLNode node) {
String type; final error = node.firstTag('error');
String error; if (error == null) {
return null;
}
final specificError = error.firstTagByXmlns(fullStanzaXmlns);
if (specificError == null) {
return UnknownStanzaError();
}
switch (specificError.tag) {
case RemoteServerNotFoundError.tag:
return RemoteServerNotFoundError();
case RemoteServerTimeoutError.tag:
return RemoteServerTimeoutError();
case ServiceUnavailableError.tag:
return ServiceUnavailableError();
}
return UnknownStanzaError();
}
/// Returns a StanzaError if [stanza] contains a <error /> element. If not, returns
/// null.
static StanzaError? fromStanza(Stanza stanza) { static StanzaError? fromStanza(Stanza stanza) {
final error = stanza.firstTag('error'); return fromXMLNode(stanza);
if (error == null) return null;
final stanzaError = error.firstTagByXmlns(fullStanzaXmlns);
if (stanzaError == null) return null;
return StanzaError(
error.attributes['type']! as String,
stanzaError.tag,
);
} }
} }
/// Recipient does not provide a given service.
/// https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-service-unavailable
class ServiceUnavailableError extends StanzaError {
static const tag = 'service-unavailable';
}
/// Could not connect to the remote server.
/// https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-remote-server-not-found
class RemoteServerNotFoundError extends StanzaError {
static const tag = 'remote-server-not-found';
}
/// The connection to the remote server timed out.
/// https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-remote-server-timeout
class RemoteServerTimeoutError extends StanzaError {
static const tag = 'remote-server-timeout';
}
/// An unknown error.
class UnknownStanzaError extends StanzaError {}
class Stanza extends XMLNode { class Stanza extends XMLNode {
// ignore: use_super_parameters // ignore: use_super_parameters
Stanza({ Stanza({

View File

@ -1,16 +0,0 @@
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;
}
}

View File

@ -33,7 +33,7 @@ class AsyncStanzaQueue {
this._canSendCallback, this._canSendCallback,
); );
/// The lock for accessing [AsyncStanzaQueue._lock] and [AsyncStanzaQueue._running]. /// The lock for accessing [AsyncStanzaQueue._queue].
final Lock _lock = Lock(); final Lock _lock = Lock();
/// The actual job queue. /// The actual job queue.
@ -44,22 +44,15 @@ class AsyncStanzaQueue {
final CanSendCallback _canSendCallback; final CanSendCallback _canSendCallback;
/// Indicates whether we are currently executing a job.
bool _running = false;
@visibleForTesting @visibleForTesting
Queue<StanzaQueueEntry> get queue => _queue; Queue<StanzaQueueEntry> get queue => _queue;
@visibleForTesting
bool get isRunning => _running;
/// Adds a job [entry] to the queue. /// Adds a job [entry] to the queue.
Future<void> enqueueStanza(StanzaQueueEntry entry) async { Future<void> enqueueStanza(StanzaQueueEntry entry) async {
await _lock.synchronized(() async { await _lock.synchronized(() async {
_queue.add(entry); _queue.add(entry);
if (!_running && _queue.isNotEmpty && await _canSendCallback()) { if (_queue.isNotEmpty && await _canSendCallback()) {
_running = true;
unawaited( unawaited(
_runJob(_queue.removeFirst()), _runJob(_queue.removeFirst()),
); );
@ -79,8 +72,6 @@ class AsyncStanzaQueue {
unawaited( unawaited(
_runJob(_queue.removeFirst()), _runJob(_queue.removeFirst()),
); );
} else {
_running = false;
} }
}); });
} }
@ -90,7 +81,6 @@ class AsyncStanzaQueue {
await _lock.synchronized(() { await _lock.synchronized(() {
if (_queue.isNotEmpty) { if (_queue.isNotEmpty) {
_running = true;
unawaited( unawaited(
_runJob(_queue.removeFirst()), _runJob(_queue.removeFirst()),
); );

View File

@ -20,4 +20,6 @@ class TypedMap<B> {
/// Return the object of type [T] from the map, if it has been stored. /// Return the object of type [T] from the map, if it has been stored.
T? get<T>() => _data[T] as T?; T? get<T>() => _data[T] as T?;
Iterable<Object> get keys => _data.keys;
} }

View File

@ -1,11 +1,11 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart'; import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart'; import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';

View File

@ -1,4 +1,4 @@
import 'package:moxlib/moxlib.dart'; import 'package:collection/collection.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
@ -80,7 +80,7 @@ class DataForm {
final List<List<DataFormField>> items; final List<List<DataFormField>> items;
DataFormField? getFieldByVar(String varAttr) { DataFormField? getFieldByVar(String varAttr) {
return firstWhereOrNull(fields, (field) => field.varAttr == varAttr); return fields.firstWhereOrNull((field) => field.varAttr == varAttr);
} }
XMLNode toXml() { XMLNode toXml() {

View File

@ -1,7 +1,10 @@
abstract class DiscoError {} import 'package:moxxmpp/src/stanza.dart';
/// Base type for disco-related errors.
abstract class DiscoError extends StanzaError {}
/// An unspecified error that is not covered by another [DiscoError].
class UnknownDiscoError extends DiscoError {} class UnknownDiscoError extends DiscoError {}
/// The received disco response is invalid in some shape or form.
class InvalidResponseDiscoError extends DiscoError {} class InvalidResponseDiscoError extends DiscoError {}
class ErrorResponseDiscoError extends DiscoError {}

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@ -9,7 +10,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/wait.dart'; import 'package:moxxmpp/src/util/wait.dart';
import 'package:moxxmpp/src/xeps/xep_0030/cache.dart'; import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
@ -44,11 +44,11 @@ class DiscoManager extends XmppManagerBase {
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {}; final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
/// The tracker for tracking disco#info queries that are in flight. /// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> final WaitForTracker<DiscoCacheKey, Result<StanzaError, DiscoInfo>>
_discoInfoTracker = WaitForTracker(); _discoInfoTracker = WaitForTracker();
/// The tracker for tracking disco#info queries that are in flight. /// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, List<DiscoItem>>> final WaitForTracker<DiscoCacheKey, Result<StanzaError, List<DiscoItem>>>
_discoItemsTracker = WaitForTracker(); _discoItemsTracker = WaitForTracker();
/// Cache lock /// Cache lock
@ -67,7 +67,7 @@ class DiscoManager extends XmppManagerBase {
List<String> get features => _features; List<String> get features => _features;
@visibleForTesting @visibleForTesting
WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> WaitForTracker<DiscoCacheKey, Result<StanzaError, DiscoInfo>>
get infoTracker => _discoInfoTracker; get infoTracker => _discoInfoTracker;
@override @override
@ -231,7 +231,7 @@ class DiscoManager extends XmppManagerBase {
Future<void> _exitDiscoInfoCriticalSection( Future<void> _exitDiscoInfoCriticalSection(
DiscoCacheKey key, DiscoCacheKey key,
Result<DiscoError, DiscoInfo> result, Result<StanzaError, DiscoInfo> result,
bool shouldCache, bool shouldCache,
) async { ) async {
await _cacheLock.synchronized(() async { await _cacheLock.synchronized(() async {
@ -252,10 +252,10 @@ class DiscoManager extends XmppManagerBase {
/// ///
/// [shouldCache] indicates whether the successful result of the disco#info query /// [shouldCache] indicates whether the successful result of the disco#info query
/// should be cached (true) or not(false). /// should be cached (true) or not(false).
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery( Future<Result<StanzaError, DiscoInfo>> discoInfoQuery(
JID entity, { JID entity, {
String? node, String? node,
bool shouldEncrypt = true, bool shouldEncrypt = false,
bool shouldCache = true, bool shouldCache = true,
}) async { }) async {
DiscoInfo? info; DiscoInfo? info;
@ -263,7 +263,7 @@ class DiscoManager extends XmppManagerBase {
final ecm = getAttributes() final ecm = getAttributes()
.getManagerById<EntityCapabilitiesManager>(entityCapabilitiesManager); .getManagerById<EntityCapabilitiesManager>(entityCapabilitiesManager);
final ffuture = await _cacheLock final ffuture = await _cacheLock
.synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>( .synchronized<Future<Future<Result<StanzaError, DiscoInfo>>?>?>(
() async { () async {
// Check if we already know what the JID supports // Check if we already know what the JID supports
if (_discoInfoCache.containsKey(cacheKey)) { if (_discoInfoCache.containsKey(cacheKey)) {
@ -294,19 +294,21 @@ class DiscoManager extends XmppManagerBase {
final stanza = (await getAttributes().sendStanza( final stanza = (await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
buildDiscoInfoQueryStanza(entity, node), buildDiscoInfoQueryStanza(entity, node),
encrypted: !shouldEncrypt, shouldEncrypt: shouldEncrypt,
), ),
))!; ))!;
final query = stanza.firstTag('query');
if (query == null) { // Error handling
final result = Result<DiscoError, DiscoInfo>(InvalidResponseDiscoError()); if (stanza.attributes['type'] == 'error') {
final result =
Result<StanzaError, DiscoInfo>(StanzaError.fromXMLNode(stanza));
await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache); await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
return result; return result;
} }
if (stanza.attributes['type'] == 'error') { final query = stanza.firstTag('query');
//final error = stanza.firstTag('error'); if (query == null) {
final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError()); final result = Result<DiscoError, DiscoInfo>(InvalidResponseDiscoError());
await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache); await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
return result; return result;
} }
@ -322,10 +324,10 @@ class DiscoManager extends XmppManagerBase {
} }
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node]. /// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery( Future<Result<StanzaError, List<DiscoItem>>> discoItemsQuery(
JID entity, { JID entity, {
String? node, String? node,
bool shouldEncrypt = true, bool shouldEncrypt = false,
}) async { }) async {
final key = DiscoCacheKey(entity, node); final key = DiscoCacheKey(entity, node);
final future = await _discoItemsTracker.waitFor(key); final future = await _discoItemsTracker.waitFor(key);
@ -340,19 +342,18 @@ class DiscoManager extends XmppManagerBase {
), ),
))!; ))!;
final query = stanza.firstTag('query'); // Error handling
if (query == null) { if (stanza.attributes['type'] == 'error') {
final result = final result =
Result<DiscoError, List<DiscoItem>>(InvalidResponseDiscoError()); Result<StanzaError, List<DiscoItem>>(StanzaError.fromXMLNode(stanza));
await _discoItemsTracker.resolve(key, result); await _discoItemsTracker.resolve(key, result);
return result; return result;
} }
if (stanza.attributes['type'] == 'error') { final query = stanza.firstTag('query');
//final error = stanza.firstTag('error'); if (query == null) {
//print("Disco Items error: " + error.toXml());
final result = final result =
Result<DiscoError, List<DiscoItem>>(ErrorResponseDiscoError()); Result<DiscoError, List<DiscoItem>>(InvalidResponseDiscoError());
await _discoItemsTracker.resolve(key, result); await _discoItemsTracker.resolve(key, result);
return result; return result;
} }
@ -419,7 +420,7 @@ class DiscoManager extends XmppManagerBase {
/// [entity] supports the disco feature [feature]. If not, returns false. /// [entity] supports the disco feature [feature]. If not, returns false.
Future<bool> supportsFeature(JID entity, String feature) async { Future<bool> supportsFeature(JID entity, String feature) async {
final info = await discoInfoQuery(entity); final info = await discoInfoQuery(entity);
if (info.isType<DiscoError>()) return false; if (info.isType<StanzaError>()) return false;
return info.get<DiscoInfo>().features.contains(feature); return info.get<DiscoInfo>().features.contains(feature);
} }

View File

@ -1,3 +1,4 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@ -7,7 +8,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
abstract class VCardError {} abstract class VCardError {}

View File

@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@ -9,7 +9,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0004.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/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
@ -202,6 +201,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -245,6 +245,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -329,6 +330,7 @@ class PubSubManager extends XmppManagerBase {
) )
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
@ -419,6 +421,7 @@ class PubSubManager extends XmppManagerBase {
) )
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -471,6 +474,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -521,6 +525,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
if (form.attributes['type'] != 'result') { if (form.attributes['type'] != 'result') {
@ -550,6 +555,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
if (submit.attributes['type'] != 'result') { if (submit.attributes['type'] != 'result') {
@ -580,6 +586,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -624,6 +631,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;

View File

@ -1,11 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.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/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
@ -225,7 +225,6 @@ class UserAvatarManager extends XmppManagerBase {
final response = await disco.discoItemsQuery( final response = await disco.discoItemsQuery(
jid, jid,
node: userAvatarDataXmlns, node: userAvatarDataXmlns,
shouldEncrypt: false,
); );
if (response.isType<DiscoError>()) return Result(UnknownAvatarError()); if (response.isType<DiscoError>()) return Result(UnknownAvatarError());

View File

@ -1,12 +1,12 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.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/state.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
@ -113,7 +113,7 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator
// We have to do this because we otherwise get a stanza stuck in the queue, // 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. // thus spamming the server on every <a /> nonza we receive.
// ignore: cascade_invocations // ignore: cascade_invocations
await sm.setState(StreamManagementState(0, 0)); await sm.setState(const StreamManagementState(0, 0));
await sm.commitState(); await sm.commitState();
_resumeFailed = true; _resumeFailed = true;

View File

@ -1,18 +1,43 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:meta/meta.dart';
part 'state.freezed.dart'; const _smNotSpecified = Object();
part 'state.g.dart';
@freezed @immutable
class StreamManagementState with _$StreamManagementState { class StreamManagementState {
factory StreamManagementState( const StreamManagementState(
int c2s, this.c2s,
int s2c, { this.s2c, {
String? streamResumptionLocation, this.streamResumptionLocation,
String? streamResumptionId, this.streamResumptionId,
}) = _StreamManagementState; });
// JSON /// The counter of stanzas sent from the client to the server.
factory StreamManagementState.fromJson(Map<String, dynamic> json) => final int c2s;
_$StreamManagementStateFromJson(json);
/// The counter of stanzas sent from the server to the client.
final int s2c;
/// If set, the server's preferred location for resumption.
final String? streamResumptionLocation;
/// If set, the token to allow using stream resumption.
final String? streamResumptionId;
StreamManagementState copyWith({
Object c2s = _smNotSpecified,
Object s2c = _smNotSpecified,
Object? streamResumptionLocation = _smNotSpecified,
Object? streamResumptionId = _smNotSpecified,
}) {
return StreamManagementState(
c2s != _smNotSpecified ? c2s as int : this.c2s,
s2c != _smNotSpecified ? s2c as int : this.s2c,
streamResumptionLocation: streamResumptionLocation != _smNotSpecified
? streamResumptionLocation as String?
: this.streamResumptionLocation,
streamResumptionId: streamResumptionId != _smNotSpecified
? streamResumptionId as String?
: this.streamResumptionId,
);
}
} }

View File

@ -1,217 +0,0 @@
// 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, unnecessary_question_mark
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, StreamManagementState>;
@useResult
$Res call(
{int c2s,
int s2c,
String? streamResumptionLocation,
String? streamResumptionId});
}
/// @nodoc
class _$StreamManagementStateCopyWithImpl<$Res,
$Val extends StreamManagementState>
implements $StreamManagementStateCopyWith<$Res> {
_$StreamManagementStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? c2s = null,
Object? s2c = null,
Object? streamResumptionLocation = freezed,
Object? streamResumptionId = freezed,
}) {
return _then(_value.copyWith(
c2s: null == c2s
? _value.c2s
: c2s // ignore: cast_nullable_to_non_nullable
as int,
s2c: null == s2c
? _value.s2c
: s2c // ignore: cast_nullable_to_non_nullable
as int,
streamResumptionLocation: freezed == streamResumptionLocation
? _value.streamResumptionLocation
: streamResumptionLocation // ignore: cast_nullable_to_non_nullable
as String?,
streamResumptionId: freezed == streamResumptionId
? _value.streamResumptionId
: streamResumptionId // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
/// @nodoc
abstract class _$$_StreamManagementStateCopyWith<$Res>
implements $StreamManagementStateCopyWith<$Res> {
factory _$$_StreamManagementStateCopyWith(_$_StreamManagementState value,
$Res Function(_$_StreamManagementState) then) =
__$$_StreamManagementStateCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int c2s,
int s2c,
String? streamResumptionLocation,
String? streamResumptionId});
}
/// @nodoc
class __$$_StreamManagementStateCopyWithImpl<$Res>
extends _$StreamManagementStateCopyWithImpl<$Res, _$_StreamManagementState>
implements _$$_StreamManagementStateCopyWith<$Res> {
__$$_StreamManagementStateCopyWithImpl(_$_StreamManagementState _value,
$Res Function(_$_StreamManagementState) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? c2s = null,
Object? s2c = null,
Object? streamResumptionLocation = freezed,
Object? streamResumptionId = freezed,
}) {
return _then(_$_StreamManagementState(
null == c2s
? _value.c2s
: c2s // ignore: cast_nullable_to_non_nullable
as int,
null == s2c
? _value.s2c
: s2c // ignore: cast_nullable_to_non_nullable
as int,
streamResumptionLocation: freezed == streamResumptionLocation
? _value.streamResumptionLocation
: streamResumptionLocation // ignore: cast_nullable_to_non_nullable
as String?,
streamResumptionId: freezed == streamResumptionId
? _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 &&
(identical(other.c2s, c2s) || other.c2s == c2s) &&
(identical(other.s2c, s2c) || other.s2c == s2c) &&
(identical(
other.streamResumptionLocation, streamResumptionLocation) ||
other.streamResumptionLocation == streamResumptionLocation) &&
(identical(other.streamResumptionId, streamResumptionId) ||
other.streamResumptionId == streamResumptionId));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType, c2s, s2c, streamResumptionLocation, streamResumptionId);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_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;
}

View File

@ -1,25 +0,0 @@
// 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,
};

View File

@ -1,8 +1,39 @@
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/stanza.dart';
class StreamManagementData implements StanzaHandlerExtension { class StreamManagementData implements StanzaHandlerExtension {
const StreamManagementData(this.exclude); const StreamManagementData(this.exclude, this.queueId);
/// Whether the stanza should be exluded from the StreamManagement's resend queue. /// Whether the stanza should be exluded from the StreamManagement's resend queue.
final bool exclude; final bool exclude;
/// The ID to use when queuing the stanza.
final int? queueId;
/// If we resend a stanza, then we will have [queueId] set, so we should skip
/// incrementing the C2S counter.
bool get shouldCountStanza => queueId == null;
}
/// A queue element for keeping track of stanzas to (potentially) resend.
@immutable
class SMQueueEntry {
const SMQueueEntry(this.stanza, this.encrypted);
/// The actual stanza.
final Stanza stanza;
/// Flag indicating whether the stanza was encrypted before sending.
final bool encrypted;
@override
bool operator ==(Object other) {
return other is SMQueueEntry &&
other.stanza == stanza &&
other.encrypted == encrypted;
}
@override
int get hashCode => stanza.hashCode ^ encrypted.hashCode;
} }

View File

@ -11,6 +11,7 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0198/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0198/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.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/nonzas.dart';
@ -28,10 +29,10 @@ class StreamManagementManager extends XmppManagerBase {
}) : super(smManager); }) : super(smManager);
/// The queue of stanzas that are not (yet) acked /// The queue of stanzas that are not (yet) acked
final Map<int, Stanza> _unackedStanzas = {}; final Map<int, SMQueueEntry> _unackedStanzas = {};
/// Commitable state of the StreamManagementManager /// Commitable state of the StreamManagementManager
StreamManagementState _state = StreamManagementState(0, 0); StreamManagementState _state = const StreamManagementState(0, 0);
/// Mutex lock for _state /// Mutex lock for _state
final Lock _stateLock = Lock(); final Lock _stateLock = Lock();
@ -61,7 +62,7 @@ class StreamManagementManager extends XmppManagerBase {
/// Functions for testing /// Functions for testing
@visibleForTesting @visibleForTesting
Map<int, Stanza> getUnackedStanzas() => _unackedStanzas; Map<int, SMQueueEntry> getUnackedStanzas() => _unackedStanzas;
@visibleForTesting @visibleForTesting
Future<int> getPendingAcks() async { Future<int> getPendingAcks() async {
@ -306,6 +307,10 @@ class StreamManagementManager extends XmppManagerBase {
if (_pendingAcks > 0) { if (_pendingAcks > 0) {
// Prevent diff from becoming negative // Prevent diff from becoming negative
final diff = max(_state.c2s - h, 0); final diff = max(_state.c2s - h, 0);
logger.finest(
'Setting _pendingAcks to $diff (was $_pendingAcks before): max(${_state.c2s} - $h, 0)',
);
_pendingAcks = diff; _pendingAcks = diff;
// Reset the timer // Reset the timer
@ -336,15 +341,18 @@ class StreamManagementManager extends XmppManagerBase {
final attrs = getAttributes(); final attrs = getAttributes();
final sequences = _unackedStanzas.keys.toList()..sort(); final sequences = _unackedStanzas.keys.toList()..sort();
for (final height in sequences) { for (final height in sequences) {
logger.finest('Unacked stanza: height $height, h $h');
// Do nothing if the ack does not concern this stanza // Do nothing if the ack does not concern this stanza
if (height > h) continue; if (height > h) continue;
final stanza = _unackedStanzas[height]!; logger.finest('Removing stanza with height $height');
final entry = _unackedStanzas[height]!;
_unackedStanzas.remove(height); _unackedStanzas.remove(height);
// Create a StanzaAckedEvent if the stanza is correct // Create a StanzaAckedEvent if the stanza is correct
if (shouldTriggerAckedEvent(stanza)) { if (shouldTriggerAckedEvent(entry.stanza)) {
attrs.sendEvent(StanzaAckedEvent(stanza)); attrs.sendEvent(StanzaAckedEvent(entry.stanza));
} }
} }
@ -401,13 +409,29 @@ class StreamManagementManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
if (isStreamManagementEnabled()) { if (isStreamManagementEnabled()) {
await _incrementC2S(); final smData = state.extensions.get<StreamManagementData>();
logger.finest('Should count stanza: ${smData?.shouldCountStanza}');
if (smData?.shouldCountStanza ?? true) {
await _incrementC2S();
}
if (state.extensions.get<StreamManagementData>()?.exclude ?? false) { if (smData?.exclude ?? false) {
return state; return state;
} }
_unackedStanzas[_state.c2s] = stanza; int queueId;
if (smData?.queueId != null) {
logger.finest('Reusing queue id ${smData!.queueId}');
queueId = smData.queueId!;
} else {
queueId = await _stateLock.synchronized(() => _state.c2s);
}
_unackedStanzas[queueId] = SMQueueEntry(
stanza,
// Prevent an E2EE message being encrypted again
state.encrypted,
);
await _sendAckRequest(); await _sendAckRequest();
} }
@ -415,16 +439,23 @@ class StreamManagementManager extends XmppManagerBase {
} }
Future<void> _resendStanzas() async { Future<void> _resendStanzas() async {
final stanzas = _unackedStanzas.values.toList(); final queueCopy = _unackedStanzas.entries.toList();
_unackedStanzas.clear(); for (final entry in queueCopy) {
logger.finest(
for (final stanza in stanzas) { 'Resending ${entry.value.stanza.tag} with id ${entry.value.stanza.attributes["id"]}',
logger );
.finest('Resending ${stanza.tag} with id ${stanza.attributes["id"]}');
await getAttributes().sendStanza( await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
stanza, entry.value.stanza,
postSendExtensions: TypedMap<StanzaHandlerExtension>.fromList([
StreamManagementData(
false,
entry.key,
),
]),
awaitable: false, awaitable: false,
// Prevent an E2EE message being encrypted again
encrypted: entry.value.encrypted,
), ),
); );
} }

View File

@ -1,10 +1,10 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0386.dart'; import 'package:moxxmpp/src/xeps/xep_0386.dart';
class CSIActiveNonza extends XMLNode { class CSIActiveNonza extends XMLNode {

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
@ -7,7 +8,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.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/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
@ -58,10 +58,10 @@ class HttpFileUploadManager extends XmppManagerBase {
/// Returns whether the entity provided an identity that tells us that we can ask it /// Returns whether the entity provided an identity that tells us that we can ask it
/// for an HTTP upload slot. /// for an HTTP upload slot.
bool _containsFileUploadIdentity(DiscoInfo info) { bool _containsFileUploadIdentity(DiscoInfo info) {
return listContains( return info.identities.firstWhereOrNull(
info.identities, (Identity id) => id.category == 'store' && id.type == 'file',
(Identity id) => id.category == 'store' && id.type == 'file', ) !=
); null;
} }
/// Extract the maximum filesize in octets from the disco response. Returns null /// Extract the maximum filesize in octets from the disco response. Returns null

View File

@ -1,3 +1,4 @@
import 'package:moxxmpp/src/managers/data.dart';
import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/omemo_dart.dart';
/// A simple wrapper class for defining elements that should not be encrypted. /// A simple wrapper class for defining elements that should not be encrypted.
@ -13,9 +14,16 @@ class DoNotEncrypt {
/// An encryption error caused by OMEMO. /// An encryption error caused by OMEMO.
class OmemoEncryptionError { class OmemoEncryptionError {
const OmemoEncryptionError(this.jids, this.devices); const OmemoEncryptionError(this.deviceEncryptionErrors);
/// See omemo_dart's EncryptionResult for info on these fields. /// See omemo_dart's EncryptionResult for info on this field.
final Map<String, OmemoException> jids; final Map<String, List<EncryptToJidError>> deviceEncryptionErrors;
final Map<RatchetMapKey, OmemoException> devices; }
class OmemoData extends StanzaHandlerExtension {
OmemoData(this.newRatchets, this.replacedRatchets);
final Map<String, List<int>> newRatchets;
final Map<String, List<int>> replacedRatchets;
} }

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@ -10,13 +11,11 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.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/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.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_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0203.dart';
import 'package:moxxmpp/src/xeps/xep_0280.dart'; import 'package:moxxmpp/src/xeps/xep_0280.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart'; import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart'; import 'package:moxxmpp/src/xeps/xep_0380.dart';
@ -24,9 +23,21 @@ 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/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0384/helpers.dart'; import 'package:moxxmpp/src/xeps/xep_0384/helpers.dart';
import 'package:moxxmpp/src/xeps/xep_0384/types.dart'; import 'package:moxxmpp/src/xeps/xep_0384/types.dart';
import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/omemo_dart.dart' as omemo;
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
/// A callback that is executed whenever we need to acquire the OmemoManager backing
/// the manager.
typedef GetOmemoManagerCallback = Future<omemo.OmemoManager> Function();
/// A callback for figuring out whether a stanza should be encrypted or not. Note that
/// returning true here does not necessarily mean that a stanza gets encrypted because
/// handlers can indicate that a stanza should not be encrypted, e.g. PubSub.
typedef ShouldEncryptStanzaCallback = Future<bool> Function(
JID toJid,
Stanza stanza,
);
const _doNotEncryptList = [ const _doNotEncryptList = [
// XEP-0033 // XEP-0033
DoNotEncrypt('addresses', extendedAddressingXmlns), DoNotEncrypt('addresses', extendedAddressingXmlns),
@ -43,8 +54,15 @@ const _doNotEncryptList = [
DoNotEncrypt('stanza-id', stableIdXmlns), DoNotEncrypt('stanza-id', stableIdXmlns),
]; ];
abstract class BaseOmemoManager extends XmppManagerBase { class OmemoManager extends XmppManagerBase {
BaseOmemoManager() : super(omemoManager); OmemoManager(this._getOmemoManager, this._shouldEncryptStanza)
: super(omemoManager);
/// Callback for getting the [omemo.OmemoManager].
final GetOmemoManagerCallback _getOmemoManager;
/// Callback for checking whether a stanza should be encrypted or not.
final ShouldEncryptStanzaCallback _shouldEncryptStanza;
// TODO(Unknown): Technically, this is not always true // TODO(Unknown): Technically, this is not always true
@override @override
@ -113,22 +131,19 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
// Tell the OmemoManager // Tell the OmemoManager
(await getOmemoManager()).onDeviceListUpdate(jid.toString(), ids); await (await _getOmemoManager()).onDeviceListUpdate(jid.toString(), ids);
// Generate an event // Generate an event
getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids)); getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids));
} }
} }
@visibleForOverriding /// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<OmemoManager> getOmemoManager(); Future<int> _getDeviceId() async => (await _getOmemoManager()).getDeviceId();
/// Wrapper around using getSessionManager and then calling getDeviceId on it. /// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId(); Future<omemo.OmemoBundle> _getDeviceBundle() async {
final om = await _getOmemoManager();
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<OmemoBundle> _getDeviceBundle() async {
final om = await getOmemoManager();
final device = await om.getDevice(); final device = await om.getDevice();
return device.toBundle(); return device.toBundle();
} }
@ -199,53 +214,45 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
XMLNode _buildEncryptedElement( XMLNode _buildEncryptedElement(
EncryptionResult result, omemo.EncryptionResult result,
String recipientJid, String recipientJid,
int deviceId, int deviceId,
) { ) {
final keyElements = <String, List<XMLNode>>{}; final keyElements = <String, List<XMLNode>>{};
for (final key in result.encryptedKeys) { for (final keys in result.encryptedKeys.entries) {
final keyElement = XMLNode( keyElements[keys.key] = keys.value
tag: 'key', .map(
attributes: <String, String>{ (ek) => XMLNode(
'rid': '${key.rid}', tag: 'key',
'kex': key.kex ? 'true' : 'false', attributes: {
}, 'rid': ek.rid.toString(),
text: key.value, if (ek.kex) 'kex': 'true',
); },
text: ek.value,
if (keyElements.containsKey(key.jid)) { ),
keyElements[key.jid]!.add(keyElement); )
} else { .toList();
keyElements[key.jid] = [keyElement];
}
} }
final keysElements = keyElements.entries.map((entry) { final keysElements = keyElements.entries.map((entry) {
return XMLNode( return XMLNode(
tag: 'keys', tag: 'keys',
attributes: <String, String>{ attributes: {
'jid': entry.key, 'jid': entry.key,
}, },
children: entry.value, children: entry.value,
); );
}).toList(); }).toList();
var payloadElement = <XMLNode>[];
if (result.ciphertext != null) {
payloadElement = [
XMLNode(
tag: 'payload',
text: base64.encode(result.ciphertext!),
),
];
}
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'encrypted', tag: 'encrypted',
xmlns: omemoXmlns, xmlns: omemoXmlns,
children: [ children: [
...payloadElement, if (result.ciphertext != null)
XMLNode(
tag: 'payload',
text: base64Encode(result.ciphertext!),
),
XMLNode( XMLNode(
tag: 'header', tag: 'header',
attributes: <String, String>{ attributes: <String, String>{
@ -259,7 +266,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// For usage with omemo_dart's OmemoManager. /// For usage with omemo_dart's OmemoManager.
Future<void> sendEmptyMessageImpl( Future<void> sendEmptyMessageImpl(
EncryptionResult result, omemo.EncryptionResult result,
String toJid, String toJid,
) async { ) async {
await getAttributes().sendStanza( await getAttributes().sendStanza(
@ -288,7 +295,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Send a heartbeat message to [jid]. /// Send a heartbeat message to [jid].
Future<void> sendOmemoHeartbeat(String jid) async { Future<void> sendOmemoHeartbeat(String jid) async {
final om = await getOmemoManager(); final om = await _getOmemoManager();
await om.sendOmemoHeartbeat(jid); await om.sendOmemoHeartbeat(jid);
} }
@ -301,17 +308,22 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
/// For usage with omemo_dart's OmemoManager /// For usage with omemo_dart's OmemoManager
Future<OmemoBundle?> fetchDeviceBundle(String jid, int id) async { Future<omemo.OmemoBundle?> fetchDeviceBundle(String jid, int id) async {
final result = await retrieveDeviceBundle(JID.fromString(jid), id); final result = await retrieveDeviceBundle(JID.fromString(jid), id);
if (result.isType<OmemoError>()) return null; if (result.isType<OmemoError>()) return null;
return result.get<OmemoBundle>(); return result.get<omemo.OmemoBundle>();
} }
Future<StanzaHandlerData> _onOutgoingStanza( Future<StanzaHandlerData> _onOutgoingStanza(
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
if (!state.shouldEncrypt) {
logger.finest('Not encrypting since state.shouldEncrypt is false');
return state;
}
if (state.encrypted) { if (state.encrypted) {
logger.finest('Not encrypting since state.encrypted is true'); logger.finest('Not encrypting since state.encrypted is true');
return state; return state;
@ -324,7 +336,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
final toJid = JID.fromString(stanza.to!).toBare(); final toJid = JID.fromString(stanza.to!).toBare();
final shouldEncryptResult = await shouldEncryptStanza(toJid, stanza); final shouldEncryptResult = await _shouldEncryptStanza(toJid, stanza);
if (!shouldEncryptResult && !state.forceEncryption) { if (!shouldEncryptResult && !state.forceEncryption) {
logger.finest( logger.finest(
'Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.', 'Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.',
@ -351,29 +363,29 @@ abstract class BaseOmemoManager extends XmppManagerBase {
.getManagerById<CarbonsManager>(carbonsManager) .getManagerById<CarbonsManager>(carbonsManager)
?.isEnabled ?? ?.isEnabled ??
false; false;
final om = await getOmemoManager(); final om = await _getOmemoManager();
final encryptToJids = [
toJid.toString(),
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
];
final result = await om.onOutgoingStanza( final result = await om.onOutgoingStanza(
OmemoOutgoingStanza( omemo.OmemoOutgoingStanza(
[ encryptToJids,
toJid.toString(),
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
],
_buildEnvelope(toEncrypt, toJid.toString()), _buildEnvelope(toEncrypt, toJid.toString()),
), ),
); );
logger.finest('Encryption done'); logger.finest('Encryption done');
if (!result.isSuccess(2)) { if (!result.canSend) {
return state return state
..cancel = true ..cancel = true
// If we have no device list for toJid, then the contact most likely does not // If we have no device list for toJid, then the contact most likely does not
// support OMEMO:2 // support OMEMO:2
..cancelReason = result.jidEncryptionErrors[toJid.toString()] ..cancelReason = result.deviceEncryptionErrors[toJid.toString()]!.first
is NoKeyMaterialAvailableException .error is omemo.NoKeyMaterialAvailableError
? OmemoNotSupportedForContactException() ? OmemoNotSupportedForContactException()
: UnknownOmemoError() : UnknownOmemoError()
..encryptionError = OmemoEncryptionError( ..encryptionError = OmemoEncryptionError(
result.jidEncryptionErrors,
result.deviceEncryptionErrors, result.deviceEncryptionErrors,
); );
} }
@ -401,53 +413,45 @@ abstract class BaseOmemoManager extends XmppManagerBase {
..encrypted = true; ..encrypted = true;
} }
/// 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);
Future<StanzaHandlerData> _onIncomingStanza( Future<StanzaHandlerData> _onIncomingStanza(
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns);
if (encrypted == null) return state;
if (stanza.from == null) return state; if (stanza.from == null) return state;
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns)!;
final fromJid = JID.fromString(stanza.from!).toBare(); final fromJid = JID.fromString(stanza.from!).toBare();
final header = encrypted.firstTag('header')!; final header = encrypted.firstTag('header')!;
final payloadElement = encrypted.firstTag('payload'); final ourJid = getAttributes().getFullJID();
final keys = List<EncryptedKey>.empty(growable: true); final ourJidString = ourJid.toBare().toString();
final keys = List<omemo.EncryptedKey>.empty(growable: true);
for (final keysElement in header.findTags('keys')) { for (final keysElement in header.findTags('keys')) {
// We only care about our own JID
final jid = keysElement.attributes['jid']! as String; final jid = keysElement.attributes['jid']! as String;
for (final key in keysElement.findTags('key')) { if (jid != ourJidString) {
keys.add( continue;
EncryptedKey(
jid,
int.parse(key.attributes['rid']! as String),
key.innerText(),
key.attributes['kex'] == 'true',
),
);
} }
keys.addAll(
keysElement.findTags('key').map(
(key) => omemo.EncryptedKey(
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); final sid = int.parse(header.attributes['sid']! as String);
final om = await _getOmemoManager();
final om = await getOmemoManager();
final result = await om.onIncomingStanza( final result = await om.onIncomingStanza(
OmemoIncomingStanza( omemo.OmemoIncomingStanza(
fromJid.toString(), fromJid.toString(),
sid, sid,
state.extensions
.get<DelayedDeliveryData>()
?.timestamp
.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch,
keys, keys,
payloadElement?.innerText(), encrypted.firstTag('payload')?.innerText(),
false,
), ),
); );
@ -464,6 +468,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
.toList(); .toList();
} }
logger.finest('Got payload: ${result.payload != null}');
if (result.payload != null) { if (result.payload != null) {
XMLNode envelope; XMLNode envelope;
try { try {
@ -481,6 +486,8 @@ abstract class BaseOmemoManager extends XmppManagerBase {
// Do not add forbidden elements from the envelope // Do not add forbidden elements from the envelope
envelopeChildren.where(shouldEncryptElement), envelopeChildren.where(shouldEncryptElement),
); );
logger.finest('Adding children: ${envelopeChildren.map((c) => c.tag)}');
} else { } else {
logger.warning('Invalid envelope element: No <content /> element'); logger.warning('Invalid envelope element: No <content /> element');
} }
@ -490,6 +497,15 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
} }
// Ignore heartbeat messages
if (stanza.tag == 'message' && encrypted.firstTag('payload') == null) {
logger.finest('Received empty OMEMO message. Ending processing early.');
return state
..encrypted = true
..skip = true
..done = true;
}
return state return state
..encrypted = true ..encrypted = true
..stanza = Stanza( ..stanza = Stanza(
@ -500,6 +516,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
children: children, children: children,
tag: stanza.tag, tag: stanza.tag,
attributes: Map<String, String>.from(stanza.attributes), attributes: Map<String, String>.from(stanza.attributes),
)
..extensions.set<OmemoData>(
OmemoData(
result.newRatchets,
result.replacedRatchets,
),
); );
} }
@ -532,7 +554,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieve all device bundles for the JID [jid]. /// Retrieve all device bundles for the JID [jid].
/// ///
/// On success, returns a list of devices. On failure, returns am OmemoError. /// On success, returns a list of devices. On failure, returns am OmemoError.
Future<Result<OmemoError, List<OmemoBundle>>> retrieveDeviceBundles( Future<Result<OmemoError, List<omemo.OmemoBundle>>> retrieveDeviceBundles(
JID jid, JID jid,
) async { ) async {
// TODO(Unknown): Should we query the device list first? // TODO(Unknown): Should we query the device list first?
@ -553,7 +575,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieves a bundle from entity [jid] with the device id [deviceId]. /// Retrieves a bundle from entity [jid] with the device id [deviceId].
/// ///
/// On success, returns the device bundle. On failure, returns an OmemoError. /// On success, returns the device bundle. On failure, returns an OmemoError.
Future<Result<OmemoError, OmemoBundle>> retrieveDeviceBundle( Future<Result<OmemoError, omemo.OmemoBundle>> retrieveDeviceBundle(
JID jid, JID jid,
int deviceId, int deviceId,
) async { ) async {
@ -569,7 +591,9 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// nodes. /// nodes.
/// ///
/// On success, returns true. On failure, returns an OmemoError. /// On success, returns true. On failure, returns an OmemoError.
Future<Result<OmemoError, bool>> publishBundle(OmemoBundle bundle) async { Future<Result<OmemoError, bool>> publishBundle(
omemo.OmemoBundle bundle,
) async {
final attrs = getAttributes(); final attrs = getAttributes();
final pm = attrs.getManagerById<PubSubManager>(pubsubManager)!; final pm = attrs.getManagerById<PubSubManager>(pubsubManager)!;
final bareJid = attrs.getFullJID().toBare(); final bareJid = attrs.getFullJID().toBare();
@ -636,6 +660,11 @@ abstract class BaseOmemoManager extends XmppManagerBase {
await pm.subscribe(JID.fromString(jid), omemoDevicesXmlns); await pm.subscribe(JID.fromString(jid), omemoDevicesXmlns);
} }
/// Implementation for publishing our device [device].
Future<void> publishDeviceImpl(omemo.OmemoDevice device) async {
await publishBundle(await device.toBundle());
}
/// Attempts to find out if [jid] supports omemo:2. /// Attempts to find out if [jid] supports omemo:2.
/// ///
/// On success, returns whether [jid] has published a device list and device bundles. /// On success, returns whether [jid] has published a device list and device bundles.

View File

@ -1,10 +1,10 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart'; import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart'; import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';

View File

@ -1,7 +1,7 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/negotiator.dart'; import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
/// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2. /// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2.
abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {

View File

@ -1,10 +1,10 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart'; import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart'; import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart'; import 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';

View File

@ -1,4 +1,4 @@
import 'package:moxlib/moxlib.dart'; import 'package:collection/collection.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
@ -114,8 +114,7 @@ class StatelessFileSharingData implements StanzaHandlerExtension {
} }
StatelessFileSharingUrlSource? getFirstUrlSource() { StatelessFileSharingUrlSource? getFirstUrlSource() {
return firstWhereOrNull( return sources.firstWhereOrNull(
sources,
(StatelessFileSharingSource source) => (StatelessFileSharingSource source) =>
source is StatelessFileSharingUrlSource, source is StatelessFileSharingUrlSource,
) as StatelessFileSharingUrlSource?; ) as StatelessFileSharingUrlSource?;

View File

@ -1,5 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:moxlib/moxlib.dart'; import 'package:collection/collection.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0300.dart'; import 'package:moxxmpp/src/xeps/xep_0300.dart';
@ -54,8 +54,7 @@ class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource {
final sources = element.firstTag('sources', xmlns: sfsXmlns)!.children; final sources = element.firstTag('sources', xmlns: sfsXmlns)!.children;
// Find the first URL source // Find the first URL source
final source = firstWhereOrNull( final source = sources.firstWhereOrNull(
sources,
(XMLNode child) => (XMLNode child) =>
child.tag == 'url-data' && child.attributes['xmlns'] == urlDataXmlns, child.tag == 'url-data' && child.attributes['xmlns'] == urlDataXmlns,
)!; )!;

View File

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
@ -9,7 +10,6 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/rfcs/rfc_4790.dart'; import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/typed_map.dart'; import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.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_0060/xep_0060.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';

View File

@ -10,18 +10,16 @@ environment:
dependencies: dependencies:
collection: ^1.16.0 collection: ^1.16.0
cryptography: ^2.0.5 cryptography: ^2.0.5
freezed: ^2.1.0+1
freezed_annotation: ^2.1.0
hex: ^0.2.0 hex: ^0.2.0
json_serializable: ^6.3.1 json_serializable: ^6.3.1
logging: ^1.0.2 logging: ^1.0.2
meta: ^1.7.0 meta: ^1.7.0
moxlib: moxlib:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.5 version: ^0.2.0
omemo_dart: omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.4.3 version: ^0.5.1
random_string: ^2.3.1 random_string: ^2.3.1
saslprep: ^1.0.2 saslprep: ^1.0.2
synchronized: ^3.0.0+2 synchronized: ^3.0.0+2
@ -32,3 +30,10 @@ dev_dependencies:
build_runner: ^2.1.11 build_runner: ^2.1.11
test: ^1.16.0 test: ^1.16.0
very_good_analysis: ^3.0.1 very_good_analysis: ^3.0.1
# TODO: Remove once we release 0.5.1
dependency_overrides:
omemo_dart:
git:
url: https://github.com/PapaTutuWawa/omemo_dart.git
rev: 49c7e114e6cf80dcde55fbbd218bba3182045862

View File

@ -30,7 +30,6 @@ void main() {
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
expect(queue.queue.length, 2); expect(queue.queue.length, 2);
expect(queue.isRunning, false);
}); });
test('Test sending', () async { test('Test sending', () async {
@ -58,7 +57,6 @@ void main() {
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
expect(queue.queue.length, 0); expect(queue.queue.length, 0);
expect(queue.isRunning, false);
}); });
test('Test partial sending and resuming', () async { test('Test partial sending and resuming', () async {
@ -89,12 +87,10 @@ void main() {
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
expect(queue.queue.length, 1); expect(queue.queue.length, 1);
expect(queue.isRunning, false);
canRun = true; canRun = true;
await queue.restart(); await queue.restart();
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
expect(queue.queue.length, 0); expect(queue.queue.length, 0);
expect(queue.isRunning, false);
}); });
} }

View File

@ -1,6 +1,8 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp/src/parser.dart'; import 'package:moxxmpp/src/parser.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'helpers/logging.dart'; import 'helpers/logging.dart';
const exampleXmlns1 = 'im:moxxmpp:example1'; const exampleXmlns1 = 'im:moxxmpp:example1';

View File

@ -1,3 +1,4 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';

View File

@ -1,3 +1,4 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../helpers/logging.dart'; import '../helpers/logging.dart';
import '../helpers/xmpp.dart'; import '../helpers/xmpp.dart';
@ -42,10 +43,10 @@ Future<void> runOutgoingStanzaHandlers(
} }
} }
XmppManagerAttributes mkAttributes(void Function(Stanza) callback) { XmppManagerAttributes mkAttributes(void Function(StanzaDetails) callback) {
return XmppManagerAttributes( return XmppManagerAttributes(
sendStanza: (StanzaDetails details) async { sendStanza: (StanzaDetails details) async {
callback(details.stanza); callback(details);
return Stanza.message(); return Stanza.message();
}, },
@ -506,7 +507,7 @@ void main() {
StreamManagementNegotiator()..resource = 'test-resource', StreamManagementNegotiator()..resource = 'test-resource',
]); ]);
await conn.getManagerById<StreamManagementManager>(smManager)!.setState( await conn.getManagerById<StreamManagementManager>(smManager)!.setState(
StreamManagementState( const StreamManagementState(
10, 10,
10, 10,
streamResumptionId: 'id-1', streamResumptionId: 'id-1',
@ -605,7 +606,7 @@ void main() {
StreamManagementNegotiator()..resource = 'abc123', StreamManagementNegotiator()..resource = 'abc123',
]); ]);
await conn.getManagerById<StreamManagementManager>(smManager)!.setState( await conn.getManagerById<StreamManagementManager>(smManager)!.setState(
StreamManagementState( const StreamManagementState(
10, 10,
10, 10,
streamResumptionId: 'id-1', streamResumptionId: 'id-1',
@ -750,7 +751,7 @@ void main() {
StreamManagementNegotiator()..resource = 'abc123', StreamManagementNegotiator()..resource = 'abc123',
]); ]);
await conn.getManagerById<StreamManagementManager>(smManager)!.setState( await conn.getManagerById<StreamManagementManager>(smManager)!.setState(
StreamManagementState( const StreamManagementState(
10, 10,
10, 10,
streamResumptionId: 'id-1', streamResumptionId: 'id-1',
@ -1069,4 +1070,49 @@ void main() {
expect(smn.streamEnablementFailed, true); expect(smn.streamEnablementFailed, true);
expect(conn.resource, 'test-resource'); expect(conn.resource, 'test-resource');
}); });
test('Test resending state changes', () async {
final manager = StreamManagementManager();
final attributes = mkAttributes((details) async {
for (final handler in manager.getOutgoingPostStanzaHandlers()) {
await handler.callback(
details.stanza,
StanzaHandlerData(
false,
false,
stanza,
details.postSendExtensions ?? TypedMap(),
),
);
}
});
manager.register(attributes);
await manager.onXmppEvent(
StreamManagementEnabledEvent(resource: 'hallo'),
);
// Send a stanza 5 times
for (var i = 0; i < 5; i++) {
await runOutgoingStanzaHandlers(manager, stanza);
}
// <a h='3'/>
await manager.runNonzaHandlers(mkAck(3));
//expect(manager.getUnackedStanzas().length, 2);
final oldC2s = manager.state.c2s;
final oldQueue = Map<int, SMQueueEntry>.from(manager.getUnackedStanzas());
// Disconnect and reconnect
await manager.onXmppEvent(ConnectingEvent());
await manager.onXmppEvent(StreamResumedEvent(h: 3));
expect(manager.state.c2s, oldC2s);
expect(manager.getUnackedStanzas(), oldQueue);
// Now they get acked
await manager.runNonzaHandlers(mkAck(5));
expect(manager.getUnackedStanzas().length, 0);
expect(manager.state.c2s, 5);
});
} }

View File

@ -1,6 +1,8 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../helpers/logging.dart'; import '../helpers/logging.dart';
import '../helpers/xmpp.dart'; import '../helpers/xmpp.dart';

View File

@ -17,25 +17,15 @@ Future<void> _runTest(String domain) async {
final connection = XmppConnection( final connection = XmppConnection(
TestingReconnectionPolicy(), TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(), AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
socket, socket,
); )..connectionSettings = ConnectionSettings(
jid: JID.fromString('testuser@$domain'),
password: 'abc123',
);
await connection.registerFeatureNegotiators([ await connection.registerFeatureNegotiators([
StartTlsNegotiator(), StartTlsNegotiator(),
]); ]);
await connection.registerManagers([
DiscoManager([]),
RosterManager(TestingRosterStateManager('', [])),
MessageManager(),
PresenceManager(),
]);
connection.setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('testuser@$domain'),
password: 'abc123',
useDirectTLS: true,
),
);
final result = await connection.connect( final result = await connection.connect(
shouldReconnect: false, shouldReconnect: false,

View File

@ -18,17 +18,15 @@ void main() {
final connection = XmppConnection( final connection = XmppConnection(
TestingSleepReconnectionPolicy(10), TestingSleepReconnectionPolicy(10),
AlwaysConnectedConnectivityManager(), AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
TCPSocketWrapper(), TCPSocketWrapper(),
); )..connectionSettings = ConnectionSettings(
jid: JID.fromString('testuser@no-sasl.badxmpp.eu'),
password: 'abc123',
);
await connection.registerFeatureNegotiators([ await connection.registerFeatureNegotiators([
StartTlsNegotiator(), StartTlsNegotiator(),
]); ]);
await connection.registerManagers([
DiscoManager([]),
RosterManager(TestingRosterStateManager('', [])),
MessageManager(),
PresenceManager(),
]);
connection.asBroadcastStream().listen((event) { connection.asBroadcastStream().listen((event) {
if (event is ConnectionStateChangedEvent) { if (event is ConnectionStateChangedEvent) {
if (event.state == XmppConnectionState.error) { if (event.state == XmppConnectionState.error) {
@ -37,14 +35,6 @@ void main() {
} }
}); });
connection.setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('testuser@no-sasl.badxmpp.eu'),
password: 'abc123',
useDirectTLS: true,
),
);
final result = await connection.connect( final result = await connection.connect(
shouldReconnect: false, shouldReconnect: false,
waitUntilLogin: true, waitUntilLogin: true,
@ -68,17 +58,15 @@ void main() {
final connection = XmppConnection( final connection = XmppConnection(
TestingReconnectionPolicy(), TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(), AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
TCPSocketWrapper(), TCPSocketWrapper(),
); )..connectionSettings = ConnectionSettings(
jid: JID.fromString('testuser@no-sasl.badxmpp.eu'),
password: 'abc123',
);
await connection.registerFeatureNegotiators([ await connection.registerFeatureNegotiators([
StartTlsNegotiator(), StartTlsNegotiator(),
]); ]);
await connection.registerManagers([
DiscoManager([]),
RosterManager(TestingRosterStateManager('', [])),
MessageManager(),
PresenceManager(),
]);
connection.asBroadcastStream().listen((event) { connection.asBroadcastStream().listen((event) {
if (event is ConnectionStateChangedEvent) { if (event is ConnectionStateChangedEvent) {
if (event.state == XmppConnectionState.error) { if (event.state == XmppConnectionState.error) {
@ -87,14 +75,6 @@ void main() {
} }
}); });
connection.setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('testuser@no-sasl.badxmpp.eu'),
password: 'abc123',
useDirectTLS: true,
),
);
final result = await connection.connect( final result = await connection.connect(
shouldReconnect: false, shouldReconnect: false,
waitUntilLogin: true, waitUntilLogin: true,

View File

@ -12,7 +12,13 @@ dependencies:
meta: ^1.6.0 meta: ^1.6.0
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.3.0 version: ^0.4.0
dependency_overrides:
moxxmpp:
git:
url: https://codeberg.org/moxxy/moxxmpp.git
rev: 05e3d804a4036e9cd93fd27473a1e970fda3c3fc
dev_dependencies: dev_dependencies:
lints: ^2.0.0 lints: ^2.0.0