110 Commits

Author SHA1 Message Date
c61ddeb338 chore(xep): Move FAST from staging into xep_0484.dart 2024-11-17 20:24:03 +01:00
e2515e25e4 fix(tests): Fix component integration test 2024-11-16 17:59:11 +01:00
09a849c6eb fix(xep): Fix failure with multiple SCRAM negotiators and SASL2 2024-11-16 17:57:29 +01:00
9eb94e5f48 fix(xep): Fix crash when the device list node is empty 2024-10-19 21:45:18 +02:00
db77790bf4 fix(meta): Fix version conflicts with moxxy 2024-09-29 18:46:04 +02:00
7ceee48d31 fix(core): Bump omemo_dart (and everything else) 2024-09-29 18:39:49 +02:00
941c3e4fd8 test(xep): Test SCRAM-SHA-1 with SASL2 2024-08-12 23:05:38 +02:00
365ff2f238 fix(xep): Fix replies in the context of Gajim 2024-06-16 14:56:07 +02:00
b3c8a6cd2f fix(docs): Update link to moxxmpp documentation 2024-04-27 00:22:45 +02:00
d4166d087e fix(core): An empty iq is okay with roster versioning
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-25 21:03:04 +02:00
ddf781daff fix(core): Remove erroneous override
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-10-05 20:55:41 +02:00
5973076b89 fix(core): Remove overrides file 2023-10-05 20:54:47 +02:00
72cb76d1f6 Merge pull request 'Various improvements and fixes' (#49) from fix/stanza-ordering into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/49
2023-10-05 18:50:32 +00:00
be7581e841 fix(core): Remove empty file 2023-10-04 22:42:36 +02:00
8a2435e4ad fix(example): Bump moxxmpp* versions to 0.4.0 2023-10-04 22:35:00 +02:00
97f082b6f5 feat(example): Add a very simple client example 2023-10-04 22:15:59 +02:00
f287d501ab fix(example): Comform examples to the new TCPSocketWrapper constructor 2023-10-04 22:15:24 +02:00
93e9d6ca22 feat(xep): Handle a user changing their nickname 2023-10-01 20:44:47 +02:00
007cdce53d fix(xep): Fix wrong event being triggered on join 2023-10-01 13:34:13 +02:00
6d3a5e98de fix(xep): Export XEP-0045 events 2023-10-01 13:33:58 +02:00
e97d6e6517 feat(xep): Track our own affiliation and role 2023-09-29 22:45:56 +02:00
882d20dc7a feat(core): Bump moxxmpp_socket_tcp's version 2023-09-29 21:19:28 +02:00
1f712151e4 feat(xep): Ignore the ack timer if we are receiving data 2023-09-29 21:18:43 +02:00
e7922668b1 feat(core): Add a callback for raw data events 2023-09-29 21:13:57 +02:00
87866bf3f5 fix(core): Allow disabling logging of all incoming and outgoing data 2023-09-29 20:53:29 +02:00
41b789fa28 feat(core): Stop an exception in a handler to deadlock the connection 2023-09-29 20:50:03 +02:00
0a68f09fb4 fix(style): Fix style issues 2023-09-29 20:46:14 +02:00
edf1d0b257 feat(core): Replace custom class with a record type 2023-09-29 20:33:56 +02:00
59b90307c2 fix(core): Remove the negotiation lock 2023-09-29 20:29:25 +02:00
49d3c6411b fix(tests): Fix tests 2023-09-29 20:24:58 +02:00
3a94dd9634 feat(core): Log handler executions 2023-09-29 20:01:09 +02:00
fb4b4c71e2 fix(core): Remove async_queue 2023-09-29 19:59:38 +02:00
d9fbb9e102 fix(xep,core): Ensure in-order processing of incoming stanzas 2023-09-29 19:58:43 +02:00
aba90f2e90 feat(example): Print the number of users in the MUC 2023-09-27 18:58:17 +02:00
9211963390 fix(xep): Fix ending presence processing too early if containing a photo 2023-09-27 18:57:42 +02:00
c7d58c3d3f feat(core): Add logging for when a manager ends processing early 2023-09-27 18:57:13 +02:00
6dbbf08be4 feat(xep): Add an event for when someone leaves the MUC
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-26 23:42:49 +02:00
7ca648c478 feat(xep): Add MUC events
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-26 23:34:53 +02:00
814f99436b fix(xep): Do not automatically request vCards
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-26 14:16:41 +02:00
5bd2466c54 feat(xep): Parse the room info from the extended disco info
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-26 14:00:27 +02:00
14b62cef96 fix(xep): Fix crash for messages with no id 2023-09-26 13:59:51 +02:00
c3088f9046 fix(xep): Make leaving the room non-awaitable
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-25 21:08:24 +02:00
64b93b536e fix(xep): Add missing metadata
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-24 18:44:23 +02:00
c1c48d0a83 feat(xep): Provide an implementation of XEP-0392 2023-09-24 18:43:06 +02:00
4a681b9483 fix(xep): Somehow fix reconnection issues
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-23 22:19:06 +02:00
c504afc944 fix(xep): Fix joining a MUC making it stuck
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-23 14:14:23 +02:00
76a9f7be7a feat(xep): Allow adding MUCs to join later
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-22 21:43:56 +02:00
afa3927720 feat(xep): Rejoin groupchats on a new stream 2023-09-22 21:22:18 +02:00
5f36289f50 fix(all): Fix linter warnings 2023-09-22 20:57:05 +02:00
fbe3b90200 feat(xep): Implement ignoring the message reflection 2023-09-22 20:42:45 +02:00
d7c13abde6 feat(xep): Allow ignoring the discussion history
Also allow specifying the amount of stanzas of discussion history we
want.
2023-09-22 19:23:35 +02:00
d4416c8a47 fix(xep): Fix null-deference
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-17 20:19:50 +02:00
9666557655 feat(xep): Make avatar queries more explicit
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-17 20:10:53 +02:00
1625f912b0 chore(docs): Fix moxxmpp docs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-04 21:11:00 +02:00
864cc0e747 feat(meta): Remove omemo_dart override
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-08-22 20:21:16 +02:00
c9e817054d fix(ci): Fix moxxmpp test pipeline
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-08-22 20:07:50 +02:00
d57bf2ef80 fix(ci): Use the pubcached pub.dev cache
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-08-22 20:02:43 +02:00
8bfdd5e54a fix(ci): Remove event restriction
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-08-19 22:49:16 +02:00
e58082bf38 feat(ci): Notify on build failure
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
2023-08-19 22:47:29 +02:00
dbb945b424 fix(docs): Remove duplicate <programming-language /> 2023-08-10 15:00:11 +02:00
2431eafa6c fix(docs): Fix broken DOAP 2023-08-10 14:58:16 +02:00
264ab130ee chore(docs): Update DOAP 2023-08-10 14:44:57 +02:00
38dba0e6b7 fix(xep): Export JingleContentThumbnail 2023-08-06 13:14:57 +02:00
94d6fe4925 Merge branch 'feat/remove-eft' 2023-08-06 13:05:55 +02:00
c8b903e5df chore(all): Fix linter issues 2023-08-06 13:04:12 +02:00
b14363319a feat(xep): Remove Extensible File Thumbnails 2023-08-06 13:01:28 +02:00
a18507cc3a chore(flake): Update flake 2023-08-06 12:23:46 +02:00
93418f0127 fix(xep): Fix missing handling of StanzaError
Fixes #48.
2023-07-29 19:27:40 +02:00
1e7279e23b Merge pull request 'Implement XEP-0045 support in moxxmpp' (#46) from ikjot-2605/moxxmpp:xep_0045 into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/46
2023-07-01 19:42:12 +00:00
ikjot-2605
b2724aba0c Merge branch 'master' into xep_0045 2023-07-01 12:01:33 +00:00
Ikjot Singh Dhody
d3742ea156 feat(xep): Small fixes - MUC Example.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-01 17:29:51 +05:30
b92e825bc1 fix(xep): Fix handling StanzaErrors from the DiscoManager 2023-07-01 13:19:26 +02:00
Ikjot Singh Dhody
8b00e85167 feat(xep): Add example for XEP 0045 Moxxmpp.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-07-01 09:16:51 +05:30
Ikjot Singh Dhody
04dfc6d2ac feat(xep): Replace DiscoError with StanzaError.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-30 18:33:00 +05:30
ikjot-2605
9e70e802ef Merge branch 'master' into xep_0045 2023-06-30 04:56:15 +00:00
Ikjot Singh Dhody
3ebd9b86ec feat(xep): Fix lint issues and use moxlib for result.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-30 10:25:17 +05:30
a928c5c877 feat(xep): Also potentially return "generic" stanza errors 2023-06-29 21:12:30 +02:00
77a1acb0e7 fix(moxxmpp): Somewhat fix (and break) moxxmpp_socket_tcp integration tests 2023-06-25 12:47:47 +02:00
Ikjot Singh Dhody
a873edb9ec feat(xep): Check for null nick before leaveRoom.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-21 12:20:14 +05:30
Ikjot Singh Dhody
e6bd6d05cd feat(xep): Remove NOOP cache access.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-21 00:41:51 +05:30
05e3d804a4 feat(example): Fix omemo_dart dependency 2023-06-20 16:36:49 +02:00
b5efc2dfae feat(xep): Expose new/replaced ratchets in the MessageEvent 2023-06-20 16:35:51 +02:00
Ikjot Singh Dhody
b7d53b8f47 feat(xep): Add docstings for the XEP-0045 routines
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-20 17:44:24 +05:30
Ikjot Singh Dhody
217c3ac236 feat(xep): Fix cache issue with join/leaveRoom.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-20 17:38:30 +05:30
d35b955259 fix(xep): Accidentally left getUnackedStanzas exposed 2023-06-19 23:15:29 +02:00
30dca67fb6 feat(all): Remove freezed
Fixes #43.
2023-06-19 23:11:58 +02:00
2db44e2f51 fix(xep): Fix resend behaviour leading to period disconnects
It seems that we were expecting acks "in the future" for old stanzas.
Also, this commit should prevent E2EE implementations from re-encrypting
resent stanzas.

Fixes #38.
2023-06-19 22:55:50 +02:00
Ikjot Singh Dhody
51bca6c25d feat(xep): XEP-0045 cache fixes.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-19 18:40:39 +05:30
4f9a0605c7 Merge pull request 'OMEMO Improvements' (#47) from omemo-changes into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/47
2023-06-18 19:58:05 +00:00
3621f2709a chore(docs): Update changelog 2023-06-18 21:57:53 +02:00
9da6d319a3 feat(all): Use 0.5.0 of omemo_dart 2023-06-18 21:30:35 +02:00
e3ca83670a feat(example): Implement common argument parsing 2023-06-18 21:16:47 +02:00
fbbe413148 feat(example): Improve the example code 2023-06-18 20:59:54 +02:00
Ikjot Singh Dhody
8728166a4d feat(xep): Add cache and roomstate to MUC implementation.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-18 20:49:13 +05:30
Ikjot Singh Dhody
1f1321b269 feat(xep): Small fixes - review cycle 1.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-18 20:09:06 +05:30
9fd2daabb2 feat(xep): Adjust to more omemo_dart changes 2023-06-17 23:51:52 +02:00
8252472fae feat(xep): Adjust to omemo_dart changes 2023-06-17 23:37:08 +02:00
3cb5a568ce feat(xep,core): Migrate to moxlib's Result type 2023-06-17 21:45:00 +02:00
c2f62e2967 feat(xep): Implement an OMEMO example client 2023-06-17 21:28:54 +02:00
Ikjot Singh Dhody
66195f66fa Merge branch 'master' into xep_0045
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-14 10:48:28 +05:30
Ikjot Singh Dhody
70fdfaf16d feat(xep): Fix imports for xep_0045 files.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-14 10:47:51 +05:30
Ikjot Singh Dhody
cd73f89e63 feat(xep): Remove duplicate manager string
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-14 10:47:51 +05:30
Ikjot Singh Dhody
05c41d3185 feat(xep): Refactor sendMessage to allow groupchat
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-14 10:47:50 +05:30
Ikjot Singh Dhody
64a8de6caa feat(xep): Set base for XEP 0045 implementation
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-14 10:47:07 +05:30
Ikjot Singh Dhody
68809469f6 feat(xep): Add joinRoom, leaveRoom routines.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-14 10:46:48 +05:30
Ikjot Singh Dhody
762cf1c77a feat(xep): Set base for XEP 0045 implementation
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-14 10:46:48 +05:30
29a5417b31 Merge pull request 'Add support for XEP-0421.' (#45) from ikjot-2605/moxxmpp:feat_xep_0421 into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/45
2023-06-12 10:58:32 +00:00
Ikjot Singh Dhody
255d0f88e0 feat(xep): Use cascading operation to return state
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-12 15:00:37 +05:30
Ikjot Singh Dhody
fa11a3a384 feat(xep): Checked for the occupant-id directly.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-11 23:29:17 +05:30
Ikjot Singh Dhody
ac5bb9e461 feat(xep): Implement XEP 0421 in Moxxmpp.
Signed-off-by: Ikjot Singh Dhody <ikjotsd@gmail.com>
2023-06-11 22:09:29 +05:30
124 changed files with 4813 additions and 1831 deletions

View File

@@ -7,7 +7,7 @@ line-length=72
[title-trailing-punctuation]
[title-hard-tab]
[title-match-regex]
regex=^((feat|fix|chore|refactor|docs|release|test)\((meta|tests|style|docs|xep|core|example|all)+(,(meta|tests|style|docs|xep|core|example|all))*\)|release): [A-Z0-9].*$
regex=^((feat|fix|chore|refactor|docs|release|test)\((meta|tests|style|docs|xep|core|example|all|flake|ci)+(,(meta|tests|style|docs|xep|core|example|all|flake|ci))*\)|release): [A-Z0-9].*$
[body-trailing-whitespace]

View File

@@ -1,28 +1,49 @@
when:
branch: master
pipeline:
# Check moxxmpp
moxxmpp-lint:
image: dart:2.18.1
image: dart:3.0.7
commands:
- cd packages/moxxmpp
- dart pub get
# Proxy requests to pub.dev using pubcached
- PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
- dart analyze --fatal-infos --fatal-warnings
moxxmpp-test:
image: dart:2.18.1
image: dart:3.0.7
commands:
- cd packages/moxxmpp
- dart pub get
# Proxy requests to pub.dev using pubcached
- PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
- dart test
# Check moxxmpp_socket_tcp
moxxmpp_socket_tcp-lint:
image: dart:2.18.1
image: dart:3.0.7
commands:
- cd packages/moxxmpp_socket_tcp
- dart pub get
# Proxy requests to pub.dev using pubcached
- PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
- dart analyze --fatal-infos --fatal-warnings
# moxxmpp-test:
# image: dart:2.18.1
# image: dart:3.0.7
# commands:
# - cd packages/moxxmpp
# - dart pub get
# # Proxy requests to pub.dev using pubcached
# - PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
# - dart test
notify:
image: git.polynom.me/papatutuwawa/woodpecker-xmpp
settings:
xmpp_tls: 1
xmpp_is_muc: 1
xmpp_recipient: moxxy-build@muc.moxxy.org
xmpp_alias: 2Bot
secrets: [ xmpp_jid, xmpp_password, xmpp_server ]
when:
status:
- failure

View File

@@ -7,7 +7,7 @@ moxxmpp is a XMPP library written purely in Dart for usage in Moxxy.
This package contains the actual XMPP code that is platform-independent.
Documentation is available [here](https://moxxy.org/developers/docs/moxxmpp/).
Documentation is available [here](https://docs.moxxy.org/moxxmpp/index.html).
### [moxxmpp_socket_tcp](./packages/moxxmpp_socket_tcp)
@@ -15,6 +15,10 @@ Documentation is available [here](https://moxxy.org/developers/docs/moxxmpp/).
implements the RFC6120 connection algorithm and XEP-0368 direct TLS connections,
if a DNS implementation is given, and supports StartTLS.
### moxxmpp_color
Implementation of [XEP-0392](https://xmpp.org/extensions/xep-0392.html).
## Development
To begin, use [melos](https://github.com/invertase/melos) to bootstrap the project: `melos bootstrap`. Then, the example

View File

@@ -3,6 +3,8 @@ import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
class TestingTCPSocketWrapper extends TCPSocketWrapper {
TestingTCPSocketWrapper() : super(true);
@override
bool onBadCertificate(dynamic certificate, String domain) {
return true;
@@ -30,11 +32,12 @@ class EchoMessageManager extends XmppManagerBase {
StanzaHandlerData state,
) async {
final body = stanza.firstTag('body');
if (body == null) return state.copyWith(done: true);
if (body == null) return state..done = true;
final bodyText = body.innerText();
await getAttributes().sendStanza(
StanzaDetails(
Stanza.message(
to: stanza.from,
children: [
@@ -45,9 +48,10 @@ class EchoMessageManager extends XmppManagerBase {
],
),
awaitable: false,
),
);
return state.copyWith(done: true);
return state..done = true;
}
}

View File

@@ -0,0 +1,111 @@
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';
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('muc', help: 'The MUC to send messages to')
..parser.addOption('nick', help: 'The nickname with which to join the MUC');
final options = parser.handleArguments(args);
if (options == null) {
return;
}
// Connect
final muc = JID.fromString(options['muc']! as String).toBare();
final nick = options['nick']! as String;
final connection = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
ExampleTCPSocketWrapper(parser.srvRecord),
)..connectionSettings = parser.connectionSettings;
// Register the managers and negotiators
await connection.registerManagers([
PresenceManager(),
DiscoManager([]),
PubSubManager(),
MessageManager(),
StableIdManager(),
MUCManager(),
]);
await connection.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StartTlsNegotiator(),
SaslScramNegotiator(10, '', '', ScramHashType.sha1),
]);
// 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.');
// Print received messages.
connection
.asBroadcastStream()
.where((event) => event is MessageEvent)
.listen((event) {
event as MessageEvent;
// Ignore messages with no <body />
final body = event.get<MessageBodyData>()?.body;
if (body == null) {
return;
}
print('=====> [${event.from}] $body');
});
// Join room
final mm = connection.getManagerById<MUCManager>(mucManager)!;
await mm.joinRoom(
muc,
nick,
maxHistoryStanzas: 0,
);
final state = (await mm.getRoomState(muc))!;
print('=====> ${state.members.length} users in room');
print('=====> ${state.members.values.map((m) => m.nick).join(", ")}');
final repl = Repl(prompt: '> ');
await for (final line in repl.runAsync()) {
await connection
.getManagerById<MessageManager>(messageManager)!
.sendMessage(
muc,
TypedMap<StanzaHandlerExtension>.fromList([
MessageBodyData(line),
StableIdData(
// NOTE: Don't do this. Use a UUID.
DateTime.now().millisecondsSinceEpoch.toString(),
null,
),
]),
type: 'groupchat');
}
// Leave room
await connection.getManagerById<MUCManager>(mucManager)!.leaveRoom(muc);
// Disconnect
await connection.disconnect();
}

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, true),
)..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,112 @@
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
/// The JID we want to authenticate as.
final xmppUser = JID.fromString('jane@example.com');
/// The password to authenticate with.
const xmppPass = 'secret';
/// The [xmppHost]:[xmppPort] server address to connect to.
/// In a real application, one might prefer to use [TCPSocketWrapper]
/// with a custom DNS implementation to let moxxmpp resolve the XMPP
/// server's address automatically. However, if we just provide a host
/// and a port, then [TCPSocketWrapper] will just skip the resolution and
/// immediately use the provided connection details.
const xmppHost = 'localhost';
const xmppPort = 5222;
void main(List<String> args) async {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
print('${record.level.name}|${record.time}: ${record.message}');
});
// This class manages every aspect of handling the XMPP stream.
final connection = XmppConnection(
// A reconnection policy tells the connection how to handle an error
// while or after connecting to the server. The [TestingReconnectionPolicy]
// immediately triggers a reconnection. In a real implementation, one might
// prefer to use a smarter strategy, like using an exponential backoff.
TestingReconnectionPolicy(),
// A connectivity manager tells the connection when it can connect. This is to
// ensure that we're not constantly trying to reconnect because we have no
// Internet connection. [AlwaysConnectedConnectivityManager] always says that
// we're connected. In a real application, one might prefer to use a smarter
// strategy, like using connectivity_plus to query the system's network connectivity
// state.
AlwaysConnectedConnectivityManager(),
// This kind of negotiator tells the connection how to handle the stream
// negotiations. The [ClientToServerNegotiator] allows to connect to the server
// as a regular client. Another negotiator would be the [ComponentToServerNegotiator] that
// allows for connections to the server where we're acting as a component.
ClientToServerNegotiator(),
// A wrapper around any kind of connection. In this case, we use the [TCPSocketWrapper], which
// uses a dart:io Socket/SecureSocket to connect to the server. If you want, you can also
// provide your own socket to use, for example, WebSockets or any other connection
// mechanism.
TCPSocketWrapper(false),
)..connectionSettings = ConnectionSettings(
jid: xmppUser,
password: xmppPass,
host: xmppHost,
port: xmppPort,
);
// Register a set of "managers" that provide you with implementations of various
// XEPs. Some have interdependencies, which need to be met. However, this example keeps
// it simple and just registers a [MessageManager], which has no required dependencies.
await connection.registerManagers([
// The [MessageManager] handles receiving and sending <message /> stanzas.
MessageManager(),
]);
// Feature negotiators are objects that tell the connection negotiator what stream features
// we can negotiate and enable. moxxmpp negotiators always try to enable their features.
await connection.registerFeatureNegotiators([
// This negotiator authenticates to the server using SASL PLAIN with the provided
// credentials.
SaslPlainNegotiator(),
// This negotiator attempts to bind a resource. By default, it's always a random one.
ResourceBindingNegotiator(),
// This negotiator attempts to do StartTLS before authenticating.
StartTlsNegotiator(),
]);
// Set up a stream handler for the connection's event stream. Managers and negotiators
// may trigger certain events. The [MessageManager], for example, triggers a [MessageEvent]
// whenever a message is received. If other managers are registered that parse a message's
// contents, then they can add their data to the event.
connection.asBroadcastStream().listen((event) {
if (event is! MessageEvent) {
return;
}
// The text body (contents of the <body /> element) are returned as a
// [MessageBodyData] object. However, a message does not have to contain a
// body, so it is nullable.
final body = event.extensions.get<MessageBodyData>()?.body;
print('[<-- ${event.from}] $body');
});
// Connect to the server.
final result = await connection.connect(
// This flag indicates that we want to reconnect in case something happens.
shouldReconnect: true,
// This flag indicates that we want the returned Future to only resolve
// once the stream negotiations are done and no negotiator has any feature left
// to negotiate.
waitUntilLogin: true,
);
// Check if the connection was successful. [connection.connect] can return a boolean
// to indicate success or a [XmppError] in case the connection attempt failed.
if (!result.isType<bool>()) {
print('Failed to connect to server');
return;
}
}

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, bool logData) : super(logData);
/// 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,25 +1,34 @@
name: example_dart
description: A sample command-line application.
description: A collection of samples for moxxmpp.
version: 1.0.0
# homepage: https://www.example.com
environment:
sdk: '>=2.18.0 <3.0.0'
sdk: '>=3.0.0 <4.0.0'
dependencies:
args: 2.4.1
chalkdart: 2.0.9
cli_repl: 0.2.3
logging: ^1.0.2
moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.3.1
version: 0.4.0
moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.3.1
version: 0.4.0
omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.5.1
dependency_overrides:
moxxmpp:
path: ../packages/moxxmpp
moxxmpp_socket_tcp:
path: ../packages/moxxmpp_socket_tcp
omemo_dart:
git:
url: https://github.com/PapaTutuWawa/omemo_dart.git
rev: 49c7e114e6cf80dcde55fbbd218bba3182045862
dev_dependencies:
lints: ^2.0.0

126
flake.lock generated
View File

@@ -1,12 +1,74 @@
{
"nodes": {
"flake-utils": {
"android-nixpkgs": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1678901627,
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
"lastModified": 1727554699,
"narHash": "sha256-puBCNL5PW7Pej+6Srmi2YjEgNeE015NFe33hbkkLqeQ=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "bc34ef1c71fe9eafcfb1d637b431fca83d746625",
"type": "github"
},
"original": {
"owner": "tadfisher",
"repo": "android-nixpkgs",
"type": "github"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
"android-nixpkgs",
"nixpkgs"
]
},
"locked": {
"lastModified": 1722113426,
"narHash": "sha256-Yo/3loq572A8Su6aY5GP56knpuKYRvM2a1meP9oJZCw=",
"owner": "numtide",
"repo": "devshell",
"rev": "67cce7359e4cd3c45296fb4aaf6a19e2a9c757ae",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1692799911,
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github"
},
"original": {
@@ -17,27 +79,27 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1676076353,
"narHash": "sha256-mdUtE8Tp40cZETwcq5tCwwLqkJVV1ULJQ5GKRtbshag=",
"owner": "AtaraxiaSjel",
"lastModified": 1727348695,
"narHash": "sha256-J+PeFKSDV+pHL7ukkfpVzCOO7mBSrrpJ3svwBFABbhI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5deb99bdccbbb97e7562dee4ba8a3ee3021688e6",
"rev": "1925c603f17fc89f4c8f6bf6f631a802ad85d784",
"type": "github"
},
"original": {
"owner": "AtaraxiaSjel",
"ref": "update/flutter",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"nixpkgs_2": {
"locked": {
"lastModified": 1680273054,
"narHash": "sha256-Bs6/5LpvYp379qVqGt9mXxxx9GSE789k3oFc+OAL07M=",
"lastModified": 1727586919,
"narHash": "sha256-e/YXG0tO5GWHDS8QQauj8aj4HhXEm602q9swrrlTlKQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3364b5b117f65fe1ce65a3cdd5612a078a3b31e3",
"rev": "2dcd9c55e8914017226f5948ac22c53872a13ee2",
"type": "github"
},
"original": {
@@ -49,9 +111,39 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable"
"android-nixpkgs": "android-nixpkgs",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},

View File

@@ -1,12 +1,12 @@
{
description = "moxxmpp";
inputs = {
nixpkgs.url = "github:AtaraxiaSjel/nixpkgs/update/flutter";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
android-nixpkgs.url = "github:tadfisher/android-nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
outputs = { self, nixpkgs, android-nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
config = {
@@ -14,9 +14,7 @@
allowUnfree = true;
};
};
unstable = import nixpkgs-unstable {
inherit system;
};
# Everything to make Flutter happy
android = pkgs.androidenv.composeAndroidPackages {
# TODO: Find a way to pin these
#toolsVersion = "26.1.1";
@@ -33,6 +31,7 @@
useGoogleAPIs = false;
useGoogleTVAddOns = false;
};
lib = pkgs.lib;
pinnedJDK = pkgs.jdk17;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
@@ -51,7 +50,7 @@
};
devShell = let
prosody-newer-community-modules = unstable.prosody.overrideAttrs (old: {
prosody-newer-community-modules = pkgs.prosody.overrideAttrs (old: {
communityModules = pkgs.fetchhg {
url = "https://hg.prosody.im/prosody-modules";
rev = "e3a3a6c86a9f";
@@ -103,7 +102,26 @@
CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include";
LD_LIBRARY_PATH = with pkgs; lib.makeLibraryPath [ atk cairo epoxy gdk-pixbuf glib gtk3 harfbuzz pango ];
ANDROID_SDK_ROOT = "${android.androidsdk}/share/android-sdk";
ANDROID_HOME = "${android.androidsdk}/share/android-sdk";
JAVA_HOME = pinnedJDK;
# Fix an issue with Flutter using an older version of aapt2, which does not know
# an used parameter.
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${android.androidsdk}/share/android-sdk/build-tools/34.0.0/aapt2";
};
apps = {
regenerateNixPackage = let
script = pkgs.writeShellScript "regenerate-nix-package.sh" ''
set -e
${pythonEnv}/bin/python ./scripts/pubspec2lock.py ./packages/moxxmpp/pubspec.lock ./nix/moxxmpp.lock
${pythonEnv}/bin/python ./scripts/lock2nix.py ./nix/moxxmpp.lock ./nix/pubcache.moxxmpp.nix moxxmpp
'';
in {
type = "app";
program = "${script}";
};
};
});
}

View File

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

View File

@@ -3,14 +3,16 @@ description: A sample command-line application.
version: 1.0.0
environment:
sdk: '>=2.18.0 <3.0.0'
sdk: ">=3.0.0 <4.0.0"
dependencies:
logging: ^1.0.2
moxxmpp: 0.3.0
moxxmpp_socket_tcp: 0.3.0
logging: ^1.3.0
moxxmpp:
path: ../packages/moxxmpp
moxxmpp_socket_tcp:
path: ../packages/moxxmpp_socket_tcp
dev_dependencies:
lints: ^2.0.0
test: ^1.16.0
very_good_analysis: ^3.0.1
build_runner: ^2.4.13
test: ^1.25.8
very_good_analysis: ^6.0.0

View File

@@ -4,6 +4,8 @@ import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
import 'package:test/test.dart';
class TestingTCPSocketWrapper extends TCPSocketWrapper {
TestingTCPSocketWrapper() : super(true);
@override
bool onBadCertificate(dynamic certificate, String domain) {
return true;

View File

@@ -4,6 +4,8 @@ import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
import 'package:test/test.dart';
class TestingTCPSocketWrapper extends TCPSocketWrapper {
TestingTCPSocketWrapper() : super(true);
@override
bool onBadCertificate(dynamic certificate, String domain) {
return true;
@@ -27,7 +29,7 @@ void main() {
ClientToServerNegotiator(),
TestingTCPSocketWrapper(),
)..connectionSettings = ConnectionSettings(
jid: JID.fromString('testuser@localhost'),
jid: JID.fromString('testuser1@localhost'),
password: 'abc123',
host: '127.0.0.1',
port: 5222,
@@ -40,6 +42,8 @@ void main() {
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
SaslScramNegotiator(9, '', '', ScramHashType.sha1),
SaslScramNegotiator(10, '', '', ScramHashType.sha256),
ResourceBindingNegotiator(),
FASTSaslNegotiator(),
Bind2Negotiator(),

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,30 @@
# GENERATED BY LOCK2NIX.py
# DO NOT EDIT BY HAND
{fetchzip, runCommand} : rec {
_fe_analyzer_shared = fetchzip {
sha256 = "1hyd5pmjcfyvfwhsc0wq6k0229abmqq5zn95g31hh42bklb2gci5";
url = "https://pub.dartlang.org/packages/_fe_analyzer_shared/versions/50.0.0.tar.gz";
sha256 = "15fh9ka41dw4qsynv07msq4i243fibprcmafdygw5x88f7m55fq3";
url = "https://pub.dartlang.org/packages/_fe_analyzer_shared/versions/61.0.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
analyzer = fetchzip {
sha256 = "0niy5b3w39aywpjpw5a84pxdilhh3zzv1c22x8ywml756pybmj4r";
url = "https://pub.dartlang.org/packages/analyzer/versions/5.2.0.tar.gz";
sha256 = "0w604zngxwfx0xqxvhbxrhdh04wgm6ad6a1lbwnyvmk57amv44np";
url = "https://pub.dartlang.org/packages/analyzer/versions/5.13.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
args = fetchzip {
sha256 = "0c78zkzg2d2kzw1qrpiyrj1qvm4pr0yhnzapbqk347m780ha408g";
url = "https://pub.dartlang.org/packages/args/versions/2.3.1.tar.gz";
sha256 = "01ps253280c6dbx0vncw4wga4l2qp1zx779qjj2x06xzb3744zbz";
url = "https://pub.dartlang.org/packages/args/versions/2.4.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
async = fetchzip {
sha256 = "00hhylamsjcqmcbxlsrfimri63gb384l31r9mqvacn6c6bvk4yfx";
url = "https://pub.dartlang.org/packages/async/versions/2.10.0.tar.gz";
sha256 = "0hfgvjajp5c2mw68186hgrk9v5zjhhi149hlhl0fap274p2v1g3q";
url = "https://pub.dartlang.org/packages/async/versions/2.11.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -49,29 +51,29 @@
};
build_daemon = fetchzip {
sha256 = "0b6hnwjc3gi5g7cnpy8xyiqigcrs0xp51c7y7v1pqn9v75g25w6j";
url = "https://pub.dartlang.org/packages/build_daemon/versions/3.1.0.tar.gz";
sha256 = "1wn7bq846vgdj62bkh9h25l95xdsndv0jdyw52nyr0591l3bpg3h";
url = "https://pub.dartlang.org/packages/build_daemon/versions/3.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
build_resolvers = fetchzip {
sha256 = "0fnrisgq6rnvbqsf8v43hb11kr1qq6azrxbsvx3wwimd37nxx8m5";
url = "https://pub.dartlang.org/packages/build_resolvers/versions/2.1.0.tar.gz";
sha256 = "00h9abhrfmnl0xxziyf6p68sxnbv2ww1c4dhgpnz00mzbmamnq5c";
url = "https://pub.dartlang.org/packages/build_resolvers/versions/2.3.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
build_runner = fetchzip {
sha256 = "0246bxl9rxgil55fhfzi7csd9a56blj9s1j1z79717hiyzsr60x6";
url = "https://pub.dartlang.org/packages/build_runner/versions/2.3.2.tar.gz";
sha256 = "0b5ha1l6k0gn2swqgqvfy2vl58klf81sxrjnmk0p7rj1wzbqjm7l";
url = "https://pub.dartlang.org/packages/build_runner/versions/2.3.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
build_runner_core = fetchzip {
sha256 = "0bpil0fw0dag3vbnin9p945ymi7xjgkiy7jrq9j52plljf7cnf5z";
url = "https://pub.dartlang.org/packages/build_runner_core/versions/7.2.7.tar.gz";
sha256 = "07r1kfy6ylm4i4xrb24ns8l26h4h1lgcskmnf8wvq2rd5d5hq790";
url = "https://pub.dartlang.org/packages/build_runner_core/versions/7.2.7%2B1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -84,29 +86,29 @@
};
built_value = fetchzip {
sha256 = "0sslr4258snvcj8qhbdk6wapka174als0viyxddwqlnhs7dlci8i";
url = "https://pub.dartlang.org/packages/built_value/versions/8.4.2.tar.gz";
sha256 = "1y84imf9xqqy3gnd5zz9bcln6mycy7qx35r70b0izm31ismlbzkv";
url = "https://pub.dartlang.org/packages/built_value/versions/8.6.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
checked_yaml = fetchzip {
sha256 = "1gf7ankc5jb7mk17br87ajv05pfg6vb8nf35ay6c35w8jp70ra7k";
url = "https://pub.dartlang.org/packages/checked_yaml/versions/2.0.1.tar.gz";
sha256 = "1sn01yrmj0pkijn08g3v45c3zmyvdygk9svigkkzybgicdwlkpqs";
url = "https://pub.dartlang.org/packages/checked_yaml/versions/2.0.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
code_builder = fetchzip {
sha256 = "1vl9dl23yd0zjw52ndrazijs6dw83fg1rvyb2gfdpd6n1lj9nbhg";
url = "https://pub.dartlang.org/packages/code_builder/versions/4.3.0.tar.gz";
sha256 = "1shgl7mxiyv0hhw326yqj2b9jxi1h74qxmsnxf1d1xc6yz766p9a";
url = "https://pub.dartlang.org/packages/code_builder/versions/4.6.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
collection = fetchzip {
sha256 = "1iyl3v3j7mj3sxjf63b1kc182fwrwd04mjp5x2i61hic8ihfw545";
url = "https://pub.dartlang.org/packages/collection/versions/1.17.0.tar.gz";
sha256 = "1mr8j0078c4z9hhckiq8m735rggsazwfprm0w9gisil51vh7j2mk";
url = "https://pub.dartlang.org/packages/collection/versions/1.18.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -119,29 +121,29 @@
};
coverage = fetchzip {
sha256 = "0akbg1yp2h4vprc8r9xvrpgvp5d26h7m80h5sbzgr5dlis1bcw0d";
url = "https://pub.dartlang.org/packages/coverage/versions/1.6.1.tar.gz";
sha256 = "1yy9bgkax5b6kk7qa07p452v82fyj4rl1j03fn366ywyvhfrh6lp";
url = "https://pub.dartlang.org/packages/coverage/versions/1.6.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
crypto = fetchzip {
sha256 = "1kjfb8fvdxazmv9ps2iqdhb8kcr31115h0nwn6v4xmr71k8jb8ds";
url = "https://pub.dartlang.org/packages/crypto/versions/3.0.2.tar.gz";
sha256 = "100ai8qa4p3dyvvd60c4xa9p0gm06yh0d68xgcfm3giraad8xmqj";
url = "https://pub.dartlang.org/packages/crypto/versions/3.0.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
cryptography = fetchzip {
sha256 = "0jqph45d9lbhdakprnb84c3qhk4aq05hhb1pmn8w23yhl41ypijs";
url = "https://pub.dartlang.org/packages/cryptography/versions/2.0.5.tar.gz";
sha256 = "1yxn9slqq93ri81fbr2nbsinz0mpk9wk39ny076ja8q31d4i8v3f";
url = "https://pub.dartlang.org/packages/cryptography/versions/2.5.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
dart_style = fetchzip {
sha256 = "01wg15kalbjlh4i3xbawc9zk8yrk28qhak7xp7mlwn2syhdckn7v";
url = "https://pub.dartlang.org/packages/dart_style/versions/2.2.4.tar.gz";
sha256 = "0cjhrb1hs8iw9smmfd0fgnxq3nm0w8sz17l6q6svyz6kif19wk9k";
url = "https://pub.dartlang.org/packages/dart_style/versions/2.3.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -154,43 +156,29 @@
};
fixnum = fetchzip {
sha256 = "1m8cdfqp9d6w1cik3fwz9bai1wf9j11rjv2z0zlv7ich87q9kkjk";
url = "https://pub.dartlang.org/packages/fixnum/versions/1.0.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
freezed = fetchzip {
sha256 = "1i9s4djf4vlz56zqn8brcck3n7sk07qay23wmaan991cqydd10iq";
url = "https://pub.dartlang.org/packages/freezed/versions/2.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
freezed_annotation = fetchzip {
sha256 = "0ym120dh1lpfnb68gxh1finm8p9l445q5x10aw8269y469b9k9z3";
url = "https://pub.dartlang.org/packages/freezed_annotation/versions/2.1.0.tar.gz";
sha256 = "0nqrzj41ys8dpxf1x70r0kfj1avj0f2j2b7498k8kvc0i9c5asz7";
url = "https://pub.dartlang.org/packages/fixnum/versions/1.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
frontend_server_client = fetchzip {
sha256 = "0nv4avkv2if9hdcfzckz36f3mclv7vxchivrg8j3miaqhnjvv4bj";
url = "https://pub.dartlang.org/packages/frontend_server_client/versions/3.1.0.tar.gz";
sha256 = "096v7ycix5hgnk750s1qgykyghl2mymhdkg39jrlk3kbj6xygq5b";
url = "https://pub.dartlang.org/packages/frontend_server_client/versions/3.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
glob = fetchzip {
sha256 = "0a6gbwsbz6rkg35dkff0zv88rvcflqdmda90hdfpn7jp1z1w9rhs";
url = "https://pub.dartlang.org/packages/glob/versions/2.1.0.tar.gz";
sha256 = "0ffab3azx8zkma36mk6wnig8bn8g5g0vjrq2gl21y77rxgw9iqxj";
url = "https://pub.dartlang.org/packages/glob/versions/2.1.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
graphs = fetchzip {
sha256 = "0cr6dgs1a7ln2ir5gd0kiwpn787lk4dwhqfjv8876hkkr1rv80m9";
url = "https://pub.dartlang.org/packages/graphs/versions/2.2.0.tar.gz";
sha256 = "0fda0j8y6sq1rc9zpzglrzysl5h49y2ji1wq2lq0wx2c609dxm7f";
url = "https://pub.dartlang.org/packages/graphs/versions/2.3.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -217,78 +205,78 @@
};
io = fetchzip {
sha256 = "1bp5l8hkrp6fjj7zw9af51hxyp52sjspc5558lq0lmi453l0czni";
url = "https://pub.dartlang.org/packages/io/versions/1.0.3.tar.gz";
sha256 = "101kd0rw26vglmr1m5p130kbrp3k7dk4p5nr77wsbwgg53w8c0d4";
url = "https://pub.dartlang.org/packages/io/versions/1.0.4.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
js = fetchzip {
sha256 = "13fbxgyg1v6bmzvxamg6494vk3923fn3mgxj6f4y476aqwk99n50";
url = "https://pub.dartlang.org/packages/js/versions/0.6.5.tar.gz";
sha256 = "124a9yqrjdw3p4nnirab9hm9ziwraldlw4q5cb3sr0dcrli74qpw";
url = "https://pub.dartlang.org/packages/js/versions/0.6.7.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
json_annotation = fetchzip {
sha256 = "1p9nvn33psx2zbalhyqjw8gr4agd76jj5jq0fdz0i584c7l77bby";
url = "https://pub.dartlang.org/packages/json_annotation/versions/4.7.0.tar.gz";
sha256 = "1jjw7p8qyqajgdq4jqvxipq5w0qrq9dpi1qmia70pk995akryh6m";
url = "https://pub.dartlang.org/packages/json_annotation/versions/4.8.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
json_serializable = fetchzip {
sha256 = "04d7laaxrbiybcgbv3y223hy8d6n9f84h5lv9sv79zd9ffzkb2hg";
url = "https://pub.dartlang.org/packages/json_serializable/versions/6.5.4.tar.gz";
sha256 = "1pmidql9x6s2pbhdx9x20pwqwvwpfkvrz0h0cm1f8pqis76c90hb";
url = "https://pub.dartlang.org/packages/json_serializable/versions/6.6.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
logging = fetchzip {
sha256 = "0hl1mjh662c44ci7z60x92i0jsyqg1zm6k6fc89n9pdcxsqdpwfs";
url = "https://pub.dartlang.org/packages/logging/versions/1.0.2.tar.gz";
sha256 = "124hfjs66r30p92ndfmy5fymgy66yk9in97h8sq6fi7r78pqyc7g";
url = "https://pub.dartlang.org/packages/logging/versions/1.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
matcher = fetchzip {
sha256 = "0pjgc38clnjbv124n8bh724db1wcc4kk125j7dxl0icz7clvm0p0";
url = "https://pub.dartlang.org/packages/matcher/versions/0.12.13.tar.gz";
sha256 = "0inznqkrxqnq09lcbwvda3xd07qfm1k3aa6dv1wy39gvci8hybss";
url = "https://pub.dartlang.org/packages/matcher/versions/0.12.16.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
meta = fetchzip {
sha256 = "01kqdd25nln5a219pr94s66p27m0kpqz0wpmwnm24kdy3ngif1v5";
url = "https://pub.dartlang.org/packages/meta/versions/1.8.0.tar.gz";
sha256 = "1l3zaz6q2s9mnm7s674xshsfqspy79p5kdbbnc99rf2l76avv4h3";
url = "https://pub.dartlang.org/packages/meta/versions/1.9.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
mime = fetchzip {
sha256 = "1dr3qikzvp10q1saka7azki5gk2kkf2v7k9wfqjsyxmza2zlv896";
url = "https://pub.dartlang.org/packages/mime/versions/1.0.2.tar.gz";
sha256 = "1dha9z64bsz8xhi0p62vmlyikr8xwbdlrw90hxghmm3rdgd9h25w";
url = "https://pub.dartlang.org/packages/mime/versions/1.0.4.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
moxlib = fetchzip {
sha256 = "1j52xglpwy8c7dbylc3f6vrh0p52xhhwqs4h0qcqk8c1rvjn5czq";
url = "https://git.polynom.me/api/packages/moxxy/pub/api/packages/moxlib/files/0.1.5.tar.gz";
sha256 = "1qaacmcqhq33grn2nq8sn23ki62dcmw0fqy589xm1zv6w0pzfmsk";
url = "https://git.polynom.me/api/packages/moxxy/pub/api/packages/moxlib/files/0.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
node_preamble = fetchzip {
sha256 = "0i0gfc2yqa09182vc01lj47qpq98kfm9m8h4n8c5fby0mjd0lvyx";
url = "https://pub.dartlang.org/packages/node_preamble/versions/2.0.1.tar.gz";
sha256 = "12ajg76r9aqmqkavvlxbnb3sszg1szcq3f30badkd0xc25mnhyh8";
url = "https://pub.dartlang.org/packages/node_preamble/versions/2.0.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
omemo_dart = fetchzip {
sha256 = "09x3jqa11hjdjp31nxnz91j6jssbc2f8a1lh44fmkc0d79hs8bbi";
url = "https://git.polynom.me/api/packages/PapaTutuWawa/pub/api/packages/omemo_dart/files/0.4.3.tar.gz";
sha256 = "0fhf89ic5mdyld25l6rfb37a1fk1f0f2b4d72xi4r7pvr0ddjhz8";
url = "https://git.polynom.me/api/packages/PapaTutuWawa/pub/api/packages/omemo_dart/files/0.5.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -301,8 +289,8 @@
};
path = fetchzip {
sha256 = "16ggdh29ciy7h8sdshhwmxn6dd12sfbykf2j82c56iwhhlljq181";
url = "https://pub.dartlang.org/packages/path/versions/1.8.2.tar.gz";
sha256 = "1mjdhq2fsz6i9krhp2mnaks2bcw34sa4p7mg0v6njk8dgx2754iv";
url = "https://pub.dartlang.org/packages/path/versions/1.8.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -315,8 +303,8 @@
};
petitparser = fetchzip {
sha256 = "1pqqqqiy9ald24qsi24q9qrr0zphgpsrnrv9rlx4vwr6xak7d8c0";
url = "https://pub.dartlang.org/packages/petitparser/versions/5.1.0.tar.gz";
sha256 = "19zqrpb1z77aw1k2s8rsxdfxczzv9934g2rdfj2jyiv3pqgdq8gh";
url = "https://pub.dartlang.org/packages/petitparser/versions/5.4.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -335,16 +323,30 @@
extension = "tar.gz";
};
protobuf = fetchzip {
sha256 = "1jriyisf8bnvq5ygjk93mn2yzdlnii7xrhy6aabz54xr3y4dcy9x";
url = "https://pub.dartlang.org/packages/protobuf/versions/2.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
protoc_plugin = fetchzip {
sha256 = "0hjjd1xkv4s4g1d5n2aza0kdwlbfl2aivq99230m3yml7irn00jk";
url = "https://pub.dartlang.org/packages/protoc_plugin/versions/20.0.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
pub_semver = fetchzip {
sha256 = "1vsj5c1f2dza4l5zmjix4zh65lp8gsg6pw01h57pijx2id0g4bwi";
url = "https://pub.dartlang.org/packages/pub_semver/versions/2.1.2.tar.gz";
sha256 = "0wpcfz1crxipbjm18m71pl4vl2ra8vw1n93ff8snr54mmlyfb9z1";
url = "https://pub.dartlang.org/packages/pub_semver/versions/2.1.4.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
pubspec_parse = fetchzip {
sha256 = "19dmr9k4wsqjnhlzp1lbrw8dv7a1gnwmr8l5j9zlw407rmfg20d1";
url = "https://pub.dartlang.org/packages/pubspec_parse/versions/1.2.1.tar.gz";
sha256 = "0dj8sf1w61g7vh1ly3sl690z0nwllzjzbapxswmgsglr0ndcyrs1";
url = "https://pub.dartlang.org/packages/pubspec_parse/versions/1.2.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -364,43 +366,43 @@
};
shelf = fetchzip {
sha256 = "0x2xl7glrnq0hdxpy2i94a4wxbdrd6dm46hvhzgjn8alsm8z0wz1";
url = "https://pub.dartlang.org/packages/shelf/versions/1.4.0.tar.gz";
sha256 = "10yk98nadrgj5d3r3241kdaywjjs1j10mg8gacv80kg1mhcfdrxp";
url = "https://pub.dartlang.org/packages/shelf/versions/1.4.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
shelf_packages_handler = fetchzip {
sha256 = "199rbdbifj46lg3iynznnsbs8zr4dfcw0s7wan8v73nvpqvli82q";
url = "https://pub.dartlang.org/packages/shelf_packages_handler/versions/3.0.1.tar.gz";
sha256 = "1h8s42nff9ar0xn7yb42m64lpvmqzq8wranqrkkixdnp7w3pmv1x";
url = "https://pub.dartlang.org/packages/shelf_packages_handler/versions/3.0.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
shelf_static = fetchzip {
sha256 = "1kqbaslz7bna9lldda3ibrjg0gczbzlwgm9cic8shg0bnl0v3s34";
url = "https://pub.dartlang.org/packages/shelf_static/versions/1.1.1.tar.gz";
sha256 = "1bcqynn2z2syrigmrclxgg8hjhd1x9742938i62cicbaga6vclaz";
url = "https://pub.dartlang.org/packages/shelf_static/versions/1.1.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
shelf_web_socket = fetchzip {
sha256 = "0rr87nx2wdf9alippxiidqlgi82fbprnsarr1jswg9qin0yy4jpn";
url = "https://pub.dartlang.org/packages/shelf_web_socket/versions/1.0.3.tar.gz";
sha256 = "110b5hrqwpnmq16shxxzjmcih5yfs5kh80dn8avfv0xj5iv7n94c";
url = "https://pub.dartlang.org/packages/shelf_web_socket/versions/1.0.4.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
source_gen = fetchzip {
sha256 = "1kxgx782lzpjhv736h0pz3lnxpcgiy05h0ysy0q77gix8q09i1hz";
url = "https://pub.dartlang.org/packages/source_gen/versions/1.2.6.tar.gz";
sha256 = "1jql5zccv4vnbbvwcpyyvz8l27pg1rviqbp4vrks5313nf4b0kjg";
url = "https://pub.dartlang.org/packages/source_gen/versions/1.3.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
source_helper = fetchzip {
sha256 = "044kzmzlfpx93s4raz5avijahizmvai0zvl0lbm4wi93ynhdp1pd";
url = "https://pub.dartlang.org/packages/source_helper/versions/1.3.3.tar.gz";
sha256 = "0mdd02vhcdcv9n58gzbx2q0bphwj0alz312ca1a8xpkf8jx3y8v4";
url = "https://pub.dartlang.org/packages/source_helper/versions/1.3.4.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -413,29 +415,29 @@
};
source_maps = fetchzip {
sha256 = "18ixrlz3l2alk3hp0884qj0mcgzhxmjpg6nq0n1200pfy62pc4z6";
url = "https://pub.dartlang.org/packages/source_maps/versions/0.10.11.tar.gz";
sha256 = "004lcfka01agxjdw7zjhrffdkisvgx5s61b5gsl8qqk2jd1rswa7";
url = "https://pub.dartlang.org/packages/source_maps/versions/0.10.12.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
source_span = fetchzip {
sha256 = "1lq4sy7lw15qsv9cijf6l48p16qr19r7njzwr4pxn8vv1kh6rb86";
url = "https://pub.dartlang.org/packages/source_span/versions/1.9.1.tar.gz";
sha256 = "1nybnf7l5chslp4fczhqnrgrhymy844lw7qrj6y08i626dshrd46";
url = "https://pub.dartlang.org/packages/source_span/versions/1.10.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
stack_trace = fetchzip {
sha256 = "0bggqvvpkrfvqz24bnir4959k0c45azc3zivk4lyv3mvba6092na";
url = "https://pub.dartlang.org/packages/stack_trace/versions/1.11.0.tar.gz";
sha256 = "0xpk2cvmgdh46iwip9jsb54fqx13jnina8pk03akxkmsxvag5izb";
url = "https://pub.dartlang.org/packages/stack_trace/versions/1.11.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
stream_channel = fetchzip {
sha256 = "054by84c60yxphr3qgg6f82gg6d22a54aqjp265anlm8dwz1ji32";
url = "https://pub.dartlang.org/packages/stream_channel/versions/2.1.1.tar.gz";
sha256 = "0nrlw6zcscgnn6818krkbgs9qiv3f7q8pa7ljw1bqkrsb7xabm8s";
url = "https://pub.dartlang.org/packages/stream_channel/versions/2.1.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -455,8 +457,8 @@
};
synchronized = fetchzip {
sha256 = "1j6108cq1hbcqpwhk9sah8q3gcidd7222bzhha2nk9syxhzqy82i";
url = "https://pub.dartlang.org/packages/synchronized/versions/3.0.0%2B2.tar.gz";
sha256 = "1fx1z1p5qkn4qnq24riw5s86vmq645ppg8f74iyv2fc9rvr301ar";
url = "https://pub.dartlang.org/packages/synchronized/versions/3.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -469,36 +471,36 @@
};
test = fetchzip {
sha256 = "08kimbjvkdw3bkj7za36p3yqdr8dnlb5v30c250kvdncb7k09h4x";
url = "https://pub.dartlang.org/packages/test/versions/1.22.0.tar.gz";
sha256 = "002phlj2pg6nll5hv449izxbqk29zwmwc77d0jx2iimz18dgy2r5";
url = "https://pub.dartlang.org/packages/test/versions/1.24.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
test_api = fetchzip {
sha256 = "0mfyjpqkkmaqdh7xygrydx12591wq9ll816f61n80dc6rmkdx7px";
url = "https://pub.dartlang.org/packages/test_api/versions/0.4.16.tar.gz";
sha256 = "0as1xcywjrd2zax3cm56qmnac12shf8c1ynnzzjwnggm23f61dxb";
url = "https://pub.dartlang.org/packages/test_api/versions/0.6.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
test_core = fetchzip {
sha256 = "1r8dnvkxxvh55z1c8lrsja1m0dkf5i4lgwwqixcx0mqvxx5w3005";
url = "https://pub.dartlang.org/packages/test_core/versions/0.4.20.tar.gz";
sha256 = "1cx2rmz1xzk5z5yh8fpbsrsz4mgjanrw4xvnp0qzdnm2d7vhaq0y";
url = "https://pub.dartlang.org/packages/test_core/versions/0.5.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
timing = fetchzip {
sha256 = "0a02znvy0fbzr0n4ai67pp8in7w6m768aynkk1kp5lnmgy17ppsg";
url = "https://pub.dartlang.org/packages/timing/versions/1.0.0.tar.gz";
sha256 = "15jvxsw7v0gwbdlykma60l1qlhlzb3brh6m0sg2bgbfir4l5s9gw";
url = "https://pub.dartlang.org/packages/timing/versions/1.0.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
typed_data = fetchzip {
sha256 = "1x402bvyzdmdvmyqhyfamjxf54p9j8sa8ns2n5dwsdhnfqbw859g";
url = "https://pub.dartlang.org/packages/typed_data/versions/1.3.1.tar.gz";
sha256 = "0q6ggc52vfpr8kqaq69h757wy942hvgshhnsr2pjdinb4zk2sxl1";
url = "https://pub.dartlang.org/packages/typed_data/versions/1.3.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -511,8 +513,8 @@
};
uuid = fetchzip {
sha256 = "12lsynr07lw9848jknmzxvzn3ia12xdj07iiva0vg0qjvpq7ladg";
url = "https://pub.dartlang.org/packages/uuid/versions/3.0.5.tar.gz";
sha256 = "1nh1hxfr6bhyadqqcxrpwrphmm75f1iq4rzfjdwa2486xwlh7vx3";
url = "https://pub.dartlang.org/packages/uuid/versions/3.0.7.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -525,8 +527,8 @@
};
vm_service = fetchzip {
sha256 = "05xaxaxzyfls6jklw1hzws2jmina1cjk10gbl7a63djh1ghnzjb5";
url = "https://pub.dartlang.org/packages/vm_service/versions/9.4.0.tar.gz";
sha256 = "15ail7rbaq9ksg73cc0mw2k5imbiidl95yfd4v49k81gp5xmj92w";
url = "https://pub.dartlang.org/packages/vm_service/versions/11.10.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -539,8 +541,8 @@
};
web_socket_channel = fetchzip {
sha256 = "147amn05v1f1a1grxjr7yzgshrczjwijwiywggsv6dgic8kxyj5a";
url = "https://pub.dartlang.org/packages/web_socket_channel/versions/2.2.0.tar.gz";
sha256 = "0f9441c4zifb5qadpjg319dcilimpkdhfacnkl543802bf8qjn4w";
url = "https://pub.dartlang.org/packages/web_socket_channel/versions/2.4.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
@@ -553,262 +555,262 @@
};
xml = fetchzip {
sha256 = "0jwknkfcnb5svg6r01xjsj0aiw06mlx54pgay1ymaaqm2mjhyz01";
url = "https://pub.dartlang.org/packages/xml/versions/6.2.0.tar.gz";
sha256 = "120azx71gazvrrn07vd83vrffzrhsqnmf9rdjxl73rra9py8ixiy";
url = "https://pub.dartlang.org/packages/xml/versions/6.3.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
yaml = fetchzip {
sha256 = "0mqqmzn3c9rr38b5xm312fz1vyp6vb36lm477r9hak77bxzpp0iw";
url = "https://pub.dartlang.org/packages/yaml/versions/3.1.1.tar.gz";
sha256 = "0awh9dynbhrlq8zgszaiyxbyyn9b6wyps1zww4z2lx62nbma0pda";
url = "https://pub.dartlang.org/packages/yaml/versions/3.1.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
pubCache = runCommand "moxxmpp-pub-cache" {} ''
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${_fe_analyzer_shared} $out/hosted/pub.dartlang.org/_fe_analyzer_shared-50.0.0
mkdir -p $out/hosted/pub.dev
ln -s ${_fe_analyzer_shared} $out/hosted/pub.dev/_fe_analyzer_shared-61.0.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${analyzer} $out/hosted/pub.dartlang.org/analyzer-5.2.0
mkdir -p $out/hosted/pub.dev
ln -s ${analyzer} $out/hosted/pub.dev/analyzer-5.13.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${args} $out/hosted/pub.dartlang.org/args-2.3.1
mkdir -p $out/hosted/pub.dev
ln -s ${args} $out/hosted/pub.dev/args-2.4.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${async} $out/hosted/pub.dartlang.org/async-2.10.0
mkdir -p $out/hosted/pub.dev
ln -s ${async} $out/hosted/pub.dev/async-2.11.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${boolean_selector} $out/hosted/pub.dartlang.org/boolean_selector-2.1.1
mkdir -p $out/hosted/pub.dev
ln -s ${boolean_selector} $out/hosted/pub.dev/boolean_selector-2.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build} $out/hosted/pub.dartlang.org/build-2.3.1
mkdir -p $out/hosted/pub.dev
ln -s ${build} $out/hosted/pub.dev/build-2.3.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_config} $out/hosted/pub.dartlang.org/build_config-1.1.1
mkdir -p $out/hosted/pub.dev
ln -s ${build_config} $out/hosted/pub.dev/build_config-1.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_daemon} $out/hosted/pub.dartlang.org/build_daemon-3.1.0
mkdir -p $out/hosted/pub.dev
ln -s ${build_daemon} $out/hosted/pub.dev/build_daemon-3.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_resolvers} $out/hosted/pub.dartlang.org/build_resolvers-2.1.0
mkdir -p $out/hosted/pub.dev
ln -s ${build_resolvers} $out/hosted/pub.dev/build_resolvers-2.3.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_runner} $out/hosted/pub.dartlang.org/build_runner-2.3.2
mkdir -p $out/hosted/pub.dev
ln -s ${build_runner} $out/hosted/pub.dev/build_runner-2.3.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_runner_core} $out/hosted/pub.dartlang.org/build_runner_core-7.2.7
mkdir -p $out/hosted/pub.dev
ln -s ${build_runner_core} $out/hosted/pub.dev/build_runner_core-7.2.7+1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${built_collection} $out/hosted/pub.dartlang.org/built_collection-5.1.1
mkdir -p $out/hosted/pub.dev
ln -s ${built_collection} $out/hosted/pub.dev/built_collection-5.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${built_value} $out/hosted/pub.dartlang.org/built_value-8.4.2
mkdir -p $out/hosted/pub.dev
ln -s ${built_value} $out/hosted/pub.dev/built_value-8.6.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${checked_yaml} $out/hosted/pub.dartlang.org/checked_yaml-2.0.1
mkdir -p $out/hosted/pub.dev
ln -s ${checked_yaml} $out/hosted/pub.dev/checked_yaml-2.0.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${code_builder} $out/hosted/pub.dartlang.org/code_builder-4.3.0
mkdir -p $out/hosted/pub.dev
ln -s ${code_builder} $out/hosted/pub.dev/code_builder-4.6.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${collection} $out/hosted/pub.dartlang.org/collection-1.17.0
mkdir -p $out/hosted/pub.dev
ln -s ${collection} $out/hosted/pub.dev/collection-1.18.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${convert} $out/hosted/pub.dartlang.org/convert-3.1.1
mkdir -p $out/hosted/pub.dev
ln -s ${convert} $out/hosted/pub.dev/convert-3.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${coverage} $out/hosted/pub.dartlang.org/coverage-1.6.1
mkdir -p $out/hosted/pub.dev
ln -s ${coverage} $out/hosted/pub.dev/coverage-1.6.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${crypto} $out/hosted/pub.dartlang.org/crypto-3.0.2
mkdir -p $out/hosted/pub.dev
ln -s ${crypto} $out/hosted/pub.dev/crypto-3.0.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${cryptography} $out/hosted/pub.dartlang.org/cryptography-2.0.5
mkdir -p $out/hosted/pub.dev
ln -s ${cryptography} $out/hosted/pub.dev/cryptography-2.5.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${dart_style} $out/hosted/pub.dartlang.org/dart_style-2.2.4
mkdir -p $out/hosted/pub.dev
ln -s ${dart_style} $out/hosted/pub.dev/dart_style-2.3.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${file} $out/hosted/pub.dartlang.org/file-6.1.4
mkdir -p $out/hosted/pub.dev
ln -s ${file} $out/hosted/pub.dev/file-6.1.4
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${fixnum} $out/hosted/pub.dartlang.org/fixnum-1.0.1
mkdir -p $out/hosted/pub.dev
ln -s ${fixnum} $out/hosted/pub.dev/fixnum-1.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${freezed} $out/hosted/pub.dartlang.org/freezed-2.1.1
mkdir -p $out/hosted/pub.dev
ln -s ${frontend_server_client} $out/hosted/pub.dev/frontend_server_client-3.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${freezed_annotation} $out/hosted/pub.dartlang.org/freezed_annotation-2.1.0
mkdir -p $out/hosted/pub.dev
ln -s ${glob} $out/hosted/pub.dev/glob-2.1.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${frontend_server_client} $out/hosted/pub.dartlang.org/frontend_server_client-3.1.0
mkdir -p $out/hosted/pub.dev
ln -s ${graphs} $out/hosted/pub.dev/graphs-2.3.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${glob} $out/hosted/pub.dartlang.org/glob-2.1.0
mkdir -p $out/hosted/pub.dev
ln -s ${hex} $out/hosted/pub.dev/hex-0.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${graphs} $out/hosted/pub.dartlang.org/graphs-2.2.0
mkdir -p $out/hosted/pub.dev
ln -s ${http_multi_server} $out/hosted/pub.dev/http_multi_server-3.2.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${hex} $out/hosted/pub.dartlang.org/hex-0.2.0
mkdir -p $out/hosted/pub.dev
ln -s ${http_parser} $out/hosted/pub.dev/http_parser-4.0.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${http_multi_server} $out/hosted/pub.dartlang.org/http_multi_server-3.2.1
mkdir -p $out/hosted/pub.dev
ln -s ${io} $out/hosted/pub.dev/io-1.0.4
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${http_parser} $out/hosted/pub.dartlang.org/http_parser-4.0.2
mkdir -p $out/hosted/pub.dev
ln -s ${js} $out/hosted/pub.dev/js-0.6.7
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${io} $out/hosted/pub.dartlang.org/io-1.0.3
mkdir -p $out/hosted/pub.dev
ln -s ${json_annotation} $out/hosted/pub.dev/json_annotation-4.8.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${js} $out/hosted/pub.dartlang.org/js-0.6.5
mkdir -p $out/hosted/pub.dev
ln -s ${json_serializable} $out/hosted/pub.dev/json_serializable-6.6.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${json_annotation} $out/hosted/pub.dartlang.org/json_annotation-4.7.0
mkdir -p $out/hosted/pub.dev
ln -s ${logging} $out/hosted/pub.dev/logging-1.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${json_serializable} $out/hosted/pub.dartlang.org/json_serializable-6.5.4
mkdir -p $out/hosted/pub.dev
ln -s ${matcher} $out/hosted/pub.dev/matcher-0.12.16
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${logging} $out/hosted/pub.dartlang.org/logging-1.0.2
mkdir -p $out/hosted/pub.dev
ln -s ${meta} $out/hosted/pub.dev/meta-1.9.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${matcher} $out/hosted/pub.dartlang.org/matcher-0.12.13
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${meta} $out/hosted/pub.dartlang.org/meta-1.8.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${mime} $out/hosted/pub.dartlang.org/mime-1.0.2
mkdir -p $out/hosted/pub.dev
ln -s ${mime} $out/hosted/pub.dev/mime-1.0.4
mkdir -p $out/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47
ln -s ${moxlib} $out/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47/moxlib-0.1.5
ln -s ${moxlib} $out/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47/moxlib-0.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${node_preamble} $out/hosted/pub.dartlang.org/node_preamble-2.0.1
mkdir -p $out/hosted/pub.dev
ln -s ${node_preamble} $out/hosted/pub.dev/node_preamble-2.0.2
mkdir -p $out/hosted/git.polynom.me%47api%47packages%47PapaTutuWawa%47pub%47
ln -s ${omemo_dart} $out/hosted/git.polynom.me%47api%47packages%47PapaTutuWawa%47pub%47/omemo_dart-0.4.3
ln -s ${omemo_dart} $out/hosted/git.polynom.me%47api%47packages%47PapaTutuWawa%47pub%47/omemo_dart-0.5.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${package_config} $out/hosted/pub.dartlang.org/package_config-2.1.0
mkdir -p $out/hosted/pub.dev
ln -s ${package_config} $out/hosted/pub.dev/package_config-2.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${path} $out/hosted/pub.dartlang.org/path-1.8.2
mkdir -p $out/hosted/pub.dev
ln -s ${path} $out/hosted/pub.dev/path-1.8.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pedantic} $out/hosted/pub.dartlang.org/pedantic-1.11.1
mkdir -p $out/hosted/pub.dev
ln -s ${pedantic} $out/hosted/pub.dev/pedantic-1.11.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${petitparser} $out/hosted/pub.dartlang.org/petitparser-5.1.0
mkdir -p $out/hosted/pub.dev
ln -s ${petitparser} $out/hosted/pub.dev/petitparser-5.4.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pinenacl} $out/hosted/pub.dartlang.org/pinenacl-0.5.1
mkdir -p $out/hosted/pub.dev
ln -s ${pinenacl} $out/hosted/pub.dev/pinenacl-0.5.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pool} $out/hosted/pub.dartlang.org/pool-1.5.1
mkdir -p $out/hosted/pub.dev
ln -s ${pool} $out/hosted/pub.dev/pool-1.5.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pub_semver} $out/hosted/pub.dartlang.org/pub_semver-2.1.2
mkdir -p $out/hosted/pub.dev
ln -s ${protobuf} $out/hosted/pub.dev/protobuf-2.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pubspec_parse} $out/hosted/pub.dartlang.org/pubspec_parse-1.2.1
mkdir -p $out/hosted/pub.dev
ln -s ${protoc_plugin} $out/hosted/pub.dev/protoc_plugin-20.0.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${random_string} $out/hosted/pub.dartlang.org/random_string-2.3.1
mkdir -p $out/hosted/pub.dev
ln -s ${pub_semver} $out/hosted/pub.dev/pub_semver-2.1.4
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${saslprep} $out/hosted/pub.dartlang.org/saslprep-1.0.2
mkdir -p $out/hosted/pub.dev
ln -s ${pubspec_parse} $out/hosted/pub.dev/pubspec_parse-1.2.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${shelf} $out/hosted/pub.dartlang.org/shelf-1.4.0
mkdir -p $out/hosted/pub.dev
ln -s ${random_string} $out/hosted/pub.dev/random_string-2.3.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${shelf_packages_handler} $out/hosted/pub.dartlang.org/shelf_packages_handler-3.0.1
mkdir -p $out/hosted/pub.dev
ln -s ${saslprep} $out/hosted/pub.dev/saslprep-1.0.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${shelf_static} $out/hosted/pub.dartlang.org/shelf_static-1.1.1
mkdir -p $out/hosted/pub.dev
ln -s ${shelf} $out/hosted/pub.dev/shelf-1.4.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${shelf_web_socket} $out/hosted/pub.dartlang.org/shelf_web_socket-1.0.3
mkdir -p $out/hosted/pub.dev
ln -s ${shelf_packages_handler} $out/hosted/pub.dev/shelf_packages_handler-3.0.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_gen} $out/hosted/pub.dartlang.org/source_gen-1.2.6
mkdir -p $out/hosted/pub.dev
ln -s ${shelf_static} $out/hosted/pub.dev/shelf_static-1.1.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_helper} $out/hosted/pub.dartlang.org/source_helper-1.3.3
mkdir -p $out/hosted/pub.dev
ln -s ${shelf_web_socket} $out/hosted/pub.dev/shelf_web_socket-1.0.4
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_map_stack_trace} $out/hosted/pub.dartlang.org/source_map_stack_trace-2.1.1
mkdir -p $out/hosted/pub.dev
ln -s ${source_gen} $out/hosted/pub.dev/source_gen-1.3.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_maps} $out/hosted/pub.dartlang.org/source_maps-0.10.11
mkdir -p $out/hosted/pub.dev
ln -s ${source_helper} $out/hosted/pub.dev/source_helper-1.3.4
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_span} $out/hosted/pub.dartlang.org/source_span-1.9.1
mkdir -p $out/hosted/pub.dev
ln -s ${source_map_stack_trace} $out/hosted/pub.dev/source_map_stack_trace-2.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${stack_trace} $out/hosted/pub.dartlang.org/stack_trace-1.11.0
mkdir -p $out/hosted/pub.dev
ln -s ${source_maps} $out/hosted/pub.dev/source_maps-0.10.12
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${stream_channel} $out/hosted/pub.dartlang.org/stream_channel-2.1.1
mkdir -p $out/hosted/pub.dev
ln -s ${source_span} $out/hosted/pub.dev/source_span-1.10.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${stream_transform} $out/hosted/pub.dartlang.org/stream_transform-2.1.0
mkdir -p $out/hosted/pub.dev
ln -s ${stack_trace} $out/hosted/pub.dev/stack_trace-1.11.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${string_scanner} $out/hosted/pub.dartlang.org/string_scanner-1.2.0
mkdir -p $out/hosted/pub.dev
ln -s ${stream_channel} $out/hosted/pub.dev/stream_channel-2.1.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${synchronized} $out/hosted/pub.dartlang.org/synchronized-3.0.0+2
mkdir -p $out/hosted/pub.dev
ln -s ${stream_transform} $out/hosted/pub.dev/stream_transform-2.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${term_glyph} $out/hosted/pub.dartlang.org/term_glyph-1.2.1
mkdir -p $out/hosted/pub.dev
ln -s ${string_scanner} $out/hosted/pub.dev/string_scanner-1.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${test} $out/hosted/pub.dartlang.org/test-1.22.0
mkdir -p $out/hosted/pub.dev
ln -s ${synchronized} $out/hosted/pub.dev/synchronized-3.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${test_api} $out/hosted/pub.dartlang.org/test_api-0.4.16
mkdir -p $out/hosted/pub.dev
ln -s ${term_glyph} $out/hosted/pub.dev/term_glyph-1.2.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${test_core} $out/hosted/pub.dartlang.org/test_core-0.4.20
mkdir -p $out/hosted/pub.dev
ln -s ${test} $out/hosted/pub.dev/test-1.24.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${timing} $out/hosted/pub.dartlang.org/timing-1.0.0
mkdir -p $out/hosted/pub.dev
ln -s ${test_api} $out/hosted/pub.dev/test_api-0.6.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${typed_data} $out/hosted/pub.dartlang.org/typed_data-1.3.1
mkdir -p $out/hosted/pub.dev
ln -s ${test_core} $out/hosted/pub.dev/test_core-0.5.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${unorm_dart} $out/hosted/pub.dartlang.org/unorm_dart-0.2.0
mkdir -p $out/hosted/pub.dev
ln -s ${timing} $out/hosted/pub.dev/timing-1.0.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${uuid} $out/hosted/pub.dartlang.org/uuid-3.0.5
mkdir -p $out/hosted/pub.dev
ln -s ${typed_data} $out/hosted/pub.dev/typed_data-1.3.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${very_good_analysis} $out/hosted/pub.dartlang.org/very_good_analysis-3.1.0
mkdir -p $out/hosted/pub.dev
ln -s ${unorm_dart} $out/hosted/pub.dev/unorm_dart-0.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${vm_service} $out/hosted/pub.dartlang.org/vm_service-9.4.0
mkdir -p $out/hosted/pub.dev
ln -s ${uuid} $out/hosted/pub.dev/uuid-3.0.7
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${watcher} $out/hosted/pub.dartlang.org/watcher-1.0.2
mkdir -p $out/hosted/pub.dev
ln -s ${very_good_analysis} $out/hosted/pub.dev/very_good_analysis-3.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${web_socket_channel} $out/hosted/pub.dartlang.org/web_socket_channel-2.2.0
mkdir -p $out/hosted/pub.dev
ln -s ${vm_service} $out/hosted/pub.dev/vm_service-11.10.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${webkit_inspection_protocol} $out/hosted/pub.dartlang.org/webkit_inspection_protocol-1.2.0
mkdir -p $out/hosted/pub.dev
ln -s ${watcher} $out/hosted/pub.dev/watcher-1.0.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${xml} $out/hosted/pub.dartlang.org/xml-6.2.0
mkdir -p $out/hosted/pub.dev
ln -s ${web_socket_channel} $out/hosted/pub.dev/web_socket_channel-2.4.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${yaml} $out/hosted/pub.dartlang.org/yaml-3.1.1
mkdir -p $out/hosted/pub.dev
ln -s ${webkit_inspection_protocol} $out/hosted/pub.dev/webkit_inspection_protocol-1.2.0
mkdir -p $out/hosted/pub.dev
ln -s ${xml} $out/hosted/pub.dev/xml-6.3.0
mkdir -p $out/hosted/pub.dev
ln -s ${yaml} $out/hosted/pub.dev/yaml-3.1.2
'';
}

View File

@@ -1,3 +1,6 @@
## 0.4.1
- Moved FAST from staging to xep_0484.dart
## 0.4.0
- **BREAKING**: Remove `lastResource` from `XmppConnection`'s `connect` method. Instead, set the `StreamManagementNegotiator`'s `resource` attribute instead. Since the resource can only really be restored by stream management, this is no issue.
@@ -16,6 +19,16 @@
- **BREAKING**: `MessageEvent` now makes use of `TypedMap`.
- **BREAKING**: Removed `PresenceReceivedEvent`. Use a manager registering handlers with priority greater than `[PresenceManager.presenceHandlerPriority]` instead.
- **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.
- **BREAKING**: Removed "Extensible File Thumbnails" (The `Thumbnail` type).
- *BREAKING*: Rename `UserAvatarManager`'s `getUserAvatar` to `getUserAvatarData`. It now also requires the id of the avatar to fetch
- *BREAKING*: `UserAvatarManager`'s `getAvatarId` with `getLatestMetadata`.
- The `PubSubManager` now supports PubSub's `max_items` in `getItems`.
- *BREAKING*: `vCardManager`'s `VCardAvatarUpdatedEvent` no longer automatically requests the newest VCard avatar.
- *BREAKING*: `XmppConnection` now tries to ensure that incoming data is processed in-order. The only exception are awaited stanzas as they are allowed to bypass the queue.
- *BREAKING*: If a stanza handler causes an exception, the handler is simply skipped while processing.
- Add better logging around what stanza handler is running and if they end processing early.
## 0.3.1

View File

@@ -38,16 +38,17 @@ export 'package:moxxmpp/src/settings.dart';
export 'package:moxxmpp/src/socket.dart';
export 'package:moxxmpp/src/stanza.dart';
export 'package:moxxmpp/src/stringxml.dart';
export 'package:moxxmpp/src/types/result.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/fast.dart';
export 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
export 'package:moxxmpp/src/xeps/xep_0004.dart';
export 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0030/helpers.dart';
export 'package:moxxmpp/src/xeps/xep_0030/types.dart';
export 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
export 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0045/events.dart';
export 'package:moxxmpp/src/xeps/xep_0045/types.dart';
export 'package:moxxmpp/src/xeps/xep_0045/xep_0045.dart';
export 'package:moxxmpp/src/xeps/xep_0054.dart';
export 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0060/helpers.dart';
@@ -63,6 +64,7 @@ export 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
export 'package:moxxmpp/src/xeps/xep_0198/state.dart';
export 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
export 'package:moxxmpp/src/xeps/xep_0203.dart';
export 'package:moxxmpp/src/xeps/xep_0264.dart';
export 'package:moxxmpp/src/xeps/xep_0280.dart';
export 'package:moxxmpp/src/xeps/xep_0297.dart';
export 'package:moxxmpp/src/xeps/xep_0300.dart';
@@ -85,6 +87,7 @@ export 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
export 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
export 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
export 'package:moxxmpp/src/xeps/xep_0421.dart';
export 'package:moxxmpp/src/xeps/xep_0424.dart';
export 'package:moxxmpp/src/xeps/xep_0444.dart';
export 'package:moxxmpp/src/xeps/xep_0446.dart';
@@ -92,3 +95,4 @@ export 'package:moxxmpp/src/xeps/xep_0447.dart';
export 'package:moxxmpp/src/xeps/xep_0448.dart';
export 'package:moxxmpp/src/xeps/xep_0449.dart';
export 'package:moxxmpp/src/xeps/xep_0461.dart';
export 'package:moxxmpp/src/xeps/xep_0484.dart';

View File

@@ -1,35 +1,13 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:synchronized/synchronized.dart';
/// A surrogate key for awaiting stanzas.
@immutable
class _StanzaSurrogateKey {
const _StanzaSurrogateKey(this.sentTo, this.id, this.tag);
/// (JID we sent a stanza to, the id of the sent stanza, the tag of the sent stanza).
// ignore: avoid_private_typedef_functions
typedef _StanzaCompositeKey = (String?, String, String);
/// The JID the original stanza was sent to. We expect the result to come from the
/// same JID.
final String sentTo;
/// The ID of the original stanza. We expect the result to have the same ID.
final String id;
/// The tag name of the stanza.
final String tag;
@override
int get hashCode => sentTo.hashCode ^ id.hashCode ^ tag.hashCode;
@override
bool operator ==(Object other) {
return other is _StanzaSurrogateKey &&
other.sentTo == sentTo &&
other.id == id &&
other.tag == tag;
}
}
/// Callback function that returns the bare JID of the connection as a String.
typedef GetBareJidCallback = String Function();
/// This class handles the await semantics for stanzas. Stanzas are given a "unique"
/// key equal to the tuple (to, id, tag) with which their response is identified.
@@ -40,8 +18,12 @@ class _StanzaSurrogateKey {
///
/// This class also handles some "edge cases" of RFC 6120, like an empty "from" attribute.
class StanzaAwaiter {
StanzaAwaiter(this._bareJidCallback);
final GetBareJidCallback _bareJidCallback;
/// The pending stanzas, identified by their surrogate key.
final Map<_StanzaSurrogateKey, Completer<XMLNode>> _pending = {};
final Map<_StanzaCompositeKey, Completer<XMLNode>> _pending = {};
/// The critical section for accessing [StanzaAwaiter._pending].
final Lock _lock = Lock();
@@ -52,30 +34,33 @@ class StanzaAwaiter {
/// [tag] is the stanza's tag name.
///
/// Returns a future that might resolve to the response to the stanza.
Future<Future<XMLNode>> addPending(String to, String id, String tag) async {
Future<Future<XMLNode>> addPending(String? to, String id, String tag) async {
// Check if we want to send a stanza to our bare JID and replace it with null.
final processedTo = to != null && to == _bareJidCallback() ? null : to;
final completer = await _lock.synchronized(() {
final completer = Completer<XMLNode>();
_pending[_StanzaSurrogateKey(to, id, tag)] = completer;
_pending[(processedTo, id, tag)] = completer;
return completer;
});
return completer.future;
}
/// Checks if the stanza [stanza] is being awaited. [bareJid] is the bare JID of
/// the connection.
/// Checks if the stanza [stanza] is being awaited.
/// If [stanza] is awaited, resolves the future and returns true. If not, returns
/// false.
Future<bool> onData(XMLNode stanza, JID bareJid) async {
assert(bareJid.isBare(), 'bareJid must be bare');
Future<bool> onData(XMLNode stanza) async {
final id = stanza.attributes['id'] as String?;
if (id == null) return false;
final key = _StanzaSurrogateKey(
// Section 8.1.2.1 § 3 of RFC 6120 says that an empty "from" indicates that the
// attribute is implicitly from our own bare JID.
stanza.attributes['from'] as String? ?? bareJid.toString(),
// Check if we want to send a stanza to our bare JID and replace it with null.
final from = stanza.attributes['from'] as String?;
final processedFrom =
from != null && from == _bareJidCallback() ? null : from;
final key = (
processedFrom,
id,
stanza.tag,
);
@@ -91,4 +76,19 @@ class StanzaAwaiter {
return false;
});
}
/// Checks if [stanza] represents a stanza that is awaited. Returns true, if [stanza]
/// is awaited. False, if not.
Future<bool> isAwaited(XMLNode stanza) async {
final id = stanza.attributes['id'] as String?;
if (id == null) return false;
final key = (
stanza.attributes['from'] as String?,
id,
stanza.tag,
);
return _lock.synchronized(() => _pending.containsKey(key));
}
}

View File

@@ -25,14 +25,12 @@ import 'package:moxxmpp/src/settings.dart';
import 'package:moxxmpp/src/socket.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/incoming_queue.dart';
import 'package:moxxmpp/src/util/queue.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_0198/types.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:moxxmpp/src/xeps/xep_0352.dart';
import 'package:synchronized/synchronized.dart';
import 'package:uuid/uuid.dart';
/// The states the XmppConnection can be in
@@ -51,6 +49,19 @@ enum XmppConnectionState {
error
}
/// (The actual stanza handler, Name of the owning manager).
typedef _StanzaHandlerWrapper = (StanzaHandler, String);
/// Wrapper around [stanzaHandlerSortComparator] for [_StanzaHandlerWrapper].
int _stanzaHandlerWrapperSortComparator(
_StanzaHandlerWrapper a,
_StanzaHandlerWrapper b,
) {
final (ha, _) = a;
final (hb, _) = b;
return stanzaHandlerSortComparator(ha, hb);
}
/// This class is a connection to the server.
class XmppConnection {
XmppConnection(
@@ -60,7 +71,11 @@ class XmppConnection {
this._socket, {
this.connectingTimeout = const Duration(minutes: 2),
}) : _reconnectionPolicy = reconnectionPolicy,
_connectivityManager = connectivityManager {
_connectivityManager = connectivityManager,
assert(
_socket.getDataStream().isBroadcast,
"The socket's data stream must be a broadcast stream",
) {
// Allow the reconnection policy to perform reconnections by itself
_reconnectionPolicy.register(
_attemptReconnection,
@@ -79,9 +94,15 @@ class XmppConnection {
},
);
_stanzaAwaiter = StanzaAwaiter(
() => connectionSettings.jid.toBare().toString(),
);
_incomingStanzaQueue = IncomingStanzaQueue(handleXmlStream, _stanzaAwaiter);
_socketStream = _socket.getDataStream();
// TODO(Unknown): Handle on done
_socketStream.transform(_streamParser).forEach(handleXmlStream);
_socketStream
.transform(_streamParser)
.forEach(_incomingStanzaQueue.addStanza);
_socketStream.listen(_handleOnDataCallbacks);
_socket.getEventStream().listen(handleSocketEvent);
_stanzaQueue = AsyncStanzaQueue(
@@ -111,16 +132,16 @@ class XmppConnection {
final ConnectivityManager _connectivityManager;
/// A helper for handling await semantics with stanzas
final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter();
late final StanzaAwaiter _stanzaAwaiter;
/// Sorted list of handlers that we call or incoming and outgoing stanzas
final List<StanzaHandler> _incomingStanzaHandlers =
final List<_StanzaHandlerWrapper> _incomingStanzaHandlers =
List.empty(growable: true);
final List<StanzaHandler> _incomingPreStanzaHandlers =
final List<_StanzaHandlerWrapper> _incomingPreStanzaHandlers =
List.empty(growable: true);
final List<StanzaHandler> _outgoingPreStanzaHandlers =
final List<_StanzaHandlerWrapper> _outgoingPreStanzaHandlers =
List.empty(growable: true);
final List<StanzaHandler> _outgoingPostStanzaHandlers =
final List<_StanzaHandlerWrapper> _outgoingPostStanzaHandlers =
List.empty(growable: true);
final StreamController<XmppEvent> _eventStreamController =
StreamController.broadcast();
@@ -159,10 +180,6 @@ class XmppConnection {
T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) =>
_negotiationsHandler.getNegotiatorById<T>(id);
/// Prevent data from being passed to _currentNegotiator.negotiator while the negotiator
/// is still running.
final Lock _negotiationLock = Lock();
/// The logger for the class
final Logger _log = Logger('XmppConnection');
@@ -171,6 +188,8 @@ class XmppConnection {
bool get isAuthenticated => _isAuthenticated;
late final IncomingStanzaQueue _incomingStanzaQueue;
late final AsyncStanzaQueue _stanzaQueue;
/// Returns the JID we authenticate with and add the resource that we have bound.
@@ -200,18 +219,25 @@ class XmppConnection {
_xmppManagers[manager.id] = manager;
_incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers());
_incomingPreStanzaHandlers.addAll(manager.getIncomingPreStanzaHandlers());
_outgoingPreStanzaHandlers.addAll(manager.getOutgoingPreStanzaHandlers());
_outgoingPostStanzaHandlers
.addAll(manager.getOutgoingPostStanzaHandlers());
_incomingStanzaHandlers.addAll(
manager.getIncomingStanzaHandlers().map((h) => (h, manager.name)),
);
_incomingPreStanzaHandlers.addAll(
manager.getIncomingPreStanzaHandlers().map((h) => (h, manager.name)),
);
_outgoingPreStanzaHandlers.addAll(
manager.getOutgoingPreStanzaHandlers().map((h) => (h, manager.name)),
);
_outgoingPostStanzaHandlers.addAll(
manager.getOutgoingPostStanzaHandlers().map((h) => (h, manager.name)),
);
}
// Sort them
_incomingStanzaHandlers.sort(stanzaHandlerSortComparator);
_incomingPreStanzaHandlers.sort(stanzaHandlerSortComparator);
_outgoingPreStanzaHandlers.sort(stanzaHandlerSortComparator);
_outgoingPostStanzaHandlers.sort(stanzaHandlerSortComparator);
_incomingStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
_incomingPreStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
_outgoingPreStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
_outgoingPostStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
// Run the post register callbacks
for (final manager in _xmppManagers.values) {
@@ -292,6 +318,13 @@ class XmppConnection {
return getManagerById(csiManager);
}
/// Called whenever we receive data on the socket.
Future<void> _handleOnDataCallbacks(String _) async {
for (final manager in _xmppManagers.values) {
unawaited(manager.onData());
}
}
/// Attempts to reconnect to the server by following an exponential backoff.
Future<void> _attemptReconnection() async {
_log.finest('_attemptReconnection: Setting state to notConnected');
@@ -314,7 +347,7 @@ class XmppConnection {
/// Called when a stream ending error has occurred
Future<void> handleError(XmppError error) async {
_log.severe('handleError called with ${error.toString()}');
_log.severe('handleError called with $error');
// Whenever we encounter an error that would trigger a reconnection attempt while
// the connection result is being awaited, don't attempt a reconnection but instead
@@ -477,8 +510,9 @@ class XmppConnection {
false,
false,
newStanza,
TypedMap(),
details.extensions ?? TypedMap(),
encrypted: details.encrypted,
shouldEncrypt: details.shouldEncrypt,
forceEncryption: details.forceEncryption,
),
);
@@ -516,7 +550,7 @@ class XmppConnection {
// A stanza with no to attribute is for direct processing by the server. As such,
// we can correlate it by just *assuming* we have that attribute
// (RFC 6120 Section 8.1.1.1)
data.stanza.to ?? connectionSettings.jid.toBare().toString(),
data.stanza.to,
data.stanza.id!,
data.stanza.tag,
)
@@ -533,15 +567,14 @@ class XmppConnection {
// Run post-send handlers
_log.fine('Running post stanza handlers..');
final extensions = TypedMap<StanzaHandlerExtension>()
..set(StreamManagementData(details.excludeFromStreamManagement));
await _runOutgoingPostStanzaHandlers(
newStanza,
initial: StanzaHandlerData(
false,
false,
newStanza,
extensions,
details.postSendExtensions ?? TypedMap<StanzaHandlerExtension>(),
encrypted: data.encrypted,
),
);
_log.fine('Done');
@@ -652,15 +685,30 @@ class XmppConnection {
/// call its callback and end the processing if the callback returned true; continue
/// if it returned false.
Future<StanzaHandlerData> _runStanzaHandlers(
List<StanzaHandler> handlers,
List<_StanzaHandlerWrapper> handlers,
Stanza stanza, {
StanzaHandlerData? initial,
}) async {
var state = initial ?? StanzaHandlerData(false, false, stanza, TypedMap());
for (final handler in handlers) {
for (final handlerRaw in handlers) {
final (handler, managerName) = handlerRaw;
if (handler.matches(state.stanza)) {
_log.finest(
'Running handler for ${stanza.tag} (${stanza.attributes["id"]}) of $managerName',
);
try {
state = await handler.callback(state.stanza, state);
if (state.done || state.cancel) return state;
} catch (ex) {
_log.severe(
'Handler from $managerName for ${stanza.tag} (${stanza.attributes["id"]}) failed with "$ex"',
);
}
if (state.done || state.cancel) {
_log.finest(
'Processing ended early for ${stanza.tag} (${stanza.attributes["id"]}) by $managerName',
);
return state;
}
}
}
@@ -736,9 +784,15 @@ class XmppConnection {
: '';
_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(
incomingPreHandlers.stanza,
connectionSettings.jid.toBare(),
);
if (awaited) {
return;
@@ -753,6 +807,7 @@ class XmppConnection {
incomingPreHandlers.stanza,
incomingPreHandlers.extensions,
encrypted: incomingPreHandlers.encrypted,
encryptionError: incomingPreHandlers.encryptionError,
cancelReason: incomingPreHandlers.cancelReason,
),
);
@@ -796,14 +851,12 @@ class XmppConnection {
// causing (a) the negotiator to become confused and (b) the stanzas/nonzas to be
// missed. This causes the data to wait while the negotiator is running and thus
// prevent this issue.
await _negotiationLock.synchronized(() async {
if (_routingState != RoutingState.negotiating) {
unawaited(handleXmlStream(event));
return;
}
await _negotiationsHandler.negotiate(event);
});
break;
case RoutingState.handleStanzas:
await _handleStanza(node);

View File

@@ -70,9 +70,9 @@ class MessageEvent extends XmppEvent {
MessageEvent(
this.from,
this.to,
this.id,
this.encrypted,
this.extensions, {
this.id,
this.type,
this.error,
this.encryptionError,
@@ -85,7 +85,7 @@ class MessageEvent extends XmppEvent {
final JID to;
/// The id attribute of the message.
final String id;
final String? id;
/// The type attribute of the message.
final String? type;
@@ -184,16 +184,12 @@ class UserAvatarUpdatedEvent extends XmppEvent {
class VCardAvatarUpdatedEvent extends XmppEvent {
VCardAvatarUpdatedEvent(
this.jid,
this.base64,
this.hash,
);
/// The JID of the entity that updated their avatar.
final JID jid;
/// The base64-encoded avatar data.
final String base64;
/// The SHA-1 hash of the avatar.
final String hash;
}

View File

@@ -55,8 +55,6 @@ class JID {
/// Converts the JID into a bare JID.
JID toBare() {
if (isBare()) return this;
return JID(local, domain, '');
}

View File

@@ -47,7 +47,6 @@ abstract class XmppManagerBase {
final result = await dm!.discoInfoQuery(
_managerAttributes.getConnectionSettings().jid.toDomain(),
shouldEncrypt: false,
);
if (result.isType<DiscoError>()) {
return false;
@@ -81,6 +80,9 @@ abstract class XmppManagerBase {
/// handler's priority, the earlier it is run.
List<NonzaHandler> getNonzaHandlers() => [];
/// Whenever the socket receives data, this method is called, if it is non-null.
Future<void> onData() async {}
/// Return a list of features that should be included in a disco response.
List<String> getDiscoFeatures() => [];

View File

@@ -13,12 +13,20 @@ class StanzaHandlerData {
this.encryptionError,
this.encrypted = false,
this.forceEncryption = false,
this.shouldEncrypt = true,
this.skip = false,
});
/// Indicates to the runner that processing is now done. This means that all
/// pre-processing is done and no other handlers should be consulted.
bool done;
/// 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
/// should run. The stanza also will not be sent.
bool cancel;
@@ -33,7 +41,7 @@ class StanzaHandlerData {
/// absolutely necessary, e.g. with Message Carbons or OMEMO.
Stanza stanza;
/// Whether the stanza was received encrypted
/// Whether the stanza is already encrypted
bool encrypted;
// If true, forces the encryption manager to encrypt to the JID, even if it
@@ -42,6 +50,10 @@ class StanzaHandlerData {
// to the JID anyway.
bool forceEncryption;
/// Flag indicating whether a E2EE implementation should encrypt the stanza (true)
/// or not (false).
bool shouldEncrypt;
/// Additional data from other managers.
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/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
@@ -100,10 +100,10 @@ class StanzaHandler extends Handler {
matches &= firstTag?.xmlns == tagXmlns;
}
} else if (tagXmlns != null) {
matches &= listContains(
node.children,
matches &= node.children.firstWhereOrNull(
(XMLNode node_) => node_.attributes['xmlns'] == tagXmlns,
);
) !=
null;
}
return matches;

View File

@@ -32,3 +32,5 @@ const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager';
const stickersManager = 'org.moxxmpp.stickersmanager';
const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities';
const messageProcessingHintManager = 'org.moxxmpp.messageprocessinghint';
const occupantIdManager = 'org.moxxmpp.occupantidmanager';
const mucManager = 'org.moxxmpp.mucmanager';

View File

@@ -66,23 +66,30 @@ class MessageManager extends XmppManagerBase {
stanzaTag: 'message',
callback: _onMessage,
priority: messageHandlerPriority,
)
),
];
@override
Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(
Stanza _,
Stanza stanza,
StanzaHandlerData state,
) async {
final body = stanza.firstTag('body');
if (body != null) {
state.extensions.set(
MessageBodyData(body.innerText()),
);
}
getAttributes().sendEvent(
MessageEvent(
JID.fromString(state.stanza.attributes['from']! as String),
JID.fromString(state.stanza.attributes['to']! as String),
state.stanza.attributes['id']! as String,
state.encrypted,
state.extensions,
id: state.stanza.attributes['id'] as String?,
type: state.stanza.attributes['type'] as String?,
error: StanzaError.fromStanza(state.stanza),
encryptionError: state.encryptionError,
@@ -96,19 +103,21 @@ class MessageManager extends XmppManagerBase {
/// data for building the message.
Future<void> sendMessage(
JID to,
TypedMap<StanzaHandlerExtension> extensions,
) async {
TypedMap<StanzaHandlerExtension> extensions, {
String type = 'chat',
}) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.message(
to: to.toString(),
id: extensions.get<MessageIdData>()?.id,
type: 'chat',
type: type,
children: _messageSendingCallbacks
.map((c) => c(extensions))
.flattened
.toList(),
),
extensions: extensions,
awaitable: false,
),
);

View File

@@ -13,6 +13,7 @@ const subscriptionPreApprovalXmlns = 'urn:xmpp:features:pre-approval';
// XEP-0004
const dataFormsXmlns = 'jabber:x:data';
const formVarFormType = 'FORM_TYPE';
// XEP-0030
const discoInfoXmlns = 'http://jabber.org/protocol/disco#info';
@@ -21,6 +22,11 @@ const discoItemsXmlns = 'http://jabber.org/protocol/disco#items';
// XEP-0033
const extendedAddressingXmlns = 'http://jabber.org/protocol/address';
// XEP-0045
const mucXmlns = 'http://jabber.org/protocol/muc';
const mucUserXmlns = 'http://jabber.org/protocol/muc#user';
const roomInfoFormType = 'http://jabber.org/protocol/muc#roominfo';
// XEP-0054
const vCardTempXmlns = 'vcard-temp';
const vCardTempUpdate = 'vcard-temp:x:update';
@@ -66,6 +72,9 @@ const delayedDeliveryXmlns = 'urn:xmpp:delay';
// XEP-0234
const jingleFileTransferXmlns = 'urn:xmpp:jingle:apps:file-transfer:5';
// XEP-0264
const jingleContentThumbnailXmlns = 'urn:xmpp:thumbs:1';
// XEP-0280
const carbonsXmlns = 'urn:xmpp:carbons:2';
@@ -123,6 +132,9 @@ const sasl2Xmlns = 'urn:xmpp:sasl:2';
// XEP-0420
const sceXmlns = 'urn:xmpp:sce:1';
// XEP-0421
const occupantIdXmlns = 'urn:xmpp:occupant-id:0';
// XEP-0422
const fasteningXmlns = 'urn:xmpp:fasten:0';
@@ -154,7 +166,6 @@ const stickersXmlns = 'urn:xmpp:stickers:0';
// XEP-0461
const replyXmlns = 'urn:xmpp:reply:0';
const fallbackXmlns = 'urn:xmpp:feature-fallback:0';
// ???
const urlDataXmlns = 'http://jabber.org/protocol/url-data';

View File

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

View File

@@ -57,9 +57,10 @@ class _ChunkedConversionBuffer<S, T> {
}
/// A buffer to put between a socket's input and a full XML stream.
class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
final StreamController<XMPPStreamObject> _streamController =
StreamController<XMPPStreamObject>();
class XMPPStreamParser
extends StreamTransformerBase<String, List<XMPPStreamObject>> {
final StreamController<List<XMPPStreamObject>> _streamController =
StreamController<List<XMPPStreamObject>>();
/// Turns a String into a list of [XmlEvent]s in a chunked fashion.
_ChunkedConversionBuffer<String, XmlEvent> _eventBuffer =
@@ -117,13 +118,14 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
}
@override
Stream<XMPPStreamObject> bind(Stream<String> stream) {
Stream<List<XMPPStreamObject>> bind(Stream<String> stream) {
// We do not want to use xml's toXmlEvents and toSubtreeEvents methods as they
// create streams we cannot close. We need to be able to destroy and recreate an
// XML parser whenever we start a new connection.
stream.listen((input) {
final events = _eventBuffer.convert(input);
final streamHeaderEvents = _streamHeaderSelector.convert(events);
final objects = List<XMPPStreamObject>.empty(growable: true);
// Process the stream header separately.
for (final event in streamHeaderEvents) {
@@ -135,7 +137,7 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
continue;
}
_streamController.add(
objects.add(
XMPPStreamHeader(
Map<String, String>.fromEntries(
event.attributes.map((attr) {
@@ -151,13 +153,15 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
final children = _childBuffer.convert(childEvents);
for (final node in children) {
if (node.nodeType == XmlNodeType.ELEMENT) {
_streamController.add(
objects.add(
XMPPStreamElement(
XMLNode.fromXmlElement(node as XmlElement),
),
);
}
}
_streamController.add(objects);
});
return _streamController.stream;

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.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/stanza.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
/// management, will be sent. Useful for managers that want to add [XMLNode]s to said
@@ -64,7 +66,7 @@ class PresenceManager extends XmppManagerBase {
stanzaTag: 'presence',
callback: _onPresence,
priority: presenceHandlerPriority,
)
),
];
@override
@@ -156,7 +158,9 @@ class PresenceManager extends XmppManagerBase {
),
awaitable: false,
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/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.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/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
@@ -13,15 +13,13 @@ abstract class SaslNegotiator extends XmppFeatureNegotiatorBase {
@override
bool matchesFeature(List<XMLNode> features) {
// Is SASL advertised?
final mechanisms = firstWhereOrNull(
features,
final mechanisms = features.firstWhereOrNull(
(XMLNode feature) => feature.attributes['xmlns'] == saslXmlns,
);
if (mechanisms == null) return false;
// Is SASL PLAIN advertised?
return firstWhereOrNull(
mechanisms.children,
return mechanisms.children.firstWhereOrNull(
(XMLNode mechanism) => mechanism.text == mechanismName,
) !=
null;

View File

@@ -1,12 +1,12 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.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/xep_0388.dart';
import 'package:saslprep/saslprep.dart';
@@ -96,7 +96,9 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
if (pickedForSasl2) {
state = NegotiatorState.done;
}
return const Result(true);
}

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:math' show Random;
import 'package:cryptography/cryptography.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/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/nonza.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/xep_0388.dart';
import 'package:random_string/random_string.dart';
@@ -246,6 +246,9 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
bool _checkSignature(String base64Signature) {
final signature =
parseKeyValue(utf8.decode(base64.decode(base64Signature)));
_log.finest(
'Expecting signature: "$_serverSignature", got: "${signature["v"]}"',
);
return signature['v']! == _serverSignature;
}
@@ -360,6 +363,11 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
// Don't do anything if we have not been picked for SASL2.
if (!pickedForSasl2) {
return const Result(true);
}
// When we're done with SASL2, check the additional data to verify the server
// signature.
state = NegotiatorState.done;

View File

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

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/attributes.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/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
@immutable
class XmppRosterItem {
@@ -122,7 +122,7 @@ class RosterManager extends XmppManagerBase {
tagName: 'query',
tagXmlns: rosterXmlns,
callback: _onRosterPush,
)
),
];
@override
@@ -182,8 +182,18 @@ class RosterManager extends XmppManagerBase {
/// Shared code between requesting rosters without and with roster versioning, if
/// the server deems a regular roster response more efficient than n roster pushes.
///
/// [query] is the <query /> child of the iq, if available.
///
/// If roster versioning was used, then [requestedRosterVersion] is the version
/// we requested the roster with.
///
/// Note that if roster versioning is used and the server returns us an empty iq,
/// it means that the roster did not change since the last version. In that case,
/// we do nothing and just return. The roster state manager will not be notified.
Future<Result<RosterRequestResult, RosterError>> _handleRosterResponse(
XMLNode? query,
String? requestedRosterVersion,
) async {
final List<XmppRosterItem> items;
String? rosterVersion;
@@ -204,6 +214,14 @@ class RosterManager extends XmppManagerBase {
.toList();
rosterVersion = query.attributes['ver'] as String?;
} else if (requestedRosterVersion != null) {
// Skip the handleRosterFetch call since nothing changed.
return Result(
RosterRequestResult(
[],
requestedRosterVersion,
),
);
} else {
logger.warning(
'Server response to roster request without roster versioning does not contain a <query /> element, while the type is not error. This violates RFC6121',
@@ -258,7 +276,7 @@ class RosterManager extends XmppManagerBase {
}
final responseQuery = response.firstTag('query', xmlns: rosterXmlns);
return _handleRosterResponse(responseQuery);
return _handleRosterResponse(responseQuery, rosterVersion);
}
/// Requests a series of roster pushes according to RFC6121. Requires that the server
@@ -266,6 +284,7 @@ class RosterManager extends XmppManagerBase {
Future<Result<RosterRequestResult?, RosterError>>
requestRosterPushes() async {
final attrs = getAttributes();
final rosterVersion = await _stateManager.getRosterVersion();
final result = (await attrs.sendStanza(
StanzaDetails(
Stanza.iq(
@@ -275,9 +294,9 @@ class RosterManager extends XmppManagerBase {
tag: 'query',
xmlns: rosterXmlns,
attributes: {
'ver': await _stateManager.getRosterVersion() ?? '',
'ver': rosterVersion ?? '',
},
)
),
],
),
),
@@ -289,7 +308,7 @@ class RosterManager extends XmppManagerBase {
}
final query = result.firstTag('query', xmlns: rosterXmlns);
return _handleRosterResponse(query);
return _handleRosterResponse(query, rosterVersion);
}
bool rosterVersioningAvailable() {
@@ -319,14 +338,12 @@ class RosterManager extends XmppManagerBase {
tag: 'item',
attributes: <String, String>{
'jid': jid,
...title == jid.split('@')[0]
? <String, String>{}
: <String, String>{'name': title}
if (title == jid.split('@')[0]) 'name': title,
},
children: (groups ?? [])
.map((group) => XMLNode(tag: 'group', text: group))
.toList(),
)
),
],
),
],
@@ -357,13 +374,13 @@ class RosterManager extends XmppManagerBase {
children: [
XMLNode(
tag: 'item',
attributes: <String, String>{
attributes: {
'jid': jid,
'subscription': 'remove'
'subscription': 'remove',
},
)
),
],
)
),
],
),
),

View File

@@ -1,30 +1,44 @@
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// A description of a stanza to send.
class StanzaDetails {
const StanzaDetails(
this.stanza, {
this.extensions,
this.addId = true,
this.awaitable = true,
this.shouldEncrypt = true,
this.encrypted = false,
this.forceEncryption = false,
this.bypassQueue = false,
this.excludeFromStreamManagement = false,
this.postSendExtensions,
});
/// The stanza to send.
final Stanza stanza;
/// The extension data used for constructing the stanza.
final TypedMap<StanzaHandlerExtension>? extensions;
/// Flag indicating whether a stanza id should be added before sending.
final bool addId;
/// Track the stanza to allow awaiting its response.
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 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
/// now, where it's okay if it does not get sent.
@@ -34,31 +48,62 @@ class StanzaDetails {
/// This makes the Stream Management implementation, when available, ignore the stanza,
/// meaning that it gets counted but excluded from resending.
/// 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
class StanzaError {
StanzaError(this.type, this.error);
String type;
String error;
/// A general error type for errors.
abstract class StanzaError {
static StanzaError? fromXMLNode(XMLNode node) {
final error = node.firstTag('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) {
final error = stanza.firstTag('error');
if (error == null) return null;
final stanzaError = error.firstTagByXmlns(fullStanzaXmlns);
if (stanzaError == null) return null;
return StanzaError(
error.attributes['type']! as String,
stanzaError.tag,
);
return fromXMLNode(stanza);
}
}
/// 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 {}
const _stanzaNotDefined = Object();
class Stanza extends XMLNode {
// ignore: use_super_parameters
Stanza({
@@ -173,7 +218,7 @@ class Stanza extends XMLNode {
Stanza copyWith({
String? id,
String? from,
Object? from = _stanzaNotDefined,
String? to,
String? type,
List<XMLNode>? children,
@@ -182,7 +227,7 @@ class Stanza extends XMLNode {
return Stanza(
tag: tag,
to: to ?? this.to,
from: from ?? this.from,
from: from != _stanzaNotDefined ? from as String? : this.from,
id: id ?? this.id,
type: type ?? this.type,
children: children ?? this.children,
@@ -205,15 +250,14 @@ XMLNode buildErrorElement(String type, String condition, {String? text}) {
XMLNode.xmlns(
tag: condition,
xmlns: fullStanzaXmlns,
children: text != null
? [
children: [
if (text != null)
XMLNode.xmlns(
tag: 'text',
xmlns: fullStanzaXmlns,
text: text,
)
]
: [],
),
],
),
],
);

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

@@ -0,0 +1,97 @@
import 'dart:async';
import 'dart:collection';
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/awaiter.dart';
import 'package:moxxmpp/src/parser.dart';
import 'package:synchronized/synchronized.dart';
/// A queue for incoming [XMPPStreamObject]s to ensure "in order"
/// processing (except for stanzas that are awaited).
class IncomingStanzaQueue {
IncomingStanzaQueue(this._callback, this._stanzaAwaiter);
/// The queue for storing the completer of each
/// incoming stanza (or stream object to be precise).
/// Only access while holding the lock [_lock].
final Queue<Completer<void>> _queue = Queue();
/// Flag indicating whether a callback is already running (true)
/// or not. "a callback" and not "the callback" because awaited stanzas
/// are allowed to bypass the queue.
/// Only access while holding the lock [_lock].
bool _isRunning = false;
/// The function to call to process an incoming stream object.
final Future<void> Function(XMPPStreamObject) _callback;
/// Lock guarding both [_queue] and [_isRunning].
final Lock _lock = Lock();
/// Logger.
final Logger _log = Logger('IncomingStanzaQueue');
final StanzaAwaiter _stanzaAwaiter;
Future<void> _processStreamObject(
Future<void>? future,
XMPPStreamObject object,
) async {
if (future == null) {
if (object is XMPPStreamElement) {
_log.finest(
'Bypassing queue for ${object.node.tag} (${object.node.attributes["id"]})',
);
}
return _callback(object);
}
// Wait for our turn.
await future;
// Run the callback.
await _callback(object);
// Run the next entry.
await _lock.synchronized(() {
if (_queue.isNotEmpty) {
_queue.removeFirst().complete();
} else {
_isRunning = false;
}
});
}
Future<void> addStanza(List<XMPPStreamObject> objects) async {
await _lock.synchronized(() async {
for (final object in objects) {
if (await canBypassQueue(object)) {
unawaited(
_processStreamObject(null, object),
);
continue;
}
final completer = Completer<void>();
if (_isRunning) {
_queue.add(completer);
} else {
_isRunning = true;
completer.complete();
}
unawaited(
_processStreamObject(completer.future, object),
);
}
});
}
Future<bool> canBypassQueue(XMPPStreamObject object) async {
if (object is XMPPStreamHeader) {
return false;
}
object as XMPPStreamElement;
return _stanzaAwaiter.isAwaited(object.node);
}
}

View File

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

View File

@@ -1,57 +0,0 @@
import 'package:moxxmpp/src/stringxml.dart';
/// NOTE: Specified by https://codeberg.org/moxxy/custom-xeps/src/branch/master/xep-xxxx-extensible-file-thumbnails.md
const fileThumbnailsXmlns = 'proto:urn:xmpp:eft:0';
const blurhashThumbnailType = '$fileThumbnailsXmlns:blurhash';
abstract class Thumbnail {}
class BlurhashThumbnail extends Thumbnail {
BlurhashThumbnail(this.hash);
final String hash;
}
Thumbnail? parseFileThumbnailElement(XMLNode node) {
assert(
node.attributes['xmlns'] == fileThumbnailsXmlns,
'Invalid element xmlns',
);
assert(node.tag == 'file-thumbnail', 'Invalid element name');
switch (node.attributes['type']!) {
case blurhashThumbnailType:
{
final hash = node.firstTag('blurhash')!.innerText();
return BlurhashThumbnail(hash);
}
}
return null;
}
XMLNode? _fromThumbnail(Thumbnail thumbnail) {
if (thumbnail is BlurhashThumbnail) {
return XMLNode(
tag: 'blurhash',
text: thumbnail.hash,
);
}
return null;
}
XMLNode constructFileThumbnailElement(Thumbnail thumbnail) {
final node = _fromThumbnail(thumbnail)!;
var type = '';
if (thumbnail is BlurhashThumbnail) {
type = blurhashThumbnailType;
}
return XMLNode.xmlns(
tag: 'file-thumbnail',
xmlns: fileThumbnailsXmlns,
attributes: {'type': type},
children: [node],
);
}

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/stringxml.dart';
@@ -10,14 +10,14 @@ class DataFormOption {
XMLNode toXml() {
return XMLNode(
tag: 'option',
attributes: label != null
? <String, dynamic>{'label': label}
: <String, dynamic>{},
attributes: {
if (label != null) 'label': label,
},
children: [
XMLNode(
tag: 'value',
text: value,
)
),
],
);
}
@@ -45,19 +45,22 @@ class DataFormField {
return XMLNode(
tag: 'field',
attributes: <String, dynamic>{
...varAttr != null
? <String, dynamic>{'var': varAttr}
: <String, dynamic>{},
...type != null ? <String, dynamic>{'type': type} : <String, dynamic>{},
...label != null
? <String, dynamic>{'label': label}
: <String, dynamic>{}
if (varAttr != null) 'var': varAttr,
if (type != null) 'type': type,
if (label != null) 'label': label,
},
children: [
...description != null ? [XMLNode(tag: 'desc', text: description)] : [],
...isRequired ? [XMLNode(tag: 'required')] : [],
if (description != null)
XMLNode(
tag: 'desc',
text: description,
),
if (isRequired)
XMLNode(
tag: 'required',
),
...values.map((value) => XMLNode(tag: 'value', text: value)),
...options.map((option) => option.toXml())
...options.map((option) => option.toXml()),
],
);
}
@@ -80,7 +83,7 @@ class DataForm {
final List<List<DataFormField>> items;
DataFormField? getFieldByVar(String varAttr) {
return firstWhereOrNull(fields, (field) => field.varAttr == varAttr);
return fields.firstWhereOrNull((field) => field.varAttr == varAttr);
}
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 {}
/// The received disco response is invalid in some shape or form.
class InvalidResponseDiscoError extends DiscoError {}
class ErrorResponseDiscoError extends DiscoError {}

View File

@@ -13,8 +13,10 @@ Stanza buildDiscoInfoQueryStanza(JID entity, String? node) {
XMLNode.xmlns(
tag: 'query',
xmlns: discoInfoXmlns,
attributes: node != null ? {'node': node} : {},
)
attributes: {
if (node != null) 'node': node,
},
),
],
);
}
@@ -27,8 +29,10 @@ Stanza buildDiscoItemsQueryStanza(JID entity, {String? node}) {
XMLNode.xmlns(
tag: 'query',
xmlns: discoItemsXmlns,
attributes: node != null ? {'node': node} : {},
)
attributes: {
if (node != null) 'node': node,
},
),
],
);
}

View File

@@ -19,13 +19,11 @@ class Identity {
XMLNode toXMLNode() {
return XMLNode(
tag: 'identity',
attributes: <String, dynamic>{
attributes: {
'category': category,
'type': type,
'name': name,
...lang == null
? <String, dynamic>{}
: <String, dynamic>{'xml:lang': lang}
if (lang != null) 'xml:lang': lang,
},
);
}

View File

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

View File

@@ -0,0 +1,25 @@
/// Represents an error related to Multi-User Chat (MUC).
abstract class MUCError {}
/// Error indicating an invalid (non-supported) stanza received while going
/// through normal operation/flow of an MUC.
class InvalidStanzaFormat extends MUCError {}
/// Represents an error indicating an abnormal condition while parsing
/// the DiscoInfo response stanza in Multi-User Chat (MUC).
class InvalidDiscoInfoResponse extends MUCError {}
/// Returned when no nickname was specified from the client side while trying to
/// perform some actions on the MUC, such as joining the room.
class NoNicknameSpecified extends MUCError {}
/// This error occurs when a user attempts to perform an action that requires
/// them to be a member of a room, but they are not currently joined to
/// that room.
class RoomNotJoinedError extends MUCError {}
/// Indicates that the MUC forbids us from joining, i.e. when we're banned.
class JoinForbiddenError extends MUCError {}
/// Indicates that an unspecific error occurred while joining.
class MUCUnspecificError extends MUCError {}

View File

@@ -0,0 +1,72 @@
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
/// Triggered when the MUC changes our nickname.
class OwnDataChangedEvent extends XmppEvent {
OwnDataChangedEvent(
this.roomJid,
this.nick,
this.affiliation,
this.role,
);
/// The JID of the room.
final JID roomJid;
/// Our nickname.
final String nick;
/// Our affiliation.
final Affiliation affiliation;
/// Our role.
final Role role;
}
/// Triggered when an entity joins the MUC.
class MemberJoinedEvent extends XmppEvent {
MemberJoinedEvent(this.roomJid, this.member);
/// The JID of the room.
final JID roomJid;
/// The new member.
final RoomMember member;
}
/// Triggered when an entity changes their presence in the MUC.
class MemberChangedEvent extends XmppEvent {
MemberChangedEvent(this.roomJid, this.member);
/// The JID of the room.
final JID roomJid;
/// The new member.
final RoomMember member;
}
/// Triggered when an entity leaves the MUC.
class MemberLeftEvent extends XmppEvent {
MemberLeftEvent(this.roomJid, this.nick);
/// The JID of the room.
final JID roomJid;
/// The nick of the user who left.
final String nick;
}
/// Triggered when an entity changes their nick.
class MemberChangedNickEvent extends XmppEvent {
MemberChangedNickEvent(this.roomJid, this.oldNick, this.newNick);
/// The JID of the room.
final JID roomJid;
/// The original nick.
final String oldNick;
/// The new nick.
final String newNick;
}

View File

@@ -0,0 +1,2 @@
const selfPresenceStatus = '110';
const nicknameChangedStatus = '303';

View File

@@ -0,0 +1,163 @@
import 'package:collection/collection.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/xeps/xep_0004.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
class InvalidAffiliationException implements Exception {}
class InvalidRoleException implements Exception {}
enum Affiliation {
owner('owner'),
admin('admin'),
member('member'),
outcast('outcast'),
none('none');
const Affiliation(this.value);
factory Affiliation.fromString(String value) {
switch (value) {
case 'owner':
return Affiliation.owner;
case 'admin':
return Affiliation.admin;
case 'member':
return Affiliation.member;
case 'outcast':
return Affiliation.outcast;
case 'none':
return Affiliation.none;
default:
throw InvalidAffiliationException();
}
}
/// The value to use for an attribute referring to this affiliation.
final String value;
}
enum Role {
moderator('moderator'),
participant('participant'),
visitor('visitor'),
none('none');
const Role(this.value);
factory Role.fromString(String value) {
switch (value) {
case 'moderator':
return Role.moderator;
case 'participant':
return Role.participant;
case 'visitor':
return Role.visitor;
case 'none':
return Role.none;
default:
throw InvalidRoleException();
}
}
/// The value to use for an attribute referring to this role.
final String value;
}
class RoomInformation {
/// Represents information about a Multi-User Chat (MUC) room.
RoomInformation({
required this.jid,
required this.features,
required this.name,
this.roomInfo,
});
/// Constructs a [RoomInformation] object from a [DiscoInfo] object.
/// The [DiscoInfo] object contains the necessary information to populate
/// the [RoomInformation] fields.
factory RoomInformation.fromDiscoInfo({
required DiscoInfo discoInfo,
}) =>
RoomInformation(
jid: discoInfo.jid!,
features: discoInfo.features,
name: discoInfo.identities
.firstWhere((i) => i.category == 'conference')
.name!,
roomInfo: discoInfo.extendedInfo.firstWhereOrNull((form) {
final field = form.getFieldByVar(formVarFormType);
return field?.type == 'hidden' &&
field?.values.first == roomInfoFormType;
}),
);
/// The JID of the Multi-User Chat (MUC) room.
final JID jid;
/// A list of features supported by the Multi-User Chat (MUC) room.
final List<String> features;
/// The name or title of the Multi-User Chat (MUC) room.
final String name;
/// The data form containing room information.
final DataForm? roomInfo;
}
/// The used message-id and an optional origin-id.
typedef PendingMessage = (String, String?);
/// An entity inside a MUC room. The name "member" here does not refer to an affiliation of member.
class RoomMember {
const RoomMember(this.nick, this.affiliation, this.role);
/// The entity's nickname.
final String nick;
/// The assigned affiliation.
final Affiliation affiliation;
/// The assigned role.
final Role role;
RoomMember copyWith({
String? nick,
Affiliation? affiliation,
Role? role,
}) {
return RoomMember(
nick ?? this.nick,
affiliation ?? this.affiliation,
role ?? this.role,
);
}
}
class RoomState {
RoomState({required this.roomJid, this.nick, required this.joined}) {
pendingMessages = List<PendingMessage>.empty(growable: true);
}
/// The JID of the room.
final JID roomJid;
/// The nick we're joined with.
String? nick;
/// Flag whether we're joined and can process messages
bool joined;
/// Our own affiliation inside the MUC.
Affiliation? affiliation;
/// Our own role inside the MUC.
Role? role;
/// The list of messages that we sent and are waiting for their echo.
late final List<PendingMessage> pendingMessages;
/// "List" of entities inside the MUC.
final Map<String, RoomMember> members = {};
}

View File

@@ -0,0 +1,512 @@
import 'dart:async';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/presence.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0045/events.dart';
import 'package:moxxmpp/src/xeps/xep_0045/status_codes.dart';
import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:synchronized/synchronized.dart';
/// (Room JID, nickname)
typedef MUCRoomJoin = (JID, String);
class MUCManager extends XmppManagerBase {
MUCManager() : super(mucManager);
@override
Future<bool> isSupported() async => true;
/// Map a room's JID to its RoomState
final Map<JID, RoomState> _mucRoomCache = {};
/// Mapp a room's JID to a completer waiting for the completion of the join process.
final Map<JID, Completer<Result<bool, MUCError>>> _mucRoomJoinCompleter = {};
/// Cache lock
final Lock _cacheLock = Lock();
/// Flag indicating whether we joined the rooms added to the room list with
/// [prepareRoomList].
bool _joinedPreparedRooms = true;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
callback: _onMessage,
// Before the message handler
priority: -99,
),
StanzaHandler(
stanzaTag: 'presence',
callback: _onPresence,
tagName: 'x',
tagXmlns: mucUserXmlns,
// Before the PresenceManager
priority: PresenceManager.presenceHandlerPriority + 1,
),
];
@override
List<StanzaHandler> getOutgoingPreStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
callback: _onMessageSent,
),
];
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is! StreamNegotiationsDoneEvent) {
return;
}
// Only attempt rejoining if we did not resume the stream and all
// prepared rooms are already joined.
if (event.resumed && _joinedPreparedRooms) {
return;
}
final mucJoins = List<MUCRoomJoin>.empty(growable: true);
await _cacheLock.synchronized(() async {
// Mark all groupchats as not joined.
for (final jid in _mucRoomCache.keys) {
_mucRoomCache[jid]!.joined = false;
_mucRoomJoinCompleter[jid] = Completer();
// Re-join all MUCs.
final state = _mucRoomCache[jid]!;
mucJoins.add((jid, state.nick!));
}
});
for (final join in mucJoins) {
final (jid, nick) = join;
await _sendMucJoin(
jid,
nick,
0,
);
}
_joinedPreparedRooms = true;
}
/// Prepares the internal room list to ensure that the rooms
/// [rooms] are joined once we are connected.
Future<void> prepareRoomList(List<MUCRoomJoin> rooms) async {
assert(
rooms.isNotEmpty,
'The room list should not be empty',
);
await _cacheLock.synchronized(() {
_joinedPreparedRooms = false;
for (final room in rooms) {
final (roomJid, nick) = room;
_mucRoomCache[roomJid] = RoomState(
roomJid: roomJid,
nick: nick,
joined: false,
);
}
});
}
/// Queries the information of a Multi-User Chat room.
///
/// Retrieves the information about the specified MUC room by performing a
/// disco info query. Returns a [Result] with the [RoomInformation] on success
/// or an appropriate [MUCError] on failure.
Future<Result<RoomInformation, MUCError>> queryRoomInformation(
JID roomJID,
) async {
final result = await getAttributes()
.getManagerById<DiscoManager>(discoManager)!
.discoInfoQuery(roomJID);
if (result.isType<StanzaError>()) {
return Result(InvalidStanzaFormat());
}
try {
final roomInformation = RoomInformation.fromDiscoInfo(
discoInfo: result.get<DiscoInfo>(),
);
return Result(roomInformation);
} catch (e) {
logger.warning('Invalid disco information: $e');
return Result(InvalidDiscoInfoResponse());
}
}
/// Joins a Multi-User Chat room.
///
/// Joins the specified MUC room using the provided nickname. Sends a presence
/// stanza with the appropriate attributes to join the room. Returns a [Result]
/// with a boolean value indicating success or failure, or an [MUCError]
/// if applicable.
Future<Result<bool, MUCError>> joinRoom(
JID roomJid,
String nick, {
int? maxHistoryStanzas,
}) async {
if (nick.isEmpty) {
return Result(NoNicknameSpecified());
}
final completer =
await _cacheLock.synchronized<Completer<Result<bool, MUCError>>>(
() {
_mucRoomCache[roomJid] = RoomState(
roomJid: roomJid,
nick: nick,
joined: false,
);
final completer = Completer<Result<bool, MUCError>>();
_mucRoomJoinCompleter[roomJid] = completer;
return completer;
},
);
await _sendMucJoin(roomJid, nick, maxHistoryStanzas);
return completer.future;
}
Future<void> _sendMucJoin(
JID roomJid,
String nick,
int? maxHistoryStanzas,
) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
to: roomJid.withResource(nick).toString(),
children: [
XMLNode.xmlns(
tag: 'x',
xmlns: mucXmlns,
children: [
if (maxHistoryStanzas != null)
XMLNode(
tag: 'history',
attributes: {
'maxstanzas': maxHistoryStanzas.toString(),
},
),
],
),
],
),
awaitable: false,
),
);
}
/// Leaves a Multi-User Chat room.
///
/// Leaves the specified MUC room by sending an 'unavailable' presence stanza.
/// Removes the corresponding room entry from the cache. Returns a [Result]
/// with a boolean value indicating success or failure, or an [MUCError]
/// if applicable.
Future<Result<bool, MUCError>> leaveRoom(
JID roomJid,
) async {
final nick = await _cacheLock.synchronized(() {
final nick = _mucRoomCache[roomJid]?.nick;
_mucRoomCache.remove(roomJid);
return nick;
});
if (nick == null) {
return Result(RoomNotJoinedError());
}
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
to: roomJid.withResource(nick).toString(),
type: 'unavailable',
),
awaitable: false,
),
);
return const Result(true);
}
Future<RoomState?> getRoomState(JID roomJid) async {
return _cacheLock.synchronized(() => _mucRoomCache[roomJid]);
}
Future<StanzaHandlerData> _onPresence(
Stanza presence,
StanzaHandlerData state,
) async {
if (presence.from == null) {
logger.finest('Ignoring presence as it has no from attribute');
return state;
}
final from = JID.fromString(presence.from!);
final bareFrom = from.toBare();
return _cacheLock.synchronized(() {
logger.finest('Lock aquired for presence from ${presence.from}');
final room = _mucRoomCache[bareFrom];
if (room == null) {
logger.finest('Ignoring presence as it does not belong to a room');
return state;
}
if (from.resource.isEmpty) {
// TODO(Unknown): Handle presence from the room itself.
logger.finest('Ignoring presence as it has no resource');
return state;
}
if (presence.type == 'error') {
final errorTag = presence.firstTag('error')!;
final error = errorTag.firstTagByXmlns(fullStanzaXmlns)!;
Result<bool, MUCError> result;
if (error.tag == 'forbidden') {
result = Result(JoinForbiddenError());
} else {
result = Result(MUCUnspecificError());
}
_mucRoomCache.remove(bareFrom);
_mucRoomJoinCompleter[bareFrom]!.complete(result);
_mucRoomJoinCompleter.remove(bareFrom);
return StanzaHandlerData(
true,
false,
presence,
state.extensions,
);
}
final x = presence.firstTag('x', xmlns: mucUserXmlns)!;
final item = x.firstTag('item')!;
final statuses = x
.findTags('status')
.map((s) => s.attributes['code']! as String)
.toList();
final role = Role.fromString(
item.attributes['role']! as String,
);
final affiliation = Affiliation.fromString(
item.attributes['affiliation']! as String,
);
if (statuses.contains(selfPresenceStatus)) {
if (room.joined) {
if (room.nick != from.resource ||
room.affiliation != affiliation ||
room.role != role) {
// Notify us of the changed data.
getAttributes().sendEvent(
OwnDataChangedEvent(
bareFrom,
from.resource,
affiliation,
role,
),
);
}
}
// Set the data to make sure we're in sync with the MUC.
room
..nick = from.resource
..affiliation = affiliation
..role = role;
logger.finest('Self-presence handled');
return StanzaHandlerData(
true,
false,
presence,
state.extensions,
);
}
if (presence.attributes['type'] == 'unavailable') {
if (role == Role.none) {
// Cannot happen while joining, so we assume we are joined
assert(
room.joined,
'Should not receive unavailable with role="none" while joining',
);
room.members.remove(from.resource);
getAttributes().sendEvent(
MemberLeftEvent(
bareFrom,
from.resource,
),
);
} else if (statuses.contains(nicknameChangedStatus)) {
assert(
room.joined,
'Should not receive nick change while joining',
);
final newNick = item.attributes['nick']! as String;
final member = RoomMember(
newNick,
Affiliation.fromString(
item.attributes['affiliation']! as String,
),
role,
);
// Remove the old member.
room.members.remove(from.resource);
// Add the "new" member".
room.members[newNick] = member;
// Trigger an event.
getAttributes().sendEvent(
MemberChangedNickEvent(
bareFrom,
from.resource,
newNick,
),
);
}
} else {
final member = RoomMember(
from.resource,
Affiliation.fromString(
item.attributes['affiliation']! as String,
),
role,
);
logger.finest('Got presence from ${from.resource} in $bareFrom');
if (room.joined) {
if (room.members.containsKey(from.resource)) {
getAttributes().sendEvent(
MemberChangedEvent(
bareFrom,
member,
),
);
} else {
getAttributes().sendEvent(
MemberJoinedEvent(
bareFrom,
member,
),
);
}
}
room.members[from.resource] = member;
logger.finest('${from.resource} added to the member list');
}
logger.finest('Ran through');
return StanzaHandlerData(
true,
false,
presence,
state.extensions,
);
});
}
Future<StanzaHandlerData> _onMessageSent(
Stanza message,
StanzaHandlerData state,
) async {
if (message.to == null) {
return state;
}
final toJid = JID.fromString(message.to!);
return _cacheLock.synchronized(() {
if (!_mucRoomCache.containsKey(toJid)) {
return state;
}
_mucRoomCache[toJid]!.pendingMessages.add(
(message.id!, state.extensions.get<StableIdData>()?.originId),
);
return state;
});
}
Future<StanzaHandlerData> _onMessage(
Stanza message,
StanzaHandlerData state,
) async {
final fromJid = JID.fromString(message.from!);
final roomJid = fromJid.toBare();
return _cacheLock.synchronized(() {
logger.finest('Lock aquired for message from ${message.from}');
final roomState = _mucRoomCache[roomJid];
if (roomState == null) {
return state;
}
if (message.type == 'groupchat' && message.firstTag('subject') != null) {
// The room subject marks the end of the join flow.
if (!roomState.joined) {
// Mark the room as joined.
_mucRoomCache[roomJid]!.joined = true;
_mucRoomJoinCompleter[roomJid]!.complete(
const Result(true),
);
_mucRoomJoinCompleter.remove(roomJid);
logger.finest('$roomJid is now joined');
}
// TODO(Unknown): Signal the subject?
return StanzaHandlerData(
true,
false,
message,
state.extensions,
);
} else {
if (!roomState.joined) {
// Ignore the discussion history.
return StanzaHandlerData(
true,
false,
message,
state.extensions,
);
}
// Check if this is the message reflection.
if (message.id == null) {
return state;
}
final pending =
(message.id!, state.extensions.get<StableIdData>()?.originId);
if (fromJid.resource == roomState.nick &&
roomState.pendingMessages.contains(pending)) {
// Silently drop the message.
roomState.pendingMessages.remove(pending);
// TODO(Unknown): Maybe send an event stating that we received the reflection.
return StanzaHandlerData(
true,
false,
message,
state.extensions,
);
}
}
return state;
});
}
}

View File

@@ -1,3 +1,4 @@
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.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/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
abstract class VCardError {}
@@ -38,7 +38,7 @@ class VCardManager extends XmppManagerBase {
tagName: 'x',
tagXmlns: vCardTempUpdate,
callback: _onPresence,
)
),
];
@override
@@ -56,27 +56,13 @@ class VCardManager extends XmppManagerBase {
final x = presence.firstTag('x', xmlns: vCardTempUpdate)!;
final hash = x.firstTag('photo')!.innerText();
final from = JID.fromString(presence.from!).toBare();
final lastHash = _lastHash[from];
if (lastHash != hash) {
_lastHash[from.toString()] = hash;
final vcardResult = await requestVCard(from);
if (vcardResult.isType<VCard>()) {
final binval = vcardResult.get<VCard>().photo?.binval;
if (binval != null) {
getAttributes().sendEvent(
VCardAvatarUpdatedEvent(from, binval, hash),
VCardAvatarUpdatedEvent(
JID.fromString(presence.from!),
hash,
),
);
} else {
logger.warning('No avatar data found');
}
} else {
logger.warning('Failed to retrieve vCard for $from');
}
}
return state..done = true;
return state;
}
VCardPhoto? _parseVCardPhoto(XMLNode? node) {
@@ -108,7 +94,7 @@ class VCardManager extends XmppManagerBase {
XMLNode.xmlns(
tag: 'vCard',
xmlns: vCardTempXmlns,
)
),
],
),
encrypted: true,

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart';
@@ -9,7 +9,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0004.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
@@ -39,26 +38,20 @@ class PubSubPublishOptions {
varAttr: 'FORM_TYPE',
type: 'hidden',
),
...accessModel != null
? [
if (accessModel != null)
DataFormField(
options: [],
isRequired: false,
values: [accessModel!],
varAttr: 'pubsub#access_model',
)
]
: [],
...maxItems != null
? [
),
if (maxItems != null)
DataFormField(
options: [],
isRequired: false,
values: [maxItems!],
varAttr: 'pubsub#max_items',
),
]
: [],
],
).toXml();
}
@@ -88,7 +81,7 @@ class PubSubManager extends XmppManagerBase {
tagName: 'event',
tagXmlns: pubsubEventXmlns,
callback: _onPubsubMessage,
)
),
];
@override
@@ -202,6 +195,7 @@ class PubSubManager extends XmppManagerBase {
),
],
),
shouldEncrypt: false,
),
))!;
@@ -245,6 +239,7 @@ class PubSubManager extends XmppManagerBase {
),
],
),
shouldEncrypt: false,
),
))!;
@@ -313,11 +308,11 @@ class PubSubManager extends XmppManagerBase {
children: [
XMLNode(
tag: 'item',
attributes: id != null
? <String, String>{'id': id}
: <String, String>{},
attributes: {
if (id != null) 'id': id,
},
children: [payload],
)
),
],
),
if (pubOptions != null)
@@ -326,9 +321,10 @@ class PubSubManager extends XmppManagerBase {
children: [pubOptions.toXml()],
),
],
)
),
],
),
shouldEncrypt: false,
),
))!;
if (result.attributes['type'] != 'result') {
@@ -399,8 +395,9 @@ class PubSubManager extends XmppManagerBase {
Future<Result<PubSubError, List<PubSubItem>>> getItems(
JID jid,
String node,
) async {
String node, {
int? maxItems,
}) async {
final result = (await getAttributes().sendStanza(
StanzaDetails(
Stanza.iq(
@@ -413,12 +410,16 @@ class PubSubManager extends XmppManagerBase {
children: [
XMLNode(
tag: 'items',
attributes: <String, String>{'node': node},
attributes: {
'node': node,
if (maxItems != null) 'max_items': maxItems.toString(),
},
),
],
)
),
],
),
shouldEncrypt: false,
),
))!;
@@ -443,7 +444,7 @@ class PubSubManager extends XmppManagerBase {
}
Future<Result<PubSubError, PubSubItem>> getItem(
String jid,
JID jid,
String node,
String id,
) async {
@@ -451,7 +452,7 @@ class PubSubManager extends XmppManagerBase {
StanzaDetails(
Stanza.iq(
type: 'get',
to: jid,
to: jid.toString(),
children: [
XMLNode.xmlns(
tag: 'pubsub',
@@ -471,6 +472,7 @@ class PubSubManager extends XmppManagerBase {
),
],
),
shouldEncrypt: false,
),
))!;
@@ -521,6 +523,7 @@ class PubSubManager extends XmppManagerBase {
),
],
),
shouldEncrypt: false,
),
))!;
if (form.attributes['type'] != 'result') {
@@ -550,6 +553,7 @@ class PubSubManager extends XmppManagerBase {
),
],
),
shouldEncrypt: false,
),
))!;
if (submit.attributes['type'] != 'result') {
@@ -580,6 +584,7 @@ class PubSubManager extends XmppManagerBase {
),
],
),
shouldEncrypt: false,
),
))!;
@@ -624,6 +629,7 @@ class PubSubManager extends XmppManagerBase {
),
],
),
shouldEncrypt: false,
),
))!;

View File

@@ -45,7 +45,7 @@ class OOBManager extends XmppManagerBase {
callback: _onMessage,
// Before the message manager
priority: -99,
)
),
];
@override

View File

@@ -1,14 +1,11 @@
import 'dart:convert';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/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/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
@@ -43,11 +40,7 @@ class UserAvatarMetadata {
);
factory UserAvatarMetadata.fromXML(XMLNode node) {
assert(
node.tag == 'metadata' &&
node.attributes['xmlns'] == userAvatarMetadataXmlns,
'<metadata /> element required',
);
assert(node.tag == 'info', 'node must be an <info /> element');
final width = node.attributes['width'] as String?;
final height = node.attributes['height'] as String?;
@@ -121,20 +114,44 @@ class UserAvatarManager extends XmppManagerBase {
/// Requests the avatar from [jid]. Returns the avatar data if the request was
/// successful. Null otherwise
Future<Result<AvatarError, UserAvatarData>> getUserAvatar(JID jid) async {
Future<Result<AvatarError, UserAvatarData>> getUserAvatarData(
JID jid,
String id,
) async {
final pubsub = _getPubSubManager();
final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns);
final resultRaw = await pubsub.getItem(jid, userAvatarDataXmlns, id);
if (resultRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
final result = resultRaw.get<PubSubItem>();
return Result(
UserAvatarData(
result.payload.innerText(),
id,
),
);
}
/// Attempts to fetch the latest item from the User Avatar metadata node. Returns the list of
/// metadata contained within it. The list may be empty.
///
/// If an error occured, returns an [AvatarError].
Future<Result<AvatarError, List<UserAvatarMetadata>>> getLatestMetadata(
JID jid,
) async {
final resultsRaw = await _getPubSubManager()
.getItems(jid, userAvatarMetadataXmlns, maxItems: 1);
if (resultsRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
final results = resultsRaw.get<List<PubSubItem>>();
if (results.isEmpty) return Result(UnknownAvatarError());
if (results.isEmpty) {
return Result(UnknownAvatarError());
}
final item = results[0];
return Result(
UserAvatarData(
item.payload.innerText(),
item.id,
),
results.first.payload
.findTags('info')
.map(UserAvatarMetadata.fromXML)
.toList(),
);
}
@@ -216,22 +233,4 @@ class UserAvatarManager extends XmppManagerBase {
return const Result(true);
}
/// Returns the PubSub Id of an avatar after doing a disco#items query.
/// Note that this assumes that there is only one (1) item published on
/// the node.
Future<Result<AvatarError, String>> getAvatarId(JID jid) async {
final disco = getAttributes().getManagerById(discoManager)! as DiscoManager;
final response = await disco.discoItemsQuery(
jid,
node: userAvatarDataXmlns,
shouldEncrypt: false,
);
if (response.isType<DiscoError>()) return Result(UnknownAvatarError());
final items = response.get<List<DiscoItem>>();
if (items.isEmpty) return Result(UnknownAvatarError());
return Result(items.first.name);
}
}

View File

@@ -69,7 +69,7 @@ class ChatStateManager extends XmppManagerBase {
callback: _onChatStateReceived,
// Before the message handler
priority: -99,
)
),
];
@override

View File

@@ -15,7 +15,6 @@ import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/list.dart';
import 'package:moxxmpp/src/xeps/xep_0004.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/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0300.dart';
@@ -174,39 +173,20 @@ class EntityCapabilitiesManager extends XmppManagerBase {
});
}
@visibleForTesting
Future<StanzaHandlerData> onPresence(
Stanza stanza,
StanzaHandlerData state,
Future<void> _performQuery(
Stanza presence,
String ver,
String hashFunctionName,
String capabilityNode,
JID from,
) async {
if (stanza.from == null) {
return state;
}
final from = JID.fromString(stanza.from!);
final c = stanza.firstTag('c', xmlns: capsXmlns)!;
final hashFunctionName = c.attributes['hash'] as String?;
final capabilityNode = c.attributes['node'] as String?;
final ver = c.attributes['ver'] as String?;
if (hashFunctionName == null || capabilityNode == null || ver == null) {
return state;
}
// Check if we know of the hash
final isCached =
await _cacheLock.synchronized(() => _capHashCache.containsKey(ver));
if (isCached) {
return state;
}
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
final discoRequest = await dm.discoInfoQuery(
from,
node: capabilityNode,
);
if (discoRequest.isType<DiscoError>()) {
return state;
if (discoRequest.isType<StanzaError>()) {
return;
}
final discoInfo = discoRequest.get<DiscoInfo>();
@@ -221,7 +201,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
discoInfo,
),
);
return state;
return;
}
// Validate the disco#info result according to XEP-0115 § 5.4
@@ -235,7 +215,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning(
'Malformed disco#info response: More than one equal identity',
);
return state;
return;
}
}
@@ -246,7 +226,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning(
'Malformed disco#info response: More than one equal feature',
);
return state;
return;
}
}
@@ -274,7 +254,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning(
'Malformed disco#info response: Extended Info FORM_TYPE contains more than one value(s) of different value.',
);
return state;
return;
}
}
@@ -289,7 +269,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
logger.warning(
'Malformed disco#info response: More than one Extended Disco Info forms with the same FORM_TYPE value',
);
return state;
return;
}
// Check if the field type is hidden
@@ -326,7 +306,43 @@ class EntityCapabilitiesManager extends XmppManagerBase {
'Capability hash mismatch from $from: Received "$ver", expected "$computedCapabilityHash".',
);
}
}
@visibleForTesting
Future<StanzaHandlerData> onPresence(
Stanza stanza,
StanzaHandlerData state,
) async {
if (stanza.from == null) {
return state;
}
final from = JID.fromString(stanza.from!);
final c = stanza.firstTag('c', xmlns: capsXmlns)!;
final hashFunctionName = c.attributes['hash'] as String?;
final capabilityNode = c.attributes['node'] as String?;
final ver = c.attributes['ver'] as String?;
if (hashFunctionName == null || capabilityNode == null || ver == null) {
return state;
}
// Check if we know of the hash
final isCached =
await _cacheLock.synchronized(() => _capHashCache.containsKey(ver));
if (isCached) {
return state;
}
unawaited(
_performQuery(
stanza,
ver,
hashFunctionName,
capabilityNode,
from,
),
);
return state;
}

View File

@@ -66,7 +66,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
callback: _onDeliveryRequestReceived,
// Before the message handler
priority: -99,
)
),
];
@override

View File

@@ -27,7 +27,7 @@ class BlockingManager extends XmppManagerBase {
tagName: 'block',
tagXmlns: blockingXmlns,
callback: _blockPush,
)
),
];
@override
@@ -107,10 +107,12 @@ class BlockingManager extends XmppManagerBase {
children: items.map((item) {
return XMLNode(
tag: 'item',
attributes: <String, String>{'jid': item},
attributes: {
'jid': item,
},
);
}).toList(),
)
),
],
),
),
@@ -128,7 +130,7 @@ class BlockingManager extends XmppManagerBase {
XMLNode.xmlns(
tag: 'unblock',
xmlns: blockingXmlns,
)
),
],
),
),
@@ -152,11 +154,13 @@ class BlockingManager extends XmppManagerBase {
.map(
(item) => XMLNode(
tag: 'item',
attributes: <String, String>{'jid': item},
attributes: {
'jid': item,
},
),
)
.toList(),
)
),
],
),
),
@@ -174,7 +178,7 @@ class BlockingManager extends XmppManagerBase {
XMLNode.xmlns(
tag: 'blocklist',
xmlns: blockingXmlns,
)
),
],
),
),

