153 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
fa2ce7c2d1 feat(xep): FASTSaslNegotiator now only takes a string as its token 2023-06-11 22:22:45 +02:00
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
aa71d3ed5d feat(xep): Allow changing the SASL2 user agent 2023-06-11 16:30:49 +02:00
f2d8c6a009 fix(xep): Fix parsing user avatar data with newlines 2023-06-10 21:06:30 +02:00
88545e3308 fix(xep): Fix the OOB fallback for SFS 2023-06-09 00:07:18 +02:00
925a46c0da feat(xep): Move to JID 2023-06-09 00:07:05 +02:00
327f695a40 Merge pull request 'Replace StanzaHandlerData with something more extensible' (#42) from feat/type-map-rework into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/42
2023-06-07 21:44:28 +00:00
8266765ff8 feat(core): Add encryption info to MessageEvent 2023-06-07 23:42:05 +02:00
e234c812ff feat(core): Add a shorthand for MessageEvent.extensions.get 2023-06-07 21:11:11 +02:00
4ff2992a03 fix(all): Last minute fixes 2023-06-07 21:04:05 +02:00
09fd5845aa feat(core): Re-fix the subscription functions 2023-06-07 18:54:05 +02:00
963f3f2cd9 feat(core): Check if we can do subscription pre-approval 2023-06-07 18:41:08 +02:00
da1d28a6d6 feat(core): Remove the PresenceReceivedEvent 2023-06-07 15:12:34 +02:00
cbd90b1163 feat(core): Add a list constructor to TypedMap 2023-06-07 14:13:46 +02:00
f0538b0447 feat(xep): Write a test for XEP-0334 2023-06-06 21:46:33 +02:00
968604b0ba fix(tests): Fix SFS parsing test 2023-06-06 21:31:14 +02:00
60279a84e0 feat(all): Restrict extensions to StanzaHandlerExtension 2023-06-06 21:19:42 +02:00
cf3287ccf4 feat(core): Remove freezed artifact 2023-06-06 20:26:35 +02:00
1ab0ed856f feat(all): Bump version to 0.4.0 2023-06-06 20:22:13 +02:00
10a5046431 feat(core): Rework [MessageEvent] 2023-06-06 16:19:16 +02:00
4d76b9f57a feat(core): Remove [MessageDetails] 2023-06-06 15:57:55 +02:00
0ec3777f44 feat(xep): Managers register the sending callback 2023-06-06 15:14:19 +02:00
6f5de9c4dc feat(xep): Implement the message sending callbacks 2023-06-06 14:12:49 +02:00
79d7e3ba64 feat(all): Move all managers to the new data system 2023-06-04 21:53:47 +02:00
8270185027 feat(core): Implement a typed map 2023-06-03 00:41:23 +02:00
9e0f38154e feat(all): Various changes
- Fix unavailable presence being sent *after* connecting
- Migrate more APIs to the JID class
- Advertise +notify for user avatar metadata
2023-06-02 22:00:44 +02:00
1475cb542f fix(xep): Just receiving <stanza-id/>s does not require disco
It is only required when we try to use them.
2023-05-25 12:42:42 +02:00
b949ec6ff5 fix(docs): Fix typo 2023-05-24 23:44:48 +02:00
c3be199cca Merge pull request 'Stanza send queue' (#40) from feat/send-queue into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/40
2023-05-24 20:50:03 +00:00
83ebe58c47 feat(tests): Test emptying stanza queue on connect 2023-05-24 22:03:17 +02:00
4db0ef6b34 feat(core): Expose StanzaDetails 2023-05-24 20:41:02 +02:00
b95e75329d feat(all): Remove StanzaAddFrom 2023-05-24 20:27:01 +02:00
3163101f82 feat(all): Migrate to the new StanzaDetails API 2023-05-24 14:28:42 +02:00
bd4e1d28ea feat(core): Implement the send queue 2023-05-24 13:34:36 +02:00
b1da6e5a53 chore(xep): Move DiscoManager from String to JID 2023-05-23 17:28:20 +02:00
c6552968d5 feat(core): Remove resumed from connection state change events 2023-05-23 15:56:38 +02:00
1d87c0ce95 feat(xep): Separate XEP-0030 and XEP-0115 support
Also, we now validate whatever we get in terms of disco#info
and capability hashes before caching.
2023-05-23 15:52:48 +02:00
da591a552d fix(xep): Fix discovery of XEP-0191 and XEP-0280 2023-05-16 19:06:50 +02:00
47b679d168 fix(xep): Do not set C2S counter to 0 after connecting 2023-05-16 13:45:52 +02:00
320f4a8d4c fix(xep): Fix issues with enhanced enums 2023-05-16 00:59:50 +02:00
1fdefacd52 feat(xep): Improve API using enhanced enums 2023-05-15 20:40:39 +02:00
09b8613c80 fix(style): Style fixes 2023-05-07 00:15:37 +02:00
f3d906e69b feat(core): Re-add xmlns for StanzaHandler 2023-05-07 00:13:25 +02:00
483cb0d7f1 fix(core): Fix stream parsing breaking after some time 2023-05-07 00:09:35 +02:00
153 changed files with 8877 additions and 4325 deletions

View File

@@ -7,7 +7,7 @@ line-length=72
[title-trailing-punctuation] [title-trailing-punctuation]
[title-hard-tab] [title-hard-tab]
[title-match-regex] [title-match-regex]
regex=^((feat|fix|chore|refactor|docs|release|test)\((meta|tests|style|docs|xep|core|example)+(,(meta|tests|style|docs|xep|core|example))*\)|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] [body-trailing-whitespace]

View File

@@ -1,28 +1,49 @@
when:
branch: master
pipeline: pipeline:
# Check moxxmpp # Check moxxmpp
moxxmpp-lint: moxxmpp-lint:
image: dart:2.18.1 image: dart:3.0.7
commands: commands:
- cd packages/moxxmpp - 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 - dart analyze --fatal-infos --fatal-warnings
moxxmpp-test: moxxmpp-test:
image: dart:2.18.1 image: dart:3.0.7
commands: commands:
- cd packages/moxxmpp - 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 - dart test
# Check moxxmpp_socket_tcp # Check moxxmpp_socket_tcp
moxxmpp_socket_tcp-lint: moxxmpp_socket_tcp-lint:
image: dart:2.18.1 image: dart:3.0.7
commands: commands:
- cd packages/moxxmpp_socket_tcp - 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 - dart analyze --fatal-infos --fatal-warnings
# moxxmpp-test: # moxxmpp-test:
# image: dart:2.18.1 # image: dart:3.0.7
# commands: # commands:
# - cd packages/moxxmpp # - 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 # - 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. 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) ### [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, implements the RFC6120 connection algorithm and XEP-0368 direct TLS connections,
if a DNS implementation is given, and supports StartTLS. if a DNS implementation is given, and supports StartTLS.
### moxxmpp_color
Implementation of [XEP-0392](https://xmpp.org/extensions/xep-0392.html).
## Development ## Development
To begin, use [melos](https://github.com/invertase/melos) to bootstrap the project: `melos bootstrap`. Then, the example 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'; import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
class TestingTCPSocketWrapper extends TCPSocketWrapper { class TestingTCPSocketWrapper extends TCPSocketWrapper {
TestingTCPSocketWrapper() : super(true);
@override @override
bool onBadCertificate(dynamic certificate, String domain) { bool onBadCertificate(dynamic certificate, String domain) {
return true; return true;
@@ -30,24 +32,26 @@ class EchoMessageManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final body = stanza.firstTag('body'); final body = stanza.firstTag('body');
if (body == null) return state.copyWith(done: true); if (body == null) return state..done = true;
final bodyText = body.innerText(); final bodyText = body.innerText();
await getAttributes().sendStanza( await getAttributes().sendStanza(
Stanza.message( StanzaDetails(
to: stanza.from, Stanza.message(
children: [ to: stanza.from,
XMLNode( children: [
tag: 'body', XMLNode(
text: 'Hello, ${stanza.from}! You said "$bodyText"', tag: 'body',
), text: 'Hello, ${stanza.from}! You said "$bodyText"',
], ),
],
),
awaitable: false,
), ),
awaitable: false,
); );
return state.copyWith(done: true); return state..done = true;
} }
} }

View File

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

126
flake.lock generated
View File

@@ -1,12 +1,74 @@
{ {
"nodes": { "nodes": {
"flake-utils": { "android-nixpkgs": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": { "locked": {
"lastModified": 1678901627, "lastModified": 1727554699,
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=", "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", "owner": "numtide",
"repo": "flake-utils", "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" "type": "github"
}, },
"original": { "original": {
@@ -17,27 +79,27 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1676076353, "lastModified": 1727348695,
"narHash": "sha256-mdUtE8Tp40cZETwcq5tCwwLqkJVV1ULJQ5GKRtbshag=", "narHash": "sha256-J+PeFKSDV+pHL7ukkfpVzCOO7mBSrrpJ3svwBFABbhI=",
"owner": "AtaraxiaSjel", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5deb99bdccbbb97e7562dee4ba8a3ee3021688e6", "rev": "1925c603f17fc89f4c8f6bf6f631a802ad85d784",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "AtaraxiaSjel", "owner": "NixOS",
"ref": "update/flutter", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"nixpkgs-unstable": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1680273054, "lastModified": 1727586919,
"narHash": "sha256-Bs6/5LpvYp379qVqGt9mXxxx9GSE789k3oFc+OAL07M=", "narHash": "sha256-e/YXG0tO5GWHDS8QQauj8aj4HhXEm602q9swrrlTlKQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3364b5b117f65fe1ce65a3cdd5612a078a3b31e3", "rev": "2dcd9c55e8914017226f5948ac22c53872a13ee2",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -49,9 +111,39 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "android-nixpkgs": "android-nixpkgs",
"nixpkgs": "nixpkgs", "flake-utils": "flake-utils_2",
"nixpkgs-unstable": "nixpkgs-unstable" "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,22 +1,20 @@
{ {
description = "moxxmpp"; description = "moxxmpp";
inputs = { inputs = {
nixpkgs.url = "github:AtaraxiaSjel/nixpkgs/update/flutter"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; android-nixpkgs.url = "github:tadfisher/android-nixpkgs";
flake-utils.url = "github:numtide/flake-utils"; 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 { pkgs = import nixpkgs {
inherit system; inherit system;
config = { config = {
android_sdk.accept_license = true; android_sdk.accept_license = true;
allowUnfree = true; allowUnfree = true;
}; };
}; };
unstable = import nixpkgs-unstable { # Everything to make Flutter happy
inherit system;
};
android = pkgs.androidenv.composeAndroidPackages { android = pkgs.androidenv.composeAndroidPackages {
# TODO: Find a way to pin these # TODO: Find a way to pin these
#toolsVersion = "26.1.1"; #toolsVersion = "26.1.1";
@@ -33,6 +31,7 @@
useGoogleAPIs = false; useGoogleAPIs = false;
useGoogleTVAddOns = false; useGoogleTVAddOns = false;
}; };
lib = pkgs.lib;
pinnedJDK = pkgs.jdk17; pinnedJDK = pkgs.jdk17;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [ pythonEnv = pkgs.python3.withPackages (ps: with ps; [
@@ -51,7 +50,7 @@
}; };
devShell = let devShell = let
prosody-newer-community-modules = unstable.prosody.overrideAttrs (old: { prosody-newer-community-modules = pkgs.prosody.overrideAttrs (old: {
communityModules = pkgs.fetchhg { communityModules = pkgs.fetchhg {
url = "https://hg.prosody.im/prosody-modules"; url = "https://hg.prosody.im/prosody-modules";
rev = "e3a3a6c86a9f"; rev = "e3a3a6c86a9f";
@@ -73,7 +72,7 @@
buildInputs = with pkgs; [ buildInputs = with pkgs; [
flutter pinnedJDK android.platform-tools dart # Dart flutter pinnedJDK android.platform-tools dart # Dart
gitlint # Code hygiene gitlint # Code hygiene
ripgrep # General utilities ripgrep # General utilities
# Flutter dependencies for Linux desktop # Flutter dependencies for Linux desktop
atk atk
@@ -103,7 +102,26 @@
CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include"; 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 ]; 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; 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 version: 1.0.0
environment: environment:
sdk: '>=2.18.0 <3.0.0' sdk: ">=3.0.0 <4.0.0"
dependencies: dependencies:
logging: ^1.0.2 logging: ^1.3.0
moxxmpp: 0.3.0 moxxmpp:
moxxmpp_socket_tcp: 0.3.0 path: ../packages/moxxmpp
moxxmpp_socket_tcp:
path: ../packages/moxxmpp_socket_tcp
dev_dependencies: dev_dependencies:
lints: ^2.0.0 build_runner: ^2.4.13
test: ^1.16.0 test: ^1.25.8
very_good_analysis: ^3.0.1 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'; import 'package:test/test.dart';
class TestingTCPSocketWrapper extends TCPSocketWrapper { class TestingTCPSocketWrapper extends TCPSocketWrapper {
TestingTCPSocketWrapper() : super(true);
@override @override
bool onBadCertificate(dynamic certificate, String domain) { bool onBadCertificate(dynamic certificate, String domain) {
return true; return true;

View File

@@ -4,6 +4,8 @@ import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
class TestingTCPSocketWrapper extends TCPSocketWrapper { class TestingTCPSocketWrapper extends TCPSocketWrapper {
TestingTCPSocketWrapper() : super(true);
@override @override
bool onBadCertificate(dynamic certificate, String domain) { bool onBadCertificate(dynamic certificate, String domain) {
return true; return true;
@@ -27,7 +29,7 @@ void main() {
ClientToServerNegotiator(), ClientToServerNegotiator(),
TestingTCPSocketWrapper(), TestingTCPSocketWrapper(),
)..connectionSettings = ConnectionSettings( )..connectionSettings = ConnectionSettings(
jid: JID.fromString('testuser@localhost'), jid: JID.fromString('testuser1@localhost'),
password: 'abc123', password: 'abc123',
host: '127.0.0.1', host: '127.0.0.1',
port: 5222, port: 5222,
@@ -40,17 +42,18 @@ void main() {
]); ]);
await conn.registerFeatureNegotiators([ await conn.registerFeatureNegotiators([
SaslPlainNegotiator(), SaslPlainNegotiator(),
SaslScramNegotiator(9, '', '', ScramHashType.sha1),
SaslScramNegotiator(10, '', '', ScramHashType.sha256),
ResourceBindingNegotiator(), ResourceBindingNegotiator(),
FASTSaslNegotiator(), FASTSaslNegotiator(),
Bind2Negotiator(), Bind2Negotiator(),
StartTlsNegotiator(), StartTlsNegotiator(),
Sasl2Negotiator( Sasl2Negotiator()
userAgent: const UserAgent( ..userAgent = const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770', id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp', software: 'moxxmpp',
device: "PapaTutuWawa's awesome device", device: "PapaTutuWawa's awesome device",
), ),
),
]); ]);
final result = await conn.connect( final result = await conn.connect(

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 { {fetchzip, runCommand} : rec {
_fe_analyzer_shared = fetchzip { _fe_analyzer_shared = fetchzip {
sha256 = "1hyd5pmjcfyvfwhsc0wq6k0229abmqq5zn95g31hh42bklb2gci5"; sha256 = "15fh9ka41dw4qsynv07msq4i243fibprcmafdygw5x88f7m55fq3";
url = "https://pub.dartlang.org/packages/_fe_analyzer_shared/versions/50.0.0.tar.gz"; url = "https://pub.dartlang.org/packages/_fe_analyzer_shared/versions/61.0.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
analyzer = fetchzip { analyzer = fetchzip {
sha256 = "0niy5b3w39aywpjpw5a84pxdilhh3zzv1c22x8ywml756pybmj4r"; sha256 = "0w604zngxwfx0xqxvhbxrhdh04wgm6ad6a1lbwnyvmk57amv44np";
url = "https://pub.dartlang.org/packages/analyzer/versions/5.2.0.tar.gz"; url = "https://pub.dartlang.org/packages/analyzer/versions/5.13.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
args = fetchzip { args = fetchzip {
sha256 = "0c78zkzg2d2kzw1qrpiyrj1qvm4pr0yhnzapbqk347m780ha408g"; sha256 = "01ps253280c6dbx0vncw4wga4l2qp1zx779qjj2x06xzb3744zbz";
url = "https://pub.dartlang.org/packages/args/versions/2.3.1.tar.gz"; url = "https://pub.dartlang.org/packages/args/versions/2.4.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
async = fetchzip { async = fetchzip {
sha256 = "00hhylamsjcqmcbxlsrfimri63gb384l31r9mqvacn6c6bvk4yfx"; sha256 = "0hfgvjajp5c2mw68186hgrk9v5zjhhi149hlhl0fap274p2v1g3q";
url = "https://pub.dartlang.org/packages/async/versions/2.10.0.tar.gz"; url = "https://pub.dartlang.org/packages/async/versions/2.11.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -49,29 +51,29 @@
}; };
build_daemon = fetchzip { build_daemon = fetchzip {
sha256 = "0b6hnwjc3gi5g7cnpy8xyiqigcrs0xp51c7y7v1pqn9v75g25w6j"; sha256 = "1wn7bq846vgdj62bkh9h25l95xdsndv0jdyw52nyr0591l3bpg3h";
url = "https://pub.dartlang.org/packages/build_daemon/versions/3.1.0.tar.gz"; url = "https://pub.dartlang.org/packages/build_daemon/versions/3.1.1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
build_resolvers = fetchzip { build_resolvers = fetchzip {
sha256 = "0fnrisgq6rnvbqsf8v43hb11kr1qq6azrxbsvx3wwimd37nxx8m5"; sha256 = "00h9abhrfmnl0xxziyf6p68sxnbv2ww1c4dhgpnz00mzbmamnq5c";
url = "https://pub.dartlang.org/packages/build_resolvers/versions/2.1.0.tar.gz"; url = "https://pub.dartlang.org/packages/build_resolvers/versions/2.3.1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
build_runner = fetchzip { build_runner = fetchzip {
sha256 = "0246bxl9rxgil55fhfzi7csd9a56blj9s1j1z79717hiyzsr60x6"; sha256 = "0b5ha1l6k0gn2swqgqvfy2vl58klf81sxrjnmk0p7rj1wzbqjm7l";
url = "https://pub.dartlang.org/packages/build_runner/versions/2.3.2.tar.gz"; url = "https://pub.dartlang.org/packages/build_runner/versions/2.3.3.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
build_runner_core = fetchzip { build_runner_core = fetchzip {
sha256 = "0bpil0fw0dag3vbnin9p945ymi7xjgkiy7jrq9j52plljf7cnf5z"; sha256 = "07r1kfy6ylm4i4xrb24ns8l26h4h1lgcskmnf8wvq2rd5d5hq790";
url = "https://pub.dartlang.org/packages/build_runner_core/versions/7.2.7.tar.gz"; url = "https://pub.dartlang.org/packages/build_runner_core/versions/7.2.7%2B1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -84,29 +86,29 @@
}; };
built_value = fetchzip { built_value = fetchzip {
sha256 = "0sslr4258snvcj8qhbdk6wapka174als0viyxddwqlnhs7dlci8i"; sha256 = "1y84imf9xqqy3gnd5zz9bcln6mycy7qx35r70b0izm31ismlbzkv";
url = "https://pub.dartlang.org/packages/built_value/versions/8.4.2.tar.gz"; url = "https://pub.dartlang.org/packages/built_value/versions/8.6.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
checked_yaml = fetchzip { checked_yaml = fetchzip {
sha256 = "1gf7ankc5jb7mk17br87ajv05pfg6vb8nf35ay6c35w8jp70ra7k"; sha256 = "1sn01yrmj0pkijn08g3v45c3zmyvdygk9svigkkzybgicdwlkpqs";
url = "https://pub.dartlang.org/packages/checked_yaml/versions/2.0.1.tar.gz"; url = "https://pub.dartlang.org/packages/checked_yaml/versions/2.0.3.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
code_builder = fetchzip { code_builder = fetchzip {
sha256 = "1vl9dl23yd0zjw52ndrazijs6dw83fg1rvyb2gfdpd6n1lj9nbhg"; sha256 = "1shgl7mxiyv0hhw326yqj2b9jxi1h74qxmsnxf1d1xc6yz766p9a";
url = "https://pub.dartlang.org/packages/code_builder/versions/4.3.0.tar.gz"; url = "https://pub.dartlang.org/packages/code_builder/versions/4.6.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
collection = fetchzip { collection = fetchzip {
sha256 = "1iyl3v3j7mj3sxjf63b1kc182fwrwd04mjp5x2i61hic8ihfw545"; sha256 = "1mr8j0078c4z9hhckiq8m735rggsazwfprm0w9gisil51vh7j2mk";
url = "https://pub.dartlang.org/packages/collection/versions/1.17.0.tar.gz"; url = "https://pub.dartlang.org/packages/collection/versions/1.18.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -119,29 +121,29 @@
}; };
coverage = fetchzip { coverage = fetchzip {
sha256 = "0akbg1yp2h4vprc8r9xvrpgvp5d26h7m80h5sbzgr5dlis1bcw0d"; sha256 = "1yy9bgkax5b6kk7qa07p452v82fyj4rl1j03fn366ywyvhfrh6lp";
url = "https://pub.dartlang.org/packages/coverage/versions/1.6.1.tar.gz"; url = "https://pub.dartlang.org/packages/coverage/versions/1.6.3.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
crypto = fetchzip { crypto = fetchzip {
sha256 = "1kjfb8fvdxazmv9ps2iqdhb8kcr31115h0nwn6v4xmr71k8jb8ds"; sha256 = "100ai8qa4p3dyvvd60c4xa9p0gm06yh0d68xgcfm3giraad8xmqj";
url = "https://pub.dartlang.org/packages/crypto/versions/3.0.2.tar.gz"; url = "https://pub.dartlang.org/packages/crypto/versions/3.0.3.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
cryptography = fetchzip { cryptography = fetchzip {
sha256 = "0jqph45d9lbhdakprnb84c3qhk4aq05hhb1pmn8w23yhl41ypijs"; sha256 = "1yxn9slqq93ri81fbr2nbsinz0mpk9wk39ny076ja8q31d4i8v3f";
url = "https://pub.dartlang.org/packages/cryptography/versions/2.0.5.tar.gz"; url = "https://pub.dartlang.org/packages/cryptography/versions/2.5.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
dart_style = fetchzip { dart_style = fetchzip {
sha256 = "01wg15kalbjlh4i3xbawc9zk8yrk28qhak7xp7mlwn2syhdckn7v"; sha256 = "0cjhrb1hs8iw9smmfd0fgnxq3nm0w8sz17l6q6svyz6kif19wk9k";
url = "https://pub.dartlang.org/packages/dart_style/versions/2.2.4.tar.gz"; url = "https://pub.dartlang.org/packages/dart_style/versions/2.3.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -154,43 +156,29 @@
}; };
fixnum = fetchzip { fixnum = fetchzip {
sha256 = "1m8cdfqp9d6w1cik3fwz9bai1wf9j11rjv2z0zlv7ich87q9kkjk"; sha256 = "0nqrzj41ys8dpxf1x70r0kfj1avj0f2j2b7498k8kvc0i9c5asz7";
url = "https://pub.dartlang.org/packages/fixnum/versions/1.0.1.tar.gz"; url = "https://pub.dartlang.org/packages/fixnum/versions/1.1.0.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";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
frontend_server_client = fetchzip { frontend_server_client = fetchzip {
sha256 = "0nv4avkv2if9hdcfzckz36f3mclv7vxchivrg8j3miaqhnjvv4bj"; sha256 = "096v7ycix5hgnk750s1qgykyghl2mymhdkg39jrlk3kbj6xygq5b";
url = "https://pub.dartlang.org/packages/frontend_server_client/versions/3.1.0.tar.gz"; url = "https://pub.dartlang.org/packages/frontend_server_client/versions/3.2.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
glob = fetchzip { glob = fetchzip {
sha256 = "0a6gbwsbz6rkg35dkff0zv88rvcflqdmda90hdfpn7jp1z1w9rhs"; sha256 = "0ffab3azx8zkma36mk6wnig8bn8g5g0vjrq2gl21y77rxgw9iqxj";
url = "https://pub.dartlang.org/packages/glob/versions/2.1.0.tar.gz"; url = "https://pub.dartlang.org/packages/glob/versions/2.1.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
graphs = fetchzip { graphs = fetchzip {
sha256 = "0cr6dgs1a7ln2ir5gd0kiwpn787lk4dwhqfjv8876hkkr1rv80m9"; sha256 = "0fda0j8y6sq1rc9zpzglrzysl5h49y2ji1wq2lq0wx2c609dxm7f";
url = "https://pub.dartlang.org/packages/graphs/versions/2.2.0.tar.gz"; url = "https://pub.dartlang.org/packages/graphs/versions/2.3.1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -217,78 +205,78 @@
}; };
io = fetchzip { io = fetchzip {
sha256 = "1bp5l8hkrp6fjj7zw9af51hxyp52sjspc5558lq0lmi453l0czni"; sha256 = "101kd0rw26vglmr1m5p130kbrp3k7dk4p5nr77wsbwgg53w8c0d4";
url = "https://pub.dartlang.org/packages/io/versions/1.0.3.tar.gz"; url = "https://pub.dartlang.org/packages/io/versions/1.0.4.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
js = fetchzip { js = fetchzip {
sha256 = "13fbxgyg1v6bmzvxamg6494vk3923fn3mgxj6f4y476aqwk99n50"; sha256 = "124a9yqrjdw3p4nnirab9hm9ziwraldlw4q5cb3sr0dcrli74qpw";
url = "https://pub.dartlang.org/packages/js/versions/0.6.5.tar.gz"; url = "https://pub.dartlang.org/packages/js/versions/0.6.7.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
json_annotation = fetchzip { json_annotation = fetchzip {
sha256 = "1p9nvn33psx2zbalhyqjw8gr4agd76jj5jq0fdz0i584c7l77bby"; sha256 = "1jjw7p8qyqajgdq4jqvxipq5w0qrq9dpi1qmia70pk995akryh6m";
url = "https://pub.dartlang.org/packages/json_annotation/versions/4.7.0.tar.gz"; url = "https://pub.dartlang.org/packages/json_annotation/versions/4.8.1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
json_serializable = fetchzip { json_serializable = fetchzip {
sha256 = "04d7laaxrbiybcgbv3y223hy8d6n9f84h5lv9sv79zd9ffzkb2hg"; sha256 = "1pmidql9x6s2pbhdx9x20pwqwvwpfkvrz0h0cm1f8pqis76c90hb";
url = "https://pub.dartlang.org/packages/json_serializable/versions/6.5.4.tar.gz"; url = "https://pub.dartlang.org/packages/json_serializable/versions/6.6.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
logging = fetchzip { logging = fetchzip {
sha256 = "0hl1mjh662c44ci7z60x92i0jsyqg1zm6k6fc89n9pdcxsqdpwfs"; sha256 = "124hfjs66r30p92ndfmy5fymgy66yk9in97h8sq6fi7r78pqyc7g";
url = "https://pub.dartlang.org/packages/logging/versions/1.0.2.tar.gz"; url = "https://pub.dartlang.org/packages/logging/versions/1.2.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
matcher = fetchzip { matcher = fetchzip {
sha256 = "0pjgc38clnjbv124n8bh724db1wcc4kk125j7dxl0icz7clvm0p0"; sha256 = "0inznqkrxqnq09lcbwvda3xd07qfm1k3aa6dv1wy39gvci8hybss";
url = "https://pub.dartlang.org/packages/matcher/versions/0.12.13.tar.gz"; url = "https://pub.dartlang.org/packages/matcher/versions/0.12.16.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
meta = fetchzip { meta = fetchzip {
sha256 = "01kqdd25nln5a219pr94s66p27m0kpqz0wpmwnm24kdy3ngif1v5"; sha256 = "1l3zaz6q2s9mnm7s674xshsfqspy79p5kdbbnc99rf2l76avv4h3";
url = "https://pub.dartlang.org/packages/meta/versions/1.8.0.tar.gz"; url = "https://pub.dartlang.org/packages/meta/versions/1.9.1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
mime = fetchzip { mime = fetchzip {
sha256 = "1dr3qikzvp10q1saka7azki5gk2kkf2v7k9wfqjsyxmza2zlv896"; sha256 = "1dha9z64bsz8xhi0p62vmlyikr8xwbdlrw90hxghmm3rdgd9h25w";
url = "https://pub.dartlang.org/packages/mime/versions/1.0.2.tar.gz"; url = "https://pub.dartlang.org/packages/mime/versions/1.0.4.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
moxlib = fetchzip { moxlib = fetchzip {
sha256 = "1j52xglpwy8c7dbylc3f6vrh0p52xhhwqs4h0qcqk8c1rvjn5czq"; sha256 = "1qaacmcqhq33grn2nq8sn23ki62dcmw0fqy589xm1zv6w0pzfmsk";
url = "https://git.polynom.me/api/packages/moxxy/pub/api/packages/moxlib/files/0.1.5.tar.gz"; url = "https://git.polynom.me/api/packages/moxxy/pub/api/packages/moxlib/files/0.2.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
node_preamble = fetchzip { node_preamble = fetchzip {
sha256 = "0i0gfc2yqa09182vc01lj47qpq98kfm9m8h4n8c5fby0mjd0lvyx"; sha256 = "12ajg76r9aqmqkavvlxbnb3sszg1szcq3f30badkd0xc25mnhyh8";
url = "https://pub.dartlang.org/packages/node_preamble/versions/2.0.1.tar.gz"; url = "https://pub.dartlang.org/packages/node_preamble/versions/2.0.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
omemo_dart = fetchzip { omemo_dart = fetchzip {
sha256 = "09x3jqa11hjdjp31nxnz91j6jssbc2f8a1lh44fmkc0d79hs8bbi"; sha256 = "0fhf89ic5mdyld25l6rfb37a1fk1f0f2b4d72xi4r7pvr0ddjhz8";
url = "https://git.polynom.me/api/packages/PapaTutuWawa/pub/api/packages/omemo_dart/files/0.4.3.tar.gz"; url = "https://git.polynom.me/api/packages/PapaTutuWawa/pub/api/packages/omemo_dart/files/0.5.1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -301,8 +289,8 @@
}; };
path = fetchzip { path = fetchzip {
sha256 = "16ggdh29ciy7h8sdshhwmxn6dd12sfbykf2j82c56iwhhlljq181"; sha256 = "1mjdhq2fsz6i9krhp2mnaks2bcw34sa4p7mg0v6njk8dgx2754iv";
url = "https://pub.dartlang.org/packages/path/versions/1.8.2.tar.gz"; url = "https://pub.dartlang.org/packages/path/versions/1.8.3.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -315,8 +303,8 @@
}; };
petitparser = fetchzip { petitparser = fetchzip {
sha256 = "1pqqqqiy9ald24qsi24q9qrr0zphgpsrnrv9rlx4vwr6xak7d8c0"; sha256 = "19zqrpb1z77aw1k2s8rsxdfxczzv9934g2rdfj2jyiv3pqgdq8gh";
url = "https://pub.dartlang.org/packages/petitparser/versions/5.1.0.tar.gz"; url = "https://pub.dartlang.org/packages/petitparser/versions/5.4.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -335,16 +323,30 @@
extension = "tar.gz"; 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 { pub_semver = fetchzip {
sha256 = "1vsj5c1f2dza4l5zmjix4zh65lp8gsg6pw01h57pijx2id0g4bwi"; sha256 = "0wpcfz1crxipbjm18m71pl4vl2ra8vw1n93ff8snr54mmlyfb9z1";
url = "https://pub.dartlang.org/packages/pub_semver/versions/2.1.2.tar.gz"; url = "https://pub.dartlang.org/packages/pub_semver/versions/2.1.4.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
pubspec_parse = fetchzip { pubspec_parse = fetchzip {
sha256 = "19dmr9k4wsqjnhlzp1lbrw8dv7a1gnwmr8l5j9zlw407rmfg20d1"; sha256 = "0dj8sf1w61g7vh1ly3sl690z0nwllzjzbapxswmgsglr0ndcyrs1";
url = "https://pub.dartlang.org/packages/pubspec_parse/versions/1.2.1.tar.gz"; url = "https://pub.dartlang.org/packages/pubspec_parse/versions/1.2.3.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -364,43 +366,43 @@
}; };
shelf = fetchzip { shelf = fetchzip {
sha256 = "0x2xl7glrnq0hdxpy2i94a4wxbdrd6dm46hvhzgjn8alsm8z0wz1"; sha256 = "10yk98nadrgj5d3r3241kdaywjjs1j10mg8gacv80kg1mhcfdrxp";
url = "https://pub.dartlang.org/packages/shelf/versions/1.4.0.tar.gz"; url = "https://pub.dartlang.org/packages/shelf/versions/1.4.1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
shelf_packages_handler = fetchzip { shelf_packages_handler = fetchzip {
sha256 = "199rbdbifj46lg3iynznnsbs8zr4dfcw0s7wan8v73nvpqvli82q"; sha256 = "1h8s42nff9ar0xn7yb42m64lpvmqzq8wranqrkkixdnp7w3pmv1x";
url = "https://pub.dartlang.org/packages/shelf_packages_handler/versions/3.0.1.tar.gz"; url = "https://pub.dartlang.org/packages/shelf_packages_handler/versions/3.0.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
shelf_static = fetchzip { shelf_static = fetchzip {
sha256 = "1kqbaslz7bna9lldda3ibrjg0gczbzlwgm9cic8shg0bnl0v3s34"; sha256 = "1bcqynn2z2syrigmrclxgg8hjhd1x9742938i62cicbaga6vclaz";
url = "https://pub.dartlang.org/packages/shelf_static/versions/1.1.1.tar.gz"; url = "https://pub.dartlang.org/packages/shelf_static/versions/1.1.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
shelf_web_socket = fetchzip { shelf_web_socket = fetchzip {
sha256 = "0rr87nx2wdf9alippxiidqlgi82fbprnsarr1jswg9qin0yy4jpn"; sha256 = "110b5hrqwpnmq16shxxzjmcih5yfs5kh80dn8avfv0xj5iv7n94c";
url = "https://pub.dartlang.org/packages/shelf_web_socket/versions/1.0.3.tar.gz"; url = "https://pub.dartlang.org/packages/shelf_web_socket/versions/1.0.4.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
source_gen = fetchzip { source_gen = fetchzip {
sha256 = "1kxgx782lzpjhv736h0pz3lnxpcgiy05h0ysy0q77gix8q09i1hz"; sha256 = "1jql5zccv4vnbbvwcpyyvz8l27pg1rviqbp4vrks5313nf4b0kjg";
url = "https://pub.dartlang.org/packages/source_gen/versions/1.2.6.tar.gz"; url = "https://pub.dartlang.org/packages/source_gen/versions/1.3.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
source_helper = fetchzip { source_helper = fetchzip {
sha256 = "044kzmzlfpx93s4raz5avijahizmvai0zvl0lbm4wi93ynhdp1pd"; sha256 = "0mdd02vhcdcv9n58gzbx2q0bphwj0alz312ca1a8xpkf8jx3y8v4";
url = "https://pub.dartlang.org/packages/source_helper/versions/1.3.3.tar.gz"; url = "https://pub.dartlang.org/packages/source_helper/versions/1.3.4.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -413,29 +415,29 @@
}; };
source_maps = fetchzip { source_maps = fetchzip {
sha256 = "18ixrlz3l2alk3hp0884qj0mcgzhxmjpg6nq0n1200pfy62pc4z6"; sha256 = "004lcfka01agxjdw7zjhrffdkisvgx5s61b5gsl8qqk2jd1rswa7";
url = "https://pub.dartlang.org/packages/source_maps/versions/0.10.11.tar.gz"; url = "https://pub.dartlang.org/packages/source_maps/versions/0.10.12.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
source_span = fetchzip { source_span = fetchzip {
sha256 = "1lq4sy7lw15qsv9cijf6l48p16qr19r7njzwr4pxn8vv1kh6rb86"; sha256 = "1nybnf7l5chslp4fczhqnrgrhymy844lw7qrj6y08i626dshrd46";
url = "https://pub.dartlang.org/packages/source_span/versions/1.9.1.tar.gz"; url = "https://pub.dartlang.org/packages/source_span/versions/1.10.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
stack_trace = fetchzip { stack_trace = fetchzip {
sha256 = "0bggqvvpkrfvqz24bnir4959k0c45azc3zivk4lyv3mvba6092na"; sha256 = "0xpk2cvmgdh46iwip9jsb54fqx13jnina8pk03akxkmsxvag5izb";
url = "https://pub.dartlang.org/packages/stack_trace/versions/1.11.0.tar.gz"; url = "https://pub.dartlang.org/packages/stack_trace/versions/1.11.1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
stream_channel = fetchzip { stream_channel = fetchzip {
sha256 = "054by84c60yxphr3qgg6f82gg6d22a54aqjp265anlm8dwz1ji32"; sha256 = "0nrlw6zcscgnn6818krkbgs9qiv3f7q8pa7ljw1bqkrsb7xabm8s";
url = "https://pub.dartlang.org/packages/stream_channel/versions/2.1.1.tar.gz"; url = "https://pub.dartlang.org/packages/stream_channel/versions/2.1.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -455,8 +457,8 @@
}; };
synchronized = fetchzip { synchronized = fetchzip {
sha256 = "1j6108cq1hbcqpwhk9sah8q3gcidd7222bzhha2nk9syxhzqy82i"; sha256 = "1fx1z1p5qkn4qnq24riw5s86vmq645ppg8f74iyv2fc9rvr301ar";
url = "https://pub.dartlang.org/packages/synchronized/versions/3.0.0%2B2.tar.gz"; url = "https://pub.dartlang.org/packages/synchronized/versions/3.1.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -469,36 +471,36 @@
}; };
test = fetchzip { test = fetchzip {
sha256 = "08kimbjvkdw3bkj7za36p3yqdr8dnlb5v30c250kvdncb7k09h4x"; sha256 = "002phlj2pg6nll5hv449izxbqk29zwmwc77d0jx2iimz18dgy2r5";
url = "https://pub.dartlang.org/packages/test/versions/1.22.0.tar.gz"; url = "https://pub.dartlang.org/packages/test/versions/1.24.3.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
test_api = fetchzip { test_api = fetchzip {
sha256 = "0mfyjpqkkmaqdh7xygrydx12591wq9ll816f61n80dc6rmkdx7px"; sha256 = "0as1xcywjrd2zax3cm56qmnac12shf8c1ynnzzjwnggm23f61dxb";
url = "https://pub.dartlang.org/packages/test_api/versions/0.4.16.tar.gz"; url = "https://pub.dartlang.org/packages/test_api/versions/0.6.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
test_core = fetchzip { test_core = fetchzip {
sha256 = "1r8dnvkxxvh55z1c8lrsja1m0dkf5i4lgwwqixcx0mqvxx5w3005"; sha256 = "1cx2rmz1xzk5z5yh8fpbsrsz4mgjanrw4xvnp0qzdnm2d7vhaq0y";
url = "https://pub.dartlang.org/packages/test_core/versions/0.4.20.tar.gz"; url = "https://pub.dartlang.org/packages/test_core/versions/0.5.3.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
timing = fetchzip { timing = fetchzip {
sha256 = "0a02znvy0fbzr0n4ai67pp8in7w6m768aynkk1kp5lnmgy17ppsg"; sha256 = "15jvxsw7v0gwbdlykma60l1qlhlzb3brh6m0sg2bgbfir4l5s9gw";
url = "https://pub.dartlang.org/packages/timing/versions/1.0.0.tar.gz"; url = "https://pub.dartlang.org/packages/timing/versions/1.0.1.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
typed_data = fetchzip { typed_data = fetchzip {
sha256 = "1x402bvyzdmdvmyqhyfamjxf54p9j8sa8ns2n5dwsdhnfqbw859g"; sha256 = "0q6ggc52vfpr8kqaq69h757wy942hvgshhnsr2pjdinb4zk2sxl1";
url = "https://pub.dartlang.org/packages/typed_data/versions/1.3.1.tar.gz"; url = "https://pub.dartlang.org/packages/typed_data/versions/1.3.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -511,8 +513,8 @@
}; };
uuid = fetchzip { uuid = fetchzip {
sha256 = "12lsynr07lw9848jknmzxvzn3ia12xdj07iiva0vg0qjvpq7ladg"; sha256 = "1nh1hxfr6bhyadqqcxrpwrphmm75f1iq4rzfjdwa2486xwlh7vx3";
url = "https://pub.dartlang.org/packages/uuid/versions/3.0.5.tar.gz"; url = "https://pub.dartlang.org/packages/uuid/versions/3.0.7.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -525,8 +527,8 @@
}; };
vm_service = fetchzip { vm_service = fetchzip {
sha256 = "05xaxaxzyfls6jklw1hzws2jmina1cjk10gbl7a63djh1ghnzjb5"; sha256 = "15ail7rbaq9ksg73cc0mw2k5imbiidl95yfd4v49k81gp5xmj92w";
url = "https://pub.dartlang.org/packages/vm_service/versions/9.4.0.tar.gz"; url = "https://pub.dartlang.org/packages/vm_service/versions/11.10.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -539,8 +541,8 @@
}; };
web_socket_channel = fetchzip { web_socket_channel = fetchzip {
sha256 = "147amn05v1f1a1grxjr7yzgshrczjwijwiywggsv6dgic8kxyj5a"; sha256 = "0f9441c4zifb5qadpjg319dcilimpkdhfacnkl543802bf8qjn4w";
url = "https://pub.dartlang.org/packages/web_socket_channel/versions/2.2.0.tar.gz"; url = "https://pub.dartlang.org/packages/web_socket_channel/versions/2.4.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
@@ -553,262 +555,262 @@
}; };
xml = fetchzip { xml = fetchzip {
sha256 = "0jwknkfcnb5svg6r01xjsj0aiw06mlx54pgay1ymaaqm2mjhyz01"; sha256 = "120azx71gazvrrn07vd83vrffzrhsqnmf9rdjxl73rra9py8ixiy";
url = "https://pub.dartlang.org/packages/xml/versions/6.2.0.tar.gz"; url = "https://pub.dartlang.org/packages/xml/versions/6.3.0.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
yaml = fetchzip { yaml = fetchzip {
sha256 = "0mqqmzn3c9rr38b5xm312fz1vyp6vb36lm477r9hak77bxzpp0iw"; sha256 = "0awh9dynbhrlq8zgszaiyxbyyn9b6wyps1zww4z2lx62nbma0pda";
url = "https://pub.dartlang.org/packages/yaml/versions/3.1.1.tar.gz"; url = "https://pub.dartlang.org/packages/yaml/versions/3.1.2.tar.gz";
stripRoot = false; stripRoot = false;
extension = "tar.gz"; extension = "tar.gz";
}; };
pubCache = runCommand "moxxmpp-pub-cache" {} '' pubCache = runCommand "moxxmpp-pub-cache" {} ''
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${_fe_analyzer_shared} $out/hosted/pub.dartlang.org/_fe_analyzer_shared-50.0.0 ln -s ${_fe_analyzer_shared} $out/hosted/pub.dev/_fe_analyzer_shared-61.0.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${analyzer} $out/hosted/pub.dartlang.org/analyzer-5.2.0 ln -s ${analyzer} $out/hosted/pub.dev/analyzer-5.13.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${args} $out/hosted/pub.dartlang.org/args-2.3.1 ln -s ${args} $out/hosted/pub.dev/args-2.4.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${async} $out/hosted/pub.dartlang.org/async-2.10.0 ln -s ${async} $out/hosted/pub.dev/async-2.11.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${boolean_selector} $out/hosted/pub.dartlang.org/boolean_selector-2.1.1 ln -s ${boolean_selector} $out/hosted/pub.dev/boolean_selector-2.1.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${build} $out/hosted/pub.dartlang.org/build-2.3.1 ln -s ${build} $out/hosted/pub.dev/build-2.3.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${build_config} $out/hosted/pub.dartlang.org/build_config-1.1.1 ln -s ${build_config} $out/hosted/pub.dev/build_config-1.1.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${build_daemon} $out/hosted/pub.dartlang.org/build_daemon-3.1.0 ln -s ${build_daemon} $out/hosted/pub.dev/build_daemon-3.1.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${build_resolvers} $out/hosted/pub.dartlang.org/build_resolvers-2.1.0 ln -s ${build_resolvers} $out/hosted/pub.dev/build_resolvers-2.3.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${build_runner} $out/hosted/pub.dartlang.org/build_runner-2.3.2 ln -s ${build_runner} $out/hosted/pub.dev/build_runner-2.3.3
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${build_runner_core} $out/hosted/pub.dartlang.org/build_runner_core-7.2.7 ln -s ${build_runner_core} $out/hosted/pub.dev/build_runner_core-7.2.7+1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${built_collection} $out/hosted/pub.dartlang.org/built_collection-5.1.1 ln -s ${built_collection} $out/hosted/pub.dev/built_collection-5.1.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${built_value} $out/hosted/pub.dartlang.org/built_value-8.4.2 ln -s ${built_value} $out/hosted/pub.dev/built_value-8.6.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${checked_yaml} $out/hosted/pub.dartlang.org/checked_yaml-2.0.1 ln -s ${checked_yaml} $out/hosted/pub.dev/checked_yaml-2.0.3
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${code_builder} $out/hosted/pub.dartlang.org/code_builder-4.3.0 ln -s ${code_builder} $out/hosted/pub.dev/code_builder-4.6.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${collection} $out/hosted/pub.dartlang.org/collection-1.17.0 ln -s ${collection} $out/hosted/pub.dev/collection-1.18.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${convert} $out/hosted/pub.dartlang.org/convert-3.1.1 ln -s ${convert} $out/hosted/pub.dev/convert-3.1.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${coverage} $out/hosted/pub.dartlang.org/coverage-1.6.1 ln -s ${coverage} $out/hosted/pub.dev/coverage-1.6.3
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${crypto} $out/hosted/pub.dartlang.org/crypto-3.0.2 ln -s ${crypto} $out/hosted/pub.dev/crypto-3.0.3
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${cryptography} $out/hosted/pub.dartlang.org/cryptography-2.0.5 ln -s ${cryptography} $out/hosted/pub.dev/cryptography-2.5.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${dart_style} $out/hosted/pub.dartlang.org/dart_style-2.2.4 ln -s ${dart_style} $out/hosted/pub.dev/dart_style-2.3.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${file} $out/hosted/pub.dartlang.org/file-6.1.4 ln -s ${file} $out/hosted/pub.dev/file-6.1.4
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${fixnum} $out/hosted/pub.dartlang.org/fixnum-1.0.1 ln -s ${fixnum} $out/hosted/pub.dev/fixnum-1.1.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${freezed} $out/hosted/pub.dartlang.org/freezed-2.1.1 ln -s ${frontend_server_client} $out/hosted/pub.dev/frontend_server_client-3.2.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${freezed_annotation} $out/hosted/pub.dartlang.org/freezed_annotation-2.1.0 ln -s ${glob} $out/hosted/pub.dev/glob-2.1.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${frontend_server_client} $out/hosted/pub.dartlang.org/frontend_server_client-3.1.0 ln -s ${graphs} $out/hosted/pub.dev/graphs-2.3.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${glob} $out/hosted/pub.dartlang.org/glob-2.1.0 ln -s ${hex} $out/hosted/pub.dev/hex-0.2.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${graphs} $out/hosted/pub.dartlang.org/graphs-2.2.0 ln -s ${http_multi_server} $out/hosted/pub.dev/http_multi_server-3.2.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${hex} $out/hosted/pub.dartlang.org/hex-0.2.0 ln -s ${http_parser} $out/hosted/pub.dev/http_parser-4.0.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${http_multi_server} $out/hosted/pub.dartlang.org/http_multi_server-3.2.1 ln -s ${io} $out/hosted/pub.dev/io-1.0.4
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${http_parser} $out/hosted/pub.dartlang.org/http_parser-4.0.2 ln -s ${js} $out/hosted/pub.dev/js-0.6.7
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${io} $out/hosted/pub.dartlang.org/io-1.0.3 ln -s ${json_annotation} $out/hosted/pub.dev/json_annotation-4.8.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${js} $out/hosted/pub.dartlang.org/js-0.6.5 ln -s ${json_serializable} $out/hosted/pub.dev/json_serializable-6.6.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${json_annotation} $out/hosted/pub.dartlang.org/json_annotation-4.7.0 ln -s ${logging} $out/hosted/pub.dev/logging-1.2.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${json_serializable} $out/hosted/pub.dartlang.org/json_serializable-6.5.4 ln -s ${matcher} $out/hosted/pub.dev/matcher-0.12.16
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${logging} $out/hosted/pub.dartlang.org/logging-1.0.2 ln -s ${meta} $out/hosted/pub.dev/meta-1.9.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${matcher} $out/hosted/pub.dartlang.org/matcher-0.12.13 ln -s ${mime} $out/hosted/pub.dev/mime-1.0.4
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/git.polynom.me%47api%47packages%47Moxxy%47pub%47 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 mkdir -p $out/hosted/pub.dev
ln -s ${node_preamble} $out/hosted/pub.dartlang.org/node_preamble-2.0.1 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 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 mkdir -p $out/hosted/pub.dev
ln -s ${package_config} $out/hosted/pub.dartlang.org/package_config-2.1.0 ln -s ${package_config} $out/hosted/pub.dev/package_config-2.1.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${path} $out/hosted/pub.dartlang.org/path-1.8.2 ln -s ${path} $out/hosted/pub.dev/path-1.8.3
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${pedantic} $out/hosted/pub.dartlang.org/pedantic-1.11.1 ln -s ${pedantic} $out/hosted/pub.dev/pedantic-1.11.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${petitparser} $out/hosted/pub.dartlang.org/petitparser-5.1.0 ln -s ${petitparser} $out/hosted/pub.dev/petitparser-5.4.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${pinenacl} $out/hosted/pub.dartlang.org/pinenacl-0.5.1 ln -s ${pinenacl} $out/hosted/pub.dev/pinenacl-0.5.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${pool} $out/hosted/pub.dartlang.org/pool-1.5.1 ln -s ${pool} $out/hosted/pub.dev/pool-1.5.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${pub_semver} $out/hosted/pub.dartlang.org/pub_semver-2.1.2 ln -s ${protobuf} $out/hosted/pub.dev/protobuf-2.1.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${pubspec_parse} $out/hosted/pub.dartlang.org/pubspec_parse-1.2.1 ln -s ${protoc_plugin} $out/hosted/pub.dev/protoc_plugin-20.0.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${random_string} $out/hosted/pub.dartlang.org/random_string-2.3.1 ln -s ${pub_semver} $out/hosted/pub.dev/pub_semver-2.1.4
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${saslprep} $out/hosted/pub.dartlang.org/saslprep-1.0.2 ln -s ${pubspec_parse} $out/hosted/pub.dev/pubspec_parse-1.2.3
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${shelf} $out/hosted/pub.dartlang.org/shelf-1.4.0 ln -s ${random_string} $out/hosted/pub.dev/random_string-2.3.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${shelf_packages_handler} $out/hosted/pub.dartlang.org/shelf_packages_handler-3.0.1 ln -s ${saslprep} $out/hosted/pub.dev/saslprep-1.0.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${shelf_static} $out/hosted/pub.dartlang.org/shelf_static-1.1.1 ln -s ${shelf} $out/hosted/pub.dev/shelf-1.4.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${shelf_web_socket} $out/hosted/pub.dartlang.org/shelf_web_socket-1.0.3 ln -s ${shelf_packages_handler} $out/hosted/pub.dev/shelf_packages_handler-3.0.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${source_gen} $out/hosted/pub.dartlang.org/source_gen-1.2.6 ln -s ${shelf_static} $out/hosted/pub.dev/shelf_static-1.1.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${source_helper} $out/hosted/pub.dartlang.org/source_helper-1.3.3 ln -s ${shelf_web_socket} $out/hosted/pub.dev/shelf_web_socket-1.0.4
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${source_map_stack_trace} $out/hosted/pub.dartlang.org/source_map_stack_trace-2.1.1 ln -s ${source_gen} $out/hosted/pub.dev/source_gen-1.3.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${source_maps} $out/hosted/pub.dartlang.org/source_maps-0.10.11 ln -s ${source_helper} $out/hosted/pub.dev/source_helper-1.3.4
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${source_span} $out/hosted/pub.dartlang.org/source_span-1.9.1 ln -s ${source_map_stack_trace} $out/hosted/pub.dev/source_map_stack_trace-2.1.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${stack_trace} $out/hosted/pub.dartlang.org/stack_trace-1.11.0 ln -s ${source_maps} $out/hosted/pub.dev/source_maps-0.10.12
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${stream_channel} $out/hosted/pub.dartlang.org/stream_channel-2.1.1 ln -s ${source_span} $out/hosted/pub.dev/source_span-1.10.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${stream_transform} $out/hosted/pub.dartlang.org/stream_transform-2.1.0 ln -s ${stack_trace} $out/hosted/pub.dev/stack_trace-1.11.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${string_scanner} $out/hosted/pub.dartlang.org/string_scanner-1.2.0 ln -s ${stream_channel} $out/hosted/pub.dev/stream_channel-2.1.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${synchronized} $out/hosted/pub.dartlang.org/synchronized-3.0.0+2 ln -s ${stream_transform} $out/hosted/pub.dev/stream_transform-2.1.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${term_glyph} $out/hosted/pub.dartlang.org/term_glyph-1.2.1 ln -s ${string_scanner} $out/hosted/pub.dev/string_scanner-1.2.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${test} $out/hosted/pub.dartlang.org/test-1.22.0 ln -s ${synchronized} $out/hosted/pub.dev/synchronized-3.1.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${test_api} $out/hosted/pub.dartlang.org/test_api-0.4.16 ln -s ${term_glyph} $out/hosted/pub.dev/term_glyph-1.2.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${test_core} $out/hosted/pub.dartlang.org/test_core-0.4.20 ln -s ${test} $out/hosted/pub.dev/test-1.24.3
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${timing} $out/hosted/pub.dartlang.org/timing-1.0.0 ln -s ${test_api} $out/hosted/pub.dev/test_api-0.6.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${typed_data} $out/hosted/pub.dartlang.org/typed_data-1.3.1 ln -s ${test_core} $out/hosted/pub.dev/test_core-0.5.3
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${unorm_dart} $out/hosted/pub.dartlang.org/unorm_dart-0.2.0 ln -s ${timing} $out/hosted/pub.dev/timing-1.0.1
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${uuid} $out/hosted/pub.dartlang.org/uuid-3.0.5 ln -s ${typed_data} $out/hosted/pub.dev/typed_data-1.3.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${very_good_analysis} $out/hosted/pub.dartlang.org/very_good_analysis-3.1.0 ln -s ${unorm_dart} $out/hosted/pub.dev/unorm_dart-0.2.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${vm_service} $out/hosted/pub.dartlang.org/vm_service-9.4.0 ln -s ${uuid} $out/hosted/pub.dev/uuid-3.0.7
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${watcher} $out/hosted/pub.dartlang.org/watcher-1.0.2 ln -s ${very_good_analysis} $out/hosted/pub.dev/very_good_analysis-3.1.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${web_socket_channel} $out/hosted/pub.dartlang.org/web_socket_channel-2.2.0 ln -s ${vm_service} $out/hosted/pub.dev/vm_service-11.10.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${webkit_inspection_protocol} $out/hosted/pub.dartlang.org/webkit_inspection_protocol-1.2.0 ln -s ${watcher} $out/hosted/pub.dev/watcher-1.0.2
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${xml} $out/hosted/pub.dartlang.org/xml-6.2.0 ln -s ${web_socket_channel} $out/hosted/pub.dev/web_socket_channel-2.4.0
mkdir -p $out/hosted/pub.dartlang.org mkdir -p $out/hosted/pub.dev
ln -s ${yaml} $out/hosted/pub.dartlang.org/yaml-3.1.1 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,6 +1,34 @@
## 0.3.2 ## 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. - **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.
- **BREAKING**: Changed order of parameters of `CryptographicHashManager.hashFromData`
- **BREAKING**: Removed support for XEP-0414, as the (supported) hash computations are already implemented by `CryptographicHashManager.hashFromData`.
- The `DiscoManager` now only handled entity capabilities if a `EntityCapabilityManager` is registered.
- The `EntityCapabilityManager` now verifies and validates its data before caching.
- **BREAKING**: Added the `resumed` parameter to `StreamNegotiationsDoneEvent`. Use this to check if the current stream is new or resumed instead of using the `ConnectionStateChangedEvent`.
- **BREAKING**: Remove `DiscoManager.discoInfoCapHashQuery`.
- **BREAKING**: The entity argument of `DiscoManager.discoInfoQuery` and `DiscoManager.discoItemsQuery` are now `JID` instead of `String`.
- **BREAKING**: `PubSubManager` and `UserAvatarManager` now use `JID` instead of `String`.
- **BREAKING**: `XmppConnection.sendStanza` not only takes a `StanzaDetails` argument.
- Sent stanzas are now kept in a queue until sent.
- **BREAKING**: `MessageManager.sendMessage` does not use `MessageDetails` anymore. Instead, use `TypedMap`.
- `MessageManager` now allows registering callbacks for adding data whenever a message is sent.
- **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 ## 0.3.1

View File

@@ -18,7 +18,6 @@ export 'package:moxxmpp/src/managers/namespaces.dart';
export 'package:moxxmpp/src/managers/priorities.dart'; export 'package:moxxmpp/src/managers/priorities.dart';
export 'package:moxxmpp/src/message.dart'; export 'package:moxxmpp/src/message.dart';
export 'package:moxxmpp/src/namespaces.dart'; export 'package:moxxmpp/src/namespaces.dart';
export 'package:moxxmpp/src/negotiators/manager.dart';
export 'package:moxxmpp/src/negotiators/namespaces.dart'; export 'package:moxxmpp/src/negotiators/namespaces.dart';
export 'package:moxxmpp/src/negotiators/negotiator.dart'; export 'package:moxxmpp/src/negotiators/negotiator.dart';
export 'package:moxxmpp/src/ping.dart'; export 'package:moxxmpp/src/ping.dart';
@@ -39,15 +38,17 @@ export 'package:moxxmpp/src/settings.dart';
export 'package:moxxmpp/src/socket.dart'; export 'package:moxxmpp/src/socket.dart';
export 'package:moxxmpp/src/stanza.dart'; export 'package:moxxmpp/src/stanza.dart';
export 'package:moxxmpp/src/stringxml.dart'; export 'package:moxxmpp/src/stringxml.dart';
export 'package:moxxmpp/src/types/result.dart'; export 'package:moxxmpp/src/util/typed_map.dart';
export 'package:moxxmpp/src/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/staging/file_upload_notification.dart';
export 'package:moxxmpp/src/xeps/xep_0004.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/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0030/helpers.dart'; export 'package:moxxmpp/src/xeps/xep_0030/helpers.dart';
export 'package:moxxmpp/src/xeps/xep_0030/types.dart'; export 'package:moxxmpp/src/xeps/xep_0030/types.dart';
export 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; export 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
export 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0045/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_0054.dart';
export 'package:moxxmpp/src/xeps/xep_0060/errors.dart'; export 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0060/helpers.dart'; export 'package:moxxmpp/src/xeps/xep_0060/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/state.dart';
export 'package:moxxmpp/src/xeps/xep_0198/xep_0198.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_0203.dart';
export 'package:moxxmpp/src/xeps/xep_0264.dart';
export 'package:moxxmpp/src/xeps/xep_0280.dart'; export 'package:moxxmpp/src/xeps/xep_0280.dart';
export 'package:moxxmpp/src/xeps/xep_0297.dart'; export 'package:moxxmpp/src/xeps/xep_0297.dart';
export 'package:moxxmpp/src/xeps/xep_0300.dart'; export 'package:moxxmpp/src/xeps/xep_0300.dart';
@@ -85,7 +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/negotiators.dart';
export 'package:moxxmpp/src/xeps/xep_0388/user_agent.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_0388/xep_0388.dart';
export 'package:moxxmpp/src/xeps/xep_0414.dart'; export 'package:moxxmpp/src/xeps/xep_0421.dart';
export 'package:moxxmpp/src/xeps/xep_0424.dart'; export 'package:moxxmpp/src/xeps/xep_0424.dart';
export 'package:moxxmpp/src/xeps/xep_0444.dart'; export 'package:moxxmpp/src/xeps/xep_0444.dart';
export 'package:moxxmpp/src/xeps/xep_0446.dart'; export 'package:moxxmpp/src/xeps/xep_0446.dart';
@@ -93,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_0448.dart';
export 'package:moxxmpp/src/xeps/xep_0449.dart'; export 'package:moxxmpp/src/xeps/xep_0449.dart';
export 'package:moxxmpp/src/xeps/xep_0461.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 'dart:async';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
/// A surrogate key for awaiting stanzas. /// (JID we sent a stanza to, the id of the sent stanza, the tag of the sent stanza).
@immutable // ignore: avoid_private_typedef_functions
class _StanzaSurrogateKey { typedef _StanzaCompositeKey = (String?, String, String);
const _StanzaSurrogateKey(this.sentTo, this.id, this.tag);
/// The JID the original stanza was sent to. We expect the result to come from the /// Callback function that returns the bare JID of the connection as a String.
/// same JID. typedef GetBareJidCallback = String Function();
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;
}
}
/// This class handles the await semantics for stanzas. Stanzas are given a "unique" /// 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. /// 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. /// This class also handles some "edge cases" of RFC 6120, like an empty "from" attribute.
class StanzaAwaiter { class StanzaAwaiter {
StanzaAwaiter(this._bareJidCallback);
final GetBareJidCallback _bareJidCallback;
/// The pending stanzas, identified by their surrogate key. /// 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]. /// The critical section for accessing [StanzaAwaiter._pending].
final Lock _lock = Lock(); final Lock _lock = Lock();
@@ -52,30 +34,33 @@ class StanzaAwaiter {
/// [tag] is the stanza's tag name. /// [tag] is the stanza's tag name.
/// ///
/// Returns a future that might resolve to the response to the stanza. /// 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 = await _lock.synchronized(() {
final completer = Completer<XMLNode>(); final completer = Completer<XMLNode>();
_pending[_StanzaSurrogateKey(to, id, tag)] = completer; _pending[(processedTo, id, tag)] = completer;
return completer; return completer;
}); });
return completer.future; return completer.future;
} }
/// Checks if the stanza [stanza] is being awaited. [bareJid] is the bare JID of /// Checks if the stanza [stanza] is being awaited.
/// the connection.
/// If [stanza] is awaited, resolves the future and returns true. If not, returns /// If [stanza] is awaited, resolves the future and returns true. If not, returns
/// false. /// false.
Future<bool> onData(XMLNode stanza, JID bareJid) async { Future<bool> onData(XMLNode stanza) async {
assert(bareJid.isBare(), 'bareJid must be bare');
final id = stanza.attributes['id'] as String?; final id = stanza.attributes['id'] as String?;
if (id == null) return false; if (id == null) return false;
final key = _StanzaSurrogateKey( // Check if we want to send a stanza to our bare JID and replace it with null.
// Section 8.1.2.1 § 3 of RFC 6120 says that an empty "from" indicates that the final from = stanza.attributes['from'] as String?;
// attribute is implicitly from our own bare JID. final processedFrom =
stanza.attributes['from'] as String? ?? bareJid.toString(), from != null && from == _bareJidCallback() ? null : from;
final key = (
processedFrom,
id, id,
stanza.tag, stanza.tag,
); );
@@ -91,4 +76,19 @@ class StanzaAwaiter {
return false; 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

@@ -15,7 +15,6 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/parser.dart'; import 'package:moxxmpp/src/parser.dart';
import 'package:moxxmpp/src/presence.dart'; import 'package:moxxmpp/src/presence.dart';
@@ -26,12 +25,12 @@ import 'package:moxxmpp/src/settings.dart';
import 'package:moxxmpp/src/socket.dart'; import 'package:moxxmpp/src/socket.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/util/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_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:moxxmpp/src/xeps/xep_0352.dart'; import 'package:moxxmpp/src/xeps/xep_0352.dart';
import 'package:synchronized/synchronized.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
/// The states the XmppConnection can be in /// The states the XmppConnection can be in
@@ -50,16 +49,17 @@ enum XmppConnectionState {
error error
} }
/// Metadata for [XmppConnection.sendStanza]. /// (The actual stanza handler, Name of the owning manager).
enum StanzaFromType { typedef _StanzaHandlerWrapper = (StanzaHandler, String);
/// Add the full JID to the stanza as the from attribute
full,
/// Add the bare JID to the stanza as the from attribute /// Wrapper around [stanzaHandlerSortComparator] for [_StanzaHandlerWrapper].
bare, int _stanzaHandlerWrapperSortComparator(
_StanzaHandlerWrapper a,
/// Add no JID as the from attribute _StanzaHandlerWrapper b,
none, ) {
final (ha, _) = a;
final (hb, _) = b;
return stanzaHandlerSortComparator(ha, hb);
} }
/// This class is a connection to the server. /// This class is a connection to the server.
@@ -71,7 +71,11 @@ class XmppConnection {
this._socket, { this._socket, {
this.connectingTimeout = const Duration(minutes: 2), this.connectingTimeout = const Duration(minutes: 2),
}) : _reconnectionPolicy = reconnectionPolicy, }) : _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 // Allow the reconnection policy to perform reconnections by itself
_reconnectionPolicy.register( _reconnectionPolicy.register(
_attemptReconnection, _attemptReconnection,
@@ -84,12 +88,27 @@ class XmppConnection {
() => _isAuthenticated, () => _isAuthenticated,
sendRawXML, sendRawXML,
() => connectionSettings, () => connectionSettings,
() {
_log.finest('Resetting stream parser');
_streamParser.reset();
},
); );
_stanzaAwaiter = StanzaAwaiter(
() => connectionSettings.jid.toBare().toString(),
);
_incomingStanzaQueue = IncomingStanzaQueue(handleXmlStream, _stanzaAwaiter);
_socketStream = _socket.getDataStream(); _socketStream = _socket.getDataStream();
// TODO(Unknown): Handle on done _socketStream
_socketStream.transform(_streamParser).forEach(handleXmlStream); .transform(_streamParser)
_socket.getEventStream().listen(_handleSocketEvent); .forEach(_incomingStanzaQueue.addStanza);
_socketStream.listen(_handleOnDataCallbacks);
_socket.getEventStream().listen(handleSocketEvent);
_stanzaQueue = AsyncStanzaQueue(
_sendStanzaImpl,
_canSendData,
);
} }
/// The state that the connection is currently in /// The state that the connection is currently in
@@ -113,16 +132,16 @@ class XmppConnection {
final ConnectivityManager _connectivityManager; final ConnectivityManager _connectivityManager;
/// A helper for handling await semantics with stanzas /// 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 /// Sorted list of handlers that we call or incoming and outgoing stanzas
final List<StanzaHandler> _incomingStanzaHandlers = final List<_StanzaHandlerWrapper> _incomingStanzaHandlers =
List.empty(growable: true); List.empty(growable: true);
final List<StanzaHandler> _incomingPreStanzaHandlers = final List<_StanzaHandlerWrapper> _incomingPreStanzaHandlers =
List.empty(growable: true); List.empty(growable: true);
final List<StanzaHandler> _outgoingPreStanzaHandlers = final List<_StanzaHandlerWrapper> _outgoingPreStanzaHandlers =
List.empty(growable: true); List.empty(growable: true);
final List<StanzaHandler> _outgoingPostStanzaHandlers = final List<_StanzaHandlerWrapper> _outgoingPostStanzaHandlers =
List.empty(growable: true); List.empty(growable: true);
final StreamController<XmppEvent> _eventStreamController = final StreamController<XmppEvent> _eventStreamController =
StreamController.broadcast(); StreamController.broadcast();
@@ -161,10 +180,6 @@ class XmppConnection {
T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) => T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) =>
_negotiationsHandler.getNegotiatorById<T>(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 /// The logger for the class
final Logger _log = Logger('XmppConnection'); final Logger _log = Logger('XmppConnection');
@@ -173,6 +188,10 @@ class XmppConnection {
bool get isAuthenticated => _isAuthenticated; 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. /// Returns the JID we authenticate with and add the resource that we have bound.
JID _getJidWithResource() { JID _getJidWithResource() {
assert(_resource.isNotEmpty, 'The resource must not be empty'); assert(_resource.isNotEmpty, 'The resource must not be empty');
@@ -200,18 +219,25 @@ class XmppConnection {
_xmppManagers[manager.id] = manager; _xmppManagers[manager.id] = manager;
_incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers()); _incomingStanzaHandlers.addAll(
_incomingPreStanzaHandlers.addAll(manager.getIncomingPreStanzaHandlers()); manager.getIncomingStanzaHandlers().map((h) => (h, manager.name)),
_outgoingPreStanzaHandlers.addAll(manager.getOutgoingPreStanzaHandlers()); );
_outgoingPostStanzaHandlers _incomingPreStanzaHandlers.addAll(
.addAll(manager.getOutgoingPostStanzaHandlers()); 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 // Sort them
_incomingStanzaHandlers.sort(stanzaHandlerSortComparator); _incomingStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
_incomingPreStanzaHandlers.sort(stanzaHandlerSortComparator); _incomingPreStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
_outgoingPreStanzaHandlers.sort(stanzaHandlerSortComparator); _outgoingPreStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
_outgoingPostStanzaHandlers.sort(stanzaHandlerSortComparator); _outgoingPostStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
// Run the post register callbacks // Run the post register callbacks
for (final manager in _xmppManagers.values) { for (final manager in _xmppManagers.values) {
@@ -292,6 +318,13 @@ class XmppConnection {
return getManagerById(csiManager); 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. /// Attempts to reconnect to the server by following an exponential backoff.
Future<void> _attemptReconnection() async { Future<void> _attemptReconnection() async {
_log.finest('_attemptReconnection: Setting state to notConnected'); _log.finest('_attemptReconnection: Setting state to notConnected');
@@ -314,7 +347,7 @@ class XmppConnection {
/// Called when a stream ending error has occurred /// Called when a stream ending error has occurred
Future<void> handleError(XmppError error) async { 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 // 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 // the connection result is being awaited, don't attempt a reconnection but instead
@@ -364,7 +397,8 @@ class XmppConnection {
} }
/// Called whenever the socket creates an event /// Called whenever the socket creates an event
Future<void> _handleSocketEvent(XmppSocketEvent event) async { @visibleForTesting
Future<void> handleSocketEvent(XmppSocketEvent event) async {
if (event is XmppSocketErrorEvent) { if (event is XmppSocketErrorEvent) {
await handleError(SocketError(event)); await handleError(SocketError(event));
} else if (event is XmppSocketClosureEvent) { } else if (event is XmppSocketClosureEvent) {
@@ -406,137 +440,144 @@ class XmppConnection {
/// Returns true if we can send data through the socket. /// Returns true if we can send data through the socket.
Future<bool> _canSendData() async { Future<bool> _canSendData() async {
return [XmppConnectionState.connected, XmppConnectionState.connecting] return await getConnectionState() == XmppConnectionState.connected;
.contains(await getConnectionState());
} }
/// Sends a [stanza] to the server. If stream management is enabled, then keeping track /// Sends a stanza described by [details] to the server. Until sent, the stanza is
/// of the stanza is taken care of. Returns a Future that resolves when we receive a /// kept in a queue, that is flushed after going online again. If Stream Management
/// response to the stanza. /// is active, stanza's acknowledgement is tracked.
///
/// If addFrom is true, then a 'from' attribute will be added to the stanza if
/// [stanza] has none.
/// If addId is true, then an 'id' attribute will be added to the stanza if [stanza] has
/// none.
// TODO(Unknown): if addId = false, the function crashes. // TODO(Unknown): if addId = false, the function crashes.
Future<XMLNode> sendStanza( Future<XMLNode?> sendStanza(StanzaDetails details) async {
Stanza stanza, {
StanzaFromType addFrom = StanzaFromType.full,
bool addId = true,
bool awaitable = true,
bool encrypted = false,
bool forceEncryption = false,
}) async {
assert( assert(
implies(addId == false && stanza.id == null, !awaitable), implies(
'Cannot await a stanza with no id', details.awaitable,
details.stanza.id != null && details.stanza.id!.isNotEmpty ||
details.addId,
),
'An awaitable stanza must have an id',
); );
// Add extra data in case it was not set final completer = details.awaitable ? Completer<XMLNode>() : null;
var stanza_ = stanza; final entry = StanzaQueueEntry(
if (addId && (stanza_.id == null || stanza_.id == '')) { details,
stanza_ = stanza.copyWith(id: generateId()); completer,
);
if (details.bypassQueue) {
await _sendStanzaImpl(entry);
} else {
await _stanzaQueue.enqueueStanza(entry);
} }
if (addFrom != StanzaFromType.none &&
(stanza_.from == null || stanza_.from == '')) { return completer?.future;
switch (addFrom) { }
case StanzaFromType.full:
{ Future<void> _sendStanzaImpl(StanzaQueueEntry entry) async {
stanza_ = stanza_.copyWith( final details = entry.details;
from: _getJidWithResource().toString(), var newStanza = details.stanza;
);
} // Generate an id, if requested
break; if (details.addId && (newStanza.id == null || newStanza.id == '')) {
case StanzaFromType.bare: newStanza = newStanza.copyWith(id: generateId());
{
stanza_ = stanza_.copyWith(
from: connectionSettings.jid.toBare().toString(),
);
}
break;
case StanzaFromType.none:
break;
}
} }
stanza_ = stanza_.copyWith(
// NOTE: Originally, we handled adding a "from" attribute to the stanza here.
// However, this is not neccessary as RFC 6120 states:
//
// > When a server receives an XML stanza from a connected client, the
// > server MUST add a 'from' attribute to the stanza or override the
// > 'from' attribute specified by the client, where the value of the
// > 'from' attribute MUST be the full JID
// > (<localpart@domainpart/resource>) determined by the server for
// > the connected resource that generated the stanza (see
// > Section 4.3.6), or the bare JID (<localpart@domainpart>) in the
// > case of subscription-related presence stanzas (see [XMPP-IM]).
//
// This means that even if we add a "from" attribute, the server will discard
// it. If we don't specify it, then the server will add the correct value
// itself.
// Add the correct stanza namespace
newStanza = newStanza.copyWith(
xmlns: _negotiationsHandler.getStanzaNamespace(), xmlns: _negotiationsHandler.getStanzaNamespace(),
); );
// Run pre-send handlers
_log.fine('Running pre stanza handlers..'); _log.fine('Running pre stanza handlers..');
final data = await _runOutgoingPreStanzaHandlers( final data = await _runOutgoingPreStanzaHandlers(
stanza_, newStanza,
initial: StanzaHandlerData( initial: StanzaHandlerData(
false, false,
false, false,
null, newStanza,
stanza_, details.extensions ?? TypedMap(),
encrypted: encrypted, encrypted: details.encrypted,
forceEncryption: forceEncryption, shouldEncrypt: details.shouldEncrypt,
forceEncryption: details.forceEncryption,
), ),
); );
_log.fine('Done'); _log.fine('Done');
// Cancel sending, if the pre-send handlers indicated it.
if (data.cancel) { if (data.cancel) {
_log.fine('A stanza handler indicated that it wants to cancel sending.'); _log.fine('A stanza handler indicated that it wants to cancel sending.');
await _sendEvent(StanzaSendingCancelledEvent(data)); await _sendEvent(StanzaSendingCancelledEvent(data));
return Stanza(
tag: data.stanza.tag, // Resolve the future, if one was given.
to: data.stanza.from, if (details.awaitable) {
from: data.stanza.to, entry.completer!.complete(
attributes: <String, String>{ Stanza(
'type': 'error', tag: data.stanza.tag,
...data.stanza.id != null to: data.stanza.from,
? { from: data.stanza.to,
'id': data.stanza.id!, attributes: <String, String>{
} 'type': 'error',
: {}, if (data.stanza.id != null) 'id': data.stanza.id!,
}, },
); ),
);
}
return;
} }
// Log the (raw) stanza
final prefix = data.encrypted ? '(Encrypted) ' : ''; final prefix = data.encrypted ? '(Encrypted) ' : '';
_log.finest('==> $prefix${stanza_.toXml()}'); _log.finest('==> $prefix${newStanza.toXml()}');
final stanzaString = data.stanza.toXml(); if (details.awaitable) {
await _stanzaAwaiter
// ignore: cascade_invocations .addPending(
_log.fine('Attempting to acquire lock for ${data.stanza.id}...');
// TODO(PapaTutuWawa): Handle this much more graceful
var future = Future.value(XMLNode(tag: 'not-used'));
if (awaitable) {
future = await _stanzaAwaiter.addPending(
// A stanza with no to attribute is for direct processing by the server. As such, // 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 // we can correlate it by just *assuming* we have that attribute
// (RFC 6120 Section 8.1.1.1) // (RFC 6120 Section 8.1.1.1)
data.stanza.to ?? connectionSettings.jid.toBare().toString(), data.stanza.to,
data.stanza.id!, data.stanza.id!,
data.stanza.tag, data.stanza.tag,
); )
.then((result) {
entry.completer!.complete(result);
});
} }
// This uses the StreamManager to behave like a send queue
if (await _canSendData()) { if (await _canSendData()) {
_socket.write(stanzaString); _socket.write(data.stanza.toXml());
// Try to ack every stanza
// NOTE: Here we have send an Ack request nonza. This is now done by StreamManagementManager when receiving the StanzaSentEvent
} else { } else {
_log.fine('_canSendData() returned false.'); _log.fine('Not sending data as _canSendData() returned false.');
} }
// Run post-send handlers
_log.fine('Running post stanza handlers..'); _log.fine('Running post stanza handlers..');
await _runOutgoingPostStanzaHandlers( await _runOutgoingPostStanzaHandlers(
stanza_, newStanza,
initial: StanzaHandlerData( initial: StanzaHandlerData(
false, false,
false, false,
null, newStanza,
stanza_, details.postSendExtensions ?? TypedMap<StanzaHandlerExtension>(),
encrypted: data.encrypted,
), ),
); );
_log.fine('Done'); _log.fine('Done');
return future;
} }
/// Called when we timeout during connecting /// Called when we timeout during connecting
@@ -560,21 +601,29 @@ class XmppConnection {
// Set the new routing state // Set the new routing state
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
// Set the connection state
await _setConnectionState(XmppConnectionState.connected);
// Enable reconnections // Enable reconnections
if (_enableReconnectOnSuccess) { if (_enableReconnectOnSuccess) {
await _reconnectionPolicy.setShouldReconnect(true); await _reconnectionPolicy.setShouldReconnect(true);
} }
// Tell consumers of the event stream that we're done with stream feature
// negotiations
await _sendEvent(
StreamNegotiationsDoneEvent(
getManagerById<StreamManagementManager>(smManager)?.streamResumed ??
false,
),
);
// Set the connection state
await _setConnectionState(XmppConnectionState.connected);
// Resolve the connection completion future // Resolve the connection completion future
_connectionCompleter?.complete(const Result(true)); _connectionCompleter?.complete(const Result(true));
_connectionCompleter = null; _connectionCompleter = null;
// Tell consumers of the event stream that we're done with stream feature // Flush the stanza send queue
// negotiations await _stanzaQueue.restart();
await _sendEvent(StreamNegotiationsDoneEvent());
} }
/// Sets the connection state to [state] and triggers an event of type /// Sets the connection state to [state] and triggers an event of type
@@ -602,15 +651,10 @@ class XmppConnection {
_destroyConnectingTimer(); _destroyConnectingTimer();
} }
final sm =
_negotiationsHandler.getNegotiatorById<StreamManagementNegotiator>(
streamManagementNegotiator,
);
await _sendEvent( await _sendEvent(
ConnectionStateChangedEvent( ConnectionStateChangedEvent(
state, state,
oldState, oldState,
sm?.isResumed ?? false,
), ),
); );
} }
@@ -641,15 +685,30 @@ class XmppConnection {
/// call its callback and end the processing if the callback returned true; continue /// call its callback and end the processing if the callback returned true; continue
/// if it returned false. /// if it returned false.
Future<StanzaHandlerData> _runStanzaHandlers( Future<StanzaHandlerData> _runStanzaHandlers(
List<StanzaHandler> handlers, List<_StanzaHandlerWrapper> handlers,
Stanza stanza, { Stanza stanza, {
StanzaHandlerData? initial, StanzaHandlerData? initial,
}) async { }) async {
var state = initial ?? StanzaHandlerData(false, false, null, stanza); 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)) { if (handler.matches(state.stanza)) {
state = await handler.callback(state.stanza, state); _log.finest(
if (state.done || state.cancel) return state; 'Running handler for ${stanza.tag} (${stanza.attributes["id"]}) of $managerName',
);
try {
state = await handler.callback(state.stanza, 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;
}
} }
} }
@@ -720,14 +779,20 @@ class XmppConnection {
// it. // it.
final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza); final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza);
final prefix = incomingPreHandlers.encrypted && final prefix = incomingPreHandlers.encrypted &&
incomingPreHandlers.other['encryption_error'] == null incomingPreHandlers.encryptionError == null
? '(Encrypted) ' ? '(Encrypted) '
: ''; : '';
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}'); _log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
if (incomingPreHandlers.skip) {
_log.fine(
'Not processing stanza (${incomingPreHandlers.stanza.tag}, ${incomingPreHandlers.stanza.id}) due to skip=true.',
);
return;
}
final awaited = await _stanzaAwaiter.onData( final awaited = await _stanzaAwaiter.onData(
incomingPreHandlers.stanza, incomingPreHandlers.stanza,
connectionSettings.jid.toBare(),
); );
if (awaited) { if (awaited) {
return; return;
@@ -739,14 +804,17 @@ class XmppConnection {
initial: StanzaHandlerData( initial: StanzaHandlerData(
false, false,
incomingPreHandlers.cancel, incomingPreHandlers.cancel,
incomingPreHandlers.cancelReason,
incomingPreHandlers.stanza, incomingPreHandlers.stanza,
incomingPreHandlers.extensions,
encrypted: incomingPreHandlers.encrypted, encrypted: incomingPreHandlers.encrypted,
other: incomingPreHandlers.other, encryptionError: incomingPreHandlers.encryptionError,
cancelReason: incomingPreHandlers.cancelReason,
), ),
); );
if (!incomingHandlers.done) { if (!incomingHandlers.done) {
_log.warning('Returning error for unhandled stanza'); _log.warning(
'Returning error for unhandled stanza ${incomingPreHandlers.stanza.tag}',
);
await handleUnhandledStanza(this, incomingPreHandlers); await handleUnhandledStanza(this, incomingPreHandlers);
} }
} }
@@ -783,14 +851,12 @@ class XmppConnection {
// causing (a) the negotiator to become confused and (b) the stanzas/nonzas to be // 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 // missed. This causes the data to wait while the negotiator is running and thus
// prevent this issue. // prevent this issue.
await _negotiationLock.synchronized(() async { if (_routingState != RoutingState.negotiating) {
if (_routingState != RoutingState.negotiating) { unawaited(handleXmlStream(event));
unawaited(handleXmlStream(event)); return;
return; }
}
await _negotiationsHandler.negotiate(event); await _negotiationsHandler.negotiate(event);
});
break; break;
case RoutingState.handleStanzas: case RoutingState.handleStanzas:
await _handleStanza(node); await _handleStanza(node);
@@ -809,8 +875,6 @@ class XmppConnection {
/// Sends an event to the connection's event stream. /// Sends an event to the connection's event stream.
Future<void> _sendEvent(XmppEvent event) async { Future<void> _sendEvent(XmppEvent event) async {
_log.finest('Event: ${event.toString()}');
for (final manager in _xmppManagers.values) { for (final manager in _xmppManagers.values) {
await manager.onXmppEvent(event); await manager.onXmppEvent(event);
} }
@@ -831,7 +895,7 @@ class XmppConnection {
await _reconnectionPolicy.setShouldReconnect(false); await _reconnectionPolicy.setShouldReconnect(false);
if (triggeredByUser) { if (triggeredByUser) {
getPresenceManager()?.sendUnavailablePresence(); await getPresenceManager()?.sendUnavailablePresence();
} }
_socket.prepareDisconnect(); _socket.prepareDisconnect();

View File

@@ -4,28 +4,20 @@ import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/roster/roster.dart'; import 'package:moxxmpp/src/roster/roster.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/xeps/xep_0084.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart'; import 'package:moxxmpp/src/xeps/xep_0333.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0385.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart';
abstract class XmppEvent {} abstract class XmppEvent {}
/// Triggered when the connection state of the XmppConnection has /// Triggered when the connection state of the XmppConnection has
/// changed. /// changed.
class ConnectionStateChangedEvent extends XmppEvent { class ConnectionStateChangedEvent extends XmppEvent {
ConnectionStateChangedEvent(this.state, this.before, this.resumed); ConnectionStateChangedEvent(this.state, this.before);
final XmppConnectionState before; final XmppConnectionState before;
final XmppConnectionState state; final XmppConnectionState state;
final bool resumed;
/// Indicates whether the connection state switched from a not connected state to a /// Indicates whether the connection state switched from a not connected state to a
/// connected state. /// connected state.
@@ -75,58 +67,42 @@ class RosterUpdatedEvent extends XmppEvent {
/// Triggered when a message is received /// Triggered when a message is received
class MessageEvent extends XmppEvent { class MessageEvent extends XmppEvent {
MessageEvent({ MessageEvent(
required this.body, this.from,
required this.fromJid, this.to,
required this.toJid, this.encrypted,
required this.sid, this.extensions, {
required this.stanzaId, this.id,
required this.isCarbon,
required this.deliveryReceiptRequested,
required this.isMarkable,
required this.encrypted,
required this.other,
this.error,
this.type, this.type,
this.oob, this.error,
this.sfs, this.encryptionError,
this.sims,
this.reply,
this.chatState,
this.fun,
this.funReplacement,
this.funCancellation,
this.messageRetraction,
this.messageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
}); });
final StanzaError? error;
final String body; /// The from attribute of the message.
final JID fromJid; final JID from;
final JID toJid;
final String sid; /// The to attribute of the message.
final JID to;
/// The id attribute of the message.
final String? id;
/// The type attribute of the message.
final String? type; final String? type;
final StableStanzaId stanzaId;
final bool isCarbon; final StanzaError? error;
final bool deliveryReceiptRequested;
final bool isMarkable; /// Flag indicating whether the message was encrypted.
final OOBData? oob;
final StatelessFileSharingData? sfs;
final StatelessMediaSharingData? sims;
final ReplyData? reply;
final ChatState? chatState;
final FileMetadataData? fun;
final String? funReplacement;
final String? funCancellation;
final bool encrypted; final bool encrypted;
final MessageRetractionData? messageRetraction;
final String? messageCorrectionId; /// The error in case an encryption error occurred.
final MessageReactions? messageReactions; final Object? encryptionError;
final List<MessageProcessingHint>? messageProcessingHints;
final String? stickerPackId; /// Data added by other handlers.
final Map<String, dynamic> other; final TypedMap<StanzaHandlerExtension> extensions;
/// Shorthand for extensions.get<T>().
T? get<T>() => extensions.get<T>();
} }
/// Triggered when a client responds to our delivery receipt request /// Triggered when a client responds to our delivery receipt request
@@ -137,13 +113,19 @@ class DeliveryReceiptReceivedEvent extends XmppEvent {
} }
class ChatMarkerEvent extends XmppEvent { class ChatMarkerEvent extends XmppEvent {
ChatMarkerEvent({ ChatMarkerEvent(
required this.type, this.from,
required this.from, this.type,
required this.id, this.id,
}); );
/// The entity that sent the chat marker.
final JID from; final JID from;
final String type;
/// The type of chat marker that was sent.
final ChatMarker type;
/// The id of the message that the marker applies to.
final String id; final String id;
} }
@@ -167,13 +149,6 @@ class ResourceBoundEvent extends XmppEvent {
final String resource; final String resource;
} }
/// Triggered when we receive presence
class PresenceReceivedEvent extends XmppEvent {
PresenceReceivedEvent(this.jid, this.presence);
final JID jid;
final Stanza presence;
}
/// Triggered when we are starting an connection attempt /// Triggered when we are starting an connection attempt
class ConnectingEvent extends XmppEvent {} class ConnectingEvent extends XmppEvent {}
@@ -191,15 +166,31 @@ class SubscriptionRequestReceivedEvent extends XmppEvent {
final JID from; final JID from;
} }
/// Triggered when we receive a new or updated avatar /// Triggered when we receive a new or updated avatar via XEP-0084
class AvatarUpdatedEvent extends XmppEvent { class UserAvatarUpdatedEvent extends XmppEvent {
AvatarUpdatedEvent({ UserAvatarUpdatedEvent(
required this.jid, this.jid,
required this.base64, this.metadata,
required this.hash, );
});
final String jid; /// The JID of the user updating their avatar.
final String base64; final JID jid;
/// The metadata of the avatar.
final List<UserAvatarMetadata> metadata;
}
/// Triggered when we receive a new or updated avatar via XEP-0054
class VCardAvatarUpdatedEvent extends XmppEvent {
VCardAvatarUpdatedEvent(
this.jid,
this.hash,
);
/// The JID of the entity that updated their avatar.
final JID jid;
/// The SHA-1 hash of the avatar.
final String hash; final String hash;
} }
@@ -257,4 +248,10 @@ class NonRecoverableErrorEvent extends XmppEvent {
} }
/// Triggered when the stream negotiations are done. /// Triggered when the stream negotiations are done.
class StreamNegotiationsDoneEvent extends XmppEvent {} class StreamNegotiationsDoneEvent extends XmppEvent {
StreamNegotiationsDoneEvent(this.resumed);
/// Flag indicating whether we resumed a previous stream (true) or are in a completely
/// new stream (false).
final bool resumed;
}

View File

@@ -22,6 +22,9 @@ typedef SendNonzaFunction = void Function(XMLNode);
/// Returns the connection settings. /// Returns the connection settings.
typedef GetConnectionSettingsFunction = ConnectionSettings Function(); typedef GetConnectionSettingsFunction = ConnectionSettings Function();
/// Resets the stream parser's state.
typedef ResetStreamParserFunction = void Function();
/// This class implements the stream feature negotiation for XmppConnection. /// This class implements the stream feature negotiation for XmppConnection.
abstract class NegotiationsHandler { abstract class NegotiationsHandler {
@protected @protected
@@ -51,6 +54,9 @@ abstract class NegotiationsHandler {
@protected @protected
late final GetConnectionSettingsFunction getConnectionSettings; late final GetConnectionSettingsFunction getConnectionSettings;
@protected
late final ResetStreamParserFunction resetStreamParser;
/// The id included in the last stream header. /// The id included in the last stream header.
@protected @protected
String? streamId; String? streamId;
@@ -72,12 +78,14 @@ abstract class NegotiationsHandler {
IsAuthenticatedFunction isAuthenticated, IsAuthenticatedFunction isAuthenticated,
SendNonzaFunction sendNonza, SendNonzaFunction sendNonza,
GetConnectionSettingsFunction getConnectionSettings, GetConnectionSettingsFunction getConnectionSettings,
ResetStreamParserFunction resetStreamParser,
) { ) {
this.onNegotiationsDone = onNegotiationsDone; this.onNegotiationsDone = onNegotiationsDone;
this.handleError = handleError; this.handleError = handleError;
this.isAuthenticated = isAuthenticated; this.isAuthenticated = isAuthenticated;
this.sendNonza = sendNonza; this.sendNonza = sendNonza;
this.getConnectionSettings = getConnectionSettings; this.getConnectionSettings = getConnectionSettings;
this.resetStreamParser = resetStreamParser;
log = Logger(toString()); log = Logger(toString());
} }

View File

@@ -60,6 +60,7 @@ class ClientToServerNegotiator extends NegotiationsHandler {
@override @override
void sendStreamHeader() { void sendStreamHeader() {
resetStreamParser();
sendNonza( sendNonza(
XMLNode( XMLNode(
tag: 'xml', tag: 'xml',

View File

@@ -49,6 +49,7 @@ class ComponentToServerNegotiator extends NegotiationsHandler {
@override @override
void sendStreamHeader() { void sendStreamHeader() {
resetStreamParser();
sendNonza( sendNonza(
XMLNode( XMLNode(
tag: 'xml', tag: 'xml',

View File

@@ -23,9 +23,11 @@ Future<void> handleUnhandledStanza(
); );
await conn.sendStanza( await conn.sendStanza(
stanza, StanzaDetails(
awaitable: false, stanza,
forceEncryption: data.encrypted, awaitable: false,
forceEncryption: data.encrypted,
),
); );
} }
} }

View File

@@ -55,14 +55,17 @@ class JID {
/// Converts the JID into a bare JID. /// Converts the JID into a bare JID.
JID toBare() { JID toBare() {
if (isBare()) return this;
return JID(local, domain, ''); return JID(local, domain, '');
} }
/// Converts the JID into one with a resource part of [resource]. /// Converts the JID into one with a resource part of [resource].
JID withResource(String resource) => JID(local, domain, resource); JID withResource(String resource) => JID(local, domain, resource);
/// Convert the JID into the JID of the domain. For example, converts alice@example.org/abc123 to example.org.
JID toDomain() {
return JID('', domain, '');
}
/// Compares the JID with [other]. This function assumes that JID and [other] /// Compares the JID with [other]. This function assumes that JID and [other]
/// are bare, i.e. only the domain- and localparts are compared. If [ensureBare] /// are bare, i.e. only the domain- and localparts are compared. If [ensureBare]
/// is optionally set to true, then [other] MUST be bare. Otherwise, false is returned. /// is optionally set to true, then [other] MUST be bare. Otherwise, false is returned.

View File

@@ -23,14 +23,7 @@ class XmppManagerAttributes {
}); });
/// Send a stanza whose response can be awaited. /// Send a stanza whose response can be awaited.
final Future<XMLNode> Function( final Future<XMLNode?> Function(StanzaDetails) sendStanza;
Stanza stanza, {
StanzaFromType addFrom,
bool addId,
bool awaitable,
bool encrypted,
bool forceEncryption,
}) sendStanza;
/// Send a nonza. /// Send a nonza.
final void Function(XMLNode) sendNonza; final void Function(XMLNode) sendNonza;

View File

@@ -5,6 +5,7 @@ import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
@@ -45,8 +46,7 @@ abstract class XmppManagerBase {
); );
final result = await dm!.discoInfoQuery( final result = await dm!.discoInfoQuery(
_managerAttributes.getConnectionSettings().jid.domain, _managerAttributes.getConnectionSettings().jid.toDomain(),
shouldEncrypt: false,
); );
if (result.isType<DiscoError>()) { if (result.isType<DiscoError>()) {
return false; return false;
@@ -80,6 +80,9 @@ abstract class XmppManagerBase {
/// handler's priority, the earlier it is run. /// handler's priority, the earlier it is run.
List<NonzaHandler> getNonzaHandlers() => []; 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. /// Return a list of features that should be included in a disco response.
List<String> getDiscoFeatures() => []; List<String> getDiscoFeatures() => [];
@@ -165,9 +168,11 @@ abstract class XmppManagerBase {
); );
await getAttributes().sendStanza( await getAttributes().sendStanza(
stanza, StanzaDetails(
awaitable: false, stanza,
forceEncryption: data.encrypted, awaitable: false,
forceEncryption: data.encrypted,
),
); );
} }
} }

View File

@@ -1,74 +1,59 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/xeps/xep_0066.dart'; import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0203.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart';
import 'package:moxxmpp/src/xeps/xep_0385.dart';
import 'package:moxxmpp/src/xeps/xep_0424.dart';
import 'package:moxxmpp/src/xeps/xep_0444.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart';
part 'data.freezed.dart'; abstract class StanzaHandlerExtension {}
@freezed class StanzaHandlerData {
class StanzaHandlerData with _$StanzaHandlerData { StanzaHandlerData(
factory StanzaHandlerData( this.done,
// Indicates to the runner that processing is now done. This means that all this.cancel,
// pre-processing is done and no other handlers should be consulted. this.stanza,
bool done, this.extensions, {
// Indicates to the runner that processing is to be cancelled and no further handlers this.cancelReason,
// should run. The stanza also will not be sent. this.encryptionError,
bool cancel, this.encrypted = false,
// The reason why we cancelled the processing and sending this.forceEncryption = false,
dynamic cancelReason, this.shouldEncrypt = true,
// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely this.skip = false,
// necessary, e.g. with Message Carbons or OMEMO });
Stanza stanza, {
// Whether the stanza is retransmitted. Only useful in the context of outgoing /// Indicates to the runner that processing is now done. This means that all
// stanza handlers. MUST NOT be overwritten. /// pre-processing is done and no other handlers should be consulted.
@Default(false) bool retransmitted, bool done;
StatelessMediaSharingData? sims,
StatelessFileSharingData? sfs, /// Only useful in combination with [done] = true: When [skip] is set to true and
OOBData? oob, /// this [StanzaHandlerData] object is returned from a IncomingPreStanzaHandler, then
StableStanzaId? stableId, /// moxxmpp will skip checking whether the stanza was awaited and will not run any actual
ReplyData? reply, /// IncomingStanzaHandler callbacks.
ChatState? chatState, bool skip;
@Default(false) bool isCarbon,
@Default(false) bool deliveryReceiptRequested, /// Indicates to the runner that processing is to be cancelled and no further handlers
@Default(false) bool isMarkable, /// should run. The stanza also will not be sent.
// File Upload Notifications bool cancel;
// A notification
FileMetadataData? fun, /// The reason why we cancelled the processing and sending.
// The stanza id this replaces Object? cancelReason;
String? funReplacement,
// The stanza id this cancels /// The reason why an encryption or decryption failed.
String? funCancellation, Object? encryptionError;
// Whether the stanza was received encrypted
@Default(false) bool encrypted, /// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is
// If true, forces the encryption manager to encrypt to the JID, even if it /// absolutely necessary, e.g. with Message Carbons or OMEMO.
// would not normally. In the case of OMEMO: If shouldEncrypt returns false Stanza stanza;
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway. /// Whether the stanza is already encrypted
@Default(false) bool forceEncryption, bool encrypted;
// The stated type of encryption used, if any was used
ExplicitEncryptionType? encryptionType, // If true, forces the encryption manager to encrypt to the JID, even if it
// Delayed Delivery // would not normally. In the case of OMEMO: If shouldEncrypt returns false
DelayedDelivery? delayedDelivery, // but forceEncryption is true, then the OMEMO manager will try to encrypt
// This is for stanza handlers that are not part of the XMPP library but still need // to the JID anyway.
// pass data around. bool forceEncryption;
@Default(<String, dynamic>{}) Map<String, dynamic> other,
// If non-null, then it indicates the origin Id of the message that should be /// Flag indicating whether a E2EE implementation should encrypt the stanza (true)
// retracted /// or not (false).
MessageRetractionData? messageRetraction, bool shouldEncrypt;
// If non-null, then the message is a correction for the specified stanza Id
String? lastMessageCorrectionSid, /// Additional data from other managers.
// Reactions data final TypedMap<StanzaHandlerExtension> extensions;
MessageReactions? messageReactions,
// The Id of the sticker pack this sticker belongs to
String? stickerPackId,
}) = _StanzaHandlerData;
} }

View File

@@ -1,747 +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
part of 'data.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$StanzaHandlerData {
// Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted.
bool get done =>
throw _privateConstructorUsedError; // Indicates to the runner that processing is to be cancelled and no further handlers
// should run. The stanza also will not be sent.
bool get cancel =>
throw _privateConstructorUsedError; // The reason why we cancelled the processing and sending
dynamic get cancelReason =>
throw _privateConstructorUsedError; // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
// necessary, e.g. with Message Carbons or OMEMO
Stanza get stanza =>
throw _privateConstructorUsedError; // Whether the stanza is retransmitted. Only useful in the context of outgoing
// stanza handlers. MUST NOT be overwritten.
bool get retransmitted => throw _privateConstructorUsedError;
StatelessMediaSharingData? get sims => throw _privateConstructorUsedError;
StatelessFileSharingData? get sfs => throw _privateConstructorUsedError;
OOBData? get oob => throw _privateConstructorUsedError;
StableStanzaId? get stableId => throw _privateConstructorUsedError;
ReplyData? get reply => throw _privateConstructorUsedError;
ChatState? get chatState => throw _privateConstructorUsedError;
bool get isCarbon => throw _privateConstructorUsedError;
bool get deliveryReceiptRequested => throw _privateConstructorUsedError;
bool get isMarkable =>
throw _privateConstructorUsedError; // File Upload Notifications
// A notification
FileMetadataData? get fun =>
throw _privateConstructorUsedError; // The stanza id this replaces
String? get funReplacement =>
throw _privateConstructorUsedError; // The stanza id this cancels
String? get funCancellation =>
throw _privateConstructorUsedError; // Whether the stanza was received encrypted
bool get encrypted =>
throw _privateConstructorUsedError; // If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
bool get forceEncryption =>
throw _privateConstructorUsedError; // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType =>
throw _privateConstructorUsedError; // Delayed Delivery
DelayedDelivery? get delayedDelivery =>
throw _privateConstructorUsedError; // This is for stanza handlers that are not part of the XMPP library but still need
// pass data around.
Map<String, dynamic> get other =>
throw _privateConstructorUsedError; // If non-null, then it indicates the origin Id of the message that should be
// retracted
MessageRetractionData? get messageRetraction =>
throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id
String? get lastMessageCorrectionSid =>
throw _privateConstructorUsedError; // Reactions data
MessageReactions? get messageReactions =>
throw _privateConstructorUsedError; // The Id of the sticker pack this sticker belongs to
String? get stickerPackId => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $StanzaHandlerDataCopyWith<$Res> {
factory $StanzaHandlerDataCopyWith(
StanzaHandlerData value, $Res Function(StanzaHandlerData) then) =
_$StanzaHandlerDataCopyWithImpl<$Res>;
$Res call(
{bool done,
bool cancel,
dynamic cancelReason,
Stanza stanza,
bool retransmitted,
StatelessMediaSharingData? sims,
StatelessFileSharingData? sfs,
OOBData? oob,
StableStanzaId? stableId,
ReplyData? reply,
ChatState? chatState,
bool isCarbon,
bool deliveryReceiptRequested,
bool isMarkable,
FileMetadataData? fun,
String? funReplacement,
String? funCancellation,
bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery,
Map<String, dynamic> other,
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
}
/// @nodoc
class _$StanzaHandlerDataCopyWithImpl<$Res>
implements $StanzaHandlerDataCopyWith<$Res> {
_$StanzaHandlerDataCopyWithImpl(this._value, this._then);
final StanzaHandlerData _value;
// ignore: unused_field
final $Res Function(StanzaHandlerData) _then;
@override
$Res call({
Object? done = freezed,
Object? cancel = freezed,
Object? cancelReason = freezed,
Object? stanza = freezed,
Object? retransmitted = freezed,
Object? sims = freezed,
Object? sfs = freezed,
Object? oob = freezed,
Object? stableId = freezed,
Object? reply = freezed,
Object? chatState = freezed,
Object? isCarbon = freezed,
Object? deliveryReceiptRequested = freezed,
Object? isMarkable = freezed,
Object? fun = freezed,
Object? funReplacement = freezed,
Object? funCancellation = freezed,
Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed,
Object? delayedDelivery = freezed,
Object? other = freezed,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) {
return _then(_value.copyWith(
done: done == freezed
? _value.done
: done // ignore: cast_nullable_to_non_nullable
as bool,
cancel: cancel == freezed
? _value.cancel
: cancel // ignore: cast_nullable_to_non_nullable
as bool,
cancelReason: cancelReason == freezed
? _value.cancelReason
: cancelReason // ignore: cast_nullable_to_non_nullable
as dynamic,
stanza: stanza == freezed
? _value.stanza
: stanza // ignore: cast_nullable_to_non_nullable
as Stanza,
retransmitted: retransmitted == freezed
? _value.retransmitted
: retransmitted // ignore: cast_nullable_to_non_nullable
as bool,
sims: sims == freezed
? _value.sims
: sims // ignore: cast_nullable_to_non_nullable
as StatelessMediaSharingData?,
sfs: sfs == freezed
? _value.sfs
: sfs // ignore: cast_nullable_to_non_nullable
as StatelessFileSharingData?,
oob: oob == freezed
? _value.oob
: oob // ignore: cast_nullable_to_non_nullable
as OOBData?,
stableId: stableId == freezed
? _value.stableId
: stableId // ignore: cast_nullable_to_non_nullable
as StableStanzaId?,
reply: reply == freezed
? _value.reply
: reply // ignore: cast_nullable_to_non_nullable
as ReplyData?,
chatState: chatState == freezed
? _value.chatState
: chatState // ignore: cast_nullable_to_non_nullable
as ChatState?,
isCarbon: isCarbon == freezed
? _value.isCarbon
: isCarbon // ignore: cast_nullable_to_non_nullable
as bool,
deliveryReceiptRequested: deliveryReceiptRequested == freezed
? _value.deliveryReceiptRequested
: deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable
as bool,
isMarkable: isMarkable == freezed
? _value.isMarkable
: isMarkable // ignore: cast_nullable_to_non_nullable
as bool,
fun: fun == freezed
? _value.fun
: fun // ignore: cast_nullable_to_non_nullable
as FileMetadataData?,
funReplacement: funReplacement == freezed
? _value.funReplacement
: funReplacement // ignore: cast_nullable_to_non_nullable
as String?,
funCancellation: funCancellation == freezed
? _value.funCancellation
: funCancellation // ignore: cast_nullable_to_non_nullable
as String?,
encrypted: encrypted == freezed
? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable
as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed
? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable
as ExplicitEncryptionType?,
delayedDelivery: delayedDelivery == freezed
? _value.delayedDelivery
: delayedDelivery // ignore: cast_nullable_to_non_nullable
as DelayedDelivery?,
other: other == freezed
? _value.other
: other // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
messageRetraction: messageRetraction == freezed
? _value.messageRetraction
: messageRetraction // ignore: cast_nullable_to_non_nullable
as MessageRetractionData?,
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?,
messageReactions: messageReactions == freezed
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
stickerPackId: stickerPackId == freezed
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
abstract class _$$_StanzaHandlerDataCopyWith<$Res>
implements $StanzaHandlerDataCopyWith<$Res> {
factory _$$_StanzaHandlerDataCopyWith(_$_StanzaHandlerData value,
$Res Function(_$_StanzaHandlerData) then) =
__$$_StanzaHandlerDataCopyWithImpl<$Res>;
@override
$Res call(
{bool done,
bool cancel,
dynamic cancelReason,
Stanza stanza,
bool retransmitted,
StatelessMediaSharingData? sims,
StatelessFileSharingData? sfs,
OOBData? oob,
StableStanzaId? stableId,
ReplyData? reply,
ChatState? chatState,
bool isCarbon,
bool deliveryReceiptRequested,
bool isMarkable,
FileMetadataData? fun,
String? funReplacement,
String? funCancellation,
bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery,
Map<String, dynamic> other,
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
}
/// @nodoc
class __$$_StanzaHandlerDataCopyWithImpl<$Res>
extends _$StanzaHandlerDataCopyWithImpl<$Res>
implements _$$_StanzaHandlerDataCopyWith<$Res> {
__$$_StanzaHandlerDataCopyWithImpl(
_$_StanzaHandlerData _value, $Res Function(_$_StanzaHandlerData) _then)
: super(_value, (v) => _then(v as _$_StanzaHandlerData));
@override
_$_StanzaHandlerData get _value => super._value as _$_StanzaHandlerData;
@override
$Res call({
Object? done = freezed,
Object? cancel = freezed,
Object? cancelReason = freezed,
Object? stanza = freezed,
Object? retransmitted = freezed,
Object? sims = freezed,
Object? sfs = freezed,
Object? oob = freezed,
Object? stableId = freezed,
Object? reply = freezed,
Object? chatState = freezed,
Object? isCarbon = freezed,
Object? deliveryReceiptRequested = freezed,
Object? isMarkable = freezed,
Object? fun = freezed,
Object? funReplacement = freezed,
Object? funCancellation = freezed,
Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed,
Object? delayedDelivery = freezed,
Object? other = freezed,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) {
return _then(_$_StanzaHandlerData(
done == freezed
? _value.done
: done // ignore: cast_nullable_to_non_nullable
as bool,
cancel == freezed
? _value.cancel
: cancel // ignore: cast_nullable_to_non_nullable
as bool,
cancelReason == freezed
? _value.cancelReason
: cancelReason // ignore: cast_nullable_to_non_nullable
as dynamic,
stanza == freezed
? _value.stanza
: stanza // ignore: cast_nullable_to_non_nullable
as Stanza,
retransmitted: retransmitted == freezed
? _value.retransmitted
: retransmitted // ignore: cast_nullable_to_non_nullable
as bool,
sims: sims == freezed
? _value.sims
: sims // ignore: cast_nullable_to_non_nullable
as StatelessMediaSharingData?,
sfs: sfs == freezed
? _value.sfs
: sfs // ignore: cast_nullable_to_non_nullable
as StatelessFileSharingData?,
oob: oob == freezed
? _value.oob
: oob // ignore: cast_nullable_to_non_nullable
as OOBData?,
stableId: stableId == freezed
? _value.stableId
: stableId // ignore: cast_nullable_to_non_nullable
as StableStanzaId?,
reply: reply == freezed
? _value.reply
: reply // ignore: cast_nullable_to_non_nullable
as ReplyData?,
chatState: chatState == freezed
? _value.chatState
: chatState // ignore: cast_nullable_to_non_nullable
as ChatState?,
isCarbon: isCarbon == freezed
? _value.isCarbon
: isCarbon // ignore: cast_nullable_to_non_nullable
as bool,
deliveryReceiptRequested: deliveryReceiptRequested == freezed
? _value.deliveryReceiptRequested
: deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable
as bool,
isMarkable: isMarkable == freezed
? _value.isMarkable
: isMarkable // ignore: cast_nullable_to_non_nullable
as bool,
fun: fun == freezed
? _value.fun
: fun // ignore: cast_nullable_to_non_nullable
as FileMetadataData?,
funReplacement: funReplacement == freezed
? _value.funReplacement
: funReplacement // ignore: cast_nullable_to_non_nullable
as String?,
funCancellation: funCancellation == freezed
? _value.funCancellation
: funCancellation // ignore: cast_nullable_to_non_nullable
as String?,
encrypted: encrypted == freezed
? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable
as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed
? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable
as ExplicitEncryptionType?,
delayedDelivery: delayedDelivery == freezed
? _value.delayedDelivery
: delayedDelivery // ignore: cast_nullable_to_non_nullable
as DelayedDelivery?,
other: other == freezed
? _value._other
: other // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
messageRetraction: messageRetraction == freezed
? _value.messageRetraction
: messageRetraction // ignore: cast_nullable_to_non_nullable
as MessageRetractionData?,
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
? _value.lastMessageCorrectionSid
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
as String?,
messageReactions: messageReactions == freezed
? _value.messageReactions
: messageReactions // ignore: cast_nullable_to_non_nullable
as MessageReactions?,
stickerPackId: stickerPackId == freezed
? _value.stickerPackId
: stickerPackId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
class _$_StanzaHandlerData implements _StanzaHandlerData {
_$_StanzaHandlerData(this.done, this.cancel, this.cancelReason, this.stanza,
{this.retransmitted = false,
this.sims,
this.sfs,
this.oob,
this.stableId,
this.reply,
this.chatState,
this.isCarbon = false,
this.deliveryReceiptRequested = false,
this.isMarkable = false,
this.fun,
this.funReplacement,
this.funCancellation,
this.encrypted = false,
this.forceEncryption = false,
this.encryptionType,
this.delayedDelivery,
final Map<String, dynamic> other = const <String, dynamic>{},
this.messageRetraction,
this.lastMessageCorrectionSid,
this.messageReactions,
this.stickerPackId})
: _other = other;
// Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted.
@override
final bool done;
// Indicates to the runner that processing is to be cancelled and no further handlers
// should run. The stanza also will not be sent.
@override
final bool cancel;
// The reason why we cancelled the processing and sending
@override
final dynamic cancelReason;
// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
// necessary, e.g. with Message Carbons or OMEMO
@override
final Stanza stanza;
// Whether the stanza is retransmitted. Only useful in the context of outgoing
// stanza handlers. MUST NOT be overwritten.
@override
@JsonKey()
final bool retransmitted;
@override
final StatelessMediaSharingData? sims;
@override
final StatelessFileSharingData? sfs;
@override
final OOBData? oob;
@override
final StableStanzaId? stableId;
@override
final ReplyData? reply;
@override
final ChatState? chatState;
@override
@JsonKey()
final bool isCarbon;
@override
@JsonKey()
final bool deliveryReceiptRequested;
@override
@JsonKey()
final bool isMarkable;
// File Upload Notifications
// A notification
@override
final FileMetadataData? fun;
// The stanza id this replaces
@override
final String? funReplacement;
// The stanza id this cancels
@override
final String? funCancellation;
// Whether the stanza was received encrypted
@override
@JsonKey()
final bool encrypted;
// If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
@override
@JsonKey()
final bool forceEncryption;
// The stated type of encryption used, if any was used
@override
final ExplicitEncryptionType? encryptionType;
// Delayed Delivery
@override
final DelayedDelivery? delayedDelivery;
// This is for stanza handlers that are not part of the XMPP library but still need
// pass data around.
final Map<String, dynamic> _other;
// This is for stanza handlers that are not part of the XMPP library but still need
// pass data around.
@override
@JsonKey()
Map<String, dynamic> get other {
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_other);
}
// If non-null, then it indicates the origin Id of the message that should be
// retracted
@override
final MessageRetractionData? messageRetraction;
// If non-null, then the message is a correction for the specified stanza Id
@override
final String? lastMessageCorrectionSid;
// Reactions data
@override
final MessageReactions? messageReactions;
// The Id of the sticker pack this sticker belongs to
@override
final String? stickerPackId;
@override
String toString() {
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, forceEncryption: $forceEncryption, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_StanzaHandlerData &&
const DeepCollectionEquality().equals(other.done, done) &&
const DeepCollectionEquality().equals(other.cancel, cancel) &&
const DeepCollectionEquality()
.equals(other.cancelReason, cancelReason) &&
const DeepCollectionEquality().equals(other.stanza, stanza) &&
const DeepCollectionEquality()
.equals(other.retransmitted, retransmitted) &&
const DeepCollectionEquality().equals(other.sims, sims) &&
const DeepCollectionEquality().equals(other.sfs, sfs) &&
const DeepCollectionEquality().equals(other.oob, oob) &&
const DeepCollectionEquality().equals(other.stableId, stableId) &&
const DeepCollectionEquality().equals(other.reply, reply) &&
const DeepCollectionEquality().equals(other.chatState, chatState) &&
const DeepCollectionEquality().equals(other.isCarbon, isCarbon) &&
const DeepCollectionEquality().equals(
other.deliveryReceiptRequested, deliveryReceiptRequested) &&
const DeepCollectionEquality()
.equals(other.isMarkable, isMarkable) &&
const DeepCollectionEquality().equals(other.fun, fun) &&
const DeepCollectionEquality()
.equals(other.funReplacement, funReplacement) &&
const DeepCollectionEquality()
.equals(other.funCancellation, funCancellation) &&
const DeepCollectionEquality().equals(other.encrypted, encrypted) &&
const DeepCollectionEquality()
.equals(other.forceEncryption, forceEncryption) &&
const DeepCollectionEquality()
.equals(other.encryptionType, encryptionType) &&
const DeepCollectionEquality()
.equals(other.delayedDelivery, delayedDelivery) &&
const DeepCollectionEquality().equals(other._other, this._other) &&
const DeepCollectionEquality()
.equals(other.messageRetraction, messageRetraction) &&
const DeepCollectionEquality().equals(
other.lastMessageCorrectionSid, lastMessageCorrectionSid) &&
const DeepCollectionEquality()
.equals(other.messageReactions, messageReactions) &&
const DeepCollectionEquality()
.equals(other.stickerPackId, stickerPackId));
}
@override
int get hashCode => Object.hashAll([
runtimeType,
const DeepCollectionEquality().hash(done),
const DeepCollectionEquality().hash(cancel),
const DeepCollectionEquality().hash(cancelReason),
const DeepCollectionEquality().hash(stanza),
const DeepCollectionEquality().hash(retransmitted),
const DeepCollectionEquality().hash(sims),
const DeepCollectionEquality().hash(sfs),
const DeepCollectionEquality().hash(oob),
const DeepCollectionEquality().hash(stableId),
const DeepCollectionEquality().hash(reply),
const DeepCollectionEquality().hash(chatState),
const DeepCollectionEquality().hash(isCarbon),
const DeepCollectionEquality().hash(deliveryReceiptRequested),
const DeepCollectionEquality().hash(isMarkable),
const DeepCollectionEquality().hash(fun),
const DeepCollectionEquality().hash(funReplacement),
const DeepCollectionEquality().hash(funCancellation),
const DeepCollectionEquality().hash(encrypted),
const DeepCollectionEquality().hash(forceEncryption),
const DeepCollectionEquality().hash(encryptionType),
const DeepCollectionEquality().hash(delayedDelivery),
const DeepCollectionEquality().hash(_other),
const DeepCollectionEquality().hash(messageRetraction),
const DeepCollectionEquality().hash(lastMessageCorrectionSid),
const DeepCollectionEquality().hash(messageReactions),
const DeepCollectionEquality().hash(stickerPackId)
]);
@JsonKey(ignore: true)
@override
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>
__$$_StanzaHandlerDataCopyWithImpl<_$_StanzaHandlerData>(
this, _$identity);
}
abstract class _StanzaHandlerData implements StanzaHandlerData {
factory _StanzaHandlerData(final bool done, final bool cancel,
final dynamic cancelReason, final Stanza stanza,
{final bool retransmitted,
final StatelessMediaSharingData? sims,
final StatelessFileSharingData? sfs,
final OOBData? oob,
final StableStanzaId? stableId,
final ReplyData? reply,
final ChatState? chatState,
final bool isCarbon,
final bool deliveryReceiptRequested,
final bool isMarkable,
final FileMetadataData? fun,
final String? funReplacement,
final String? funCancellation,
final bool encrypted,
final bool forceEncryption,
final ExplicitEncryptionType? encryptionType,
final DelayedDelivery? delayedDelivery,
final Map<String, dynamic> other,
final MessageRetractionData? messageRetraction,
final String? lastMessageCorrectionSid,
final MessageReactions? messageReactions,
final String? stickerPackId}) = _$_StanzaHandlerData;
@override // Indicates to the runner that processing is now done. This means that all
// pre-processing is done and no other handlers should be consulted.
bool get done;
@override // Indicates to the runner that processing is to be cancelled and no further handlers
// should run. The stanza also will not be sent.
bool get cancel;
@override // The reason why we cancelled the processing and sending
dynamic get cancelReason;
@override // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
// necessary, e.g. with Message Carbons or OMEMO
Stanza get stanza;
@override // Whether the stanza is retransmitted. Only useful in the context of outgoing
// stanza handlers. MUST NOT be overwritten.
bool get retransmitted;
@override
StatelessMediaSharingData? get sims;
@override
StatelessFileSharingData? get sfs;
@override
OOBData? get oob;
@override
StableStanzaId? get stableId;
@override
ReplyData? get reply;
@override
ChatState? get chatState;
@override
bool get isCarbon;
@override
bool get deliveryReceiptRequested;
@override
bool get isMarkable;
@override // File Upload Notifications
// A notification
FileMetadataData? get fun;
@override // The stanza id this replaces
String? get funReplacement;
@override // The stanza id this cancels
String? get funCancellation;
@override // Whether the stanza was received encrypted
bool get encrypted;
@override // If true, forces the encryption manager to encrypt to the JID, even if it
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
// but forceEncryption is true, then the OMEMO manager will try to encrypt
// to the JID anyway.
bool get forceEncryption;
@override // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType;
@override // Delayed Delivery
DelayedDelivery? get delayedDelivery;
@override // This is for stanza handlers that are not part of the XMPP library but still need
// pass data around.
Map<String, dynamic> get other;
@override // If non-null, then it indicates the origin Id of the message that should be
// retracted
MessageRetractionData? get messageRetraction;
@override // If non-null, then the message is a correction for the specified stanza Id
String? get lastMessageCorrectionSid;
@override // Reactions data
MessageReactions? get messageReactions;
@override // The Id of the sticker pack this sticker belongs to
String? get stickerPackId;
@override
@JsonKey(ignore: true)
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,4 +1,4 @@
import 'package:moxlib/moxlib.dart'; import 'package:collection/collection.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
@@ -56,7 +56,7 @@ class StanzaHandler extends Handler {
this.tagName, this.tagName,
this.priority = 0, this.priority = 0,
this.stanzaTag, this.stanzaTag,
this.xmlns = stanzaXmlns, this.xmlns,
}); });
/// If specified, then the stanza must contain a direct child with a tag equal to /// If specified, then the stanza must contain a direct child with a tag equal to
@@ -100,10 +100,10 @@ class StanzaHandler extends Handler {
matches &= firstTag?.xmlns == tagXmlns; matches &= firstTag?.xmlns == tagXmlns;
} }
} else if (tagXmlns != null) { } else if (tagXmlns != null) {
matches &= listContains( matches &= node.children.firstWhereOrNull(
node.children, (XMLNode node_) => node_.attributes['xmlns'] == tagXmlns,
(XMLNode node_) => node_.attributes['xmlns'] == tagXmlns, ) !=
); null;
} }
return matches; return matches;

View File

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

View File

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

View File

@@ -9,9 +9,11 @@ const fullStanzaXmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas';
// RFC 6121 // RFC 6121
const rosterXmlns = 'jabber:iq:roster'; const rosterXmlns = 'jabber:iq:roster';
const rosterVersioningXmlns = 'urn:xmpp:features:rosterver'; const rosterVersioningXmlns = 'urn:xmpp:features:rosterver';
const subscriptionPreApprovalXmlns = 'urn:xmpp:features:pre-approval';
// XEP-0004 // XEP-0004
const dataFormsXmlns = 'jabber:x:data'; const dataFormsXmlns = 'jabber:x:data';
const formVarFormType = 'FORM_TYPE';
// XEP-0030 // XEP-0030
const discoInfoXmlns = 'http://jabber.org/protocol/disco#info'; const discoInfoXmlns = 'http://jabber.org/protocol/disco#info';
@@ -20,6 +22,11 @@ const discoItemsXmlns = 'http://jabber.org/protocol/disco#items';
// XEP-0033 // XEP-0033
const extendedAddressingXmlns = 'http://jabber.org/protocol/address'; 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 // XEP-0054
const vCardTempXmlns = 'vcard-temp'; const vCardTempXmlns = 'vcard-temp';
const vCardTempUpdate = 'vcard-temp:x:update'; const vCardTempUpdate = 'vcard-temp:x:update';
@@ -65,6 +72,9 @@ const delayedDeliveryXmlns = 'urn:xmpp:delay';
// XEP-0234 // XEP-0234
const jingleFileTransferXmlns = 'urn:xmpp:jingle:apps:file-transfer:5'; const jingleFileTransferXmlns = 'urn:xmpp:jingle:apps:file-transfer:5';
// XEP-0264
const jingleContentThumbnailXmlns = 'urn:xmpp:thumbs:1';
// XEP-0280 // XEP-0280
const carbonsXmlns = 'urn:xmpp:carbons:2'; const carbonsXmlns = 'urn:xmpp:carbons:2';
@@ -74,12 +84,6 @@ const forwardedXmlns = 'urn:xmpp:forward:0';
// XEP-0300 // XEP-0300
const hashXmlns = 'urn:xmpp:hashes:2'; const hashXmlns = 'urn:xmpp:hashes:2';
const hashFunctionNameBaseXmlns = 'urn:xmpp:hash-function-text-names'; const hashFunctionNameBaseXmlns = 'urn:xmpp:hash-function-text-names';
const hashSha256 = 'sha-256';
const hashSha512 = 'sha-512';
const hashSha3256 = 'sha3-256';
const hashSha3512 = 'sha3-512';
const hashBlake2b256 = 'blake2b-256';
const hashBlake2b512 = 'blake2b-512';
// XEP-0308 // XEP-0308
const lmcXmlns = 'urn:xmpp:message-correct:0'; const lmcXmlns = 'urn:xmpp:message-correct:0';
@@ -102,7 +106,7 @@ const httpFileUploadXmlns = 'urn:xmpp:http:upload:0';
// XEP-0372 // XEP-0372
const referenceXmlns = 'urn:xmpp:reference:0'; const referenceXmlns = 'urn:xmpp:reference:0';
// XEP-380 // XEP-0380
const emeXmlns = 'urn:xmpp:eme:0'; const emeXmlns = 'urn:xmpp:eme:0';
const emeOtr = 'urn:xmpp:otr:0'; const emeOtr = 'urn:xmpp:otr:0';
const emeLegacyOpenPGP = 'jabber:x:encrypted'; const emeLegacyOpenPGP = 'jabber:x:encrypted';
@@ -128,6 +132,9 @@ const sasl2Xmlns = 'urn:xmpp:sasl:2';
// XEP-0420 // XEP-0420
const sceXmlns = 'urn:xmpp:sce:1'; const sceXmlns = 'urn:xmpp:sce:1';
// XEP-0421
const occupantIdXmlns = 'urn:xmpp:occupant-id:0';
// XEP-0422 // XEP-0422
const fasteningXmlns = 'urn:xmpp:fasten:0'; const fasteningXmlns = 'urn:xmpp:fasten:0';
@@ -159,7 +166,6 @@ const stickersXmlns = 'urn:xmpp:stickers:0';
// XEP-0461 // XEP-0461
const replyXmlns = 'urn:xmpp:reply:0'; const replyXmlns = 'urn:xmpp:reply:0';
const fallbackXmlns = 'urn:xmpp:feature-fallback:0';
// ??? // ???
const urlDataXmlns = 'http://jabber.org/protocol/url-data'; const urlDataXmlns = 'http://jabber.org/protocol/url-data';

View File

@@ -11,3 +11,4 @@ const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2';
const bind2Negotiator = 'org.moxxmpp.bind2'; const bind2Negotiator = 'org.moxxmpp.bind2';
const saslFASTNegotiator = 'org.moxxmpp.sasl.fast'; const saslFASTNegotiator = 'org.moxxmpp.sasl.fast';
const carbonsNegotiator = 'org.moxxmpp.bind2.carbons'; const carbonsNegotiator = 'org.moxxmpp.bind2.carbons';
const presenceNegotiator = 'org.moxxmpp.core.presence';

View File

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

View File

@@ -57,9 +57,10 @@ class _ChunkedConversionBuffer<S, T> {
} }
/// A buffer to put between a socket's input and a full XML stream. /// A buffer to put between a socket's input and a full XML stream.
class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> { class XMPPStreamParser
final StreamController<XMPPStreamObject> _streamController = extends StreamTransformerBase<String, List<XMPPStreamObject>> {
StreamController<XMPPStreamObject>(); final StreamController<List<XMPPStreamObject>> _streamController =
StreamController<List<XMPPStreamObject>>();
/// Turns a String into a list of [XmlEvent]s in a chunked fashion. /// Turns a String into a list of [XmlEvent]s in a chunked fashion.
_ChunkedConversionBuffer<String, XmlEvent> _eventBuffer = _ChunkedConversionBuffer<String, XmlEvent> _eventBuffer =
@@ -69,6 +70,16 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
_ChunkedConversionBuffer<List<XmlEvent>, XmlNode> _childBuffer = _ChunkedConversionBuffer<List<XmlEvent>, XmlNode> _childBuffer =
_ChunkedConversionBuffer<List<XmlEvent>, XmlNode>(const XmlNodeDecoder()); _ChunkedConversionBuffer<List<XmlEvent>, XmlNode>(const XmlNodeDecoder());
/// The selectors.
_ChunkedConversionBuffer<List<XmlEvent>, XmlEvent> _childSelector =
_ChunkedConversionBuffer<List<XmlEvent>, XmlEvent>(
XmlSubtreeSelector((event) => event.qualifiedName != 'stream:stream'),
);
_ChunkedConversionBuffer<List<XmlEvent>, XmlEvent> _streamHeaderSelector =
_ChunkedConversionBuffer<List<XmlEvent>, XmlEvent>(
XmlSubtreeSelector((event) => event.qualifiedName == 'stream:stream'),
);
void reset() { void reset() {
try { try {
_eventBuffer.close(); _eventBuffer.close();
@@ -81,6 +92,16 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
} catch (_) { } catch (_) {
// Do nothing. // Do nothing.
} }
try {
_childSelector.close();
} catch (_) {
// Do nothing.
}
try {
_streamHeaderSelector.close();
} catch (_) {
// Do nothing.
}
// Recreate the buffers. // Recreate the buffers.
_eventBuffer = _eventBuffer =
@@ -88,21 +109,23 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
_childBuffer = _ChunkedConversionBuffer<List<XmlEvent>, XmlNode>( _childBuffer = _ChunkedConversionBuffer<List<XmlEvent>, XmlNode>(
const XmlNodeDecoder(), const XmlNodeDecoder(),
); );
_childSelector = _ChunkedConversionBuffer<List<XmlEvent>, XmlEvent>(
XmlSubtreeSelector((event) => event.qualifiedName != 'stream:stream'),
);
_streamHeaderSelector = _ChunkedConversionBuffer<List<XmlEvent>, XmlEvent>(
XmlSubtreeSelector((event) => event.qualifiedName == 'stream:stream'),
);
} }
@override @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 // 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 // create streams we cannot close. We need to be able to destroy and recreate an
// XML parser whenever we start a new connection. // XML parser whenever we start a new connection.
final childSelector =
XmlSubtreeSelector((event) => event.qualifiedName != 'stream:stream');
final streamHeaderSelector =
XmlSubtreeSelector((event) => event.qualifiedName == 'stream:stream');
stream.listen((input) { stream.listen((input) {
final events = _eventBuffer.convert(input); final events = _eventBuffer.convert(input);
final streamHeaderEvents = streamHeaderSelector.convert(events); final streamHeaderEvents = _streamHeaderSelector.convert(events);
final objects = List<XMPPStreamObject>.empty(growable: true);
// Process the stream header separately. // Process the stream header separately.
for (final event in streamHeaderEvents) { for (final event in streamHeaderEvents) {
@@ -114,7 +137,7 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
continue; continue;
} }
_streamController.add( objects.add(
XMPPStreamHeader( XMPPStreamHeader(
Map<String, String>.fromEntries( Map<String, String>.fromEntries(
event.attributes.map((attr) { event.attributes.map((attr) {
@@ -126,17 +149,19 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
} }
// Process the children of the <stream:stream> element. // Process the children of the <stream:stream> element.
final childEvents = childSelector.convert(events); final childEvents = _childSelector.convert(events);
final children = _childBuffer.convert(childEvents); final children = _childBuffer.convert(childEvents);
for (final node in children) { for (final node in children) {
if (node.nodeType == XmlNodeType.ELEMENT) { if (node.nodeType == XmlNodeType.ELEMENT) {
_streamController.add( objects.add(
XMPPStreamElement( XMPPStreamElement(
XMLNode.fromXmlElement(node as XmlElement), XMLNode.fromXmlElement(node as XmlElement),
), ),
); );
} }
} }
_streamController.add(objects);
}); });
return _streamController.stream; return _streamController.stream;

View File

@@ -1,5 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:moxxmpp/src/connection.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@@ -8,15 +8,44 @@ import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart'; import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
/// A function that will be called when presence, outside of subscription request /// A function that will be called when presence, outside of subscription request
/// management, will be sent. Useful for managers that want to add [XMLNode]s to said /// management, will be sent. Useful for managers that want to add [XMLNode]s to said
/// presence. /// presence.
typedef PresencePreSendCallback = Future<List<XMLNode>> Function(); typedef PresencePreSendCallback = Future<List<XMLNode>> Function();
/// A pseudo-negotiator that does not really negotiate anything. Instead, its purpose
/// is to look for a stream feature indicating that we can pre-approve subscription
/// requests, shown by [PresenceNegotiator.preApprovalSupported].
class PresenceNegotiator extends XmppFeatureNegotiatorBase {
PresenceNegotiator()
: super(11, false, subscriptionPreApprovalXmlns, presenceNegotiator);
/// Flag indicating whether presence subscription pre-approval is supported
bool _supported = false;
bool get preApprovalSupported => _supported;
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
_supported = true;
return const Result(NegotiatorState.done);
}
@override
void reset() {
_supported = false;
super.reset();
}
}
/// A mandatory manager that handles initial presence sending, sending of subscription /// A mandatory manager that handles initial presence sending, sending of subscription
/// request management requests and triggers events for incoming presence stanzas. /// request management requests and triggers events for incoming presence stanzas.
class PresenceManager extends XmppManagerBase { class PresenceManager extends XmppManagerBase {
@@ -26,12 +55,18 @@ class PresenceManager extends XmppManagerBase {
final List<PresencePreSendCallback> _presenceCallbacks = final List<PresencePreSendCallback> _presenceCallbacks =
List.empty(growable: true); List.empty(growable: true);
/// The priority of the presence handler. If a handler should run before this one,
/// which terminates processing, make sure the handler has a priority greater than
/// [presenceHandlerPriority].
static int presenceHandlerPriority = -100;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
stanzaTag: 'presence', stanzaTag: 'presence',
callback: _onPresence, callback: _onPresence,
) priority: presenceHandlerPriority,
),
]; ];
@override @override
@@ -49,12 +84,8 @@ class PresenceManager extends XmppManagerBase {
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is StreamNegotiationsDoneEvent) { if (event is StreamNegotiationsDoneEvent) {
// Send initial presence only when we have not resumed the stream // Send initial presence only when we have not resumed the stream
final sm = getAttributes().getNegotiatorById<StreamManagementNegotiator>( if (!event.resumed) {
streamManagementNegotiator, await sendInitialPresence();
);
final isResumed = sm?.isResumed ?? false;
if (!isResumed) {
unawaited(sendInitialPresence());
} }
} }
} }
@@ -73,7 +104,7 @@ class PresenceManager extends XmppManagerBase {
from: JID.fromString(presence.from!), from: JID.fromString(presence.from!),
), ),
); );
return state.copyWith(done: true); return state..done = true;
} }
default: default:
break; break;
@@ -82,10 +113,7 @@ class PresenceManager extends XmppManagerBase {
if (presence.from != null) { if (presence.from != null) {
logger.finest("Received presence from '${presence.from}'"); logger.finest("Received presence from '${presence.from}'");
getAttributes().sendEvent( return state..done = true;
PresenceReceivedEvent(JID.fromString(presence.from!), presence),
);
return state.copyWith(done: true);
} }
return state; return state;
@@ -108,66 +136,111 @@ class PresenceManager extends XmppManagerBase {
final attrs = getAttributes(); final attrs = getAttributes();
await attrs.sendStanza( await attrs.sendStanza(
Stanza.presence( StanzaDetails(
from: attrs.getFullJID().toString(), Stanza.presence(
children: children, children: children,
),
awaitable: false,
addId: false,
), ),
awaitable: false,
addId: false,
); );
} }
/// Send an unavailable presence with no 'to' attribute. /// Send an unavailable presence with no 'to' attribute.
void sendUnavailablePresence() { Future<void> sendUnavailablePresence() async {
getAttributes().sendStanza( // Bypass the queue so that this get's sent immediately.
Stanza.presence( // If we do it like this, we can also block the disconnection
type: 'unavailable', // until we're actually ready.
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'unavailable',
),
awaitable: false,
bypassQueue: true,
postSendExtensions: TypedMap<StanzaHandlerExtension>.fromList([
const StreamManagementData(true, null),
]),
), ),
addFrom: StanzaFromType.full,
); );
} }
/// Sends a subscription request to [to]. /// Similar to [requestSubscription], but it also tells the server to automatically
void sendSubscriptionRequest(String to) { /// accept a subscription request from [to], should it arrive.
getAttributes().sendStanza( /// This requires a [PresenceNegotiator] to be registered as this feature is optional.
Stanza.presence( ///
type: 'subscribe', /// Returns true, when the stanza was sent. Returns false, when the stanza was not sent,
to: to, /// for example because the server does not support subscription pre-approvals.
Future<bool> preApproveSubscription(JID to) async {
final negotiator = getAttributes()
.getNegotiatorById<PresenceNegotiator>(presenceNegotiator);
assert(negotiator != null, 'No PresenceNegotiator registered');
if (!negotiator!.preApprovalSupported) {
return false;
}
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'subscribed',
to: to.toString(),
),
awaitable: false,
),
);
return true;
}
/// Sends a subscription request to [to].
Future<void> requestSubscription(JID to) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'subscribe',
to: to.toString(),
),
awaitable: false,
),
);
}
/// Accept a subscription request from [to].
Future<void> acceptSubscriptionRequest(JID to) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'subscribed',
to: to.toString(),
),
awaitable: false,
),
);
}
/// Send a subscription request rejection to [to].
Future<void> rejectSubscriptionRequest(JID to) async {
await getAttributes().sendStanza(
StanzaDetails(
Stanza.presence(
type: 'unsubscribed',
to: to.toString(),
),
awaitable: false,
), ),
addFrom: StanzaFromType.none,
); );
} }
/// Sends an unsubscription request to [to]. /// Sends an unsubscription request to [to].
void sendUnsubscriptionRequest(String to) { Future<void> unsubscribe(JID to) async {
getAttributes().sendStanza( await getAttributes().sendStanza(
Stanza.presence( StanzaDetails(
type: 'unsubscribe', Stanza.presence(
to: to, type: 'unsubscribe',
to: to.toString(),
),
awaitable: false,
), ),
addFrom: StanzaFromType.none,
);
}
/// Accept a presence subscription request for [to].
void sendSubscriptionRequestApproval(String to) {
getAttributes().sendStanza(
Stanza.presence(
type: 'subscribed',
to: to,
),
addFrom: StanzaFromType.none,
);
}
/// Reject a presence subscription request for [to].
void sendSubscriptionRequestRejection(String to) {
getAttributes().sendStanza(
Stanza.presence(
type: 'unsubscribed',
to: to,
),
addFrom: StanzaFromType.none,
); );
} }
} }

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:math' show Random; import 'dart:math' show Random;
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
@@ -10,7 +11,6 @@ import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart'; import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart'; import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart'; import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart'; import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
import 'package:random_string/random_string.dart'; import 'package:random_string/random_string.dart';
@@ -246,6 +246,9 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
bool _checkSignature(String base64Signature) { bool _checkSignature(String base64Signature) {
final signature = final signature =
parseKeyValue(utf8.decode(base64.decode(base64Signature))); parseKeyValue(utf8.decode(base64.decode(base64Signature)));
_log.finest(
'Expecting signature: "$_serverSignature", got: "${signature["v"]}"',
);
return signature['v']! == _serverSignature; return signature['v']! == _serverSignature;
} }
@@ -360,6 +363,11 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
@override @override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async { 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 // When we're done with SASL2, check the additional data to verify the server
// signature. // signature.
state = NegotiatorState.done; state = NegotiatorState.done;

View File

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

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/attributes.dart'; import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@@ -14,7 +15,6 @@ import 'package:moxxmpp/src/roster/errors.dart';
import 'package:moxxmpp/src/roster/state.dart'; import 'package:moxxmpp/src/roster/state.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
@immutable @immutable
class XmppRosterItem { class XmppRosterItem {
@@ -122,7 +122,7 @@ class RosterManager extends XmppManagerBase {
tagName: 'query', tagName: 'query',
tagXmlns: rosterXmlns, tagXmlns: rosterXmlns,
callback: _onRosterPush, callback: _onRosterPush,
) ),
]; ];
@override @override
@@ -145,7 +145,7 @@ class RosterManager extends XmppManagerBase {
logger.warning( logger.warning(
'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}', 'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}',
); );
return state.copyWith(done: true); return state..done = true;
} }
final query = stanza.firstTag('query', xmlns: rosterXmlns)!; final query = stanza.firstTag('query', xmlns: rosterXmlns)!;
@@ -154,7 +154,7 @@ class RosterManager extends XmppManagerBase {
if (item == null) { if (item == null) {
logger.warning('Received empty roster push'); logger.warning('Received empty roster push');
return state.copyWith(done: true); return state..done = true;
} }
unawaited( unawaited(
@@ -177,13 +177,23 @@ class RosterManager extends XmppManagerBase {
[], [],
); );
return state.copyWith(done: true); return state..done = true;
} }
/// Shared code between requesting rosters without and with roster versioning, if /// Shared code between requesting rosters without and with roster versioning, if
/// the server deems a regular roster response more efficient than n roster pushes. /// 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( Future<Result<RosterRequestResult, RosterError>> _handleRosterResponse(
XMLNode? query, XMLNode? query,
String? requestedRosterVersion,
) async { ) async {
final List<XmppRosterItem> items; final List<XmppRosterItem> items;
String? rosterVersion; String? rosterVersion;
@@ -204,6 +214,14 @@ class RosterManager extends XmppManagerBase {
.toList(); .toList();
rosterVersion = query.attributes['ver'] as String?; rosterVersion = query.attributes['ver'] as String?;
} else if (requestedRosterVersion != null) {
// Skip the handleRosterFetch call since nothing changed.
return Result(
RosterRequestResult(
[],
requestedRosterVersion,
),
);
} else { } else {
logger.warning( 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', 'Server response to roster request without roster versioning does not contain a <query /> element, while the type is not error. This violates RFC6121',
@@ -223,26 +241,34 @@ class RosterManager extends XmppManagerBase {
return Result(result); return Result(result);
} }
/// Requests the roster following RFC 6121. /// Requests the roster following RFC 6121. If [useRosterVersion] is set to false, then
Future<Result<RosterRequestResult, RosterError>> requestRoster() async { /// roster versioning will not be used, even if the server supports it and we have a last
/// known roster version.
Future<Result<RosterRequestResult, RosterError>> requestRoster({
bool useRosterVersion = true,
}) async {
final attrs = getAttributes(); final attrs = getAttributes();
final query = XMLNode.xmlns( final query = XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: rosterXmlns, xmlns: rosterXmlns,
); );
final rosterVersion = await _stateManager.getRosterVersion(); final rosterVersion = await _stateManager.getRosterVersion();
if (rosterVersion != null && rosterVersioningAvailable()) { if (rosterVersion != null &&
rosterVersioningAvailable() &&
useRosterVersion) {
query.attributes['ver'] = rosterVersion; query.attributes['ver'] = rosterVersion;
} }
final response = await attrs.sendStanza( final response = (await attrs.sendStanza(
Stanza.iq( StanzaDetails(
type: 'get', Stanza.iq(
children: [ type: 'get',
query, children: [
], query,
],
),
), ),
); ))!;
if (response.attributes['type'] != 'result') { if (response.attributes['type'] != 'result') {
logger.warning('Error requesting roster: ${response.toXml()}'); logger.warning('Error requesting roster: ${response.toXml()}');
@@ -250,7 +276,7 @@ class RosterManager extends XmppManagerBase {
} }
final responseQuery = response.firstTag('query', xmlns: rosterXmlns); 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 /// Requests a series of roster pushes according to RFC6121. Requires that the server
@@ -258,20 +284,23 @@ class RosterManager extends XmppManagerBase {
Future<Result<RosterRequestResult?, RosterError>> Future<Result<RosterRequestResult?, RosterError>>
requestRosterPushes() async { requestRosterPushes() async {
final attrs = getAttributes(); final attrs = getAttributes();
final result = await attrs.sendStanza( final rosterVersion = await _stateManager.getRosterVersion();
Stanza.iq( final result = (await attrs.sendStanza(
type: 'get', StanzaDetails(
children: [ Stanza.iq(
XMLNode.xmlns( type: 'get',
tag: 'query', children: [
xmlns: rosterXmlns, XMLNode.xmlns(
attributes: { tag: 'query',
'ver': await _stateManager.getRosterVersion() ?? '', xmlns: rosterXmlns,
}, attributes: {
) 'ver': rosterVersion ?? '',
], },
),
],
),
), ),
); ))!;
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
logger.warning('Requesting roster pushes failed: ${result.toXml()}'); logger.warning('Requesting roster pushes failed: ${result.toXml()}');
@@ -279,7 +308,7 @@ class RosterManager extends XmppManagerBase {
} }
final query = result.firstTag('query', xmlns: rosterXmlns); final query = result.firstTag('query', xmlns: rosterXmlns);
return _handleRosterResponse(query); return _handleRosterResponse(query, rosterVersion);
} }
bool rosterVersioningAvailable() { bool rosterVersioningAvailable() {
@@ -296,31 +325,31 @@ class RosterManager extends XmppManagerBase {
List<String>? groups, List<String>? groups,
}) async { }) async {
final attrs = getAttributes(); final attrs = getAttributes();
final response = await attrs.sendStanza( final response = (await attrs.sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
children: [ type: 'set',
XMLNode.xmlns( children: [
tag: 'query', XMLNode.xmlns(
xmlns: rosterXmlns, tag: 'query',
children: [ xmlns: rosterXmlns,
XMLNode( children: [
tag: 'item', XMLNode(
attributes: <String, String>{ tag: 'item',
'jid': jid, attributes: <String, String>{
...title == jid.split('@')[0] 'jid': jid,
? <String, String>{} if (title == jid.split('@')[0]) 'name': title,
: <String, String>{'name': title} },
}, children: (groups ?? [])
children: (groups ?? []) .map((group) => XMLNode(tag: 'group', text: group))
.map((group) => XMLNode(tag: 'group', text: group)) .toList(),
.toList(), ),
) ],
], ),
) ],
], ),
), ),
); ))!;
if (response.attributes['type'] != 'result') { if (response.attributes['type'] != 'result') {
logger.severe('Error adding $jid to roster: $response'); logger.severe('Error adding $jid to roster: $response');
@@ -334,26 +363,28 @@ class RosterManager extends XmppManagerBase {
/// false otherwise. /// false otherwise.
Future<RosterRemovalResult> removeFromRoster(String jid) async { Future<RosterRemovalResult> removeFromRoster(String jid) async {
final attrs = getAttributes(); final attrs = getAttributes();
final response = await attrs.sendStanza( final response = (await attrs.sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
children: [ type: 'set',
XMLNode.xmlns( children: [
tag: 'query', XMLNode.xmlns(
xmlns: rosterXmlns, tag: 'query',
children: [ xmlns: rosterXmlns,
XMLNode( children: [
tag: 'item', XMLNode(
attributes: <String, String>{ tag: 'item',
'jid': jid, attributes: {
'subscription': 'remove' 'jid': jid,
}, 'subscription': 'remove',
) },
], ),
) ],
], ),
],
),
), ),
); ))!;
if (response.attributes['type'] != 'result') { if (response.attributes['type'] != 'result') {
logger.severe('Failed to remove roster item: ${response.toXml()}'); logger.severe('Failed to remove roster item: ${response.toXml()}');

View File

@@ -19,4 +19,7 @@ class ConnectionSettings {
/// The port to connect to. Skips DNS resolution if specified. /// The port to connect to. Skips DNS resolution if specified.
final int? port; final int? port;
/// The JID of the server we're connected to.
JID get serverJid => JID('', jid.domain, '');
} }

View File

@@ -1,28 +1,109 @@
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// A simple description of the <error /> element that may be inside a stanza /// A description of a stanza to send.
class StanzaError { class StanzaDetails {
StanzaError(this.type, this.error); const StanzaDetails(
String type; this.stanza, {
String error; this.extensions,
this.addId = true,
this.awaitable = true,
this.shouldEncrypt = true,
this.encrypted = false,
this.forceEncryption = false,
this.bypassQueue = 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;
/// 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.
/// This should never have to be set to true.
final bool bypassQueue;
/// 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 TypedMap<StanzaHandlerExtension>? postSendExtensions;
}
/// 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) { static StanzaError? fromStanza(Stanza stanza) {
final error = stanza.firstTag('error'); return fromXMLNode(stanza);
if (error == null) return null;
final stanzaError = error.firstTagByXmlns(fullStanzaXmlns);
if (stanzaError == null) return null;
return StanzaError(
error.attributes['type']! as String,
stanzaError.tag,
);
} }
} }
/// Recipient does not provide a given service.
/// https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-service-unavailable
class ServiceUnavailableError extends StanzaError {
static const tag = 'service-unavailable';
}
/// Could not connect to the remote server.
/// https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-remote-server-not-found
class RemoteServerNotFoundError extends StanzaError {
static const tag = 'remote-server-not-found';
}
/// The connection to the remote server timed out.
/// https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-remote-server-timeout
class RemoteServerTimeoutError extends StanzaError {
static const tag = 'remote-server-timeout';
}
/// An unknown error.
class UnknownStanzaError extends StanzaError {}
const _stanzaNotDefined = Object();
class Stanza extends XMLNode { class Stanza extends XMLNode {
// ignore: use_super_parameters // ignore: use_super_parameters
Stanza({ Stanza({
@@ -137,7 +218,7 @@ class Stanza extends XMLNode {
Stanza copyWith({ Stanza copyWith({
String? id, String? id,
String? from, Object? from = _stanzaNotDefined,
String? to, String? to,
String? type, String? type,
List<XMLNode>? children, List<XMLNode>? children,
@@ -146,7 +227,7 @@ class Stanza extends XMLNode {
return Stanza( return Stanza(
tag: tag, tag: tag,
to: to ?? this.to, to: to ?? this.to,
from: from ?? this.from, from: from != _stanzaNotDefined ? from as String? : this.from,
id: id ?? this.id, id: id ?? this.id,
type: type ?? this.type, type: type ?? this.type,
children: children ?? this.children, children: children ?? this.children,
@@ -169,15 +250,14 @@ XMLNode buildErrorElement(String type, String condition, {String? text}) {
XMLNode.xmlns( XMLNode.xmlns(
tag: condition, tag: condition,
xmlns: fullStanzaXmlns, xmlns: fullStanzaXmlns,
children: text != null children: [
? [ if (text != null)
XMLNode.xmlns( XMLNode.xmlns(
tag: 'text', tag: 'text',
xmlns: fullStanzaXmlns, xmlns: fullStanzaXmlns,
text: text, 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

@@ -0,0 +1,5 @@
extension ListItemCountExtension<T> on List<T> {
int count(bool Function(T) matches) {
return where(matches).length;
}
}

View File

@@ -1,37 +1,61 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
/// A job to be submitted to an [AsyncQueue]. class StanzaQueueEntry {
typedef AsyncQueueJob = Future<void> Function(); const StanzaQueueEntry(
this.details,
this.completer,
);
/// The actual data to send.
final StanzaDetails details;
/// The [Completer] to resolve when the response is received.
final Completer<XMLNode>? completer;
}
/// A function that is executed when a job is popped from the queue.
typedef SendStanzaFunction = Future<void> Function(StanzaQueueEntry);
/// A function that is called before popping a queue item. Should return true when
/// the [SendStanzaFunction] can be executed.
typedef CanSendCallback = Future<bool> Function();
/// A (hopefully) async-safe queue that attempts to force /// A (hopefully) async-safe queue that attempts to force
/// in-order execution of its jobs. /// in-order execution of its jobs.
class AsyncQueue { class AsyncStanzaQueue {
/// The lock for accessing [AsyncQueue._lock] and [AsyncQueue._running]. AsyncStanzaQueue(
this._sendStanzaFunction,
this._canSendCallback,
);
/// The lock for accessing [AsyncStanzaQueue._queue].
final Lock _lock = Lock(); final Lock _lock = Lock();
/// The actual job queue. /// The actual job queue.
final Queue<AsyncQueueJob> _queue = Queue<AsyncQueueJob>(); final Queue<StanzaQueueEntry> _queue = Queue<StanzaQueueEntry>();
/// Indicates whether we are currently executing a job. /// Sends the stanza when we can pop from the queue.
bool _running = false; final SendStanzaFunction _sendStanzaFunction;
final CanSendCallback _canSendCallback;
@visibleForTesting @visibleForTesting
Queue<AsyncQueueJob> get queue => _queue; Queue<StanzaQueueEntry> get queue => _queue;
@visibleForTesting /// Adds a job [entry] to the queue.
bool get isRunning => _running; Future<void> enqueueStanza(StanzaQueueEntry entry) async {
await _lock.synchronized(() async {
_queue.add(entry);
/// Adds a job [job] to the queue. if (_queue.isNotEmpty && await _canSendCallback()) {
Future<void> addJob(AsyncQueueJob job) async { unawaited(
await _lock.synchronized(() { _runJob(_queue.removeFirst()),
_queue.add(job); );
if (!_running && _queue.isNotEmpty) {
_running = true;
unawaited(_popJob());
} }
}); });
} }
@@ -40,16 +64,26 @@ class AsyncQueue {
await _lock.synchronized(_queue.clear); await _lock.synchronized(_queue.clear);
} }
Future<void> _popJob() async { Future<void> _runJob(StanzaQueueEntry details) async {
final job = _queue.removeFirst(); await _sendStanzaFunction(details);
final future = job();
await future; await _lock.synchronized(() async {
if (_queue.isNotEmpty && await _canSendCallback()) {
unawaited(
_runJob(_queue.removeFirst()),
);
}
});
}
Future<void> restart() async {
if (!(await _canSendCallback())) return;
await _lock.synchronized(() { await _lock.synchronized(() {
if (_queue.isNotEmpty) { if (_queue.isNotEmpty) {
unawaited(_popJob()); unawaited(
} else { _runJob(_queue.removeFirst()),
_running = false; );
} }
}); });
} }

View File

@@ -0,0 +1,25 @@
/// A map, similar to Map, but always uses the type of the value as the key.
class TypedMap<B> {
/// Create an empty typed map.
TypedMap();
/// Create a typed map from a list of values.
TypedMap.fromList(List<B> items) {
for (final item in items) {
_data[item.runtimeType] = item;
}
}
/// The internal mapping of type -> data
final Map<Object, B> _data = {};
/// Associate the type of [value] with [value] in the map.
void set<T extends B>(T value) {
_data[T] = value;
}
/// 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

@@ -2,14 +2,70 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0446.dart'; import 'package:moxxmpp/src/xeps/xep_0446.dart';
/// NOTE: Specified by https://github.com/PapaTutuWawa/custom-xeps/blob/master/xep-xxxx-file-upload-notifications.md /// NOTE: Specified by https://github.com/PapaTutuWawa/custom-xeps/blob/master/xep-xxxx-file-upload-notifications.md
const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0'; const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0';
/// Indicates a file upload notification.
class FileUploadNotificationData implements StanzaHandlerExtension {
const FileUploadNotificationData(this.metadata);
/// The file metadata indicated in the upload notification.
final FileMetadataData metadata;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'file-upload',
xmlns: fileUploadNotificationXmlns,
children: [
metadata.toXML(),
],
);
}
}
/// Indicates that a file upload has been cancelled.
class FileUploadNotificationCancellationData implements StanzaHandlerExtension {
const FileUploadNotificationCancellationData(this.id);
/// The id of the upload notifiaction that is cancelled.
final String id;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'cancelled',
xmlns: fileUploadNotificationXmlns,
attributes: {
'id': id,
},
);
}
}
/// Indicates that a file upload has been completed.
class FileUploadNotificationReplacementData implements StanzaHandlerExtension {
const FileUploadNotificationReplacementData(this.id);
/// The id of the upload notifiaction that is replaced.
final String id;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'replaces',
xmlns: fileUploadNotificationXmlns,
attributes: {
'id': id,
},
);
}
}
class FileUploadNotificationManager extends XmppManagerBase { class FileUploadNotificationManager extends XmppManagerBase {
FileUploadNotificationManager() : super(fileUploadNotificationManager); FileUploadNotificationManager() : super(fileUploadNotificationManager);
@@ -47,11 +103,14 @@ class FileUploadNotificationManager extends XmppManagerBase {
) async { ) async {
final funElement = final funElement =
message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!; message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith( return state
fun: FileMetadataData.fromXML( ..extensions.set(
funElement.firstTag('file', xmlns: fileMetadataXmlns)!, FileUploadNotificationData(
), FileMetadataData.fromXML(
); funElement.firstTag('file', xmlns: fileMetadataXmlns)!,
),
),
);
} }
Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived( Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived(
@@ -60,9 +119,12 @@ class FileUploadNotificationManager extends XmppManagerBase {
) async { ) async {
final element = final element =
message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!; message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith( return state
funReplacement: element.attributes['id']! as String, ..extensions.set(
); FileUploadNotificationReplacementData(
element.attributes['id']! as String,
),
);
} }
Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived( Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived(
@@ -71,8 +133,42 @@ class FileUploadNotificationManager extends XmppManagerBase {
) async { ) async {
final element = final element =
message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!; message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith( return state
funCancellation: element.attributes['id']! as String, ..extensions.set(
); FileUploadNotificationCancellationData(
element.attributes['id']! as String,
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final fun = extensions.get<FileUploadNotificationData>();
if (fun != null) {
return [fun.toXML()];
}
final cancel = extensions.get<FileUploadNotificationCancellationData>();
if (cancel != null) {
return [cancel.toXML()];
}
final replace = extensions.get<FileUploadNotificationReplacementData>();
if (replace != null) {
return [replace.toXML()];
}
return [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
} }
} }

View File

@@ -1,4 +1,4 @@
import 'package:moxlib/moxlib.dart'; import 'package:collection/collection.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
@@ -10,14 +10,14 @@ class DataFormOption {
XMLNode toXml() { XMLNode toXml() {
return XMLNode( return XMLNode(
tag: 'option', tag: 'option',
attributes: label != null attributes: {
? <String, dynamic>{'label': label} if (label != null) 'label': label,
: <String, dynamic>{}, },
children: [ children: [
XMLNode( XMLNode(
tag: 'value', tag: 'value',
text: value, text: value,
) ),
], ],
); );
} }
@@ -45,19 +45,22 @@ class DataFormField {
return XMLNode( return XMLNode(
tag: 'field', tag: 'field',
attributes: <String, dynamic>{ attributes: <String, dynamic>{
...varAttr != null if (varAttr != null) 'var': varAttr,
? <String, dynamic>{'var': varAttr} if (type != null) 'type': type,
: <String, dynamic>{}, if (label != null) 'label': label,
...type != null ? <String, dynamic>{'type': type} : <String, dynamic>{},
...label != null
? <String, dynamic>{'label': label}
: <String, dynamic>{}
}, },
children: [ children: [
...description != null ? [XMLNode(tag: 'desc', text: description)] : [], if (description != null)
...isRequired ? [XMLNode(tag: 'required')] : [], XMLNode(
tag: 'desc',
text: description,
),
if (isRequired)
XMLNode(
tag: 'required',
),
...values.map((value) => XMLNode(tag: 'value', text: value)), ...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; final List<List<DataFormField>> items;
DataFormField? getFieldByVar(String varAttr) { DataFormField? getFieldByVar(String varAttr) {
return firstWhereOrNull(fields, (field) => field.varAttr == varAttr); return fields.firstWhereOrNull((field) => field.varAttr == varAttr);
} }
XMLNode toXml() { XMLNode toXml() {

View File

@@ -1,4 +1,5 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart';
@internal @internal
@immutable @immutable
@@ -6,7 +7,7 @@ class DiscoCacheKey {
const DiscoCacheKey(this.jid, this.node); const DiscoCacheKey(this.jid, this.node);
/// The JID we're requesting disco data from. /// The JID we're requesting disco data from.
final String jid; final JID jid;
/// Optionally the node we are requesting from. /// Optionally the node we are requesting from.
final String? node; final String? node;

View File

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

View File

@@ -1,33 +1,38 @@
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
// TODO(PapaTutuWawa): Move types into types.dart // TODO(PapaTutuWawa): Move types into types.dart
Stanza buildDiscoInfoQueryStanza(String entity, String? node) { Stanza buildDiscoInfoQueryStanza(JID entity, String? node) {
return Stanza.iq( return Stanza.iq(
to: entity, to: entity.toString(),
type: 'get', type: 'get',
children: [ children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoInfoXmlns, xmlns: discoInfoXmlns,
attributes: node != null ? {'node': node} : {}, attributes: {
) if (node != null) 'node': node,
},
),
], ],
); );
} }
Stanza buildDiscoItemsQueryStanza(String entity, {String? node}) { Stanza buildDiscoItemsQueryStanza(JID entity, {String? node}) {
return Stanza.iq( return Stanza.iq(
to: entity, to: entity.toString(),
type: 'get', type: 'get',
children: [ children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoItemsXmlns, xmlns: discoItemsXmlns,
attributes: node != null ? {'node': node} : {}, attributes: {
) if (node != null) 'node': node,
},
),
], ],
); );
} }

View File

@@ -19,13 +19,11 @@ class Identity {
XMLNode toXMLNode() { XMLNode toXMLNode() {
return XMLNode( return XMLNode(
tag: 'identity', tag: 'identity',
attributes: <String, dynamic>{ attributes: {
'category': category, 'category': category,
'type': type, 'type': type,
'name': name, 'name': name,
...lang == null if (lang != null) 'xml:lang': lang,
? <String, dynamic>{}
: <String, dynamic>{'xml:lang': lang}
}, },
); );
} }
@@ -108,13 +106,13 @@ class DiscoInfo {
@immutable @immutable
class DiscoItem { class DiscoItem {
const DiscoItem({required this.jid, this.node, this.name}); const DiscoItem({required this.jid, this.node, this.name});
final String jid; final JID jid;
final String? node; final String? node;
final String? name; final String? name;
XMLNode toXml() { XMLNode toXml() {
final attributes = { final attributes = {
'jid': jid, 'jid': jid.toString(),
}; };
if (node != null) { if (node != null) {
attributes['node'] = node!; attributes['node'] = node!;

View File

@@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/connection.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@@ -10,7 +10,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/util/wait.dart'; import 'package:moxxmpp/src/util/wait.dart';
import 'package:moxxmpp/src/xeps/xep_0030/cache.dart'; import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
@@ -41,21 +40,15 @@ class DiscoManager extends XmppManagerBase {
/// Disco identities that we advertise /// Disco identities that we advertise
final List<Identity> _identities; final List<Identity> _identities;
/// Map full JID to Capability hashes
final Map<String, CapabilityHashInfo> _capHashCache = {};
/// Map capability hash to the disco info
final Map<String, DiscoInfo> _capHashInfoCache = {};
/// Map full JID to Disco Info /// Map full JID to Disco Info
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {}; final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
/// The tracker for tracking disco#info queries that are in flight. /// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> final WaitForTracker<DiscoCacheKey, Result<StanzaError, DiscoInfo>>
_discoInfoTracker = WaitForTracker(); _discoInfoTracker = WaitForTracker();
/// The tracker for tracking disco#info queries that are in flight. /// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, List<DiscoItem>>> final WaitForTracker<DiscoCacheKey, Result<StanzaError, List<DiscoItem>>>
_discoItemsTracker = WaitForTracker(); _discoItemsTracker = WaitForTracker();
/// Cache lock /// Cache lock
@@ -74,7 +67,7 @@ class DiscoManager extends XmppManagerBase {
List<String> get features => _features; List<String> get features => _features;
@visibleForTesting @visibleForTesting
WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> WaitForTracker<DiscoCacheKey, Result<StanzaError, DiscoInfo>>
get infoTracker => _discoInfoTracker; get infoTracker => _discoInfoTracker;
@override @override
@@ -101,13 +94,7 @@ class DiscoManager extends XmppManagerBase {
@override @override
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is PresenceReceivedEvent) { if (event is StreamNegotiationsDoneEvent) {
await _onPresence(event.jid, event.presence);
} else if (event is ConnectionStateChangedEvent) {
// TODO(Unknown): This handling is stupid. We should have an event that is
// triggered when we cannot guarantee that everything is as
// it was before.
if (event.state != XmppConnectionState.connected) return;
if (event.resumed) return; if (event.resumed) return;
// Cancel all waiting requests // Cancel all waiting requests
@@ -135,6 +122,15 @@ class DiscoManager extends XmppManagerBase {
_discoItemsCallbacks[node] = callback; _discoItemsCallbacks[node] = callback;
} }
/// Add a [DiscoCacheKey]-[DiscoInfo] pair [discoInfoEntry] to the internal cache.
Future<void> addCachedDiscoInfo(
MapEntry<DiscoCacheKey, DiscoInfo> discoInfoEntry,
) async {
await _cacheLock.synchronized(() {
_discoInfoCache[discoInfoEntry.key] = discoInfoEntry.value;
});
}
/// Adds a list of features to the possible disco info response. /// Adds a list of features to the possible disco info response.
/// This function only adds features that are not already present in the disco features. /// This function only adds features that are not already present in the disco features.
void addFeatures(List<String> features) { void addFeatures(List<String> features) {
@@ -155,39 +151,6 @@ class DiscoManager extends XmppManagerBase {
} }
} }
Future<void> _onPresence(JID from, Stanza presence) async {
final c = presence.firstTag('c', xmlns: capsXmlns);
if (c == null) return;
final info = CapabilityHashInfo(
c.attributes['ver']! as String,
c.attributes['node']! as String,
c.attributes['hash']! as String,
);
// Check if we already know of that cache
var cached = false;
await _cacheLock.synchronized(() async {
if (!_capHashCache.containsKey(info.ver)) {
cached = true;
}
});
if (cached) return;
// Request the cap hash
logger.finest(
"Received capability hash we don't know about. Requesting it...",
);
final result =
await discoInfoQuery(from.toString(), node: '${info.node}#${info.ver}');
if (result.isType<DiscoError>()) return;
await _cacheLock.synchronized(() async {
_capHashCache[from.toString()] = info;
_capHashInfoCache[info.ver] = result.get<DiscoInfo>();
});
}
/// Returns the [DiscoInfo] object that would be used as the response to a disco#info /// Returns the [DiscoInfo] object that would be used as the response to a disco#info
/// query against our bare JID with no node. The results node attribute is set /// query against our bare JID with no node. The results node attribute is set
/// to [node]. /// to [node].
@@ -221,7 +184,7 @@ class DiscoManager extends XmppManagerBase {
], ],
); );
return state.copyWith(done: true); return state..done = true;
} }
await reply( await reply(
@@ -232,7 +195,7 @@ class DiscoManager extends XmppManagerBase {
], ],
); );
return state.copyWith(done: true); return state..done = true;
} }
Future<StanzaHandlerData> _onDiscoItemsRequest( Future<StanzaHandlerData> _onDiscoItemsRequest(
@@ -260,7 +223,7 @@ class DiscoManager extends XmppManagerBase {
], ],
); );
return state.copyWith(done: true); return state..done = true;
} }
return state; return state;
@@ -268,11 +231,12 @@ class DiscoManager extends XmppManagerBase {
Future<void> _exitDiscoInfoCriticalSection( Future<void> _exitDiscoInfoCriticalSection(
DiscoCacheKey key, DiscoCacheKey key,
Result<DiscoError, DiscoInfo> result, Result<StanzaError, DiscoInfo> result,
bool shouldCache,
) async { ) async {
await _cacheLock.synchronized(() async { await _cacheLock.synchronized(() async {
// Add to cache if it is a result // Add to cache if it is a result
if (result.isType<DiscoInfo>()) { if (result.isType<DiscoInfo>() && shouldCache) {
_discoInfoCache[key] = result.get<DiscoInfo>(); _discoInfoCache[key] = result.get<DiscoInfo>();
} }
}); });
@@ -280,22 +244,40 @@ class DiscoManager extends XmppManagerBase {
await _discoInfoTracker.resolve(key, result); await _discoInfoTracker.resolve(key, result);
} }
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node]. /// Send a disco#info query to [entity]. If [node] is specified, then the disco#info
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery( /// request will be directed against that one node of [entity].
String entity, { ///
/// [shouldEncrypt] indicates to possible end-to-end encryption implementations whether
/// the request should be encrypted (true) or not (false).
///
/// [shouldCache] indicates whether the successful result of the disco#info query
/// should be cached (true) or not(false).
Future<Result<StanzaError, DiscoInfo>> discoInfoQuery(
JID entity, {
String? node, String? node,
bool shouldEncrypt = true, bool shouldEncrypt = false,
bool shouldCache = true,
}) async { }) async {
final cacheKey = DiscoCacheKey(entity, node);
DiscoInfo? info; DiscoInfo? info;
final cacheKey = DiscoCacheKey(entity, node);
final ecm = getAttributes()
.getManagerById<EntityCapabilitiesManager>(entityCapabilitiesManager);
final ffuture = await _cacheLock final ffuture = await _cacheLock
.synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>( .synchronized<Future<Future<Result<StanzaError, DiscoInfo>>?>?>(
() async { () async {
// Check if we already know what the JID supports // Check if we already know what the JID supports
if (_discoInfoCache.containsKey(cacheKey)) { if (_discoInfoCache.containsKey(cacheKey)) {
info = _discoInfoCache[cacheKey]; info = _discoInfoCache[cacheKey];
return null; return null;
} else { } else {
// Check if we know entity capabilities
if (ecm != null && node == null) {
info = await ecm.getCachedDiscoInfoFromJid(entity);
if (info != null) {
return null;
}
}
return _discoInfoTracker.waitFor(cacheKey); return _discoInfoTracker.waitFor(cacheKey);
} }
}); });
@@ -309,39 +291,43 @@ class DiscoManager extends XmppManagerBase {
} }
} }
final stanza = await getAttributes().sendStanza( final stanza = (await getAttributes().sendStanza(
buildDiscoInfoQueryStanza(entity, node), StanzaDetails(
encrypted: !shouldEncrypt, buildDiscoInfoQueryStanza(entity, node),
); shouldEncrypt: shouldEncrypt,
final query = stanza.firstTag('query'); ),
if (query == null) { ))!;
final result = Result<DiscoError, DiscoInfo>(InvalidResponseDiscoError());
await _exitDiscoInfoCriticalSection(cacheKey, result); // Error handling
if (stanza.attributes['type'] == 'error') {
final result =
Result<StanzaError, DiscoInfo>(StanzaError.fromXMLNode(stanza));
await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
return result; return result;
} }
if (stanza.attributes['type'] == 'error') { final query = stanza.firstTag('query');
//final error = stanza.firstTag('error'); if (query == null) {
final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError()); final result = Result<DiscoError, DiscoInfo>(InvalidResponseDiscoError());
await _exitDiscoInfoCriticalSection(cacheKey, result); await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
return result; return result;
} }
final result = Result<DiscoError, DiscoInfo>( final result = Result<DiscoError, DiscoInfo>(
DiscoInfo.fromQuery( DiscoInfo.fromQuery(
query, query,
JID.fromString(entity), entity,
), ),
); );
await _exitDiscoInfoCriticalSection(cacheKey, result); await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
return result; return result;
} }
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node]. /// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery( Future<Result<StanzaError, List<DiscoItem>>> discoItemsQuery(
String entity, { JID entity, {
String? node, String? node,
bool shouldEncrypt = true, bool shouldEncrypt = false,
}) async { }) async {
final key = DiscoCacheKey(entity, node); final key = DiscoCacheKey(entity, node);
final future = await _discoItemsTracker.waitFor(key); final future = await _discoItemsTracker.waitFor(key);
@@ -349,10 +335,20 @@ class DiscoManager extends XmppManagerBase {
return future; return future;
} }
final stanza = await getAttributes().sendStanza( final stanza = (await getAttributes().sendStanza(
buildDiscoItemsQueryStanza(entity, node: node), StanzaDetails(
encrypted: !shouldEncrypt, buildDiscoItemsQueryStanza(entity, node: node),
) as Stanza; encrypted: !shouldEncrypt,
),
))!;
// Error handling
if (stanza.attributes['type'] == 'error') {
final result =
Result<StanzaError, List<DiscoItem>>(StanzaError.fromXMLNode(stanza));
await _discoItemsTracker.resolve(key, result);
return result;
}
final query = stanza.firstTag('query'); final query = stanza.firstTag('query');
if (query == null) { if (query == null) {
@@ -362,20 +358,11 @@ class DiscoManager extends XmppManagerBase {
return result; return result;
} }
if (stanza.type == 'error') {
//final error = stanza.firstTag('error');
//print("Disco Items error: " + error.toXml());
final result =
Result<DiscoError, List<DiscoItem>>(ErrorResponseDiscoError());
await _discoItemsTracker.resolve(key, result);
return result;
}
final items = query final items = query
.findTags('item') .findTags('item')
.map( .map(
(node) => DiscoItem( (node) => DiscoItem(
jid: node.attributes['jid']! as String, jid: JID.fromString(node.attributes['jid']! as String),
node: node.attributes['node'] as String?, node: node.attributes['node'] as String?,
name: node.attributes['name'] as String?, name: node.attributes['name'] as String?,
), ),
@@ -387,18 +374,9 @@ class DiscoManager extends XmppManagerBase {
return result; return result;
} }
/// Queries information about a jid based on its node and capability hash.
Future<Result<DiscoError, DiscoInfo>> discoInfoCapHashQuery(
String jid,
String node,
String ver,
) async {
return discoInfoQuery(jid, node: '$node#$ver');
}
Future<Result<DiscoError, List<DiscoInfo>>> performDiscoSweep() async { Future<Result<DiscoError, List<DiscoInfo>>> performDiscoSweep() async {
final attrs = getAttributes(); final attrs = getAttributes();
final serverJid = attrs.getConnectionSettings().jid.domain; final serverJid = attrs.getConnectionSettings().jid.toDomain();
final infoResults = List<DiscoInfo>.empty(growable: true); final infoResults = List<DiscoInfo>.empty(growable: true);
final result = await discoInfoQuery(serverJid); final result = await discoInfoQuery(serverJid);
if (result.isType<DiscoInfo>()) { if (result.isType<DiscoInfo>()) {
@@ -441,8 +419,8 @@ class DiscoManager extends XmppManagerBase {
/// A wrapper function around discoInfoQuery: Returns true if the entity with JID /// A wrapper function around discoInfoQuery: Returns true if the entity with JID
/// [entity] supports the disco feature [feature]. If not, returns false. /// [entity] supports the disco feature [feature]. If not, returns false.
Future<bool> supportsFeature(JID entity, String feature) async { Future<bool> supportsFeature(JID entity, String feature) async {
final info = await discoInfoQuery(entity.toString()); final info = await discoInfoQuery(entity);
if (info.isType<DiscoError>()) return false; if (info.isType<StanzaError>()) return false;
return info.get<DiscoInfo>().features.contains(feature); return info.get<DiscoInfo>().features.contains(feature);
} }

View File

@@ -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/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@@ -7,7 +8,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
abstract class VCardError {} abstract class VCardError {}
@@ -38,7 +38,7 @@ class VCardManager extends XmppManagerBase {
tagName: 'x', tagName: 'x',
tagXmlns: vCardTempUpdate, tagXmlns: vCardTempUpdate,
callback: _onPresence, callback: _onPresence,
) ),
]; ];
@override @override
@@ -56,31 +56,13 @@ class VCardManager extends XmppManagerBase {
final x = presence.firstTag('x', xmlns: vCardTempUpdate)!; final x = presence.firstTag('x', xmlns: vCardTempUpdate)!;
final hash = x.firstTag('photo')!.innerText(); final hash = x.firstTag('photo')!.innerText();
final from = JID.fromString(presence.from!).toBare().toString(); getAttributes().sendEvent(
final lastHash = _lastHash[from]; VCardAvatarUpdatedEvent(
if (lastHash != hash) { JID.fromString(presence.from!),
_lastHash[from] = hash; hash,
final vcardResult = await requestVCard(from); ),
);
if (vcardResult.isType<VCard>()) { return state;
final binval = vcardResult.get<VCard>().photo?.binval;
if (binval != null) {
getAttributes().sendEvent(
AvatarUpdatedEvent(
jid: from,
base64: binval,
hash: hash,
),
);
} else {
logger.warning('No avatar data found');
}
} else {
logger.warning('Failed to retrieve vCard for $from');
}
}
return state.copyWith(done: true);
} }
VCardPhoto? _parseVCardPhoto(XMLNode? node) { VCardPhoto? _parseVCardPhoto(XMLNode? node) {
@@ -102,20 +84,22 @@ class VCardManager extends XmppManagerBase {
); );
} }
Future<Result<VCardError, VCard>> requestVCard(String jid) async { Future<Result<VCardError, VCard>> requestVCard(JID jid) async {
final result = await getAttributes().sendStanza( final result = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
to: jid, Stanza.iq(
type: 'get', to: jid.toString(),
children: [ type: 'get',
XMLNode.xmlns( children: [
tag: 'vCard', XMLNode.xmlns(
xmlns: vCardTempXmlns, tag: 'vCard',
) xmlns: vCardTempXmlns,
], ),
],
),
encrypted: true,
), ),
encrypted: true, ))!;
);
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
return Result(UnknownVCardError()); return Result(UnknownVCardError());

View File

@@ -1,5 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@@ -9,7 +9,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0004.dart'; import 'package:moxxmpp/src/xeps/xep_0004.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
@@ -39,26 +38,20 @@ class PubSubPublishOptions {
varAttr: 'FORM_TYPE', varAttr: 'FORM_TYPE',
type: 'hidden', type: 'hidden',
), ),
...accessModel != null if (accessModel != null)
? [ DataFormField(
DataFormField( options: [],
options: [], isRequired: false,
isRequired: false, values: [accessModel!],
values: [accessModel!], varAttr: 'pubsub#access_model',
varAttr: 'pubsub#access_model', ),
) if (maxItems != null)
] DataFormField(
: [], options: [],
...maxItems != null isRequired: false,
? [ values: [maxItems!],
DataFormField( varAttr: 'pubsub#max_items',
options: [], ),
isRequired: false,
values: [maxItems!],
varAttr: 'pubsub#max_items',
),
]
: [],
], ],
).toXml(); ).toXml();
} }
@@ -88,7 +81,7 @@ class PubSubManager extends XmppManagerBase {
tagName: 'event', tagName: 'event',
tagXmlns: pubsubEventXmlns, tagXmlns: pubsubEventXmlns,
callback: _onPubsubMessage, callback: _onPubsubMessage,
) ),
]; ];
@override @override
@@ -114,10 +107,10 @@ class PubSubManager extends XmppManagerBase {
), ),
); );
return state.copyWith(done: true); return state..done = true;
} }
Future<int> _getNodeItemCount(String jid, String node) async { Future<int> _getNodeItemCount(JID jid, String node) async {
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!; final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
final response = await dm.discoItemsQuery(jid, node: node); final response = await dm.discoItemsQuery(jid, node: node);
var count = 0; var count = 0;
@@ -136,7 +129,7 @@ class PubSubManager extends XmppManagerBase {
// with the requested configuration. // with the requested configuration.
@visibleForTesting @visibleForTesting
Future<PubSubPublishOptions> preprocessPublishOptions( Future<PubSubPublishOptions> preprocessPublishOptions(
String jid, JID jid,
String node, String node,
PubSubPublishOptions options, PubSubPublishOptions options,
) async { ) async {
@@ -179,29 +172,32 @@ class PubSubManager extends XmppManagerBase {
return options; return options;
} }
Future<Result<PubSubError, bool>> subscribe(String jid, String node) async { Future<Result<PubSubError, bool>> subscribe(JID jid, String node) async {
final attrs = getAttributes(); final attrs = getAttributes();
final result = await attrs.sendStanza( final result = (await attrs.sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
to: jid, type: 'set',
children: [ to: jid.toString(),
XMLNode.xmlns( children: [
tag: 'pubsub', XMLNode.xmlns(
xmlns: pubsubXmlns, tag: 'pubsub',
children: [ xmlns: pubsubXmlns,
XMLNode( children: [
tag: 'subscribe', XMLNode(
attributes: <String, String>{ tag: 'subscribe',
'node': node, attributes: <String, String>{
'jid': attrs.getFullJID().toBare().toString(), 'node': node,
}, 'jid': attrs.getFullJID().toBare().toString(),
), },
], ),
), ],
], ),
],
),
shouldEncrypt: false,
), ),
); ))!;
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
return Result(UnknownPubSubError()); return Result(UnknownPubSubError());
@@ -220,29 +216,32 @@ class PubSubManager extends XmppManagerBase {
return Result(subscription.attributes['subscription'] == 'subscribed'); return Result(subscription.attributes['subscription'] == 'subscribed');
} }
Future<Result<PubSubError, bool>> unsubscribe(String jid, String node) async { Future<Result<PubSubError, bool>> unsubscribe(JID jid, String node) async {
final attrs = getAttributes(); final attrs = getAttributes();
final result = await attrs.sendStanza( final result = (await attrs.sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
to: jid, type: 'set',
children: [ to: jid.toString(),
XMLNode.xmlns( children: [
tag: 'pubsub', XMLNode.xmlns(
xmlns: pubsubXmlns, tag: 'pubsub',
children: [ xmlns: pubsubXmlns,
XMLNode( children: [
tag: 'unsubscribe', XMLNode(
attributes: <String, String>{ tag: 'unsubscribe',
'node': node, attributes: <String, String>{
'jid': attrs.getFullJID().toBare().toString(), 'node': node,
}, 'jid': attrs.getFullJID().toBare().toString(),
), },
], ),
), ],
], ),
],
),
shouldEncrypt: false,
), ),
); ))!;
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
return Result(UnknownPubSubError()); return Result(UnknownPubSubError());
@@ -264,7 +263,7 @@ class PubSubManager extends XmppManagerBase {
/// Publish [payload] to the PubSub node [node] on JID [jid]. Returns true if it /// Publish [payload] to the PubSub node [node] on JID [jid]. Returns true if it
/// was successful. False otherwise. /// was successful. False otherwise.
Future<Result<PubSubError, bool>> publish( Future<Result<PubSubError, bool>> publish(
String jid, JID jid,
String node, String node,
XMLNode payload, { XMLNode payload, {
String? id, String? id,
@@ -280,7 +279,7 @@ class PubSubManager extends XmppManagerBase {
} }
Future<Result<PubSubError, bool>> _publish( Future<Result<PubSubError, bool>> _publish(
String jid, JID jid,
String node, String node,
XMLNode payload, { XMLNode payload, {
String? id, String? id,
@@ -293,38 +292,41 @@ class PubSubManager extends XmppManagerBase {
pubOptions = await preprocessPublishOptions(jid, node, options); pubOptions = await preprocessPublishOptions(jid, node, options);
} }
final result = await getAttributes().sendStanza( final result = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
to: jid, type: 'set',
children: [ to: jid.toString(),
XMLNode.xmlns( children: [
tag: 'pubsub', XMLNode.xmlns(
xmlns: pubsubXmlns, tag: 'pubsub',
children: [ xmlns: pubsubXmlns,
XMLNode( children: [
tag: 'publish',
attributes: <String, String>{'node': node},
children: [
XMLNode(
tag: 'item',
attributes: id != null
? <String, String>{'id': id}
: <String, String>{},
children: [payload],
)
],
),
if (pubOptions != null)
XMLNode( XMLNode(
tag: 'publish-options', tag: 'publish',
children: [pubOptions.toXml()], attributes: <String, String>{'node': node},
children: [
XMLNode(
tag: 'item',
attributes: {
if (id != null) 'id': id,
},
children: [payload],
),
],
), ),
], if (pubOptions != null)
) XMLNode(
], tag: 'publish-options',
children: [pubOptions.toXml()],
),
],
),
],
),
shouldEncrypt: false,
), ),
); ))!;
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
final error = getPubSubError(result); final error = getPubSubError(result);
@@ -392,24 +394,34 @@ class PubSubManager extends XmppManagerBase {
} }
Future<Result<PubSubError, List<PubSubItem>>> getItems( Future<Result<PubSubError, List<PubSubItem>>> getItems(
String jid, JID jid,
String node, String node, {
) async { int? maxItems,
final result = await getAttributes().sendStanza( }) async {
Stanza.iq( final result = (await getAttributes().sendStanza(
type: 'get', StanzaDetails(
to: jid, Stanza.iq(
children: [ type: 'get',
XMLNode.xmlns( to: jid.toString(),
tag: 'pubsub', children: [
xmlns: pubsubXmlns, XMLNode.xmlns(
children: [ tag: 'pubsub',
XMLNode(tag: 'items', attributes: <String, String>{'node': node}), xmlns: pubsubXmlns,
], children: [
) XMLNode(
], tag: 'items',
attributes: {
'node': node,
if (maxItems != null) 'max_items': maxItems.toString(),
},
),
],
),
],
),
shouldEncrypt: false,
), ),
); ))!;
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
return Result(getPubSubError(result)); return Result(getPubSubError(result));
@@ -432,34 +444,37 @@ class PubSubManager extends XmppManagerBase {
} }
Future<Result<PubSubError, PubSubItem>> getItem( Future<Result<PubSubError, PubSubItem>> getItem(
String jid, JID jid,
String node, String node,
String id, String id,
) async { ) async {
final result = await getAttributes().sendStanza( final result = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
type: 'get', Stanza.iq(
to: jid, type: 'get',
children: [ to: jid.toString(),
XMLNode.xmlns( children: [
tag: 'pubsub', XMLNode.xmlns(
xmlns: pubsubXmlns, tag: 'pubsub',
children: [ xmlns: pubsubXmlns,
XMLNode( children: [
tag: 'items', XMLNode(
attributes: <String, String>{'node': node}, tag: 'items',
children: [ attributes: <String, String>{'node': node},
XMLNode( children: [
tag: 'item', XMLNode(
attributes: <String, String>{'id': id}, tag: 'item',
), attributes: <String, String>{'id': id},
], ),
), ],
], ),
), ],
], ),
],
),
shouldEncrypt: false,
), ),
); ))!;
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
return Result(getPubSubError(result)); return Result(getPubSubError(result));
@@ -481,60 +496,66 @@ class PubSubManager extends XmppManagerBase {
} }
Future<Result<PubSubError, bool>> configure( Future<Result<PubSubError, bool>> configure(
String jid, JID jid,
String node, String node,
PubSubPublishOptions options, PubSubPublishOptions options,
) async { ) async {
final attrs = getAttributes(); final attrs = getAttributes();
// Request the form // Request the form
final form = await attrs.sendStanza( final form = (await attrs.sendStanza(
Stanza.iq( StanzaDetails(
type: 'get', Stanza.iq(
to: jid, type: 'get',
children: [ to: jid.toString(),
XMLNode.xmlns( children: [
tag: 'pubsub', XMLNode.xmlns(
xmlns: pubsubOwnerXmlns, tag: 'pubsub',
children: [ xmlns: pubsubOwnerXmlns,
XMLNode( children: [
tag: 'configure', XMLNode(
attributes: <String, String>{ tag: 'configure',
'node': node, attributes: <String, String>{
}, 'node': node,
), },
], ),
), ],
], ),
],
),
shouldEncrypt: false,
), ),
); ))!;
if (form.attributes['type'] != 'result') { if (form.attributes['type'] != 'result') {
return Result(getPubSubError(form)); return Result(getPubSubError(form));
} }
final submit = await attrs.sendStanza( final submit = (await attrs.sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
to: jid, type: 'set',
children: [ to: jid.toString(),
XMLNode.xmlns( children: [
tag: 'pubsub', XMLNode.xmlns(
xmlns: pubsubOwnerXmlns, tag: 'pubsub',
children: [ xmlns: pubsubOwnerXmlns,
XMLNode( children: [
tag: 'configure', XMLNode(
attributes: <String, String>{ tag: 'configure',
'node': node, attributes: <String, String>{
}, 'node': node,
children: [ },
options.toXml(), children: [
], options.toXml(),
), ],
], ),
), ],
], ),
],
),
shouldEncrypt: false,
), ),
); ))!;
if (submit.attributes['type'] != 'result') { if (submit.attributes['type'] != 'result') {
return Result(getPubSubError(form)); return Result(getPubSubError(form));
} }
@@ -543,28 +564,31 @@ class PubSubManager extends XmppManagerBase {
} }
Future<Result<PubSubError, bool>> delete(JID host, String node) async { Future<Result<PubSubError, bool>> delete(JID host, String node) async {
final request = await getAttributes().sendStanza( final request = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
to: host.toString(), type: 'set',
children: [ to: host.toString(),
XMLNode.xmlns( children: [
tag: 'pubsub', XMLNode.xmlns(
xmlns: pubsubOwnerXmlns, tag: 'pubsub',
children: [ xmlns: pubsubOwnerXmlns,
XMLNode( children: [
tag: 'delete', XMLNode(
attributes: <String, String>{ tag: 'delete',
'node': node, attributes: <String, String>{
}, 'node': node,
), },
], ),
), ],
], ),
],
),
shouldEncrypt: false,
), ),
) as Stanza; ))!;
if (request.type != 'result') { if (request.attributes['type'] != 'result') {
// TODO(Unknown): Be more specific // TODO(Unknown): Be more specific
return Result(UnknownPubSubError()); return Result(UnknownPubSubError());
} }
@@ -577,36 +601,39 @@ class PubSubManager extends XmppManagerBase {
String node, String node,
String itemId, String itemId,
) async { ) async {
final request = await getAttributes().sendStanza( final request = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
to: host.toString(), type: 'set',
children: [ to: host.toString(),
XMLNode.xmlns( children: [
tag: 'pubsub', XMLNode.xmlns(
xmlns: pubsubXmlns, tag: 'pubsub',
children: [ xmlns: pubsubXmlns,
XMLNode( children: [
tag: 'retract', XMLNode(
attributes: <String, String>{ tag: 'retract',
'node': node, attributes: <String, String>{
}, 'node': node,
children: [ },
XMLNode( children: [
tag: 'item', XMLNode(
attributes: <String, String>{ tag: 'item',
'id': itemId, attributes: <String, String>{
}, 'id': itemId,
), },
], ),
), ],
], ),
), ],
], ),
],
),
shouldEncrypt: false,
), ),
) as Stanza; ))!;
if (request.type != 'result') { if (request.attributes['type'] != 'result') {
// TODO(Unknown): Be more specific // TODO(Unknown): Be more specific
return Result(UnknownPubSubError()); return Result(UnknownPubSubError());
} }

View File

@@ -2,32 +2,32 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
/// A data class representing the jabber:x:oob tag. /// A data class representing the jabber:x:oob tag.
class OOBData { class OOBData implements StanzaHandlerExtension {
const OOBData({this.url, this.desc}); const OOBData(this.url, this.desc);
/// The communicated URL of the OOB data
final String? url; final String? url;
/// The description of the url.
final String? desc; final String? desc;
}
XMLNode constructOOBNode(OOBData data) { XMLNode toXML() {
final children = List<XMLNode>.empty(growable: true); return XMLNode.xmlns(
tag: 'x',
if (data.url != null) { xmlns: oobDataXmlns,
children.add(XMLNode(tag: 'url', text: data.url)); children: [
if (url != null) XMLNode(tag: 'url', text: url),
if (desc != null) XMLNode(tag: 'desc', text: desc),
],
);
} }
if (data.desc != null) {
children.add(XMLNode(tag: 'desc', text: data.desc));
}
return XMLNode.xmlns(
tag: 'x',
xmlns: oobDataXmlns,
children: children,
);
} }
class OOBManager extends XmppManagerBase { class OOBManager extends XmppManagerBase {
@@ -45,7 +45,7 @@ class OOBManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the message manager // Before the message manager
priority: -99, priority: -99,
) ),
]; ];
@override @override
@@ -59,11 +59,33 @@ class OOBManager extends XmppManagerBase {
final url = x.firstTag('url'); final url = x.firstTag('url');
final desc = x.firstTag('desc'); final desc = x.firstTag('desc');
return state.copyWith( return state
oob: OOBData( ..extensions.set(
url: url?.innerText(), OOBData(
desc: desc?.innerText(), url?.innerText(),
), desc?.innerText(),
); ),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<OOBData>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
} }
} }

View File

@@ -1,12 +1,11 @@
import 'dart:convert';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/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/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart'; import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
@@ -14,10 +13,20 @@ abstract class AvatarError {}
class UnknownAvatarError extends AvatarError {} class UnknownAvatarError extends AvatarError {}
class UserAvatar { /// The result of a successful query of a users avatar.
const UserAvatar({required this.base64, required this.hash}); class UserAvatarData {
const UserAvatarData(this.base64, this.hash);
/// The base64-encoded avatar data.
final String base64; final String base64;
/// The SHA-1 hash of the raw avatar data.
final String hash; final String hash;
/// The raw avatar data.
/// NOTE: Remove newlines because "Line feeds SHOULD NOT be added but MUST be accepted"
/// (https://xmpp.org/extensions/xep-0084.html#proto-data).
List<int> get data => base64Decode(base64.replaceAll('\n', ''));
} }
class UserAvatarMetadata { class UserAvatarMetadata {
@@ -26,21 +35,40 @@ class UserAvatarMetadata {
this.length, this.length,
this.width, this.width,
this.height, this.height,
this.mime, this.type,
this.url,
); );
/// The amount of bytes in the file factory UserAvatarMetadata.fromXML(XMLNode node) {
assert(node.tag == 'info', 'node must be an <info /> element');
final width = node.attributes['width'] as String?;
final height = node.attributes['height'] as String?;
return UserAvatarMetadata(
node.attributes['id']! as String,
int.parse(node.attributes['bytes']! as String),
width != null ? int.parse(width) : null,
height != null ? int.parse(height) : null,
node.attributes['type']! as String,
node.attributes['url'] as String?,
);
}
/// The amount of bytes in the file.
final int length; final int length;
/// The identifier of the avatar /// The identifier of the avatar.
final String id; final String id;
/// Image proportions /// Image proportions.
final int width; final int? width;
final int height; final int? height;
/// The MIME type of the avatar /// The URL where the avatar can be found.
final String mime; final String? url;
/// The MIME type of the avatar.
final String type;
} }
/// NOTE: This class requires a PubSubManager /// NOTE: This class requires a PubSubManager
@@ -50,13 +78,18 @@ class UserAvatarManager extends XmppManagerBase {
PubSubManager _getPubSubManager() => PubSubManager _getPubSubManager() =>
getAttributes().getManagerById(pubsubManager)! as PubSubManager; getAttributes().getManagerById(pubsubManager)! as PubSubManager;
@override
List<String> getDiscoFeatures() => [
'$userAvatarMetadataXmlns+notify',
];
@override @override
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is PubSubNotificationEvent) { if (event is PubSubNotificationEvent) {
if (event.item.node != userAvatarDataXmlns) return; if (event.item.node != userAvatarMetadataXmlns) return;
if (event.item.payload.tag != 'data' || if (event.item.payload.tag != 'metadata' ||
event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) { event.item.payload.attributes['xmlns'] != userAvatarMetadataXmlns) {
logger.warning( logger.warning(
'Received avatar update from ${event.from} but the payload is invalid. Ignoring...', 'Received avatar update from ${event.from} but the payload is invalid. Ignoring...',
); );
@@ -64,10 +97,12 @@ class UserAvatarManager extends XmppManagerBase {
} }
getAttributes().sendEvent( getAttributes().sendEvent(
AvatarUpdatedEvent( UserAvatarUpdatedEvent(
jid: event.from, JID.fromString(event.from),
base64: event.item.payload.innerText(), event.item.payload
hash: event.item.id, .findTags('metadata', xmlns: userAvatarMetadataXmlns)
.map(UserAvatarMetadata.fromXML)
.toList(),
), ),
); );
} }
@@ -79,20 +114,44 @@ class UserAvatarManager extends XmppManagerBase {
/// Requests the avatar from [jid]. Returns the avatar data if the request was /// Requests the avatar from [jid]. Returns the avatar data if the request was
/// successful. Null otherwise /// successful. Null otherwise
Future<Result<AvatarError, UserAvatar>> getUserAvatar(String jid) async { Future<Result<AvatarError, UserAvatarData>> getUserAvatarData(
JID jid,
String id,
) async {
final pubsub = _getPubSubManager(); 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()); if (resultsRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
final results = resultsRaw.get<List<PubSubItem>>(); final results = resultsRaw.get<List<PubSubItem>>();
if (results.isEmpty) return Result(UnknownAvatarError()); if (results.isEmpty) {
return Result(UnknownAvatarError());
}
final item = results[0];
return Result( return Result(
UserAvatar( results.first.payload
base64: item.payload.innerText(), .findTags('info')
hash: item.id, .map(UserAvatarMetadata.fromXML)
), .toList(),
); );
} }
@@ -106,7 +165,7 @@ class UserAvatarManager extends XmppManagerBase {
) async { ) async {
final pubsub = _getPubSubManager(); final pubsub = _getPubSubManager();
final result = await pubsub.publish( final result = await pubsub.publish(
getAttributes().getFullJID().toBare().toString(), getAttributes().getFullJID().toBare(),
userAvatarDataXmlns, userAvatarDataXmlns,
XMLNode.xmlns( XMLNode.xmlns(
tag: 'data', tag: 'data',
@@ -133,7 +192,7 @@ class UserAvatarManager extends XmppManagerBase {
) async { ) async {
final pubsub = _getPubSubManager(); final pubsub = _getPubSubManager();
final result = await pubsub.publish( final result = await pubsub.publish(
getAttributes().getFullJID().toBare().toString(), getAttributes().getFullJID().toBare(),
userAvatarMetadataXmlns, userAvatarMetadataXmlns,
XMLNode.xmlns( XMLNode.xmlns(
tag: 'metadata', tag: 'metadata',
@@ -145,7 +204,7 @@ class UserAvatarManager extends XmppManagerBase {
'bytes': metadata.length.toString(), 'bytes': metadata.length.toString(),
'height': metadata.height.toString(), 'height': metadata.height.toString(),
'width': metadata.width.toString(), 'width': metadata.width.toString(),
'type': metadata.mime, 'type': metadata.type,
'id': metadata.id, 'id': metadata.id,
}, },
), ),
@@ -162,34 +221,16 @@ class UserAvatarManager extends XmppManagerBase {
} }
/// Subscribe the data and metadata node of [jid]. /// Subscribe the data and metadata node of [jid].
Future<Result<AvatarError, bool>> subscribe(String jid) async { Future<Result<AvatarError, bool>> subscribe(JID jid) async {
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return const Result(true); return const Result(true);
} }
/// Unsubscribe the data and metadata node of [jid]. /// Unsubscribe the data and metadata node of [jid].
Future<Result<AvatarError, bool>> unsubscribe(String jid) async { Future<Result<AvatarError, bool>> unsubscribe(JID jid) async {
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return const Result(true); 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(String 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

@@ -2,43 +2,59 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum ChatState { active, composing, paused, inactive, gone } enum ChatState implements StanzaHandlerExtension {
active,
composing,
paused,
inactive,
gone;
ChatState chatStateFromString(String raw) { factory ChatState.fromName(String state) {
switch (raw) { switch (state) {
case 'active': case 'active':
{
return ChatState.active; return ChatState.active;
} case 'composing':
case 'composing':
{
return ChatState.composing; return ChatState.composing;
} case 'paused':
case 'paused':
{
return ChatState.paused; return ChatState.paused;
} case 'inactive':
case 'inactive':
{
return ChatState.inactive; return ChatState.inactive;
} case 'gone':
case 'gone':
{
return ChatState.gone; return ChatState.gone;
} }
default:
{ throw Exception('Invalid chat state $state');
return ChatState.gone; }
}
String toName() {
switch (this) {
case ChatState.active:
return 'active';
case ChatState.composing:
return 'composing';
case ChatState.paused:
return 'paused';
case ChatState.inactive:
return 'inactive';
case ChatState.gone:
return 'gone';
}
}
XMLNode toXML() {
return XMLNode.xmlns(
tag: toName(),
xmlns: chatStateXmlns,
);
} }
} }
String chatStateToString(ChatState state) => state.toString().split('.').last;
class ChatStateManager extends XmppManagerBase { class ChatStateManager extends XmppManagerBase {
ChatStateManager() : super(chatStateManager); ChatStateManager() : super(chatStateManager);
@@ -53,7 +69,7 @@ class ChatStateManager extends XmppManagerBase {
callback: _onChatStateReceived, callback: _onChatStateReceived,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override
@@ -64,58 +80,55 @@ class ChatStateManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final element = state.stanza.firstTagByXmlns(chatStateXmlns)!; final element = state.stanza.firstTagByXmlns(chatStateXmlns)!;
ChatState? chatState;
switch (element.tag) { try {
case 'active': state.extensions.set(ChatState.fromName(element.tag));
{ } catch (_) {
chatState = ChatState.active; logger.finest('Ignoring invalid chat state ${element.tag}');
}
break;
case 'composing':
{
chatState = ChatState.composing;
}
break;
case 'paused':
{
chatState = ChatState.paused;
}
break;
case 'inactive':
{
chatState = ChatState.inactive;
}
break;
case 'gone':
{
chatState = ChatState.gone;
}
break;
default:
{
logger.warning("Received invalid chat state '${element.tag}'");
}
} }
return state.copyWith(chatState: chatState); return state;
} }
/// Send a chat state notification to [to]. You can specify the type attribute /// Send a chat state notification to [to]. You can specify the type attribute
/// of the message with [messageType]. /// of the message with [messageType].
void sendChatState( Future<void> sendChatState(
ChatState state, ChatState state,
String to, { String to, {
String messageType = 'chat', String messageType = 'chat',
}) { }) async {
final tagName = state.toString().split('.').last; await getAttributes().sendStanza(
StanzaDetails(
getAttributes().sendStanza( Stanza.message(
Stanza.message( to: to,
to: to, type: messageType,
type: messageType, children: [
children: [XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns)], state.toXML(),
],
),
awaitable: false,
), ),
); );
} }
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<ChatState>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
} }

View File

@@ -1,37 +1,37 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/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/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/presence.dart'; import 'package:moxxmpp/src/presence.dart';
import 'package:moxxmpp/src/rfcs/rfc_4790.dart'; import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; 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/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0414.dart'; import 'package:moxxmpp/src/xeps/xep_0300.dart';
import 'package:synchronized/synchronized.dart';
@immutable /// Given an identity [i], compute the string according to XEP-0115 § 5.1 step 2.
class CapabilityHashInfo { String _identityString(Identity i) =>
const CapabilityHashInfo(this.ver, this.node, this.hash); '${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}';
final String ver;
final String node;
final String hash;
}
/// Calculates the Entitiy Capability hash according to XEP-0115 based on the /// Calculates the Entitiy Capability hash according to XEP-0115 based on the
/// disco information. /// disco information.
Future<String> calculateCapabilityHash( Future<String> calculateCapabilityHash(
HashFunction algorithm,
DiscoInfo info, DiscoInfo info,
HashAlgorithm algorithm,
) async { ) async {
final buffer = StringBuffer(); final buffer = StringBuffer();
final identitiesSorted = info.identities final identitiesSorted = info.identities.map(_identityString).toList();
.map(
(Identity i) =>
'${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}',
)
.toList();
// ignore: cascade_invocations // ignore: cascade_invocations
identitiesSorted.sort(ioctetSortComparator); identitiesSorted.sort(ioctetSortComparator);
buffer.write('${identitiesSorted.join("<")}<'); buffer.write('${identitiesSorted.join("<")}<');
@@ -72,8 +72,11 @@ Future<String> calculateCapabilityHash(
} }
} }
return base64 final rawHash = await CryptographicHashManager.hashFromData(
.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes); algorithm,
utf8.encode(buffer.toString()),
);
return base64.encode(rawHash);
} }
/// A manager implementing the advertising of XEP-0115. It responds to the /// A manager implementing the advertising of XEP-0115. It responds to the
@@ -91,20 +94,42 @@ class EntityCapabilitiesManager extends XmppManagerBase {
/// The cached capability hash. /// The cached capability hash.
String? _capabilityHash; String? _capabilityHash;
/// Cache the mapping between the full JID and the capability hash string.
final Map<String, String> _jidToCapHashCache = {};
/// Cache the mapping between capability hash string and the resulting disco#info.
final Map<String, DiscoInfo> _capHashCache = {};
/// A lock guarding access to the capability hash cache.
final Lock _cacheLock = Lock();
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
@override @override
List<String> getDiscoFeatures() => [capsXmlns]; List<String> getDiscoFeatures() => [
capsXmlns,
];
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'presence',
tagName: 'c',
tagXmlns: capsXmlns,
callback: onPresence,
priority: PresenceManager.presenceHandlerPriority + 1,
),
];
/// Computes, if required, the capability hash of the data provided by /// Computes, if required, the capability hash of the data provided by
/// the DiscoManager. /// the DiscoManager.
Future<String> getCapabilityHash() async { Future<String> getCapabilityHash() async {
_capabilityHash ??= await calculateCapabilityHash( _capabilityHash ??= await calculateCapabilityHash(
HashFunction.sha1,
getAttributes() getAttributes()
.getManagerById<DiscoManager>(discoManager)! .getManagerById<DiscoManager>(discoManager)!
.getDiscoInfo(null), .getDiscoInfo(null),
getHashByName('sha-1')!,
); );
return _capabilityHash!; return _capabilityHash!;
@@ -135,20 +160,220 @@ class EntityCapabilitiesManager extends XmppManagerBase {
]; ];
} }
/// If we know of [jid]'s capability hash, look up the [DiscoInfo] associated with
/// that capability hash. If we don't know of [jid]'s capability hash, return null.
Future<DiscoInfo?> getCachedDiscoInfoFromJid(JID jid) async {
return _cacheLock.synchronized(() {
final capHash = _jidToCapHashCache[jid.toString()];
if (capHash == null) {
return null;
}
return _capHashCache[capHash];
});
}
Future<void> _performQuery(
Stanza presence,
String ver,
String hashFunctionName,
String capabilityNode,
JID from,
) async {
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
final discoRequest = await dm.discoInfoQuery(
from,
node: capabilityNode,
);
if (discoRequest.isType<StanzaError>()) {
return;
}
final discoInfo = discoRequest.get<DiscoInfo>();
final hashFunction = HashFunction.maybeFromName(hashFunctionName);
if (hashFunction == null) {
await dm.addCachedDiscoInfo(
MapEntry<DiscoCacheKey, DiscoInfo>(
DiscoCacheKey(
from,
null,
),
discoInfo,
),
);
return;
}
// Validate the disco#info result according to XEP-0115 § 5.4
// > If the response includes more than one service discovery identity with the
// > same category/type/lang/name, consider the entire response to be ill-formed.
for (final identity in discoInfo.identities) {
final identityString = _identityString(identity);
if (discoInfo.identities
.count((i) => _identityString(i) == identityString) >
1) {
logger.warning(
'Malformed disco#info response: More than one equal identity',
);
return;
}
}
// > If the response includes more than one service discovery feature with the same
// > XML character data, consider the entire response to be ill-formed.
for (final feature in discoInfo.features) {
if (discoInfo.features.count((f) => f == feature) > 1) {
logger.warning(
'Malformed disco#info response: More than one equal feature',
);
return;
}
}
// > If the response includes more than one extended service discovery information
// > form with the same FORM_TYPE or the FORM_TYPE field contains more than one
// > <value/> element with different XML character data, consider the entire response
// > to be ill-formed.
// >
// > If the response includes an extended service discovery information form where
// > the FORM_TYPE field is not of type "hidden" or the form does not include a
// > FORM_TYPE field, ignore the form but continue processing.
final validExtendedInfoItems = List<DataForm>.empty(growable: true);
for (final extendedInfo in discoInfo.extendedInfo) {
final formType = extendedInfo.getFieldByVar('FORM_TYPE');
// Form should have a FORM_TYPE field
if (formType == null) {
logger.fine('Skipping extended info as it contains no FORM_TYPE field');
continue;
}
// Check if we only have one unique FORM_TYPE value
if (formType.values.length > 1) {
if (Set<String>.from(formType.values).length > 1) {
logger.warning(
'Malformed disco#info response: Extended Info FORM_TYPE contains more than one value(s) of different value.',
);
return;
}
}
// Check if we have more than one extended info forms of the same type
final sameFormTypeFormsNumber = discoInfo.extendedInfo.count((form) {
final type = form.getFieldByVar('FORM_TYPE')?.values.first;
if (type == null) return false;
return type == formType.values.first;
});
if (sameFormTypeFormsNumber > 1) {
logger.warning(
'Malformed disco#info response: More than one Extended Disco Info forms with the same FORM_TYPE value',
);
return;
}
// Check if the field type is hidden
if (formType.type != 'hidden') {
logger.fine(
'Skipping extended info as the FORM_TYPE field is not of type "hidden"',
);
continue;
}
validExtendedInfoItems.add(extendedInfo);
}
// Validate the capability hash
final newDiscoInfo = DiscoInfo(
discoInfo.features,
discoInfo.identities,
validExtendedInfoItems,
discoInfo.node,
discoInfo.jid,
);
final computedCapabilityHash = await calculateCapabilityHash(
hashFunction,
newDiscoInfo,
);
if (computedCapabilityHash == ver) {
await _cacheLock.synchronized(() {
_jidToCapHashCache[from.toString()] = ver;
_capHashCache[ver] = newDiscoInfo;
});
} else {
logger.warning(
'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;
}
@visibleForTesting
void injectIntoCache(JID jid, String ver, DiscoInfo info) {
_jidToCapHashCache[jid.toString()] = ver;
_capHashCache[ver] = info;
}
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is StreamNegotiationsDoneEvent) {
// Clear the JID to cap. hash mapping.
await _cacheLock.synchronized(_jidToCapHashCache.clear);
}
}
@override @override
Future<void> postRegisterCallback() async { Future<void> postRegisterCallback() async {
await super.postRegisterCallback(); await super.postRegisterCallback();
getAttributes() getAttributes()
.getManagerById<DiscoManager>(discoManager)! .getManagerById<DiscoManager>(discoManager)
.registerInfoCallback( ?.registerInfoCallback(
await _getNode(), await _getNode(),
_onInfoQuery, _onInfoQuery,
); );
getAttributes() getAttributes()
.getManagerById<PresenceManager>(presenceManager)! .getManagerById<PresenceManager>(presenceManager)
.registerPreSendCallback( ?.registerPreSendCallback(
_prePresenceSent, _prePresenceSent,
); );
} }

View File

@@ -4,23 +4,43 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
XMLNode makeMessageDeliveryRequest() { class MessageDeliveryReceiptData implements StanzaHandlerExtension {
return XMLNode.xmlns( const MessageDeliveryReceiptData(this.receiptRequested);
tag: 'request',
xmlns: deliveryXmlns, /// Indicates whether a delivery receipt is requested or not.
); final bool receiptRequested;
XMLNode toXML() {
assert(
receiptRequested,
'This method makes little sense with receiptRequested == false',
);
return XMLNode.xmlns(
tag: 'request',
xmlns: deliveryXmlns,
);
}
} }
XMLNode makeMessageDeliveryResponse(String id) { class MessageDeliveryReceivedData implements StanzaHandlerExtension {
return XMLNode.xmlns( const MessageDeliveryReceivedData(this.id);
tag: 'received',
xmlns: deliveryXmlns, /// The stanza id of the message we received.
attributes: {'id': id}, final String id;
);
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'received',
xmlns: deliveryXmlns,
attributes: {'id': id},
);
}
} }
class MessageDeliveryReceiptManager extends XmppManagerBase { class MessageDeliveryReceiptManager extends XmppManagerBase {
@@ -46,7 +66,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
callback: _onDeliveryRequestReceived, callback: _onDeliveryRequestReceived,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override
@@ -56,7 +76,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
Stanza message, Stanza message,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
return state.copyWith(deliveryReceiptRequested: true); return state..extensions.set(const MessageDeliveryReceiptData(true));
} }
Future<StanzaHandlerData> _onDeliveryReceiptReceived( Future<StanzaHandlerData> _onDeliveryReceiptReceived(
@@ -64,16 +84,16 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final received = message.firstTag('received', xmlns: deliveryXmlns)!; final received = message.firstTag('received', xmlns: deliveryXmlns)!;
for (final item in message.children) { // for (final item in message.children) {
if (!['origin-id', 'stanza-id', 'delay', 'store', 'received'] // if (!['origin-id', 'stanza-id', 'delay', 'store', 'received']
.contains(item.tag)) { // .contains(item.tag)) {
logger.info( // logger.info(
"Won't handle stanza as delivery receipt because we found an '${item.tag}' element", // "Won't handle stanza as delivery receipt because we found an '${item.tag}' element",
); // );
return state.copyWith(done: true); // return state.copyWith(done: true);
} // }
} // }
getAttributes().sendEvent( getAttributes().sendEvent(
DeliveryReceiptReceivedEvent( DeliveryReceiptReceivedEvent(
@@ -81,6 +101,27 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
id: received.attributes['id']! as String, id: received.attributes['id']! as String,
), ),
); );
return state.copyWith(done: true); return state..done = true;
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<MessageDeliveryReceivedData>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
} }
} }

View File

@@ -27,7 +27,7 @@ class BlockingManager extends XmppManagerBase {
tagName: 'block', tagName: 'block',
tagXmlns: blockingXmlns, tagXmlns: blockingXmlns,
callback: _blockPush, callback: _blockPush,
) ),
]; ];
@override @override
@@ -37,7 +37,7 @@ class BlockingManager extends XmppManagerBase {
// Query the server // Query the server
final disco = getAttributes().getManagerById<DiscoManager>(discoManager)!; final disco = getAttributes().getManagerById<DiscoManager>(discoManager)!;
_supported = await disco.supportsFeature( _supported = await disco.supportsFeature(
getAttributes().getConnectionSettings().jid.toBare(), getAttributes().getConnectionSettings().serverJid,
blockingXmlns, blockingXmlns,
); );
_gotSupported = true; _gotSupported = true;
@@ -70,7 +70,7 @@ class BlockingManager extends XmppManagerBase {
), ),
); );
return state.copyWith(done: true); return state..done = true;
} }
Future<StanzaHandlerData> _unblockPush( Future<StanzaHandlerData> _unblockPush(
@@ -92,43 +92,49 @@ class BlockingManager extends XmppManagerBase {
); );
} }
return state.copyWith(done: true); return state..done = true;
} }
Future<bool> block(List<String> items) async { Future<bool> block(List<String> items) async {
final result = await getAttributes().sendStanza( final result = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
children: [ type: 'set',
XMLNode.xmlns( children: [
tag: 'block', XMLNode.xmlns(
xmlns: blockingXmlns, tag: 'block',
children: items.map((item) { xmlns: blockingXmlns,
return XMLNode( children: items.map((item) {
tag: 'item', return XMLNode(
attributes: <String, String>{'jid': item}, tag: 'item',
); attributes: {
}).toList(), 'jid': item,
) },
], );
}).toList(),
),
],
),
), ),
); ))!;
return result.attributes['type'] == 'result'; return result.attributes['type'] == 'result';
} }
Future<bool> unblockAll() async { Future<bool> unblockAll() async {
final result = await getAttributes().sendStanza( final result = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
children: [ type: 'set',
XMLNode.xmlns( children: [
tag: 'unblock', XMLNode.xmlns(
xmlns: blockingXmlns, tag: 'unblock',
) xmlns: blockingXmlns,
], ),
],
),
), ),
); ))!;
return result.attributes['type'] == 'result'; return result.attributes['type'] == 'result';
} }
@@ -136,41 +142,47 @@ class BlockingManager extends XmppManagerBase {
Future<bool> unblock(List<String> items) async { Future<bool> unblock(List<String> items) async {
assert(items.isNotEmpty, 'The list of items to unblock must be non-empty'); assert(items.isNotEmpty, 'The list of items to unblock must be non-empty');
final result = await getAttributes().sendStanza( final result = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
children: [ type: 'set',
XMLNode.xmlns( children: [
tag: 'unblock', XMLNode.xmlns(
xmlns: blockingXmlns, tag: 'unblock',
children: items xmlns: blockingXmlns,
.map( children: items
(item) => XMLNode( .map(
tag: 'item', (item) => XMLNode(
attributes: <String, String>{'jid': item}, tag: 'item',
), attributes: {
) 'jid': item,
.toList(), },
) ),
], )
.toList(),
),
],
),
), ),
); ))!;
return result.attributes['type'] == 'result'; return result.attributes['type'] == 'result';
} }
Future<List<String>> getBlocklist() async { Future<List<String>> getBlocklist() async {
final result = await getAttributes().sendStanza( final result = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
type: 'get', Stanza.iq(
children: [ type: 'get',
XMLNode.xmlns( children: [
tag: 'blocklist', XMLNode.xmlns(
xmlns: blockingXmlns, tag: 'blocklist',
) xmlns: blockingXmlns,
], ),
],
),
), ),
); ))!;
final blocklist = result.firstTag('blocklist', xmlns: blockingXmlns)!; final blocklist = result.firstTag('blocklist', xmlns: blockingXmlns)!;
return blocklist return blocklist

View File

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

View File

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

View File

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

View File

@@ -1,217 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
part of 'state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
StreamManagementState _$StreamManagementStateFromJson(
Map<String, dynamic> json) {
return _StreamManagementState.fromJson(json);
}
/// @nodoc
mixin _$StreamManagementState {
int get c2s => throw _privateConstructorUsedError;
int get s2c => throw _privateConstructorUsedError;
String? get streamResumptionLocation => throw _privateConstructorUsedError;
String? get streamResumptionId => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$StreamManagementStateCopyWith<StreamManagementState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $StreamManagementStateCopyWith<$Res> {
factory $StreamManagementStateCopyWith(StreamManagementState value,
$Res Function(StreamManagementState) then) =
_$StreamManagementStateCopyWithImpl<$Res>;
$Res call(
{int c2s,
int s2c,
String? streamResumptionLocation,
String? streamResumptionId});
}
/// @nodoc
class _$StreamManagementStateCopyWithImpl<$Res>
implements $StreamManagementStateCopyWith<$Res> {
_$StreamManagementStateCopyWithImpl(this._value, this._then);
final StreamManagementState _value;
// ignore: unused_field
final $Res Function(StreamManagementState) _then;
@override
$Res call({
Object? c2s = freezed,
Object? s2c = freezed,
Object? streamResumptionLocation = freezed,
Object? streamResumptionId = freezed,
}) {
return _then(_value.copyWith(
c2s: c2s == freezed
? _value.c2s
: c2s // ignore: cast_nullable_to_non_nullable
as int,
s2c: s2c == freezed
? _value.s2c
: s2c // ignore: cast_nullable_to_non_nullable
as int,
streamResumptionLocation: streamResumptionLocation == freezed
? _value.streamResumptionLocation
: streamResumptionLocation // ignore: cast_nullable_to_non_nullable
as String?,
streamResumptionId: streamResumptionId == freezed
? _value.streamResumptionId
: streamResumptionId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
abstract class _$$_StreamManagementStateCopyWith<$Res>
implements $StreamManagementStateCopyWith<$Res> {
factory _$$_StreamManagementStateCopyWith(_$_StreamManagementState value,
$Res Function(_$_StreamManagementState) then) =
__$$_StreamManagementStateCopyWithImpl<$Res>;
@override
$Res call(
{int c2s,
int s2c,
String? streamResumptionLocation,
String? streamResumptionId});
}
/// @nodoc
class __$$_StreamManagementStateCopyWithImpl<$Res>
extends _$StreamManagementStateCopyWithImpl<$Res>
implements _$$_StreamManagementStateCopyWith<$Res> {
__$$_StreamManagementStateCopyWithImpl(_$_StreamManagementState _value,
$Res Function(_$_StreamManagementState) _then)
: super(_value, (v) => _then(v as _$_StreamManagementState));
@override
_$_StreamManagementState get _value =>
super._value as _$_StreamManagementState;
@override
$Res call({
Object? c2s = freezed,
Object? s2c = freezed,
Object? streamResumptionLocation = freezed,
Object? streamResumptionId = freezed,
}) {
return _then(_$_StreamManagementState(
c2s == freezed
? _value.c2s
: c2s // ignore: cast_nullable_to_non_nullable
as int,
s2c == freezed
? _value.s2c
: s2c // ignore: cast_nullable_to_non_nullable
as int,
streamResumptionLocation: streamResumptionLocation == freezed
? _value.streamResumptionLocation
: streamResumptionLocation // ignore: cast_nullable_to_non_nullable
as String?,
streamResumptionId: streamResumptionId == freezed
? _value.streamResumptionId
: streamResumptionId // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$_StreamManagementState implements _StreamManagementState {
_$_StreamManagementState(this.c2s, this.s2c,
{this.streamResumptionLocation, this.streamResumptionId});
factory _$_StreamManagementState.fromJson(Map<String, dynamic> json) =>
_$$_StreamManagementStateFromJson(json);
@override
final int c2s;
@override
final int s2c;
@override
final String? streamResumptionLocation;
@override
final String? streamResumptionId;
@override
String toString() {
return 'StreamManagementState(c2s: $c2s, s2c: $s2c, streamResumptionLocation: $streamResumptionLocation, streamResumptionId: $streamResumptionId)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_StreamManagementState &&
const DeepCollectionEquality().equals(other.c2s, c2s) &&
const DeepCollectionEquality().equals(other.s2c, s2c) &&
const DeepCollectionEquality().equals(
other.streamResumptionLocation, streamResumptionLocation) &&
const DeepCollectionEquality()
.equals(other.streamResumptionId, streamResumptionId));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(c2s),
const DeepCollectionEquality().hash(s2c),
const DeepCollectionEquality().hash(streamResumptionLocation),
const DeepCollectionEquality().hash(streamResumptionId));
@JsonKey(ignore: true)
@override
_$$_StreamManagementStateCopyWith<_$_StreamManagementState> get copyWith =>
__$$_StreamManagementStateCopyWithImpl<_$_StreamManagementState>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_StreamManagementStateToJson(
this,
);
}
}
abstract class _StreamManagementState implements StreamManagementState {
factory _StreamManagementState(final int c2s, final int s2c,
{final String? streamResumptionLocation,
final String? streamResumptionId}) = _$_StreamManagementState;
factory _StreamManagementState.fromJson(Map<String, dynamic> json) =
_$_StreamManagementState.fromJson;
@override
int get c2s;
@override
int get s2c;
@override
String? get streamResumptionLocation;
@override
String? get streamResumptionId;
@override
@JsonKey(ignore: true)
_$$_StreamManagementStateCopyWith<_$_StreamManagementState> get copyWith =>
throw _privateConstructorUsedError;
}

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

@@ -0,0 +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, 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,10 +11,12 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0198/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0198/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart'; import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart'; import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
import 'package:moxxmpp/src/xeps/xep_0198/state.dart'; import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
const xmlUintMax = 4294967296; // 2**32 const xmlUintMax = 4294967296; // 2**32
@@ -27,10 +29,10 @@ class StreamManagementManager extends XmppManagerBase {
}) : super(smManager); }) : super(smManager);
/// The queue of stanzas that are not (yet) acked /// The queue of stanzas that are not (yet) acked
final Map<int, Stanza> _unackedStanzas = {}; final Map<int, SMQueueEntry> _unackedStanzas = {};
/// Commitable state of the StreamManagementManager /// Commitable state of the StreamManagementManager
StreamManagementState _state = StreamManagementState(0, 0); StreamManagementState _state = const StreamManagementState(0, 0);
/// Mutex lock for _state /// Mutex lock for _state
final Lock _stateLock = Lock(); final Lock _stateLock = Lock();
@@ -60,7 +62,7 @@ class StreamManagementManager extends XmppManagerBase {
/// Functions for testing /// Functions for testing
@visibleForTesting @visibleForTesting
Map<int, Stanza> getUnackedStanzas() => _unackedStanzas; Map<int, SMQueueEntry> getUnackedStanzas() => _unackedStanzas;
@visibleForTesting @visibleForTesting
Future<int> getPendingAcks() async { Future<int> getPendingAcks() async {
@@ -73,6 +75,17 @@ class StreamManagementManager extends XmppManagerBase {
return acks; 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 /// Called when a stanza has been acked to decide whether we should trigger a
/// StanzaAckedEvent. /// StanzaAckedEvent.
/// ///
@@ -138,7 +151,7 @@ class StreamManagementManager extends XmppManagerBase {
nonzaTag: 'a', nonzaTag: 'a',
nonzaXmlns: smXmlns, nonzaXmlns: smXmlns,
callback: _handleAckResponse, callback: _handleAckResponse,
) ),
]; ];
@override @override
@@ -146,14 +159,14 @@ class StreamManagementManager extends XmppManagerBase {
StanzaHandler( StanzaHandler(
callback: _onServerStanzaReceived, callback: _onServerStanzaReceived,
priority: 9999, priority: 9999,
) ),
]; ];
@override @override
List<StanzaHandler> getOutgoingPostStanzaHandlers() => [ List<StanzaHandler> getOutgoingPostStanzaHandlers() => [
StanzaHandler( StanzaHandler(
callback: _onClientStanzaSent, callback: _onClientStanzaSent,
) ),
]; ];
@override @override
@@ -188,7 +201,9 @@ class StreamManagementManager extends XmppManagerBase {
switch (event.state) { switch (event.state) {
case XmppConnectionState.connected: case XmppConnectionState.connected:
// Push out all pending stanzas // Push out all pending stanzas
await onStreamResumed(0); if (!_streamResumed) {
await _resendStanzas();
}
break; break;
case XmppConnectionState.error: case XmppConnectionState.error:
case XmppConnectionState.notConnected: case XmppConnectionState.notConnected:
@@ -221,6 +236,12 @@ class StreamManagementManager extends XmppManagerBase {
_ackTimer = null; _ackTimer = null;
} }
/// Resets the ack timer.
void _resetAckTimer() {
_stopAckTimer();
_startAckTimer();
}
@visibleForTesting @visibleForTesting
Future<void> handleAckTimeout() async { Future<void> handleAckTimeout() async {
_stopAckTimer(); _stopAckTimer();
@@ -255,7 +276,7 @@ class StreamManagementManager extends XmppManagerBase {
_pendingAcks++; _pendingAcks++;
_startAckTimer(); _startAckTimer();
logger.fine('_pendingAcks is now at $_pendingAcks'); logger.fine('_pendingAcks is now at $_pendingAcks (caused by <r/>)');
getAttributes().sendNonza(StreamManagementRequestNonza()); getAttributes().sendNonza(StreamManagementRequestNonza());
@@ -294,6 +315,7 @@ class StreamManagementManager extends XmppManagerBase {
/// Called when we receive an <a /> nonza from the server. /// Called when we receive an <a /> nonza from the server.
/// This is a response to the question "How many of my stanzas have you handled". /// This is a response to the question "How many of my stanzas have you handled".
Future<bool> _handleAckResponse(XMLNode nonza) async { Future<bool> _handleAckResponse(XMLNode nonza) async {
logger.finest('Received ack');
final h = int.parse(nonza.attributes['h']! as String); final h = int.parse(nonza.attributes['h']! as String);
_lastAckTimestamp = DateTime.now().millisecondsSinceEpoch; _lastAckTimestamp = DateTime.now().millisecondsSinceEpoch;
@@ -302,11 +324,15 @@ class StreamManagementManager extends XmppManagerBase {
if (_pendingAcks > 0) { if (_pendingAcks > 0) {
// Prevent diff from becoming negative // Prevent diff from becoming negative
final diff = max(_state.c2s - h, 0); final diff = max(_state.c2s - h, 0);
logger.finest(
'Setting _pendingAcks to $diff (was $_pendingAcks before): max(${_state.c2s} - $h, 0)',
);
_pendingAcks = diff; _pendingAcks = diff;
// Reset the timer // Reset the timer
if (_pendingAcks > 0) { if (_pendingAcks > 0) {
_startAckTimer(); _resetAckTimer();
} }
} }
@@ -314,7 +340,7 @@ class StreamManagementManager extends XmppManagerBase {
_stopAckTimer(); _stopAckTimer();
} }
logger.fine('_pendingAcks is now at $_pendingAcks'); logger.fine('_pendingAcks is now at $_pendingAcks (caused by <a/>)');
}); });
}); });
@@ -331,15 +357,18 @@ class StreamManagementManager extends XmppManagerBase {
final attrs = getAttributes(); final attrs = getAttributes();
final sequences = _unackedStanzas.keys.toList()..sort(); final sequences = _unackedStanzas.keys.toList()..sort();
for (final height in sequences) { for (final height in sequences) {
logger.finest('Unacked stanza: height $height, h $h');
// Do nothing if the ack does not concern this stanza // Do nothing if the ack does not concern this stanza
if (height > h) continue; if (height > h) continue;
final stanza = _unackedStanzas[height]!; logger.finest('Removing stanza with height $height');
final entry = _unackedStanzas[height]!;
_unackedStanzas.remove(height); _unackedStanzas.remove(height);
// Create a StanzaAckedEvent if the stanza is correct // Create a StanzaAckedEvent if the stanza is correct
if (shouldTriggerAckedEvent(stanza)) { if (shouldTriggerAckedEvent(entry.stanza)) {
attrs.sendEvent(StanzaAckedEvent(stanza)); attrs.sendEvent(StanzaAckedEvent(entry.stanza));
} }
} }
@@ -395,30 +424,67 @@ class StreamManagementManager extends XmppManagerBase {
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
await _incrementC2S();
_unackedStanzas[_state.c2s] = stanza;
if (isStreamManagementEnabled()) { if (isStreamManagementEnabled()) {
final smData = state.extensions.get<StreamManagementData>();
logger.finest('Should count stanza: ${smData?.shouldCountStanza}');
if (smData?.shouldCountStanza ?? true) {
await _incrementC2S();
}
if (smData?.exclude ?? false) {
return state;
}
int queueId;
if (smData?.queueId != null) {
logger.finest('Reusing queue id ${smData!.queueId}');
queueId = smData.queueId!;
} else {
queueId = await _stateLock.synchronized(() => _state.c2s);
}
_unackedStanzas[queueId] = SMQueueEntry(
stanza,
// Prevent an E2EE message being encrypted again
state.encrypted,
);
await _sendAckRequest(); await _sendAckRequest();
} }
return state; return state;
} }
Future<void> _resendStanzas() async {
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(
entry.value.stanza,
postSendExtensions: TypedMap<StanzaHandlerExtension>.fromList([
StreamManagementData(
false,
entry.key,
),
]),
awaitable: false,
// Prevent an E2EE message being encrypted again
encrypted: entry.value.encrypted,
),
);
}
}
/// To be called when the stream has been resumed /// To be called when the stream has been resumed
@visibleForTesting @visibleForTesting
Future<void> onStreamResumed(int h) async { Future<void> onStreamResumed(int h) async {
_streamResumed = true; _streamResumed = true;
await _handleAckResponse(StreamManagementAckNonza(h)); await _handleAckResponse(StreamManagementAckNonza(h));
final stanzas = _unackedStanzas.values.toList();
_unackedStanzas.clear();
// Retransmit the rest of the queue // Retransmit the rest of the queue
final attrs = getAttributes(); await _resendStanzas();
for (final stanza in stanzas) {
await attrs.sendStanza(stanza, awaitable: false);
}
} }
/// Pings the connection open by send an ack request /// Pings the connection open by send an ack request

View File

@@ -1,4 +1,5 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
@@ -7,10 +8,14 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
@immutable @immutable
class DelayedDelivery { class DelayedDeliveryData implements StanzaHandlerExtension {
const DelayedDelivery(this.from, this.timestamp); const DelayedDeliveryData(this.from, this.timestamp);
/// The timestamp the message was originally sent.
final DateTime timestamp; final DateTime timestamp;
final String from;
/// The JID that originally sent the message.
final JID from;
} }
class DelayedDeliveryManager extends XmppManagerBase { class DelayedDeliveryManager extends XmppManagerBase {
@@ -23,6 +28,8 @@ class DelayedDeliveryManager extends XmppManagerBase {
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
stanzaTag: 'message', stanzaTag: 'message',
tagName: 'delay',
tagXmlns: delayedDeliveryXmlns,
callback: _onIncomingMessage, callback: _onIncomingMessage,
priority: 200, priority: 200,
), ),
@@ -32,14 +39,14 @@ class DelayedDeliveryManager extends XmppManagerBase {
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns); final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns)!;
if (delay == null) return state;
return state.copyWith( return state
delayedDelivery: DelayedDelivery( ..extensions.set(
delay.attributes['from']! as String, DelayedDeliveryData(
DateTime.parse(delay.attributes['stamp']! as String), JID.fromString(delay.attributes['from']! as String),
), DateTime.parse(delay.attributes['stamp']! as String),
); ),
);
} }
} }

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

@@ -1,6 +1,5 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@@ -15,6 +14,13 @@ import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0297.dart'; import 'package:moxxmpp/src/xeps/xep_0297.dart';
import 'package:moxxmpp/src/xeps/xep_0386.dart'; import 'package:moxxmpp/src/xeps/xep_0386.dart';
class CarbonsData implements StanzaHandlerExtension {
const CarbonsData(this.isCarbon);
/// Indicates whether this message is a carbon.
final bool isCarbon;
}
/// This manager class implements support for XEP-0280. /// This manager class implements support for XEP-0280.
class CarbonsManager extends XmppManagerBase { class CarbonsManager extends XmppManagerBase {
CarbonsManager() : super(carbonsManager); CarbonsManager() : super(carbonsManager);
@@ -43,7 +49,7 @@ class CarbonsManager extends XmppManagerBase {
tagXmlns: carbonsXmlns, tagXmlns: carbonsXmlns,
callback: _onMessageSent, callback: _onMessageSent,
priority: -98, priority: -98,
) ),
]; ];
@override @override
@@ -53,7 +59,7 @@ class CarbonsManager extends XmppManagerBase {
// Query the server // Query the server
final disco = getAttributes().getManagerById<DiscoManager>(discoManager)!; final disco = getAttributes().getManagerById<DiscoManager>(discoManager)!;
_supported = await disco.supportsFeature( _supported = await disco.supportsFeature(
getAttributes().getConnectionSettings().jid.toBare(), getAttributes().getConnectionSettings().serverJid,
carbonsXmlns, carbonsXmlns,
); );
_gotSupported = true; _gotSupported = true;
@@ -78,15 +84,14 @@ class CarbonsManager extends XmppManagerBase {
) async { ) async {
final from = JID.fromString(message.attributes['from']! as String); final from = JID.fromString(message.attributes['from']! as String);
final received = message.firstTag('received', xmlns: carbonsXmlns)!; final received = message.firstTag('received', xmlns: carbonsXmlns)!;
if (!isCarbonValid(from)) return state.copyWith(done: true); if (!isCarbonValid(from)) return state..done = true;
final forwarded = received.firstTag('forwarded', xmlns: forwardedXmlns)!; final forwarded = received.firstTag('forwarded', xmlns: forwardedXmlns)!;
final carbon = unpackForwarded(forwarded); final carbon = unpackForwarded(forwarded);
return state.copyWith( return state
isCarbon: true, ..extensions.set(const CarbonsData(true))
stanza: carbon, ..stanza = carbon;
);
} }
Future<StanzaHandlerData> _onMessageSent( Future<StanzaHandlerData> _onMessageSent(
@@ -95,15 +100,14 @@ class CarbonsManager extends XmppManagerBase {
) async { ) async {
final from = JID.fromString(message.attributes['from']! as String); final from = JID.fromString(message.attributes['from']! as String);
final sent = message.firstTag('sent', xmlns: carbonsXmlns)!; final sent = message.firstTag('sent', xmlns: carbonsXmlns)!;
if (!isCarbonValid(from)) return state.copyWith(done: true); if (!isCarbonValid(from)) return state..done = true;
final forwarded = sent.firstTag('forwarded', xmlns: forwardedXmlns)!; final forwarded = sent.firstTag('forwarded', xmlns: forwardedXmlns)!;
final carbon = unpackForwarded(forwarded); final carbon = unpackForwarded(forwarded);
return state.copyWith( return state
isCarbon: true, ..extensions.set(const CarbonsData(true))
stanza: carbon, ..stanza = carbon;
);
} }
/// Send a request to the server, asking it to enable Message Carbons. /// Send a request to the server, asking it to enable Message Carbons.
@@ -111,20 +115,20 @@ class CarbonsManager extends XmppManagerBase {
/// Returns true if carbons were enabled. False, if not. /// Returns true if carbons were enabled. False, if not.
Future<bool> enableCarbons() async { Future<bool> enableCarbons() async {
final attrs = getAttributes(); final attrs = getAttributes();
final result = await attrs.sendStanza( final result = (await attrs.sendStanza(
Stanza.iq( StanzaDetails(
to: attrs.getFullJID().toBare().toString(), Stanza.iq(
type: 'set', to: attrs.getFullJID().toBare().toString(),
children: [ type: 'set',
XMLNode.xmlns( children: [
tag: 'enable', XMLNode.xmlns(
xmlns: carbonsXmlns, tag: 'enable',
) xmlns: carbonsXmlns,
], ),
],
),
), ),
addFrom: StanzaFromType.full, ))!;
addId: true,
);
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
logger.warning('Failed to enable message carbons'); logger.warning('Failed to enable message carbons');
@@ -142,19 +146,19 @@ class CarbonsManager extends XmppManagerBase {
/// ///
/// Returns true if carbons were disabled. False, if not. /// Returns true if carbons were disabled. False, if not.
Future<bool> disableCarbons() async { Future<bool> disableCarbons() async {
final result = await getAttributes().sendStanza( final result = (await getAttributes().sendStanza(
Stanza.iq( StanzaDetails(
type: 'set', Stanza.iq(
children: [ type: 'set',
XMLNode.xmlns( children: [
tag: 'disable', XMLNode.xmlns(
xmlns: carbonsXmlns, tag: 'disable',
) xmlns: carbonsXmlns,
], ),
],
),
), ),
addFrom: StanzaFromType.full, ))!;
addId: true,
);
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
logger.warning('Failed to disable message carbons'); logger.warning('Failed to disable message carbons');

View File

@@ -4,85 +4,145 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
XMLNode constructHashElement(String algo, String base64Hash) { /// Hash names
const _hashSha1 = 'sha-1';
const _hashSha256 = 'sha-256';
const _hashSha512 = 'sha-512';
const _hashSha3256 = 'sha3-256';
const _hashSha3512 = 'sha3-512';
const _hashBlake2b256 = 'blake2b-256';
const _hashBlake2b512 = 'blake2b-512';
/// Helper method for building a <hash /> element according to XEP-0300.
XMLNode constructHashElement(HashFunction hash, String value) {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'hash', tag: 'hash',
xmlns: hashXmlns, xmlns: hashXmlns,
attributes: {'algo': algo}, attributes: {'algo': hash.toName()},
text: base64Hash, text: value,
); );
} }
enum HashFunction { enum HashFunction {
sha256, /// SHA-1
sha512, sha1,
sha3_256,
sha3_512,
blake2b256,
blake2b512,
}
extension HashNameToEnumExtension on HashFunction { /// SHA-256
sha256,
/// SHA-256
sha512,
/// SHA3-256
sha3_256,
/// SHA3-512
sha3_512,
/// BLAKE2b-256
blake2b256,
/// BLAKE2b-512
blake2b512;
/// Get a HashFunction from its name [name] according to either
/// - IANA's hash name register (http://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xhtml)
/// - XEP-0300
factory HashFunction.fromName(String name) {
switch (name) {
case _hashSha1:
return HashFunction.sha1;
case _hashSha256:
return HashFunction.sha256;
case _hashSha512:
return HashFunction.sha512;
case _hashSha3256:
return HashFunction.sha3_256;
case _hashSha3512:
return HashFunction.sha3_512;
case _hashBlake2b256:
return HashFunction.blake2b256;
case _hashBlake2b512:
return HashFunction.blake2b512;
}
throw Exception('Invalid hash function $name');
}
/// Like [HashFunction.fromName], but returns null if the hash function is unknown
static HashFunction? maybeFromName(String name) {
switch (name) {
case _hashSha1:
return HashFunction.sha1;
case _hashSha256:
return HashFunction.sha256;
case _hashSha512:
return HashFunction.sha512;
case _hashSha3256:
return HashFunction.sha3_256;
case _hashSha3512:
return HashFunction.sha3_512;
case _hashBlake2b256:
return HashFunction.blake2b256;
case _hashBlake2b512:
return HashFunction.blake2b512;
}
return null;
}
/// Return the hash function's name according to IANA's hash name register or XEP-0300.
String toName() { String toName() {
switch (this) { switch (this) {
case HashFunction.sha1:
return _hashSha1;
case HashFunction.sha256: case HashFunction.sha256:
return hashSha256; return _hashSha256;
case HashFunction.sha512: case HashFunction.sha512:
return hashSha512; return _hashSha512;
case HashFunction.sha3_256: case HashFunction.sha3_256:
return hashSha3512; return _hashSha3512;
case HashFunction.sha3_512: case HashFunction.sha3_512:
return hashSha3512; return _hashSha3512;
case HashFunction.blake2b256: case HashFunction.blake2b256:
return hashBlake2b256; return _hashBlake2b256;
case HashFunction.blake2b512: case HashFunction.blake2b512:
return hashBlake2b512; return _hashBlake2b512;
} }
} }
} }
HashFunction hashFunctionFromName(String name) {
switch (name) {
case hashSha256:
return HashFunction.sha256;
case hashSha512:
return HashFunction.sha512;
case hashSha3256:
return HashFunction.sha3_256;
case hashSha3512:
return HashFunction.sha3_512;
case hashBlake2b256:
return HashFunction.blake2b256;
case hashBlake2b512:
return HashFunction.blake2b512;
}
throw Exception();
}
class CryptographicHashManager extends XmppManagerBase { class CryptographicHashManager extends XmppManagerBase {
CryptographicHashManager() : super(cryptographicHashManager); CryptographicHashManager() : super(cryptographicHashManager);
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
/// NOTE: We intentionally do not advertise support for SHA-1, as it is marked as
/// MUST NOT. Sha-1 support is only for providing a wrapper over its hash
/// function, for example for XEP-0115.
@override @override
List<String> getDiscoFeatures() => [ List<String> getDiscoFeatures() => [
'$hashFunctionNameBaseXmlns:$hashSha256', '$hashFunctionNameBaseXmlns:$_hashSha256',
'$hashFunctionNameBaseXmlns:$hashSha512', '$hashFunctionNameBaseXmlns:$_hashSha512',
//'$hashFunctionNameBaseXmlns:$hashSha3256', //'$hashFunctionNameBaseXmlns:$_hashSha3256',
//'$hashFunctionNameBaseXmlns:$hashSha3512', //'$hashFunctionNameBaseXmlns:$_hashSha3512',
//'$hashFunctionNameBaseXmlns:$hashBlake2b256', //'$hashFunctionNameBaseXmlns:$_hashBlake2b256',
'$hashFunctionNameBaseXmlns:$hashBlake2b512', '$hashFunctionNameBaseXmlns:$_hashBlake2b512',
]; ];
/// Compute the raw hash value of [data] using the algorithm specified by [function].
/// If the function is not supported, an exception will be thrown.
static Future<List<int>> hashFromData( static Future<List<int>> hashFromData(
List<int> data,
HashFunction function, HashFunction function,
List<int> data,
) async { ) async {
// TODO(PapaTutuWawa): Implement the others as well // TODO(PapaTutuWawa): Implement the others as well
HashAlgorithm algo; HashAlgorithm algo;
switch (function) { switch (function) {
case HashFunction.sha1:
algo = Sha1();
break;
case HashFunction.sha256: case HashFunction.sha256:
algo = Sha256(); algo = Sha256();
break; break;

View File

@@ -2,18 +2,27 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
XMLNode makeLastMessageCorrectionEdit(String id) { class LastMessageCorrectionData implements StanzaHandlerExtension {
return XMLNode.xmlns( const LastMessageCorrectionData(this.id);
tag: 'replace',
xmlns: lmcXmlns, /// The id the LMC applies to.
attributes: <String, String>{ final String id;
'id': id,
}, XMLNode toXML() {
); return XMLNode.xmlns(
tag: 'replace',
xmlns: lmcXmlns,
attributes: {
'id': id,
},
);
}
} }
class LastMessageCorrectionManager extends XmppManagerBase { class LastMessageCorrectionManager extends XmppManagerBase {
@@ -31,7 +40,7 @@ class LastMessageCorrectionManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override
@@ -42,8 +51,30 @@ class LastMessageCorrectionManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!; final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!;
return state.copyWith( return state
lastMessageCorrectionSid: edit.attributes['id']! as String, ..extensions.set(
); LastMessageCorrectionData(edit.attributes['id']! as String),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<LastMessageCorrectionData>();
return data != null
? [
data.toXML(),
]
: [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
} }
} }

View File

@@ -4,27 +4,86 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
XMLNode makeChatMarkerMarkable() { enum ChatMarker {
return XMLNode.xmlns( received,
tag: 'markable', displayed,
xmlns: chatMarkersXmlns, acknowledged;
);
factory ChatMarker.fromName(String name) {
switch (name) {
case 'received':
return ChatMarker.received;
case 'displayed':
return ChatMarker.displayed;
case 'acknowledged':
return ChatMarker.acknowledged;
}
throw Exception('Invalid chat marker $name');
}
XMLNode toXML() {
String tag;
switch (this) {
case ChatMarker.received:
tag = 'received';
break;
case ChatMarker.displayed:
tag = 'displayed';
break;
case ChatMarker.acknowledged:
tag = 'acknowledged';
break;
}
return XMLNode.xmlns(
tag: tag,
xmlns: chatMarkersXmlns,
);
}
} }
XMLNode makeChatMarker(String tag, String id) { class MarkableData implements StanzaHandlerExtension {
assert( const MarkableData(this.isMarkable);
['received', 'displayed', 'acknowledged'].contains(tag),
'Invalid chat marker', /// Indicates whether the message can be replied to with a chat marker.
); final bool isMarkable;
return XMLNode.xmlns(
tag: tag, XMLNode toXML() {
xmlns: chatMarkersXmlns, assert(isMarkable, '');
attributes: {'id': id},
); return XMLNode.xmlns(
tag: 'markable',
xmlns: chatMarkersXmlns,
);
}
}
class ChatMarkerData implements StanzaHandlerExtension {
const ChatMarkerData(this.marker, this.id);
/// The actual chat state
final ChatMarker marker;
/// The ID the chat marker applies to
final String id;
XMLNode toXML() {
final tag = marker.toXML();
return XMLNode.xmlns(
tag: tag.tag,
xmlns: chatMarkersXmlns,
attributes: {
'id': id,
},
);
}
} }
class ChatMarkerManager extends XmppManagerBase { class ChatMarkerManager extends XmppManagerBase {
@@ -41,7 +100,7 @@ class ChatMarkerManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override
@@ -51,23 +110,52 @@ class ChatMarkerManager extends XmppManagerBase {
Stanza message, Stanza message,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final marker = message.firstTagByXmlns(chatMarkersXmlns)!; final element = message.firstTagByXmlns(chatMarkersXmlns)!;
// Handle the <markable /> explicitly // Handle the <markable /> explicitly
if (marker.tag == 'markable') return state.copyWith(isMarkable: true); if (element.tag == 'markable') {
return state..extensions.set(const MarkableData(true));
if (!['received', 'displayed', 'acknowledged'].contains(marker.tag)) {
logger.warning("Unknown message marker '${marker.tag}' found.");
} else {
getAttributes().sendEvent(
ChatMarkerEvent(
from: JID.fromString(message.from!),
type: marker.tag,
id: marker.attributes['id']! as String,
),
);
} }
return state.copyWith(done: true); try {
getAttributes().sendEvent(
ChatMarkerEvent(
JID.fromString(message.from!),
ChatMarker.fromName(element.tag),
element.attributes['id']! as String,
),
);
} catch (_) {
logger.warning("Unknown message marker '${element.tag}' found.");
}
return state..done = true;
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final children = List<XMLNode>.empty(growable: true);
final marker = extensions.get<ChatMarkerData>();
if (marker != null) {
children.add(marker.toXML());
}
final markable = extensions.get<MarkableData>();
if (markable != null) {
children.add(markable.toXML());
}
return children;
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
} }
} }

View File

@@ -1,31 +1,36 @@
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/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/util/typed_map.dart';
enum MessageProcessingHint { enum MessageProcessingHint {
noPermanentStore, noPermanentStore,
noStore, noStore,
noCopies, noCopies,
store, store;
}
MessageProcessingHint messageProcessingHintFromXml(XMLNode element) { factory MessageProcessingHint.fromName(String name) {
switch (element.tag) { switch (name) {
case 'no-permanent-store': case 'no-permanent-store':
return MessageProcessingHint.noPermanentStore; return MessageProcessingHint.noPermanentStore;
case 'no-store': case 'no-store':
return MessageProcessingHint.noStore; return MessageProcessingHint.noStore;
case 'no-copy': case 'no-copy':
return MessageProcessingHint.noCopies; return MessageProcessingHint.noCopies;
case 'store': case 'store':
return MessageProcessingHint.store; return MessageProcessingHint.store;
}
assert(false, 'Invalid Message Processing Hint: $name');
return MessageProcessingHint.noStore;
} }
assert(false, 'Invalid Message Processing Hint: ${element.tag}'); XMLNode toXML() {
return MessageProcessingHint.noStore;
}
extension XmlExtension on MessageProcessingHint {
XMLNode toXml() {
String tag; String tag;
switch (this) { switch (this) {
case MessageProcessingHint.noPermanentStore: case MessageProcessingHint.noPermanentStore:
@@ -48,3 +53,60 @@ extension XmlExtension on MessageProcessingHint {
); );
} }
} }
class MessageProcessingHintData implements StanzaHandlerExtension {
const MessageProcessingHintData(this.hints);
/// The attached message processing hints.
final List<MessageProcessingHint> hints;
}
class MessageProcessingHintManager extends XmppManagerBase {
MessageProcessingHintManager() : super(messageProcessingHintManager);
@override
Future<bool> isSupported() async => true;
@override
List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler(
stanzaTag: 'message',
tagXmlns: messageProcessingHintsXmlns,
callback: _onMessage,
// Before the message handler
priority: -99,
),
];
Future<StanzaHandlerData> _onMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
final elements = stanza.findTagsByXmlns(messageProcessingHintsXmlns);
return state
..extensions.set(
MessageProcessingHintData(
elements
.map((element) => MessageProcessingHint.fromName(element.tag))
.toList(),
),
);
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<MessageProcessingHintData>();
return data != null ? data.hints.map((hint) => hint.toXML()).toList() : [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
}
}

View File

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

View File

@@ -3,28 +3,69 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart'; import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/message.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/util/typed_map.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
/// Represents data provided by XEP-0359. /// Representation of a <stanza-id /> element.
/// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of class StanzaId {
/// the message stanza. const StanzaId(
class StableStanzaId { this.id,
const StableStanzaId({this.originId, this.stanzaId, this.stanzaIdBy}); this.by,
final String? originId; );
final String? stanzaId;
final String? stanzaIdBy; /// The unique stanza id.
final String id;
/// The JID the id was generated by.
final JID by;
XMLNode toXML() {
return XMLNode.xmlns(
tag: 'stanza-id',
xmlns: stableIdXmlns,
attributes: {
'id': id,
'by': by.toString(),
},
);
}
} }
XMLNode makeOriginIdElement(String id) { class StableIdData implements StanzaHandlerExtension {
return XMLNode.xmlns( const StableIdData(this.originId, this.stanzaIds);
tag: 'origin-id',
xmlns: stableIdXmlns, /// <origin-id />
attributes: {'id': id}, final String? originId;
);
/// Stanza ids
final List<StanzaId>? stanzaIds;
XMLNode toOriginIdElement() {
assert(
originId != null,
'Can only build the XML element if originId != null',
);
return XMLNode.xmlns(
tag: 'origin-id',
xmlns: stableIdXmlns,
attributes: {'id': originId!},
);
}
List<XMLNode> toXML() {
return [
if (originId != null)
XMLNode.xmlns(
tag: 'origin-id',
xmlns: stableIdXmlns,
attributes: {'id': originId!},
),
if (stanzaIds != null) ...stanzaIds!.map((s) => s.toXML()),
];
}
} }
class StableIdManager extends XmppManagerBase { class StableIdManager extends XmppManagerBase {
@@ -40,7 +81,7 @@ class StableIdManager extends XmppManagerBase {
callback: _onMessage, callback: _onMessage,
// Before the MessageManager // Before the MessageManager
priority: -99, priority: -99,
) ),
]; ];
@override @override
@@ -50,50 +91,58 @@ class StableIdManager extends XmppManagerBase {
Stanza message, Stanza message,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final from = JID.fromString(message.attributes['from']! as String);
String? originId; String? originId;
String? stanzaId; List<StanzaId>? stanzaIds;
String? stanzaIdBy; final originIdElement = message.firstTag('origin-id', xmlns: stableIdXmlns);
final originIdTag = message.firstTag('origin-id', xmlns: stableIdXmlns); final stanzaIdElements =
final stanzaIdTag = message.firstTag('stanza-id', xmlns: stableIdXmlns); message.findTags('stanza-id', xmlns: stableIdXmlns);
// Process the origin id // Process the origin id
if (originIdTag != null) { if (originIdElement != null) {
logger.finest('Found origin Id tag'); originId = originIdElement.attributes['id']! as String;
originId = originIdTag.attributes['id']! as String;
} }
// Process the stanza id tag // Process the stanza id tag
if (stanzaIdTag != null) { if (stanzaIdElements.isNotEmpty) {
logger.finest('Found stanza Id tag'); stanzaIds = stanzaIdElements
final attrs = getAttributes(); .map(
final disco = attrs.getManagerById<DiscoManager>(discoManager)!; (element) => StanzaId(
final result = await disco.discoInfoQuery(from.toString()); element.attributes['id']! as String,
if (result.isType<DiscoInfo>()) { JID.fromString(element.attributes['by']! as String),
final info = result.get<DiscoInfo>(); ),
logger.finest('Got info for ${from.toString()}'); )
if (info.features.contains(stableIdXmlns)) { .toList();
logger.finest('${from.toString()} supports $stableIdXmlns.');
stanzaId = stanzaIdTag.attributes['id']! as String;
stanzaIdBy = stanzaIdTag.attributes['by']! as String;
} else {
logger.finest(
'${from.toString()} does not support $stableIdXmlns. Ignoring stanza id... ',
);
}
} else {
logger.finest(
'Failed to find out if ${from.toString()} supports $stableIdXmlns. Ignoring... ',
);
}
} }
return state.copyWith( return state
stableId: StableStanzaId( ..extensions.set(
originId: originId, StableIdData(
stanzaId: stanzaId, originId,
stanzaIdBy: stanzaIdBy, stanzaIds,
), ),
); );
}
List<XMLNode> _messageSendingCallback(
TypedMap<StanzaHandlerExtension> extensions,
) {
final data = extensions.get<StableIdData>();
if (data?.originId != null) {
return [
data!.toOriginIdElement(),
];
}
return [];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
// Register the sending callback
getAttributes()
.getManagerById<MessageManager>(messageManager)
?.registerMessageSendingCallback(_messageSendingCallback);
} }
} }

View File

@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
@@ -7,7 +8,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
@@ -58,10 +58,10 @@ class HttpFileUploadManager extends XmppManagerBase {
/// Returns whether the entity provided an identity that tells us that we can ask it /// Returns whether the entity provided an identity that tells us that we can ask it
/// for an HTTP upload slot. /// for an HTTP upload slot.
bool _containsFileUploadIdentity(DiscoInfo info) { bool _containsFileUploadIdentity(DiscoInfo info) {
return listContains( return info.identities.firstWhereOrNull(
info.identities, (Identity id) => id.category == 'store' && id.type == 'file',
(Identity id) => id.category == 'store' && id.type == 'file', ) !=
); null;
} }
/// Extract the maximum filesize in octets from the disco response. Returns null /// Extract the maximum filesize in octets from the disco response. Returns null
@@ -149,23 +149,25 @@ class HttpFileUploadManager extends XmppManagerBase {
} }
final attrs = getAttributes(); final attrs = getAttributes();
final response = await attrs.sendStanza( final response = (await attrs.sendStanza(
Stanza.iq( StanzaDetails(
to: _entityJid.toString(), Stanza.iq(
type: 'get', to: _entityJid.toString(),
children: [ type: 'get',
XMLNode.xmlns( children: [
tag: 'request', XMLNode.xmlns(
xmlns: httpFileUploadXmlns, tag: 'request',
attributes: { xmlns: httpFileUploadXmlns,
'filename': filename, attributes: {
'size': filesize.toString(), 'filename': filename,
...contentType != null ? {'content-type': contentType} : {} 'size': filesize.toString(),
}, if (contentType != null) 'content-type': contentType,
) },
], ),
],
),
), ),
); ))!;
if (response.attributes['type']! != 'result') { if (response.attributes['type']! != 'result') {
logger.severe('Failed to request HTTP File Upload slot.'); logger.severe('Failed to request HTTP File Upload slot.');

View File

@@ -6,64 +6,64 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
enum ExplicitEncryptionType { enum ExplicitEncryptionType implements StanzaHandlerExtension {
otr, otr,
legacyOpenPGP, legacyOpenPGP,
openPGP, openPGP,
omemo, omemo,
omemo1, omemo1,
omemo2, omemo2,
unknown, unknown;
}
String _explicitEncryptionTypeToString(ExplicitEncryptionType type) { factory ExplicitEncryptionType.fromNamespace(String namespace) {
switch (type) { switch (namespace) {
case ExplicitEncryptionType.otr: case emeOtr:
return emeOtr; return ExplicitEncryptionType.otr;
case ExplicitEncryptionType.legacyOpenPGP: case emeLegacyOpenPGP:
return emeLegacyOpenPGP; return ExplicitEncryptionType.legacyOpenPGP;
case ExplicitEncryptionType.openPGP: case emeOpenPGP:
return emeOpenPGP; return ExplicitEncryptionType.openPGP;
case ExplicitEncryptionType.omemo: case emeOmemo:
return emeOmemo; return ExplicitEncryptionType.omemo;
case ExplicitEncryptionType.omemo1: case emeOmemo1:
return emeOmemo1; return ExplicitEncryptionType.omemo1;
case ExplicitEncryptionType.omemo2: case emeOmemo2:
return emeOmemo2; return ExplicitEncryptionType.omemo2;
case ExplicitEncryptionType.unknown: default:
return ''; return ExplicitEncryptionType.unknown;
}
} }
}
ExplicitEncryptionType _explicitEncryptionTypeFromString(String str) { String toNamespace() {
switch (str) { switch (this) {
case emeOtr: case ExplicitEncryptionType.otr:
return ExplicitEncryptionType.otr; return emeOtr;
case emeLegacyOpenPGP: case ExplicitEncryptionType.legacyOpenPGP:
return ExplicitEncryptionType.legacyOpenPGP; return emeLegacyOpenPGP;
case emeOpenPGP: case ExplicitEncryptionType.openPGP:
return ExplicitEncryptionType.openPGP; return emeOpenPGP;
case emeOmemo: case ExplicitEncryptionType.omemo:
return ExplicitEncryptionType.omemo; return emeOmemo;
case emeOmemo1: case ExplicitEncryptionType.omemo1:
return ExplicitEncryptionType.omemo1; return emeOmemo1;
case emeOmemo2: case ExplicitEncryptionType.omemo2:
return ExplicitEncryptionType.omemo2; return emeOmemo2;
default: case ExplicitEncryptionType.unknown:
return ExplicitEncryptionType.unknown; return '';
}
} }
}
/// Create an <encryption /> element with [type] indicating which type of encryption was /// Create an <encryption /> element with an xmlns indicating what type of encryption was
/// used. /// used.
XMLNode buildEmeElement(ExplicitEncryptionType type) { XMLNode toXML() {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'encryption', tag: 'encryption',
xmlns: emeXmlns, xmlns: emeXmlns,
attributes: <String, String>{ attributes: <String, String>{
'namespace': _explicitEncryptionTypeToString(type), 'namespace': toNamespace(),
}, },
); );
}
} }
class EmeManager extends XmppManagerBase { class EmeManager extends XmppManagerBase {
@@ -91,10 +91,11 @@ class EmeManager extends XmppManagerBase {
) async { ) async {
final encryption = message.firstTag('encryption', xmlns: emeXmlns)!; final encryption = message.firstTag('encryption', xmlns: emeXmlns)!;
return state.copyWith( return state
encryptionType: _explicitEncryptionTypeFromString( ..extensions.set(
encryption.attributes['namespace']! as String, ExplicitEncryptionType.fromNamespace(
), encryption.attributes['namespace']! as String,
); ),
);
} }
} }

View File

@@ -2,10 +2,14 @@ abstract class OmemoError {}
class UnknownOmemoError extends 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 OmemoNotSupportedForContactException extends OmemoError {}
class EncryptionFailedException with Exception {} class EncryptionFailedException implements Exception {}
class InvalidEnvelopePayloadException with Exception {} class InvalidEnvelopePayloadException implements Exception {}

View File

@@ -1,6 +1,29 @@
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. /// A simple wrapper class for defining elements that should not be encrypted.
class DoNotEncrypt { class DoNotEncrypt {
const DoNotEncrypt(this.tag, this.xmlns); const DoNotEncrypt(this.tag, this.xmlns);
/// The tag of the element.
final String tag; final String tag;
/// The xmlns attribute of the element.
final String xmlns; final String xmlns;
} }
/// An encryption error caused by OMEMO.
class OmemoEncryptionError {
const OmemoEncryptionError(this.deviceEncryptionErrors);
/// 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:async';
import 'dart:convert'; import 'dart:convert';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/base.dart'; import 'package:moxxmpp/src/managers/base.dart';
@@ -10,7 +11,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
@@ -23,9 +23,21 @@ import 'package:moxxmpp/src/xeps/xep_0384/crypto.dart';
import 'package:moxxmpp/src/xeps/xep_0384/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0384/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0384/helpers.dart'; import 'package:moxxmpp/src/xeps/xep_0384/helpers.dart';
import 'package:moxxmpp/src/xeps/xep_0384/types.dart'; import 'package:moxxmpp/src/xeps/xep_0384/types.dart';
import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/omemo_dart.dart' as omemo;
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
/// A callback that is executed whenever we need to acquire the OmemoManager backing
/// the manager.
typedef GetOmemoManagerCallback = Future<omemo.OmemoManager> Function();
/// A callback for figuring out whether a stanza should be encrypted or not. Note that
/// returning true here does not necessarily mean that a stanza gets encrypted because
/// handlers can indicate that a stanza should not be encrypted, e.g. PubSub.
typedef ShouldEncryptStanzaCallback = Future<bool> Function(
JID toJid,
Stanza stanza,
);
const _doNotEncryptList = [ const _doNotEncryptList = [
// XEP-0033 // XEP-0033
DoNotEncrypt('addresses', extendedAddressingXmlns), DoNotEncrypt('addresses', extendedAddressingXmlns),
@@ -42,8 +54,15 @@ const _doNotEncryptList = [
DoNotEncrypt('stanza-id', stableIdXmlns), DoNotEncrypt('stanza-id', stableIdXmlns),
]; ];
abstract class BaseOmemoManager extends XmppManagerBase { class OmemoManager extends XmppManagerBase {
BaseOmemoManager() : super(omemoManager); OmemoManager(this._getOmemoManager, this._shouldEncryptStanza)
: super(omemoManager);
/// Callback for getting the [omemo.OmemoManager].
final GetOmemoManagerCallback _getOmemoManager;
/// Callback for checking whether a stanza should be encrypted or not.
final ShouldEncryptStanzaCallback _shouldEncryptStanza;
// TODO(Unknown): Technically, this is not always true // TODO(Unknown): Technically, this is not always true
@override @override
@@ -112,22 +131,19 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
// Tell the OmemoManager // Tell the OmemoManager
(await getOmemoManager()).onDeviceListUpdate(jid.toString(), ids); await (await _getOmemoManager()).onDeviceListUpdate(jid.toString(), ids);
// Generate an event // Generate an event
getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids)); getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids));
} }
} }
@visibleForOverriding /// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<OmemoManager> getOmemoManager(); Future<int> _getDeviceId() async => (await _getOmemoManager()).getDeviceId();
/// Wrapper around using getSessionManager and then calling getDeviceId on it. /// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId(); Future<omemo.OmemoBundle> _getDeviceBundle() async {
final om = await _getOmemoManager();
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<OmemoBundle> _getDeviceBundle() async {
final om = await getOmemoManager();
final device = await om.getDevice(); final device = await om.getDevice();
return device.toBundle(); return device.toBundle();
} }
@@ -198,53 +214,45 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
XMLNode _buildEncryptedElement( XMLNode _buildEncryptedElement(
EncryptionResult result, omemo.EncryptionResult result,
String recipientJid, String recipientJid,
int deviceId, int deviceId,
) { ) {
final keyElements = <String, List<XMLNode>>{}; final keyElements = <String, List<XMLNode>>{};
for (final key in result.encryptedKeys) { for (final keys in result.encryptedKeys.entries) {
final keyElement = XMLNode( keyElements[keys.key] = keys.value
tag: 'key', .map(
attributes: <String, String>{ (ek) => XMLNode(
'rid': '${key.rid}', tag: 'key',
'kex': key.kex ? 'true' : 'false', attributes: {
}, 'rid': ek.rid.toString(),
text: key.value, if (ek.kex) 'kex': 'true',
); },
text: ek.value,
if (keyElements.containsKey(key.jid)) { ),
keyElements[key.jid]!.add(keyElement); )
} else { .toList();
keyElements[key.jid] = [keyElement];
}
} }
final keysElements = keyElements.entries.map((entry) { final keysElements = keyElements.entries.map((entry) {
return XMLNode( return XMLNode(
tag: 'keys', tag: 'keys',
attributes: <String, String>{ attributes: {
'jid': entry.key, 'jid': entry.key,
}, },
children: entry.value, children: entry.value,
); );
}).toList(); }).toList();
var payloadElement = <XMLNode>[];
if (result.ciphertext != null) {
payloadElement = [
XMLNode(
tag: 'payload',
text: base64.encode(result.ciphertext!),
),
];
}
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'encrypted', tag: 'encrypted',
xmlns: omemoXmlns, xmlns: omemoXmlns,
children: [ children: [
...payloadElement, if (result.ciphertext != null)
XMLNode(
tag: 'payload',
text: base64Encode(result.ciphertext!),
),
XMLNode( XMLNode(
tag: 'header', tag: 'header',
attributes: <String, String>{ attributes: <String, String>{
@@ -258,34 +266,36 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// For usage with omemo_dart's OmemoManager. /// For usage with omemo_dart's OmemoManager.
Future<void> sendEmptyMessageImpl( Future<void> sendEmptyMessageImpl(
EncryptionResult result, omemo.EncryptionResult result,
String toJid, String toJid,
) async { ) async {
await getAttributes().sendStanza( await getAttributes().sendStanza(
Stanza.message( StanzaDetails(
to: toJid, Stanza.message(
type: 'chat', to: toJid,
children: [ type: 'chat',
_buildEncryptedElement( children: [
result, _buildEncryptedElement(
toJid, result,
await _getDeviceId(), toJid,
), await _getDeviceId(),
),
// Add a storage hint in case this is a message // Add a storage hint in case this is a message
// Taken from the example at // Taken from the example at
// https://xmpp.org/extensions/xep-0384.html#message-structure-description. // https://xmpp.org/extensions/xep-0384.html#message-structure-description.
MessageProcessingHint.store.toXml(), MessageProcessingHint.store.toXML(),
], ],
),
awaitable: false,
encrypted: true,
), ),
awaitable: false,
encrypted: true,
); );
} }
/// Send a heartbeat message to [jid]. /// Send a heartbeat message to [jid].
Future<void> sendOmemoHeartbeat(String jid) async { Future<void> sendOmemoHeartbeat(String jid) async {
final om = await getOmemoManager(); final om = await _getOmemoManager();
await om.sendOmemoHeartbeat(jid); await om.sendOmemoHeartbeat(jid);
} }
@@ -298,17 +308,22 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
/// For usage with omemo_dart's OmemoManager /// For usage with omemo_dart's OmemoManager
Future<OmemoBundle?> fetchDeviceBundle(String jid, int id) async { Future<omemo.OmemoBundle?> fetchDeviceBundle(String jid, int id) async {
final result = await retrieveDeviceBundle(JID.fromString(jid), id); final result = await retrieveDeviceBundle(JID.fromString(jid), id);
if (result.isType<OmemoError>()) return null; if (result.isType<OmemoError>()) return null;
return result.get<OmemoBundle>(); return result.get<omemo.OmemoBundle>();
} }
Future<StanzaHandlerData> _onOutgoingStanza( Future<StanzaHandlerData> _onOutgoingStanza(
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
if (!state.shouldEncrypt) {
logger.finest('Not encrypting since state.shouldEncrypt is false');
return state;
}
if (state.encrypted) { if (state.encrypted) {
logger.finest('Not encrypting since state.encrypted is true'); logger.finest('Not encrypting since state.encrypted is true');
return state; return state;
@@ -321,7 +336,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
final toJid = JID.fromString(stanza.to!).toBare(); final toJid = JID.fromString(stanza.to!).toBare();
final shouldEncryptResult = await shouldEncryptStanza(toJid, stanza); final shouldEncryptResult = await _shouldEncryptStanza(toJid, stanza);
if (!shouldEncryptResult && !state.forceEncryption) { if (!shouldEncryptResult && !state.forceEncryption) {
logger.finest( logger.finest(
'Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.', 'Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.',
@@ -348,32 +363,31 @@ abstract class BaseOmemoManager extends XmppManagerBase {
.getManagerById<CarbonsManager>(carbonsManager) .getManagerById<CarbonsManager>(carbonsManager)
?.isEnabled ?? ?.isEnabled ??
false; false;
final om = await getOmemoManager(); final om = await _getOmemoManager();
final encryptToJids = [
toJid.toString(),
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
];
final result = await om.onOutgoingStanza( final result = await om.onOutgoingStanza(
OmemoOutgoingStanza( omemo.OmemoOutgoingStanza(
[ encryptToJids,
toJid.toString(),
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
],
_buildEnvelope(toEncrypt, toJid.toString()), _buildEnvelope(toEncrypt, toJid.toString()),
), ),
); );
logger.finest('Encryption done'); logger.finest('Encryption done');
if (!result.isSuccess(2)) { if (!result.canSend) {
final other = Map<String, dynamic>.from(state.other); return state
other['encryption_error_jids'] = result.jidEncryptionErrors; ..cancel = true
other['encryption_error_devices'] = result.deviceEncryptionErrors;
return state.copyWith(
other: other,
// If we have no device list for toJid, then the contact most likely does not // If we have no device list for toJid, then the contact most likely does not
// support OMEMO:2 // support OMEMO:2
cancelReason: result.jidEncryptionErrors[toJid.toString()] ..cancelReason = result.deviceEncryptionErrors[toJid.toString()]!.first
is NoKeyMaterialAvailableException .error is omemo.NoKeyMaterialAvailableError
? OmemoNotSupportedForContactException() ? OmemoNotSupportedForContactException()
: UnknownOmemoError(), : UnknownOmemoError()
cancel: true, ..encryptionError = OmemoEncryptionError(
); result.deviceEncryptionErrors,
);
} }
final encrypted = _buildEncryptedElement( final encrypted = _buildEncryptedElement(
@@ -387,72 +401,63 @@ abstract class BaseOmemoManager extends XmppManagerBase {
if (stanza.tag == 'message') { if (stanza.tag == 'message') {
children children
// Add EME data // Add EME data
..add(buildEmeElement(ExplicitEncryptionType.omemo2)) ..add(ExplicitEncryptionType.omemo2.toXML())
// Add a storage hint in case this is a message // Add a storage hint in case this is a message
// Taken from the example at // Taken from the example at
// https://xmpp.org/extensions/xep-0384.html#message-structure-description. // https://xmpp.org/extensions/xep-0384.html#message-structure-description.
..add(MessageProcessingHint.store.toXml()); ..add(MessageProcessingHint.store.toXML());
} }
return state.copyWith( return state
stanza: state.stanza.copyWith( ..stanza = state.stanza.copyWith(children: children)
children: children, ..encrypted = true;
),
encrypted: true,
);
} }
/// This function is called whenever a message is to be encrypted. If it returns true,
/// then the message will be encrypted. If it returns false, the message won't be
/// encrypted.
@visibleForOverriding
Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza);
Future<StanzaHandlerData> _onIncomingStanza( Future<StanzaHandlerData> _onIncomingStanza(
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns);
if (encrypted == null) return state;
if (stanza.from == null) return state; if (stanza.from == null) return state;
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns)!;
final fromJid = JID.fromString(stanza.from!).toBare(); final fromJid = JID.fromString(stanza.from!).toBare();
final header = encrypted.firstTag('header')!; final header = encrypted.firstTag('header')!;
final payloadElement = encrypted.firstTag('payload'); final ourJid = getAttributes().getFullJID();
final keys = List<EncryptedKey>.empty(growable: true); final ourJidString = ourJid.toBare().toString();
final keys = List<omemo.EncryptedKey>.empty(growable: true);
for (final keysElement in header.findTags('keys')) { for (final keysElement in header.findTags('keys')) {
// We only care about our own JID
final jid = keysElement.attributes['jid']! as String; final jid = keysElement.attributes['jid']! as String;
for (final key in keysElement.findTags('key')) { if (jid != ourJidString) {
keys.add( continue;
EncryptedKey(
jid,
int.parse(key.attributes['rid']! as String),
key.innerText(),
key.attributes['kex'] == 'true',
),
);
} }
keys.addAll(
keysElement.findTags('key').map(
(key) => omemo.EncryptedKey(
int.parse(key.attributes['rid']! as String),
key.innerText(),
key.attributes['kex'] == 'true',
),
),
);
} }
final ourJid = getAttributes().getFullJID();
final sid = int.parse(header.attributes['sid']! as String); final sid = int.parse(header.attributes['sid']! as String);
final om = await _getOmemoManager();
final om = await getOmemoManager();
final result = await om.onIncomingStanza( final result = await om.onIncomingStanza(
OmemoIncomingStanza( omemo.OmemoIncomingStanza(
fromJid.toString(), fromJid.toString(),
sid, sid,
state.delayedDelivery?.timestamp.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch,
keys, keys,
payloadElement?.innerText(), encrypted.firstTag('payload')?.innerText(),
false,
), ),
); );
final other = Map<String, dynamic>.from(state.other);
var children = stanza.children; var children = stanza.children;
if (result.error != null) { if (result.error != null) {
other['encryption_error'] = result.error; state.encryptionError = result.error;
} else { } else {
children = stanza.children children = stanza.children
.where( .where(
@@ -463,17 +468,16 @@ abstract class BaseOmemoManager extends XmppManagerBase {
.toList(); .toList();
} }
logger.finest('Got payload: ${result.payload != null}');
if (result.payload != null) { if (result.payload != null) {
XMLNode envelope; XMLNode envelope;
try { try {
envelope = XMLNode.fromString(result.payload!); envelope = XMLNode.fromString(result.payload!);
} on XmlParserException catch (_) { } on XmlParserException catch (_) {
logger.warning('Failed to parse envelope payload: ${result.payload!}'); logger.warning('Failed to parse envelope payload: ${result.payload!}');
other['encryption_error'] = InvalidEnvelopePayloadException(); return state
return state.copyWith( ..encrypted = true
encrypted: true, ..encryptionError = InvalidEnvelopePayloadException();
other: other,
);
} }
final envelopeChildren = envelope.firstTag('content')?.children; final envelopeChildren = envelope.firstTag('content')?.children;
@@ -482,18 +486,29 @@ abstract class BaseOmemoManager extends XmppManagerBase {
// Do not add forbidden elements from the envelope // Do not add forbidden elements from the envelope
envelopeChildren.where(shouldEncryptElement), envelopeChildren.where(shouldEncryptElement),
); );
logger.finest('Adding children: ${envelopeChildren.map((c) => c.tag)}');
} else { } else {
logger.warning('Invalid envelope element: No <content /> element'); logger.warning('Invalid envelope element: No <content /> element');
} }
if (!checkAffixElements(envelope, stanza.from!, ourJid)) { if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
other['encryption_error'] = InvalidAffixElementsException(); state.encryptionError = InvalidAffixElementsException();
} }
} }
return state.copyWith( // Ignore heartbeat messages
encrypted: true, if (stanza.tag == 'message' && encrypted.firstTag('payload') == null) {
stanza: Stanza( logger.finest('Received empty OMEMO message. Ending processing early.');
return state
..encrypted = true
..skip = true
..done = true;
}
return state
..encrypted = true
..stanza = Stanza(
to: stanza.to, to: stanza.to,
from: stanza.from, from: stanza.from,
id: stanza.id, id: stanza.id,
@@ -501,9 +516,13 @@ abstract class BaseOmemoManager extends XmppManagerBase {
children: children, children: children,
tag: stanza.tag, tag: stanza.tag,
attributes: Map<String, String>.from(stanza.attributes), attributes: Map<String, String>.from(stanza.attributes),
), )
other: other, ..extensions.set<OmemoData>(
); OmemoData(
result.newRatchets,
result.replacedRatchets,
),
);
} }
/// Convenience function that attempts to retrieve the raw XML payload from the /// Convenience function that attempts to retrieve the raw XML payload from the
@@ -514,10 +533,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
JID jid, JID jid,
) async { ) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final result = final result = await pm.getItems(jid.toBare(), omemoDevicesXmlns);
await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns);
if (result.isType<PubSubError>()) return Result(UnknownOmemoError()); 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]. /// Retrieves the OMEMO device list from [jid].
@@ -536,12 +557,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieve all device bundles for the JID [jid]. /// Retrieve all device bundles for the JID [jid].
/// ///
/// On success, returns a list of devices. On failure, returns am OmemoError. /// On success, returns a list of devices. On failure, returns am OmemoError.
Future<Result<OmemoError, List<OmemoBundle>>> retrieveDeviceBundles( Future<Result<OmemoError, List<omemo.OmemoBundle>>> retrieveDeviceBundles(
JID jid, JID jid,
) async { ) async {
// TODO(Unknown): Should we query the device list first? // TODO(Unknown): Should we query the device list first?
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final bundlesRaw = await pm.getItems(jid.toString(), omemoBundlesXmlns); final bundlesRaw = await pm.getItems(jid, omemoBundlesXmlns);
if (bundlesRaw.isType<PubSubError>()) return Result(UnknownOmemoError()); if (bundlesRaw.isType<PubSubError>()) return Result(UnknownOmemoError());
final bundles = bundlesRaw final bundles = bundlesRaw
@@ -557,12 +578,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieves a bundle from entity [jid] with the device id [deviceId]. /// Retrieves a bundle from entity [jid] with the device id [deviceId].
/// ///
/// On success, returns the device bundle. On failure, returns an OmemoError. /// On success, returns the device bundle. On failure, returns an OmemoError.
Future<Result<OmemoError, OmemoBundle>> retrieveDeviceBundle( Future<Result<OmemoError, omemo.OmemoBundle>> retrieveDeviceBundle(
JID jid, JID jid,
int deviceId, int deviceId,
) async { ) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final bareJid = jid.toBare().toString(); final bareJid = jid.toBare();
final item = await pm.getItem(bareJid, omemoBundlesXmlns, '$deviceId'); final item = await pm.getItem(bareJid, omemoBundlesXmlns, '$deviceId');
if (item.isType<PubSubError>()) return Result(UnknownOmemoError()); if (item.isType<PubSubError>()) return Result(UnknownOmemoError());
@@ -573,7 +594,9 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// nodes. /// nodes.
/// ///
/// On success, returns true. On failure, returns an OmemoError. /// On success, returns true. On failure, returns an OmemoError.
Future<Result<OmemoError, bool>> publishBundle(OmemoBundle bundle) async { Future<Result<OmemoError, bool>> publishBundle(
omemo.OmemoBundle bundle,
) async {
final attrs = getAttributes(); final attrs = getAttributes();
final pm = attrs.getManagerById<PubSubManager>(pubsubManager)!; final pm = attrs.getManagerById<PubSubManager>(pubsubManager)!;
final bareJid = attrs.getFullJID().toBare(); final bareJid = attrs.getFullJID().toBare();
@@ -609,7 +632,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
); );
final deviceListPublish = await pm.publish( final deviceListPublish = await pm.publish(
bareJid.toString(), bareJid,
omemoDevicesXmlns, omemoDevicesXmlns,
newDeviceList, newDeviceList,
id: 'current', id: 'current',
@@ -621,7 +644,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
final deviceBundlePublish = await pm.publish( final deviceBundlePublish = await pm.publish(
bareJid.toString(), bareJid,
omemoBundlesXmlns, omemoBundlesXmlns,
bundleToXML(bundle), bundleToXML(bundle),
id: '${bundle.id}', id: '${bundle.id}',
@@ -637,7 +660,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Subscribes to the device list PubSub node of [jid]. /// Subscribes to the device list PubSub node of [jid].
Future<void> subscribeToDeviceListImpl(String jid) async { Future<void> subscribeToDeviceListImpl(String jid) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
await pm.subscribe(jid, omemoDevicesXmlns); await pm.subscribe(JID.fromString(jid), omemoDevicesXmlns);
}
/// Implementation for publishing our device [device].
Future<void> publishDeviceImpl(omemo.OmemoDevice device) async {
await publishBundle(await device.toBundle());
} }
/// Attempts to find out if [jid] supports omemo:2. /// Attempts to find out if [jid] supports omemo:2.
@@ -646,7 +674,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// On failure, returns an OmemoError. /// On failure, returns an OmemoError.
Future<Result<OmemoError, bool>> supportsOmemo(JID jid) async { Future<Result<OmemoError, bool>> supportsOmemo(JID jid) async {
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!; final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
final items = await dm.discoItemsQuery(jid.toBare().toString()); final items = await dm.discoItemsQuery(jid.toBare());
if (items.isType<DiscoError>()) return Result(UnknownOmemoError()); if (items.isType<DiscoError>()) return Result(UnknownOmemoError());
@@ -686,7 +714,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
.toList(), .toList(),
); );
final publishResult = await pm.publish( final publishResult = await pm.publish(
jid.toString(), jid,
omemoDevicesXmlns, omemoDevicesXmlns,
newPayload, newPayload,
id: 'current', id: 'current',

View File

@@ -5,9 +5,9 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; import 'package:moxxmpp/src/xeps/xep_0264.dart';
class StatelessMediaSharingData { class StatelessMediaSharingData implements StanzaHandlerExtension {
const StatelessMediaSharingData({ const StatelessMediaSharingData({
required this.mediaType, required this.mediaType,
required this.size, required this.size,
@@ -20,7 +20,7 @@ class StatelessMediaSharingData {
final int size; final int size;
final String description; final String description;
final Map<String, String> hashes; // algo -> hash value final Map<String, String> hashes; // algo -> hash value
final List<Thumbnail> thumbnails; final List<JingleContentThumbnail> thumbnails;
final String url; final String url;
} }
@@ -48,16 +48,11 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
break; break;
} }
final thumbnails = List<Thumbnail>.empty(growable: true); // Thumbnails
for (final child in file.children) { final thumbnails = List<JingleContentThumbnail>.empty(growable: true);
// TODO(Unknown): Handle other thumbnails for (final i
if (child.tag == 'file-thumbnail' && in file.findTags('thumbnail', xmlns: jingleContentThumbnailXmlns)) {
child.attributes['xmlns'] == fileThumbnailsXmlns) { thumbnails.add(JingleContentThumbnail.fromXML(i));
final thumb = parseFileThumbnailElement(child);
if (thumb != null) {
thumbnails.add(thumb);
}
}
} }
return StatelessMediaSharingData( return StatelessMediaSharingData(
@@ -70,7 +65,9 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
); );
} }
@Deprecated('Not maintained')
class SIMSManager extends XmppManagerBase { class SIMSManager extends XmppManagerBase {
@Deprecated('Not maintained')
SIMSManager() : super(simsManager); SIMSManager() : super(simsManager);
@override @override
@@ -85,7 +82,7 @@ class SIMSManager extends XmppManagerBase {
tagXmlns: referenceXmlns, tagXmlns: referenceXmlns,
// Before the message handler // Before the message handler
priority: -99, priority: -99,
) ),
]; ];
@override @override
@@ -98,7 +95,7 @@ class SIMSManager extends XmppManagerBase {
final references = message.findTags('reference', xmlns: referenceXmlns); final references = message.findTags('reference', xmlns: referenceXmlns);
for (final ref in references) { for (final ref in references) {
final sims = ref.firstTag('media-sharing', xmlns: simsXmlns); final sims = ref.firstTag('media-sharing', xmlns: simsXmlns);
if (sims != null) return state.copyWith(sims: parseSIMSElement(sims)); if (sims != null) return state..extensions.set(parseSIMSElement(sims));
} }
return state; return state;

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
/// A data class describing the user agent. See https://dyn.eightysoft.de/final/xep-0388.html#initiation /// A data class describing the user agent. See https://xmpp.org/extensions/xep-0388.html#initiation.
class UserAgent { class UserAgent {
const UserAgent({ const UserAgent({
this.id, this.id,
@@ -24,11 +24,9 @@ class UserAgent {
); );
return XMLNode( return XMLNode(
tag: 'user-agent', tag: 'user-agent',
attributes: id != null attributes: {
? { if (id != null) 'id': id,
'id': id, },
}
: {},
children: [ children: [
if (software != null) if (software != null)
XMLNode( XMLNode(

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