View File

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

View File

@@ -5,7 +5,10 @@ class StreamManagementEnableNonza extends XMLNode {
StreamManagementEnableNonza()
: super(
tag: 'enable',
attributes: <String, String>{'xmlns': smXmlns, 'resume': 'true'},
attributes: {
'xmlns': smXmlns,
'resume': 'true',
},
);
}
@@ -13,10 +16,10 @@ class StreamManagementResumeNonza extends XMLNode {
StreamManagementResumeNonza(String id, int h)
: super(
tag: 'resume',
attributes: <String, String>{
attributes: {
'xmlns': smXmlns,
'previd': id,
'h': h.toString()
'h': h.toString(),
},
);
}
@@ -25,7 +28,10 @@ class StreamManagementAckNonza extends XMLNode {
StreamManagementAckNonza(int h)
: super(
tag: 'a',
attributes: <String, String>{'xmlns': smXmlns, 'h': h.toString()},
attributes: {
'xmlns': smXmlns,
'h': h.toString(),
},
);
}
@@ -33,7 +39,7 @@ class StreamManagementRequestNonza extends XMLNode {
StreamManagementRequestNonza()
: super(
tag: 'r',
attributes: <String, String>{
attributes: {
'xmlns': smXmlns,
},
);

View File

@@ -1,18 +1,43 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:meta/meta.dart';
part 'state.freezed.dart';
part 'state.g.dart';
const _smNotSpecified = Object();
@freezed
class StreamManagementState with _$StreamManagementState {
factory StreamManagementState(
int c2s,
int s2c, {
String? streamResumptionLocation,
String? streamResumptionId,
}) = _StreamManagementState;
@immutable
class StreamManagementState {
const StreamManagementState(
this.c2s,
this.s2c, {
this.streamResumptionLocation,
this.streamResumptionId,
});
// JSON
factory StreamManagementState.fromJson(Map<String, dynamic> json) =>
_$StreamManagementStateFromJson(json);
/// The counter of stanzas sent from the client to the server.
final int c2s;
/// 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/stanza.dart';
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.
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/stanza.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/negotiator.dart';
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
@@ -28,10 +29,10 @@ class StreamManagementManager extends XmppManagerBase {
}) : super(smManager);
/// The queue of stanzas that are not (yet) acked
final Map<int, Stanza> _unackedStanzas = {};
final Map<int, SMQueueEntry> _unackedStanzas = {};
/// Commitable state of the StreamManagementManager
StreamManagementState _state = StreamManagementState(0, 0);
StreamManagementState _state = const StreamManagementState(0, 0);
/// Mutex lock for _state
final Lock _stateLock = Lock();
@@ -61,7 +62,7 @@ class StreamManagementManager extends XmppManagerBase {
/// Functions for testing
@visibleForTesting
Map<int, Stanza> getUnackedStanzas() => _unackedStanzas;
Map<int, SMQueueEntry> getUnackedStanzas() => _unackedStanzas;
@visibleForTesting
Future<int> getPendingAcks() async {
@@ -74,6 +75,17 @@ class StreamManagementManager extends XmppManagerBase {
return acks;
}
@override
Future<void> onData() async {
// The ack timer does not matter if we are currently in the middle of receiving
// data.
await _ackLock.synchronized(() {
if (_pendingAcks > 0) {
_resetAckTimer();
}
});
}
/// Called when a stanza has been acked to decide whether we should trigger a
/// StanzaAckedEvent.
///
@@ -139,7 +151,7 @@ class StreamManagementManager extends XmppManagerBase {
nonzaTag: 'a',
nonzaXmlns: smXmlns,
callback: _handleAckResponse,
)
),
];
@override
@@ -147,14 +159,14 @@ class StreamManagementManager extends XmppManagerBase {
StanzaHandler(
callback: _onServerStanzaReceived,
priority: 9999,
)
),
];
@override
List<StanzaHandler> getOutgoingPostStanzaHandlers() => [
StanzaHandler(
callback: _onClientStanzaSent,
)
),
];
@override
@@ -224,6 +236,12 @@ class StreamManagementManager extends XmppManagerBase {
_ackTimer = null;
}
/// Resets the ack timer.
void _resetAckTimer() {
_stopAckTimer();
_startAckTimer();
}
@visibleForTesting
Future<void> handleAckTimeout() async {
_stopAckTimer();
@@ -306,12 +324,15 @@ class StreamManagementManager extends XmppManagerBase {
if (_pendingAcks > 0) {
// Prevent diff from becoming negative
final diff = max(_state.c2s - h, 0);
logger.finest(
'Setting _pendingAcks to $diff (was $_pendingAcks before): max(${_state.c2s} - $h, 0)',
);
_pendingAcks = diff;
// Reset the timer
if (_pendingAcks > 0) {
_stopAckTimer();
_startAckTimer();
_resetAckTimer();
}
}
@@ -336,15 +357,18 @@ class StreamManagementManager extends XmppManagerBase {
final attrs = getAttributes();
final sequences = _unackedStanzas.keys.toList()..sort();
for (final height in sequences) {
logger.finest('Unacked stanza: height $height, h $h');
// Do nothing if the ack does not concern this stanza
if (height > h) continue;
final stanza = _unackedStanzas[height]!;
logger.finest('Removing stanza with height $height');
final entry = _unackedStanzas[height]!;
_unackedStanzas.remove(height);
// Create a StanzaAckedEvent if the stanza is correct
if (shouldTriggerAckedEvent(stanza)) {
attrs.sendEvent(StanzaAckedEvent(stanza));
if (shouldTriggerAckedEvent(entry.stanza)) {
attrs.sendEvent(StanzaAckedEvent(entry.stanza));
}
}
@@ -401,13 +425,29 @@ class StreamManagementManager extends XmppManagerBase {
StanzaHandlerData state,
) async {
if (isStreamManagementEnabled()) {
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;
}
_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();
}
@@ -415,16 +455,23 @@ class StreamManagementManager extends XmppManagerBase {
}
Future<void> _resendStanzas() async {
final stanzas = _unackedStanzas.values.toList();
_unackedStanzas.clear();
for (final stanza in stanzas) {
logger
.finest('Resending ${stanza.tag} with id ${stanza.attributes["id"]}');
final queueCopy = _unackedStanzas.entries.toList();
for (final entry in queueCopy) {
logger.finest(
'Resending ${entry.value.stanza.tag} with id ${entry.value.stanza.attributes["id"]}',
);
await getAttributes().sendStanza(
StanzaDetails(
stanza,
entry.value.stanza,
postSendExtensions: TypedMap<StanzaHandlerExtension>.fromList([
StreamManagementData(
false,
entry.key,
),
]),
awaitable: false,
// Prevent an E2EE message being encrypted again
encrypted: entry.value.encrypted,
),
);
}

View File

@@ -0,0 +1,59 @@
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart';
extension _StringToInt on String {
int toInt() => int.parse(this);
}
class JingleContentThumbnail {
const JingleContentThumbnail(
this.uri,
this.mediaType,
this.width,
this.height,
);
factory JingleContentThumbnail.fromXML(XMLNode thumbnail) {
assert(
thumbnail.tag == 'thumbnail',
'thumbnail must be Jingle Content Thumbnail',
);
assert(
thumbnail.attributes['xmlns'] == jingleContentThumbnailXmlns,
'thumbnail must be Jingle Content Thumbnail',
);
return JingleContentThumbnail(
Uri.parse(thumbnail.attributes['uri']! as String),
thumbnail.attributes['media-type'] as String?,
(thumbnail.attributes['width'] as String?)?.toInt(),
(thumbnail.attributes['height'] as String?)?.toInt(),
);
}
/// The URI of the thumbnail data.
final Uri uri;
/// The MIME type of the thumbnail
final String? mediaType;
/// The width of the thumbnail.
final int? width;
/// The height of the thumbnail.
final int? height;
/// Convert the thumbnail to its XML representation.
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'thumbnail',
xmlns: jingleContentThumbnailXmlns,
attributes: {
'uri': uri.toString(),
if (mediaType != null) 'media-type': mediaType!,
if (width != null) 'width': width.toString(),
if (height != null) 'height': height.toString(),
},
);
}
}

View File

@@ -49,7 +49,7 @@ class CarbonsManager extends XmppManagerBase {
tagXmlns: carbonsXmlns,
callback: _onMessageSent,
priority: -98,
)
),
];
@override
@@ -124,7 +124,7 @@ class CarbonsManager extends XmppManagerBase {
XMLNode.xmlns(
tag: 'enable',
xmlns: carbonsXmlns,
)
),
],
),
),
@@ -154,7 +154,7 @@ class CarbonsManager extends XmppManagerBase {
XMLNode.xmlns(
tag: 'disable',
xmlns: carbonsXmlns,
)
),
],
),
),

View File

@@ -40,7 +40,7 @@ class LastMessageCorrectionManager extends XmppManagerBase {
callback: _onMessage,
// Before the message handler
priority: -99,
)
),
];
@override

View File

@@ -100,7 +100,7 @@ class ChatMarkerManager extends XmppManagerBase {
callback: _onMessage,
// Before the message handler
priority: -99,
)
),
];
@override

View File

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

View File

@@ -81,7 +81,7 @@ class StableIdManager extends XmppManagerBase {
callback: _onMessage,
// Before the MessageManager
priority: -99,
)
),
];
@override
@@ -127,7 +127,13 @@ class StableIdManager extends XmppManagerBase {
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<StableIdData>();
return data != null ? data.toXML() : [];
if (data?.originId != null) {
return [
data!.toOriginIdElement(),
];
}
return [];
}
@override

View File

@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.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/stanza.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/types.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
/// for an HTTP upload slot.
bool _containsFileUploadIdentity(DiscoInfo info) {
return listContains(
info.identities,
return info.identities.firstWhereOrNull(
(Identity id) => id.category == 'store' && id.type == 'file',
);
) !=
null;
}
/// Extract the maximum filesize in octets from the disco response. Returns null
@@ -161,9 +161,9 @@ class HttpFileUploadManager extends XmppManagerBase {
attributes: {
'filename': filename,
'size': filesize.toString(),
...contentType != null ? {'content-type': contentType} : {}
if (contentType != null) 'content-type': contentType,
},
)
),
],
),
),

View File

@@ -2,10 +2,14 @@ abstract class OmemoError {}
class UnknownOmemoError extends OmemoError {}
class InvalidAffixElementsException with Exception {}
class InvalidAffixElementsException implements Exception {}
/// Internal exception that is returned when the device list cannot be
/// fetched because the returned list is empty.
class EmptyDeviceListException implements OmemoError {}
class OmemoNotSupportedForContactException extends OmemoError {}
class EncryptionFailedException with Exception {}
class EncryptionFailedException implements Exception {}
class InvalidEnvelopePayloadException with Exception {}
class InvalidEnvelopePayloadException implements Exception {}

View File

@@ -1,3 +1,4 @@
import 'package:moxxmpp/src/managers/data.dart';
import 'package:omemo_dart/omemo_dart.dart';
/// A simple wrapper class for defining elements that should not be encrypted.
@@ -13,9 +14,16 @@ class DoNotEncrypt {
/// An encryption error caused by OMEMO.
class OmemoEncryptionError {
const OmemoEncryptionError(this.jids, this.devices);
const OmemoEncryptionError(this.deviceEncryptionErrors);
/// See omemo_dart's EncryptionResult for info on these fields.
final Map<String, OmemoException> jids;
final Map<RatchetMapKey, OmemoException> devices;
/// See omemo_dart's EncryptionResult for info on this field.
final Map<String, List<EncryptToJidError>> deviceEncryptionErrors;
}
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:convert';
import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart';
@@ -10,13 +11,11 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0203.dart';
import 'package:moxxmpp/src/xeps/xep_0280.dart';
import 'package:moxxmpp/src/xeps/xep_0334.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/helpers.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';
/// 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 = [
// XEP-0033
DoNotEncrypt('addresses', extendedAddressingXmlns),
@@ -43,8 +54,15 @@ const _doNotEncryptList = [
DoNotEncrypt('stanza-id', stableIdXmlns),
];
abstract class BaseOmemoManager extends XmppManagerBase {
BaseOmemoManager() : super(omemoManager);
class OmemoManager extends XmppManagerBase {
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
@override
@@ -113,22 +131,19 @@ abstract class BaseOmemoManager extends XmppManagerBase {
}
// Tell the OmemoManager
(await getOmemoManager()).onDeviceListUpdate(jid.toString(), ids);
await (await _getOmemoManager()).onDeviceListUpdate(jid.toString(), ids);
// Generate an event
getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids));
}
}
@visibleForOverriding
Future<OmemoManager> getOmemoManager();
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<int> _getDeviceId() async => (await _getOmemoManager()).getDeviceId();
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId();
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<OmemoBundle> _getDeviceBundle() async {
final om = await getOmemoManager();
Future<omemo.OmemoBundle> _getDeviceBundle() async {
final om = await _getOmemoManager();
final device = await om.getDevice();
return device.toBundle();
}
@@ -199,53 +214,45 @@ abstract class BaseOmemoManager extends XmppManagerBase {
}
XMLNode _buildEncryptedElement(
EncryptionResult result,
omemo.EncryptionResult result,
String recipientJid,
int deviceId,
) {
final keyElements = <String, List<XMLNode>>{};
for (final key in result.encryptedKeys) {
final keyElement = XMLNode(
for (final keys in result.encryptedKeys.entries) {
keyElements[keys.key] = keys.value
.map(
(ek) => XMLNode(
tag: 'key',
attributes: <String, String>{
'rid': '${key.rid}',
'kex': key.kex ? 'true' : 'false',
attributes: {
'rid': ek.rid.toString(),
if (ek.kex) 'kex': 'true',
},
text: key.value,
);
if (keyElements.containsKey(key.jid)) {
keyElements[key.jid]!.add(keyElement);
} else {
keyElements[key.jid] = [keyElement];
}
text: ek.value,
),
)
.toList();
}
final keysElements = keyElements.entries.map((entry) {
return XMLNode(
tag: 'keys',
attributes: <String, String>{
attributes: {
'jid': entry.key,
},
children: entry.value,
);
}).toList();
var payloadElement = <XMLNode>[];
if (result.ciphertext != null) {
payloadElement = [
XMLNode(
tag: 'payload',
text: base64.encode(result.ciphertext!),
),
];
}
return XMLNode.xmlns(
tag: 'encrypted',
xmlns: omemoXmlns,
children: [
...payloadElement,
if (result.ciphertext != null)
XMLNode(
tag: 'payload',
text: base64Encode(result.ciphertext!),
),
XMLNode(
tag: 'header',
attributes: <String, String>{
@@ -259,7 +266,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// For usage with omemo_dart's OmemoManager.
Future<void> sendEmptyMessageImpl(
EncryptionResult result,
omemo.EncryptionResult result,
String toJid,
) async {
await getAttributes().sendStanza(
@@ -288,7 +295,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Send a heartbeat message to [jid].
Future<void> sendOmemoHeartbeat(String jid) async {
final om = await getOmemoManager();
final om = await _getOmemoManager();
await om.sendOmemoHeartbeat(jid);
}
@@ -301,17 +308,22 @@ abstract class BaseOmemoManager extends XmppManagerBase {
}
/// 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);
if (result.isType<OmemoError>()) return null;
return result.get<OmemoBundle>();
return result.get<omemo.OmemoBundle>();
}
Future<StanzaHandlerData> _onOutgoingStanza(
Stanza stanza,
StanzaHandlerData state,
) async {
if (!state.shouldEncrypt) {
logger.finest('Not encrypting since state.shouldEncrypt is false');
return state;
}
if (state.encrypted) {
logger.finest('Not encrypting since state.encrypted is true');
return state;
@@ -324,7 +336,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
}
final toJid = JID.fromString(stanza.to!).toBare();
final shouldEncryptResult = await shouldEncryptStanza(toJid, stanza);
final shouldEncryptResult = await _shouldEncryptStanza(toJid, stanza);
if (!shouldEncryptResult && !state.forceEncryption) {
logger.finest(
'Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.',
@@ -351,29 +363,29 @@ abstract class BaseOmemoManager extends XmppManagerBase {
.getManagerById<CarbonsManager>(carbonsManager)
?.isEnabled ??
false;
final om = await getOmemoManager();
final result = await om.onOutgoingStanza(
OmemoOutgoingStanza(
[
final om = await _getOmemoManager();
final encryptToJids = [
toJid.toString(),
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
],
];
final result = await om.onOutgoingStanza(
omemo.OmemoOutgoingStanza(
encryptToJids,
_buildEnvelope(toEncrypt, toJid.toString()),
),
);
logger.finest('Encryption done');
if (!result.isSuccess(2)) {
if (!result.canSend) {
return state
..cancel = true
// If we have no device list for toJid, then the contact most likely does not
// support OMEMO:2
..cancelReason = result.jidEncryptionErrors[toJid.toString()]
is NoKeyMaterialAvailableException
..cancelReason = result.deviceEncryptionErrors[toJid.toString()]!.first
.error is omemo.NoKeyMaterialAvailableError
? OmemoNotSupportedForContactException()
: UnknownOmemoError()
..encryptionError = OmemoEncryptionError(
result.jidEncryptionErrors,
result.deviceEncryptionErrors,
);
}
@@ -401,53 +413,45 @@ abstract class BaseOmemoManager extends XmppManagerBase {
..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(
Stanza stanza,
StanzaHandlerData state,
) async {
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns);
if (encrypted == null) return state;
if (stanza.from == null) return state;
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns)!;
final fromJid = JID.fromString(stanza.from!).toBare();
final header = encrypted.firstTag('header')!;
final payloadElement = encrypted.firstTag('payload');
final keys = List<EncryptedKey>.empty(growable: true);
final ourJid = getAttributes().getFullJID();
final ourJidString = ourJid.toBare().toString();
final keys = List<omemo.EncryptedKey>.empty(growable: true);
for (final keysElement in header.findTags('keys')) {
// We only care about our own JID
final jid = keysElement.attributes['jid']! as String;
for (final key in keysElement.findTags('key')) {
keys.add(
EncryptedKey(
jid,
if (jid != ourJidString) {
continue;
}
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 om = await getOmemoManager();
final om = await _getOmemoManager();
final result = await om.onIncomingStanza(
OmemoIncomingStanza(
omemo.OmemoIncomingStanza(
fromJid.toString(),
sid,
state.extensions
.get<DelayedDeliveryData>()
?.timestamp
.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch,
keys,
payloadElement?.innerText(),
encrypted.firstTag('payload')?.innerText(),
false,
),
);
@@ -464,6 +468,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
.toList();
}
logger.finest('Got payload: ${result.payload != null}');
if (result.payload != null) {
XMLNode envelope;
try {
@@ -481,6 +486,8 @@ abstract class BaseOmemoManager extends XmppManagerBase {
// Do not add forbidden elements from the envelope
envelopeChildren.where(shouldEncryptElement),
);
logger.finest('Adding children: ${envelopeChildren.map((c) => c.tag)}');
} else {
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
..encrypted = true
..stanza = Stanza(
@@ -500,6 +516,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
children: children,
tag: stanza.tag,
attributes: Map<String, String>.from(stanza.attributes),
)
..extensions.set<OmemoData>(
OmemoData(
result.newRatchets,
result.replacedRatchets,
),
);
}
@@ -513,7 +535,10 @@ abstract class BaseOmemoManager extends XmppManagerBase {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final result = await pm.getItems(jid.toBare(), omemoDevicesXmlns);
if (result.isType<PubSubError>()) return Result(UnknownOmemoError());
return Result(result.get<List<PubSubItem>>().first.payload);
final itemList = result.get<List<PubSubItem>>();
if (itemList.isEmpty) return Result(EmptyDeviceListException());
return Result(itemList.first.payload);
}
/// Retrieves the OMEMO device list from [jid].
@@ -532,7 +557,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieve all device bundles for the JID [jid].
///
/// On success, returns a list of devices. On failure, returns am OmemoError.
Future<Result<OmemoError, List<OmemoBundle>>> retrieveDeviceBundles(
Future<Result<OmemoError, List<omemo.OmemoBundle>>> retrieveDeviceBundles(
JID jid,
) async {
// TODO(Unknown): Should we query the device list first?
@@ -553,12 +578,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieves a bundle from entity [jid] with the device id [deviceId].
///
/// On success, returns the device bundle. On failure, returns an OmemoError.
Future<Result<OmemoError, OmemoBundle>> retrieveDeviceBundle(
Future<Result<OmemoError, omemo.OmemoBundle>> retrieveDeviceBundle(
JID jid,
int deviceId,
) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final bareJid = jid.toBare().toString();
final bareJid = jid.toBare();
final item = await pm.getItem(bareJid, omemoBundlesXmlns, '$deviceId');
if (item.isType<PubSubError>()) return Result(UnknownOmemoError());
@@ -569,7 +594,9 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// nodes.
///
/// 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 pm = attrs.getManagerById<PubSubManager>(pubsubManager)!;
final bareJid = attrs.getFullJID().toBare();
@@ -636,6 +663,11 @@ abstract class BaseOmemoManager extends XmppManagerBase {
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.
///
/// On success, returns whether [jid] has published a device list and device bundles.

View File

@@ -5,7 +5,7 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
import 'package:moxxmpp/src/xeps/xep_0264.dart';
class StatelessMediaSharingData implements StanzaHandlerExtension {
const StatelessMediaSharingData({
@@ -20,7 +20,7 @@ class StatelessMediaSharingData implements StanzaHandlerExtension {
final int size;
final String description;
final Map<String, String> hashes; // algo -> hash value
final List<Thumbnail> thumbnails;
final List<JingleContentThumbnail> thumbnails;
final String url;
}
@@ -48,16 +48,11 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
break;
}
final thumbnails = List<Thumbnail>.empty(growable: true);
for (final child in file.children) {
// TODO(Unknown): Handle other thumbnails
if (child.tag == 'file-thumbnail' &&
child.attributes['xmlns'] == fileThumbnailsXmlns) {
final thumb = parseFileThumbnailElement(child);
if (thumb != null) {
thumbnails.add(thumb);
}
}
// Thumbnails
final thumbnails = List<JingleContentThumbnail>.empty(growable: true);
for (final i
in file.findTags('thumbnail', xmlns: jingleContentThumbnailXmlns)) {
thumbnails.add(JingleContentThumbnail.fromXML(i));
}
return StatelessMediaSharingData(
@@ -87,7 +82,7 @@ class SIMSManager extends XmppManagerBase {
tagXmlns: referenceXmlns,
// Before the message handler
priority: -99,
)
),
];
@override

View File

@@ -1,10 +1,10 @@
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.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/rfcs/rfc_6120/sasl/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
/// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2.
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/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.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/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';

View File

@@ -0,0 +1,61 @@
import 'dart:async';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
/// Representation of a <occupant-id /> element.
class OccupantIdData implements StanzaHandlerExtension {
const OccupantIdData(
this.id,
);
/// The unique occupant id.
final String id;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'occupant-id',
xmlns: occupantIdXmlns,
attributes: {
'id': id,
},
);
}
}
class OccupantIdManager extends XmppManagerBase {
OccupantIdManager() : super(occupantIdManager);
@override
List<String> getDiscoFeatures() => [
occupantIdXmlns,
];
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagName: 'occupant-id',
tagXmlns: occupantIdXmlns,
callback: _onMessage,
// Before the MessageManager
priority: MessageManager.messageHandlerPriority + 1,
),
];
@override
Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
return state
..extensions.set(OccupantIdData(stanza.attributes['id']! as String));
}
}

View File

@@ -31,7 +31,7 @@ class MessageRetractionManager extends XmppManagerBase {
callback: _onMessage,
// Before the MessageManager
priority: -99,
)
),
];
@override

View File

@@ -1,6 +1,6 @@
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
import 'package:moxxmpp/src/xeps/xep_0264.dart';
import 'package:moxxmpp/src/xeps/xep_0300.dart';
class FileMetadataData {
@@ -39,12 +39,10 @@ class FileMetadataData {
}
// Thumbnails
final thumbnails = List<Thumbnail>.empty(growable: true);
for (final i in node.findTags('file-thumbnail')) {
final thumbnail = parseFileThumbnailElement(i);
if (thumbnail != null) {
thumbnails.add(thumbnail);
}
final thumbnails = List<JingleContentThumbnail>.empty(growable: true);
for (final i
in node.findTags('thumbnail', xmlns: jingleContentThumbnailXmlns)) {
thumbnails.add(JingleContentThumbnail.fromXML(i));
}
// Length and height
@@ -75,7 +73,7 @@ class FileMetadataData {
final String? mediaType;
final int? width;
final int? height;
final List<Thumbnail> thumbnails;
final List<JingleContentThumbnail> thumbnails;
final String? desc;
final Map<HashFunction, String> hashes;
final int? length;
@@ -119,7 +117,7 @@ class FileMetadataData {
for (final thumbnail in thumbnails) {
node.addChild(
constructFileThumbnailElement(thumbnail),
thumbnail.toXML(),
);
}

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/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
@@ -114,8 +114,7 @@ class StatelessFileSharingData implements StanzaHandlerExtension {
}
StatelessFileSharingUrlSource? getFirstUrlSource() {
return firstWhereOrNull(
sources,
return sources.firstWhereOrNull(
(StatelessFileSharingSource source) =>
source is StatelessFileSharingUrlSource,
) as StatelessFileSharingUrlSource?;
@@ -134,7 +133,7 @@ class SFSManager extends XmppManagerBase {
callback: _onMessage,
// Before the message handler
priority: -98,
)
),
];
@override

View File

@@ -1,5 +1,5 @@
import 'dart:convert';
import 'package:moxlib/moxlib.dart';
import 'package:collection/collection.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.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;
// Find the first URL source
final source = firstWhereOrNull(
sources,
final source = sources.firstWhereOrNull(
(XMLNode child) =>
child.tag == 'url-data' && child.attributes['xmlns'] == urlDataXmlns,
)!;

View File

@@ -1,4 +1,5 @@
import 'dart:convert';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.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/stanza.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_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
@@ -344,7 +344,7 @@ class StickersManager extends XmppManagerBase {
) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final stickerPackDataRaw = await pm.getItem(
jid.toBare().toString(),
jid.toBare(),
stickersXmlns,
id,
);

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/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
@@ -96,7 +96,7 @@ class MessageRepliesManager extends XmppManagerBase {
callback: _onMessage,
// Before the message handler
priority: -99,
)
),
];
@override
@@ -107,8 +107,10 @@ class MessageRepliesManager extends XmppManagerBase {
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<ReplyData>();
return data != null
? [
if (data == null) {
return [];
}
return [
XMLNode.xmlns(
tag: 'reply',
xmlns: replyXmlns,
@@ -127,7 +129,7 @@ class MessageRepliesManager extends XmppManagerBase {
if (data.body != null)
XMLNode.xmlns(
tag: 'fallback',
xmlns: fallbackXmlns,
xmlns: fallbackIndicationXmlns,
attributes: {'for': replyXmlns},
children: [
XMLNode(
@@ -139,8 +141,7 @@ class MessageRepliesManager extends XmppManagerBase {
),
],
),
]
: [];
];
}
Future<StanzaHandlerData> _onMessage(
@@ -154,7 +155,8 @@ class MessageRepliesManager extends XmppManagerBase {
int? end;
// TODO(Unknown): Maybe extend firstTag to also look for attributes
final fallback = stanza.firstTag('fallback', xmlns: fallbackXmlns);
final fallback =
stanza.firstTag('fallback', xmlns: fallbackIndicationXmlns);
if (fallback != null) {
final body = fallback.firstTag('body')!;
start = int.parse(body.attributes['start']! as String);

View File

@@ -1,11 +1,11 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/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/xep_0388.dart';

View File

@@ -1,60 +1,76 @@
<?xml version='1.0' encoding='UTF-8'?>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
xmlns='http://usefulinc.com/ns/doap#'
xmlns:foaf='http://xmlns.com/foaf/0.1/'
xmlns:xmpp='https://linkmauve.fr/ns/xmpp-doap#'>
<Project xml:lang='en'>
xmlns="http://usefulinc.com/ns/doap#"
xmlns:foaf="http://xmlns.com/foaf/0.1/"
xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#"
xmlns:schema="https://schema.org/">
<Project xml:lang="en">
<name>moxxmpp</name>
<created>2021-12-26</created>
<homepage rdf:resource='https://codeberg.org/moxxy/moxxmpp'/>
<homepage rdf:resource="https://codeberg.org/moxxy/moxxmpp"/>
<os>Linux</os>
<os>Windows</os>
<os>macOS</os>
<os>Android</os>
<os>iOS</os>
<programming-language>Dart</programming-language>
<maintainer>
<foaf:Person>
<foaf:name>Alexander "Polynomdivision"</foaf:name>
<foaf:homepage rdf:resource="https://blog.polynom.me" />
<foaf:homepage rdf:resource="https://polynom.me"/>
</foaf:Person>
</maintainer>
<implements rdf:resource="https://xmpp.org/rfcs/rfc6120.html" />
<!-- Channel list -->
<developer-forum rdf:resource='xmpp:dev@muc.moxxy.org?join'/>
<support-forum rdf:resource='xmpp:dev@muc.moxxy.org?join'/>
<!-- Repository information -->
<programming-language>Dart</programming-language>
<bug-database rdf:resource="https://codeberg.org/moxxy/moxxmpp/issues" />
<license rdf:resource="https://codeberg.org/moxxy/moxxmpp/src/branch/master/packages/moxxmpp/LICENSE" />
<repository>
<GitRepository>
<browse rdf:resource="https://codeberg.org/moxxy/moxxmpp" />
<location rdf:resource="https://codeberg.org/moxxy/moxxmpp.git" />
</GitRepository>
</repository>
<!-- RFC list -->
<implements rdf:resource="https://xmpp.org/rfcs/rfc6120.html"/>
<implements rdf:resource="https://xmpp.org/rfcs/rfc6121.html"/>
<!-- Standard XEP list -->
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.13.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.5rc3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.24.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0066.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0066.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Only jabber:x:oob</xmpp:note>
<xmpp:version>1.5</xmpp:version>
@@ -62,65 +78,70 @@
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Receiving data</xmpp:note>
<xmpp:version>1.1.4</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>2.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.5.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.4.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.3.0</xmpp:version>
<xmpp:note xml:lang="en">Not plugged into the UI</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.6</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0264.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.4.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Exists only as part of support for XEP-0280</xmpp:note>
<xmpp:version>1.0</xmpp:version>
@@ -128,7 +149,7 @@
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0300.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0300.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Supports only Sha256, Sha512 and blake2b512</xmpp:note>
<xmpp:version>1.0.0</xmpp:version>
@@ -136,137 +157,136 @@
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.2.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:note xml:lang="en">Read-only support</xmpp:note>
<xmpp:version>0.4</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0334.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0334.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:note xml:lang="en">Write-only support</xmpp:note>
<xmpp:version>0.3.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0352.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0352.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.6.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.1.0</xmpp:version>
<xmpp:note xml:lang="en">Only handles the success case; not accessible via the App</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0368.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>1.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.4.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.8.3</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0420.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0420.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.4.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0424.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0424.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.3.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0444.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0444.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0446.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0446.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0447.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0447.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.2</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0448.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0448.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0449.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0449.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.1.1</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0461.html" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0461.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://codeberg.org/moxxy/custom-xeps/src/branch/master/xep-xxxx-extensible-file-thumbnails.md" />
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0484.html" />
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.2.1</xmpp:version>
<xmpp:note xml:lang="en">Only Blurhash is implemented</xmpp:note>
<xmpp:version>0.1.1</xmpp:version>
<xmpp:note xml:lang="en">Invalidation is never requested</xmpp:note>
</xmpp:SupportedXep>
</implements>
<!-- Non-Standard (Proto) XEPs -->
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://codeberg.org/moxxy/custom-xeps/src/branch/master/xep-xxxx-file-upload-notification.md" />
<xmpp:xep rdf:resource="https://codeberg.org/moxxy/custom-xeps/src/branch/master/xep-xxxx-file-upload-notification.md"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.0.5</xmpp:version>
<xmpp:note xml:lang="en">Sending and receiving implemented; cancellation not implemented</xmpp:note>

View File

@@ -5,30 +5,28 @@ homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub
environment:
sdk: '>=2.17.5 <3.0.0'
sdk: ">=3.0.0 <4.0.0"
dependencies:
collection: ^1.16.0
cryptography: ^2.0.5
freezed: ^2.1.0+1
freezed_annotation: ^2.1.0
collection: ^1.18.0
cryptography: ^2.7.0
hex: ^0.2.0
json_serializable: ^6.3.1
logging: ^1.0.2
meta: ^1.7.0
json_serializable: ^6.8.0
logging: ^1.2.0
meta: ^1.15.0
moxlib:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.5
version: ^0.2.0
omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.4.3
version: ^0.6.0
random_string: ^2.3.1
saslprep: ^1.0.2
synchronized: ^3.0.0+2
uuid: ^3.0.5
xml: ^6.1.0
saslprep: ^1.0.3
synchronized: ^3.3.0+3
uuid: ^3.0.7
xml: ^6.5.0
dev_dependencies:
build_runner: ^2.1.11
test: ^1.16.0
very_good_analysis: ^3.0.1
build_runner: ^2.4.12
test: ^1.25.8
very_good_analysis: ^6.0.0

View File

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

View File

@@ -2,11 +2,12 @@ import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp/src/awaiter.dart';
import 'package:test/test.dart';
void main() {
const bareJid = JID('moxxmpp', 'server3.example', '');
const bareJid = 'user4@example.org';
String getBareJidCallback() => bareJid;
void main() {
test('Test awaiting an awaited stanza with a from attribute', () async {
final awaiter = StanzaAwaiter();
final awaiter = StanzaAwaiter(getBareJidCallback);
// "Send" a stanza
final future = await awaiter.addPending(
@@ -20,14 +21,12 @@ void main() {
XMLNode.fromString(
'<iq from="user3@server.example" id="abc123" type="result" />',
),
bareJid,
);
expect(result1, false);
final result2 = await awaiter.onData(
XMLNode.fromString(
'<iq from="user1@server.example" id="lol" type="result" />',
),
bareJid,
);
expect(result2, false);
@@ -37,22 +36,20 @@ void main() {
);
final result3 = await awaiter.onData(
stanza,
bareJid,
);
expect(result3, true);
expect(await future, stanza);
});
test('Test awaiting an awaited stanza without a from attribute', () async {
final awaiter = StanzaAwaiter();
final awaiter = StanzaAwaiter(getBareJidCallback);
// "Send" a stanza
final future = await awaiter.addPending(bareJid.toString(), 'abc123', 'iq');
final future = await awaiter.addPending(null, 'abc123', 'iq');
// Receive the wrong answer
final result1 = await awaiter.onData(
XMLNode.fromString('<iq id="lol" type="result" />'),
bareJid,
);
expect(result1, false);
@@ -60,23 +57,21 @@ void main() {
final stanza = XMLNode.fromString('<iq id="abc123" type="result" />');
final result2 = await awaiter.onData(
stanza,
bareJid,
);
expect(result2, true);
expect(await future, stanza);
});
test('Test awaiting a stanza that was already awaited', () async {
final awaiter = StanzaAwaiter();
final awaiter = StanzaAwaiter(getBareJidCallback);
// "Send" a stanza
final future = await awaiter.addPending(bareJid.toString(), 'abc123', 'iq');
final future = await awaiter.addPending(null, 'abc123', 'iq');
// Receive the correct answer
final stanza = XMLNode.fromString('<iq id="abc123" type="result" />');
final result1 = await awaiter.onData(
stanza,
bareJid,
);
expect(result1, true);
expect(await future, stanza);
@@ -84,31 +79,55 @@ void main() {
// Receive it again
final result2 = await awaiter.onData(
stanza,
bareJid,
);
expect(result2, false);
});
test('Test ignoring a stanza that has the wrong tag', () async {
final awaiter = StanzaAwaiter();
final awaiter = StanzaAwaiter(getBareJidCallback);
// "Send" a stanza
final future = await awaiter.addPending(bareJid.toString(), 'abc123', 'iq');
final future = await awaiter.addPending(null, 'abc123', 'iq');
// Receive the wrong answer
final stanza = XMLNode.fromString('<iq id="abc123" type="result" />');
final result1 = await awaiter.onData(
XMLNode.fromString('<message id="abc123" type="result" />'),
bareJid,
);
expect(result1, false);
// Receive the correct answer
final result2 = await awaiter.onData(
stanza,
bareJid,
);
expect(result2, true);
expect(await future, stanza);
});
test('Sending a stanza to our bare JID', () async {
final awaiter = StanzaAwaiter(getBareJidCallback);
// "Send" a stanza
final future = await awaiter.addPending(bareJid, 'abc123', 'iq');
// Receive the response.
final stanza = XMLNode.fromString('<iq id="abc123" type="result" />');
await awaiter.onData(stanza);
expect(await future, stanza);
});
test(
'Sending a stanza to our bare JID and receiving stanza with a from attribute',
() async {
final awaiter = StanzaAwaiter(getBareJidCallback);
// "Send" a stanza
final future = await awaiter.addPending(bareJid, 'abc123', 'iq');
// Receive the response.
final stanza =
XMLNode.fromString('<iq from="$bareJid" id="abc123" type="result" />');
await awaiter.onData(stanza);
expect(await future, stanza);
});
}

View File

@@ -58,7 +58,7 @@ List<ExpectationBase> buildAuthenticatedPlay(ConnectionSettings settings) {
);
return [
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' from='${settings.jid.toBare().toString()}' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' from='${settings.jid.toBare()}' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@@ -77,7 +77,7 @@ List<ExpectationBase> buildAuthenticatedPlay(ConnectionSettings settings) {
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' from='${settings.jid.toBare().toString()}' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' from='${settings.jid.toBare()}' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@@ -89,6 +89,7 @@ List<ExpectationBase> buildAuthenticatedPlay(ConnectionSettings settings) {
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
<ver xmlns='urn:xmpp:features:rosterver'/>
</stream:features>
''',
),

View File

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

View File

@@ -0,0 +1,53 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import 'helpers/logging.dart';
import 'helpers/xmpp.dart';
void main() {
initLogger();
test('Test a versioned roster fetch returning an empty iq', () async {
final sm = TestingRosterStateManager('ver14', []);
final rm = RosterManager(sm);
final cs = ConnectionSettings(
jid: JID.fromString('user@example.org'),
password: 'abc123',
);
final socket = StubTCPSocket([
...buildAuthenticatedPlay(cs),
StanzaExpectation(
'<iq xmlns="jabber:client" id="r1h3vzp7" type="get"><query xmlns="jabber:iq:roster" ver="ver14"/></iq>',
'<iq xmlns="jabber:client" id="r1h3vzp7" type="result" />',
ignoreId: true,
adjustId: true,
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
socket,
)..connectionSettings = cs;
await conn.registerManagers([
rm,
PresenceManager(),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
RosterFeatureNegotiator(),
]);
// Connect
await conn.connect(
shouldReconnect: false,
waitUntilLogin: true,
);
// Request the roster
final rawResult = await rm.requestRoster();
expect(rawResult.isType<RosterRequestResult>(), true);
final result = rawResult.get<RosterRequestResult>();
expect(result.items.isEmpty, true);
});
}

View File

@@ -14,9 +14,9 @@ final scramSha1StreamFeatures = XMLNode(
XMLNode(
tag: 'mechanism',
text: 'SCRAM-SHA-1',
)
),
],
)
),
],
);
final scramSha256StreamFeatures = XMLNode(
@@ -29,9 +29,9 @@ final scramSha256StreamFeatures = XMLNode(
XMLNode(
tag: 'mechanism',
text: 'SCRAM-SHA-256',
)
),
],
)
),
],
);

Some files were not shown because too many files have changed in this diff Show More