172 Commits

Author SHA1 Message Date
7f3538875b feat(core): Make all managers optional 2023-04-04 15:59:07 +02:00
f6efa20ff4 chore(meta): Bump versions 2023-04-04 15:55:52 +02:00
8443411f07 fix(docs): XEP-0144 -> XEP-0114 2023-04-04 15:53:57 +02:00
dc24b3c48a chore(core): Remove useDirectTLS 2023-04-04 15:52:56 +02:00
f6abf3d5b5 feat(xep): Implement XEP-0114 2023-04-04 15:48:26 +02:00
63c84d9479 feat(core): Allow tracking the stream id 2023-04-03 23:18:10 +02:00
3e43ac22d7 feat(core): Remove getter and setter for connectionSettings 2023-04-03 17:56:31 +02:00
47d821c02e docs(core): Add comments to NegotiationsHandler 2023-04-03 16:17:09 +02:00
355d580a9a fix(tests): Fix negotiator tests 2023-04-03 16:15:46 +02:00
03328bdf7a feat(core): Allow implementing different negotiation strategies
Fixes #27.
2023-04-03 16:05:20 +02:00
275d6e0346 feat(core): Attempt to improve the ReconnectionPolicy 2023-04-03 13:40:13 +02:00
0d9afd546c feat(core): Remove ignoreLock 2023-04-03 12:47:38 +02:00
3da334b5cf feat(core): Remove the connection lock 2023-04-03 12:46:15 +02:00
2947e2c539 Merge pull request 'SASL2 and friends' (#34) from feat/sasl2 into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/34
2023-04-02 21:36:02 +00:00
ac8433f51f chore(core): Refactor RFC6120 implementations 2023-04-02 23:33:53 +02:00
808371b271 chore(tests): Fix tests 2023-04-02 23:23:50 +02:00
7fdd83ea69 chore(xep): Clean the SASL2 implementation 2023-04-02 23:06:02 +02:00
68e2a65dcf docs(tests): Mention that we need prosody-trunk 2023-04-02 19:49:00 +02:00
d977a74446 feat(tests): Add an integration test for SASL2 2023-04-02 17:20:14 +02:00
29f0419154 feat(meta): Remove log redaction 2023-04-02 14:38:32 +02:00
b354ca8d0a feat(xep): Improve the ergonomics of Bind2 negotiators 2023-04-02 13:39:43 +02:00
ec6b5ab753 feat(xep): Allow inline enablement of carbons 2023-04-02 12:44:09 +02:00
ce1815d1f3 fix(tests): Fix namespace of <bound /> 2023-04-02 12:43:28 +02:00
fbb495dc2f feat(xep): Allow inlining CSI 2023-04-01 23:16:37 +02:00
4a6aa79e56 fix(xep): When using FAST, fallback to other SASL mechanisms on failure 2023-04-01 21:42:52 +02:00
0033d0eb6e feat(xep): Implement FAST 2023-04-01 21:10:46 +02:00
24cb05f91b feat(xep): Handle inline stream enablement with Bind2 2023-04-01 17:38:40 +02:00
91f763ac26 feat(xep): Allow negotiating SM enabling inline with Bind2 2023-04-01 17:16:29 +02:00
51edb61443 feat(xep): Implement SASL2 inline stream resumption 2023-04-01 15:50:13 +02:00
4e01d32e90 feat(xep): Allow setting a tag when using Bind2 2023-04-01 13:15:46 +02:00
f2fe06104c fix(core): Fix formatting 2023-04-01 13:13:19 +02:00
89fe8f0a9c feat(core): Make the PresenceManager optional 2023-04-01 13:09:43 +02:00
9358175925 feat(xep): Inline resource binding with Bind2 2023-04-01 13:00:35 +02:00
564a237986 feat(xep): Set the resource if SASL2 resulted in a resource 2023-04-01 12:51:26 +02:00
cf425917cf feat(core): Reset the resource if lastResource is null 2023-04-01 12:39:15 +02:00
63b7abd6f9 fix(core): Prevent resource binding if we already have a resource 2023-04-01 12:38:18 +02:00
f460e5ebe9 feat(core): Handle less resource binding in the core connection class 2023-04-01 12:28:11 +02:00
af8bc606d6 feat(xep): Guard against random data in the SASL2 result 2023-04-01 00:51:51 +02:00
30482c86f0 feat(xep): Implement inline negotiation 2023-04-01 00:47:45 +02:00
f86dbe6af8 feat(core): Verify the server signature with SASL2 2023-03-31 23:52:48 +02:00
478b5b8770 feat(core): Make SCRAM SASL2 aware 2023-03-31 21:09:16 +02:00
7ab3f4f0d9 feat(xep): Implement negotiating PLAIN via SASL2 2023-03-31 20:53:06 +02:00
2e60e9841e feat(xep): Begin work on SASL2 2023-03-31 19:02:57 +02:00
52ad9a7ddb fix(core): Reset the connection tracker when timing out 2023-03-31 15:53:11 +02:00
ac5e0c13b7 fix(xep): Do not subscribe to the data node 2023-03-31 15:52:50 +02:00
b49658784b fix(example): Adjust example to changes 2023-03-30 16:18:28 +02:00
d4a972e073 feat(core): Close the socket on an error 2023-03-30 16:17:42 +02:00
1009a2f967 feat(core): Fix typing and remove logging parameter 2023-03-30 16:17:12 +02:00
f355f01fc8 fix(tests): Fix integration tests 2023-03-30 16:15:44 +02:00
85995d51e4 feat(core): Remove SendPingEvent 2023-03-29 17:18:03 +02:00
2557a2fe5b feat(core): Make the ping manager optional 2023-03-29 15:25:17 +02:00
4321573dfb fix(core): Fix reconnections not working properly 2023-03-21 12:01:22 +01:00
70d4d6c56f feat(core): Use _testAndSetIsConnectionRunning 2023-03-18 18:41:58 +01:00
e1e492832e chore(tests): Format and lint tests 2023-03-18 16:13:45 +01:00
1950394f7d fix(meta): Add 'example' as a 'commit target' 2023-03-18 15:00:54 +01:00
308f7d93f5 chore(example): Update the example 2023-03-18 14:59:47 +01:00
de85bf848d fix(core): Fix crash when no negotiator matches
Fixes #30.

Also removes the `allowPlainAuth` attribute of `ConnectionSettings` as
users who want to disable SASL PLAIN can just not register the
negotiator or extend it.
2023-03-18 14:54:39 +01:00
7a6bf468bc test(xep): Add a test for parsing sticker packs 2023-03-16 22:04:25 +01:00
9cb6346c4d fix(style): Format and lint test helpers 2023-03-12 19:19:53 +01:00
f49eb66bb7 fix(xep): Fix usage of 'max' in publish options (#33)
This commit fixes two issues:
1. Fix an issue where [PubSubManager.publish] would always, if given
   publish options with maxItems set to 'max', use 'max' in the
   max_items publish options, even if the server indicates it does not
   support that.
2. Fix an issue with the StanzaExpectation, where it would let every
   stanza pass.
2023-03-12 19:11:55 +01:00
324ef9ca29 Merge pull request 'Merge connect and connectAwaitable' (#32) from refactor/connect into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/32
2023-03-12 16:31:21 +00:00
5b4dcc67b2 refactor(core): Remove XmppConnectionResult 2023-03-11 19:07:03 +01:00
9010218b10 refactor(core): Move connection errors into their own file 2023-03-11 19:06:28 +01:00
61144a10b3 fix(core): Minor API change 2023-03-11 19:01:55 +01:00
7a1f737c65 chore(meta): Bump version 2023-03-11 19:00:42 +01:00
546c032d43 fix(tests): Fix broken test 2023-03-11 18:59:40 +01:00
b1869be3d9 chore(docs): Update changelog 2023-03-11 18:59:22 +01:00
574fdfecaa feat(core): Merge connect and connectAwaitable 2023-03-11 18:54:36 +01:00
25c778965c feat(core): Merge connect and connectAwaitable 2023-03-11 00:10:50 +01:00
976c0040b5 chore(meta): Update JDK to 17 2023-03-11 00:10:50 +01:00
b53c62b40c Merge pull request 'fix: make the moxxmpp example work again' (#29) from bleonard252/moxxmpp-patch:master into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/29
2023-03-10 23:07:49 +00:00
Blake Leonard
2cdc56c882 chore: format corrections, comment clarifications
Signed-off-by: Blake Leonard <me@blakes.dev>
2023-03-10 13:29:30 -05:00
Blake Leonard
f5059d8008 Merge remote-tracking branch 'origin/master' into HEAD 2023-03-08 15:08:56 -05:00
Blake Leonard
792ec4d731 chore(example): format and fix lint errors
Signed-off-by: Blake Leonard <me@blakes.dev>
2023-03-08 15:07:55 -05:00
93d08188ea feat(docs): Add CONTRIBUTING.md 2023-03-08 20:51:38 +01:00
e9ad5a6c66 feat(flake): Update Flutter 2023-03-08 20:47:49 +01:00
8b0f118e2d fix(style): Format using dart format 2023-03-08 20:25:45 +01:00
Blake Leonard
60c89e28d3 chore(example): switch to connectAwaitable
That way it only acts connected when the credentials have been accepted.

Also I had to correct the value of "allowPlainAuth" which should be
true since a bug with it has been identified, and not yet fixed.

Signed-off-by: Blake Leonard <me@blakes.dev>
2023-03-08 14:09:37 -05:00
38155051f5 fix(style): Format using dart format 2023-03-08 19:32:03 +01:00
Blake Leonard
7b215d5c6e fix: make the moxxmpp example work again
Note: to do this, I could not use the ExampleTcpSocketWrapper.
If I did, the app crashed on launch.

I also added some functionality: the header bar turns green when
connected, the FAB says what it does, and you can disconnect.

Signed-off-by: Blake Leonard <me@blakes.dev>
2023-03-08 13:02:43 -05:00
1000e0756b feat: Improve detecting new streams
Fixes #26.
2023-01-31 21:11:52 +01:00
902b497526 docs: Add links to the hosted documentation 2023-01-28 15:33:21 +01:00
039f954e70 docs: Add some doc string to the scripts 2023-01-28 15:31:47 +01:00
5dc2b127fa flake: Remove nix-dart 2023-01-28 15:31:36 +01:00
252cc44841 feat: Make moxxmpp docs buildable using flakes 2023-01-28 15:22:37 +01:00
96d9ce4761 feat: Don't attempt reconnections when the error is unrecoverable
Fixes #25.
Should fix #24.
2023-01-28 13:20:16 +01:00
7f294d6632 fix: Bump version requirement for moxxmpp 2023-01-27 22:03:20 +01:00
e17de9065b feat: Bump moxxmpp_socket_tcp to 0.2.0 2023-01-27 22:01:32 +01:00
098687de45 feat: Bump moxxmpp version to 0.2.0 2023-01-27 21:57:26 +01:00
6da3342f22 feat: Make defining managers better 2023-01-27 21:54:16 +01:00
47337540f5 feat: Factor out "multiple-waiting" into its own thing 2023-01-27 21:14:35 +01:00
7e588f01b0 feat: Add DOAP
Fixes #11.
2023-01-27 19:09:05 +01:00
c7c6c9dae4 feat: Update Message Replies to 0.2.0
Fixes #22.
2023-01-27 19:08:57 +01:00
c77cfc4dcd feat: Change namespaces 2023-01-27 18:34:13 +01:00
1bd61076ea feat: Improve the API provided by the DiscoManager
Fixes #21.
2023-01-27 16:26:01 +01:00
bff4a6f707 feat: Rework how the ReconnectionPolicy system works 2023-01-27 00:14:44 +01:00
1cc266c675 fix: Just use shouldEncryptElement for the envelope "validation" 2023-01-23 13:11:07 +01:00
72099dfde5 feat: Only add envelope elements that should be encrypted 2023-01-23 13:10:07 +01:00
c9c45baabc feat: Allow easier responding to incoming stanzas
Should fix #20.
2023-01-23 12:47:30 +01:00
a01022c217 feat: Bump omemo_dart 2023-01-22 19:25:52 +01:00
c3459e6820 feat: Always set a cancel reason on failure 2023-01-21 20:46:45 +01:00
e031e6d760 feat: Add cancelReason if recipient likely does not support OMEMO:2 2023-01-21 15:42:00 +01:00
6c63b53cf4 fix: Fix crash with direct server IQs
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-01-15 00:52:34 +01:00
1aa50699ad feat: Improve the stanza await system
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This fixes an issue where publishing an avatar would fail, if returned
an error where the "from" attribute is missing from the stanza.
2023-01-14 16:28:37 +01:00
b2c54ae8c0 ci: Add Woodpecker CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-01-14 15:00:55 +01:00
b16c9f4b30 docs: Add more doc strings 2023-01-14 15:00:02 +01:00
a8d80eaddf fix: Add the closure expectation to the event 2023-01-14 12:41:17 +01:00
9baf1ed73c fix: Attempt to fix reconnection issues
Call _reconnectionPolicy.onSuccess when negotiations are done. Never
call onFailure directly; only ever call handleError.
2023-01-14 12:39:55 +01:00
ce3ea656ad fix: Stop ack timer on connection drops
Fixes #17.
2023-01-14 12:04:52 +01:00
ed49212f5a fix: Enabling carbons crashes 2023-01-13 15:17:21 +01:00
ad1242c47d feat: Try to lock reconnections behind a flag 2023-01-13 15:16:51 +01:00
890fcfb506 feat: Do initialization inline 2023-01-13 14:44:06 +01:00
d7723615fe fix: Fix message quote generation 2023-01-13 13:39:18 +01:00
6517065a1a feat: Track stanza responses as a tuple of (to, id)
Also fixes an invalid test case in the XEP-0198 tests, where
the IQ reply sets the "to" instead of the "from".
2023-01-10 12:50:07 +01:00
9223a7d403 feat: Add docs for including a tagged version 2023-01-10 12:25:56 +01:00
7ce6703c5b tests: Fix the XEP-0198 test 2023-01-09 12:53:22 +01:00
37261cddbb fix: Run Stream Management very early 2023-01-09 12:48:30 +01:00
d8c2ef6f3b Merge pull request 'Roster Rework' (#15) from fix/roster-rework into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/15
2023-01-07 21:23:12 +00:00
98e5324409 fix: Copy the pre handler's encryption state 2023-01-07 22:18:20 +01:00
a69c2a23f2 fix: Bump omemo_dart version 2023-01-07 22:18:05 +01:00
d8de093e4d fix: Somewhat fix OMEMO 2023-01-07 21:57:56 +01:00
678564dbb3 fix: Roster request being treated as a roster push 2023-01-07 21:23:45 +01:00
09d2601e85 fix: Compare groups 2023-01-07 19:58:46 +01:00
41560682a1 fix: Handle roster items staying the same 2023-01-07 19:03:35 +01:00
473f8e4bb6 tests: Fix tests 2023-01-07 18:40:36 +01:00
67446285c1 feat: Refactor RosterPushEvent to RosterPushResult 2023-01-07 18:36:18 +01:00
e12f4688d3 feat: Trigger an event when the roster changes 2023-01-07 18:32:15 +01:00
2581bbe203 feat: Document the RosterStateManager better 2023-01-07 18:11:41 +01:00
995f2e0248 feat: Integrate the BaseRosterStateManager with the RosterManager 2023-01-07 16:26:15 +01:00
e2c8f79429 feat: Add a management class for roster state 2023-01-07 15:51:10 +01:00
763c93857d feat: Simplify the JID parser
Fixes #13.
2023-01-04 17:19:37 +01:00
55d2ef9c25 style: Remove newline 2023-01-02 17:53:06 +01:00
f37cbd1616 feat: Allow specifying XEP-0449's access model 2023-01-02 17:52:55 +01:00
2a3449d0f2 fix: Fix user avatar update being triggered for every PubSub event 2023-01-02 17:35:47 +01:00
596693c206 feat: Update to omemo_dart 0.4.1 2023-01-02 13:58:27 +01:00
22aa07c4ba feat: Propagate errors and encrypt to self if carbons are enabled 2023-01-02 13:52:06 +01:00
62001c1e29 feat: Upgrade omemo_dart to 0.4.0 2023-01-01 18:17:41 +01:00
ca85c94fe5 fix: Fix wrong XML serialisation 2023-01-01 16:38:54 +01:00
637e1e25a6 feat: Migrate to the new omemo_dart API 2023-01-01 16:19:25 +01:00
09696c1c4d fix: Fix VCard and User Avatar queries being encrypted 2022-12-25 13:05:16 +01:00
298a8342b8 docs: Add funding.yml 2022-12-23 15:18:53 +01:00
d64220426b feat: Implement XEP-0449 2022-12-19 14:14:05 +01:00
88efdc361c fix: Only add a <body> element when specified 2022-12-09 12:52:00 +01:00
cc1b371198 feat: Allow clients to read Message Processing Hints 2022-12-09 12:46:17 +01:00
d9e4a3c1d4 feat: Implement XEP-0444 2022-12-06 14:09:07 +01:00
0ae13acca0 chore(release): publish packages
- moxxmpp@0.1.6+1
 - moxxmpp_socket_tcp@0.1.2+9
2022-11-26 15:48:48 +01:00
d383fa31ae fix: Fix LMC not working 2022-11-26 15:48:29 +01:00
d1de394cd9 chore(release): publish packages
- moxxmpp@0.1.6
 - moxxmpp_socket_tcp@0.1.2+8
2022-11-26 15:09:05 +01:00
14c48bcc64 feat: Implement XEP-0308 2022-11-26 15:08:20 +01:00
138edffb0a chore(release): publish packages
- moxxmpp@0.1.5
 - moxxmpp_socket_tcp@0.1.2+7
2022-11-22 22:49:41 +01:00
eb8f6ba17a feat: Message events now contain the stanza error, if available 2022-11-22 22:49:10 +01:00
beff05765b chore(release): publish packages
- moxxmpp@0.1.4
 - moxxmpp_socket_tcp@0.1.2+6
2022-11-21 16:06:49 +01:00
3b7ded3b96 fix: Only stanza-id required 'sid:0' support 2022-11-21 15:27:53 +01:00
edc86a10b3 feat: Implement parsing and sending of retractions 2022-11-20 23:43:58 +01:00
39e9c55fae chore(release): publish packages
- moxxmpp@0.1.3+1
 - moxxmpp_socket_tcp@0.1.2+5
2022-11-19 22:50:39 +01:00
1b2c567787 fix: Expose the error classes 2022-11-19 22:50:28 +01:00
d3955479f7 chore(release): publish packages
- moxxmpp@0.1.3
 - moxxmpp_socket_tcp@0.1.2+4
2022-11-19 22:32:56 +01:00
300a52f9fe refactor: Replace MayFail by Result 2022-11-19 22:26:02 +01:00
2e3472d88f feat: Rework how the negotiator system works
We can now return what exactly made a connection attempt fail.
2022-11-19 21:48:28 +01:00
6b106fe365 test: Add integration test for ExponentialBackoffReconnectionPolicy 2022-11-16 20:48:18 +01:00
bfd28c281e fix: Remove the old Results API
Closes #8.
2022-11-16 15:51:33 +01:00
c307567025 chore(release): publish packages
- moxxmpp@0.1.2+3
 - moxxmpp_socket_tcp@0.1.2+3
2022-11-16 15:37:44 +01:00
5dd96f518b fix: SASL SCRAM-SHA-{256,512} should now work 2022-11-16 15:37:20 +01:00
6d9010b11c chore(release): publish packages
- moxxmpp@0.1.2+2
 - moxxmpp_socket_tcp@0.1.2+2
2022-11-12 21:49:29 +01:00
9cc735d854 fix: Fix reconnections when the connection is awaited 2022-11-12 21:49:13 +01:00
988db718a2 chore(release): publish packages
- moxxmpp@0.1.2+1
 - moxxmpp_socket_tcp@0.1.2+1
2022-11-12 21:00:16 +01:00
afaca7a558 flake: Remove ANDROID_* from the dev shell 2022-11-12 20:59:50 +01:00
3172450b70 fix: A certificate rejection does not crash the connection
Fixes moxxy/moxxyv2#137.
2022-11-12 20:57:39 +01:00
848d83dc1f chore(release): publish packages
- moxxmpp@0.1.2
 - moxxmpp_socket_tcp@0.1.2
2022-11-12 12:46:36 +01:00
2f089535a3 fix(moxxmpp{,_socket_tcp): Fix path to analysis_options.yaml 2022-11-12 12:42:19 +01:00
608ba8ce4a feat(moxxmpp_socket_tcp): Make onBadCertificate available 2022-11-12 12:41:27 +01:00
d5493a185a feat: Remove Moxxy specific strings 2022-11-09 16:52:59 +01:00
185 changed files with 15446 additions and 5435 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
ko_fi: papatutuwawa

6
.gitignore vendored
View File

@@ -10,3 +10,9 @@ build/
# Omit committing pubspec.lock for library packages; see # Omit committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock. # https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock pubspec.lock
# Omit pubspec override files generated by melos
**/pubspec_overrides.yaml
# Flake results
result

14
.gitlint Normal file
View File

@@ -0,0 +1,14 @@
[general]
ignore=B5,B6,B7,B8
[title-max-length]
line-length=72
[title-trailing-punctuation]
[title-hard-tab]
[title-match-regex]
regex=^((feat|fix|chore|refactor|docs|release|test)\((meta|tests|style|docs|xep|core|example)+(,(meta|tests|style|docs|xep|core|example))*\)|release): [A-Z0-9].*$
[body-trailing-whitespace]
[body-first-line-empty]

28
.woodpecker.yml Normal file
View File

@@ -0,0 +1,28 @@
pipeline:
# Check moxxmpp
moxxmpp-lint:
image: dart:2.18.1
commands:
- cd packages/moxxmpp
- dart pub get
- dart analyze --fatal-infos --fatal-warnings
moxxmpp-test:
image: dart:2.18.1
commands:
- cd packages/moxxmpp
- dart pub get
- dart test
# Check moxxmpp_socket_tcp
moxxmpp_socket_tcp-lint:
image: dart:2.18.1
commands:
- cd packages/moxxmpp_socket_tcp
- dart pub get
- dart analyze --fatal-infos --fatal-warnings
# moxxmpp-test:
# image: dart:2.18.1
# commands:
# - cd packages/moxxmpp
# - dart pub get
# - dart test

19
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,19 @@
# Contribution Guide
Thanks for your interest in the moxxmpp XMPP library! This document contains guidelines and guides for working on the moxxmpp codebase.
## Contributing
If you want to fix a small issue, you can just fork, create a new branch, and start working right away. However, if you want to work
on a bigger feature, please first create an issue (if an issue does not already exist) or join the [development chat](xmpp:moxxy@muc.moxxy.org?join) (xmpp:moxxy@muc.moxxy.org?join)
to discuss the feature first.
Before creating a pull request, please make sure you checked every item on the following checklist:
- [ ] I formatted the code with the dart formatter (`dart format`) before running the linter
- [ ] I ran the linter (`dart analyze`) and introduced no new linter warnings
- [ ] I ran the tests (`dart test`) and introduced no new failing tests
- [ ] I used [gitlint](https://github.com/jorisroovers/gitlint) to ensure propper formatting of my commig messages
If you think that your code is ready for a pull request, but you are not sure if it is ready, prefix the PR's title with "WIP: ", so that discussion
can happen there. If you think your PR is ready for review, remove the "WIP: " prefix.

View File

@@ -3,13 +3,15 @@
moxxmpp is a XMPP library written purely in Dart for usage in Moxxy. moxxmpp is a XMPP library written purely in Dart for usage in Moxxy.
## Packages ## Packages
### moxxmpp ### [moxxmpp](./packages/moxxmpp)
This package contains the actual XMPP code that is platform-independent. This package contains the actual XMPP code that is platform-independent.
### moxxmpp_socket Documentation is available [here](https://moxxy.org/developers/docs/moxxmpp/).
`moxxmpp_socket` contains the implementation of the `BaseSocketWrapper` class that ### [moxxmpp_socket_tcp](./packages/moxxmpp_socket_tcp)
`moxxmpp_socket_tcp` contains the implementation of the `BaseSocketWrapper` class that
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.
@@ -18,6 +20,16 @@ if a DNS implementation is given, and supports StartTLS.
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
can be run with `flutter run` on Linux or Android. can be run with `flutter run` on Linux or Android.
To run the example, make sure that Flutter is correctly set up and working. If you use
the development shell provided by the NixOS Flake, ensure that `ANDROID_HOME` and
`ANDROID_AVD_HOME` are pointing to the correct directories.
## License ## License
See `./LICENSE`. See `./LICENSE`.
## Support
If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/assets/repo/kofi.png" height="36" style="height: 36px; border: 0px;"></img>](https://ko-fi.com/papatutuwawa)

View File

@@ -11,4 +11,3 @@ analyzer:
exclude: exclude:
- "**/*.g.dart" - "**/*.g.dart"
- "**/*.freezed.dart" - "**/*.freezed.dart"
- "test/"

View File

@@ -5,18 +5,20 @@ import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart'; import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
class ExampleTcpSocketWrapper extends TCPSocketWrapper { class ExampleTcpSocketWrapper extends TCPSocketWrapper {
ExampleTcpSocketWrapper() : super(false); ExampleTcpSocketWrapper() : super();
@override @override
Future<List<MoxSrvRecord>> srvQuery(String domain, bool dnssec) async { Future<List<MoxSrvRecord>> srvQuery(String domain, bool dnssec) async {
final records = await MoxdnsPlugin.srvQuery(domain, false); final records = await MoxdnsPlugin.srvQuery(domain, false);
return records return records
.map((record) => MoxSrvRecord( .map(
(record) => MoxSrvRecord(
record.priority, record.priority,
record.weight, record.weight,
record.target, record.target,
record.port, record.port,
),) ),
)
.toList(); .toList();
} }
} }
@@ -24,6 +26,7 @@ class ExampleTcpSocketWrapper extends TCPSocketWrapper {
void main() { void main() {
Logger.root.level = Level.ALL; Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) { Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print('${record.level.name}: ${record.time}: ${record.message}'); print('${record.level.name}: ${record.time}: ${record.message}');
}); });
@@ -54,20 +57,30 @@ class MyHomePage extends StatefulWidget {
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
final logger = Logger('MyHomePage');
final XmppConnection connection = XmppConnection( final XmppConnection connection = XmppConnection(
ExponentialBackoffReconnectionPolicy(), RandomBackoffReconnectionPolicy(1, 60),
ExampleTcpSocketWrapper(), AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
// The below causes the app to crash.
//ExampleTcpSocketWrapper(),
// In a production app, the below should be false.
TCPSocketWrapper(),
); );
TextEditingController jidController = TextEditingController(); TextEditingController jidController = TextEditingController();
TextEditingController passwordController = TextEditingController(); TextEditingController passwordController = TextEditingController();
bool connected = false;
bool loading = false;
_MyHomePageState() : super() { _MyHomePageState() : super() {
connection connection
..registerManagers([ ..registerManagers([
StreamManagementManager(), StreamManagementManager(),
DiscoManager(), DiscoManager([]),
RosterManager(), RosterManager(TestingRosterStateManager("", [])),
PingManager(), PingManager(
const Duration(minutes: 3),
),
MessageManager(), MessageManager(),
PresenceManager(), PresenceManager(),
]) ])
@@ -78,21 +91,45 @@ class _MyHomePageState extends State<MyHomePage> {
CSINegotiator(), CSINegotiator(),
RosterFeatureNegotiator(), RosterFeatureNegotiator(),
SaslPlainNegotiator(), SaslPlainNegotiator(),
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
SaslScramNegotiator(9, '', '', ScramHashType.sha256), SaslScramNegotiator(9, '', '', ScramHashType.sha256),
SaslScramNegotiator(8, '', '', ScramHashType.sha1), SaslScramNegotiator(8, '', '', ScramHashType.sha1),
]); ]);
} }
Future<void> _buttonPressed() async { Future<void> _buttonPressed() async {
connection.setConnectionSettings( if (connected) {
ConnectionSettings( await connection.disconnect();
setState(() {
connected = false;
});
return;
}
setState(() {
loading = true;
});
connection.connectionSettings = ConnectionSettings(
jid: JID.fromString(jidController.text), jid: JID.fromString(jidController.text),
password: passwordController.text, password: passwordController.text,
useDirectTLS: true, useDirectTLS: true,
allowPlainAuth: false, );
final result = await connection.connect(waitUntilLogin: true);
setState(() {
connected = result.isType<bool>() && result.get<bool>();
loading = false;
});
if (result.isType<XmppError>()) {
logger.severe(result.get<XmppError>());
if (context.mounted) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Error'),
content: Text(result.get<XmppError>().toString()),
), ),
); );
await connection.connect(); }
}
} }
@override @override
@@ -100,20 +137,24 @@ class _MyHomePageState extends State<MyHomePage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(widget.title), title: Text(widget.title),
backgroundColor: connected ? Colors.green : Colors.deepPurple[800],
foregroundColor: connected ? Colors.black : Colors.white,
), ),
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
TextField( TextField(
enabled: !loading,
controller: jidController, controller: jidController,
decoration: InputDecoration( decoration: const InputDecoration(
labelText: 'JID', labelText: 'JID',
), ),
), ),
TextField( TextField(
enabled: !loading,
controller: passwordController, controller: passwordController,
decoration: InputDecoration( decoration: const InputDecoration(
labelText: 'Password', labelText: 'Password',
), ),
obscureText: true, obscureText: true,
@@ -121,10 +162,13 @@ class _MyHomePageState extends State<MyHomePage> {
], ],
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton.extended(
onPressed: _buttonPressed, onPressed: _buttonPressed,
label: Text(connected ? 'Disconnect' : 'Connect'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
tooltip: 'Connect', tooltip: 'Connect',
child: const Icon(Icons.add), icon: const Icon(Icons.power),
), ),
); );
} }

View File

@@ -16,10 +16,10 @@ dependencies:
version: 0.1.4+1 version: 0.1.4+1
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.1 version: 0.3.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.1.1 version: 0.3.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

31
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1667395993, "lastModified": 1678901627,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -17,11 +17,27 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1667610399, "lastModified": 1676076353,
"narHash": "sha256-XZd0f4ZWAY0QOoUSdiNWj/eFiKb4B9CJPtl9uO9SYY4=", "narHash": "sha256-mdUtE8Tp40cZETwcq5tCwwLqkJVV1ULJQ5GKRtbshag=",
"owner": "AtaraxiaSjel",
"repo": "nixpkgs",
"rev": "5deb99bdccbbb97e7562dee4ba8a3ee3021688e6",
"type": "github"
},
"original": {
"owner": "AtaraxiaSjel",
"ref": "update/flutter",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1680273054,
"narHash": "sha256-Bs6/5LpvYp379qVqGt9mXxxx9GSE789k3oFc+OAL07M=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "1dd8696f96db47156e1424a49578fe7dd4ce99a4", "rev": "3364b5b117f65fe1ce65a3cdd5612a078a3b31e3",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -34,7 +50,8 @@
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable"
} }
} }
}, },

View File

@@ -1,11 +1,12 @@
{ {
description = "moxxmpp"; description = "moxxmpp";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:AtaraxiaSjel/nixpkgs/update/flutter";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
config = { config = {
@@ -13,6 +14,9 @@
allowUnfree = true; allowUnfree = true;
}; };
}; };
unstable = import nixpkgs-unstable {
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";
@@ -29,15 +33,49 @@
useGoogleAPIs = false; useGoogleAPIs = false;
useGoogleTVAddOns = false; useGoogleTVAddOns = false;
}; };
pinnedJDK = pkgs.jdk; pinnedJDK = pkgs.jdk17;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
pyyaml
requests
]);
moxxmppPubCache = import ./nix/pubcache.moxxmpp.nix {
inherit (pkgs) fetchzip runCommand;
};
in { in {
devShell = pkgs.mkShell { packages = {
moxxmppDartDocs = pkgs.callPackage ./nix/moxxmpp-docs.nix {
inherit (moxxmppPubCache) pubCache;
};
};
devShell = let
prosody-newer-community-modules = unstable.prosody.overrideAttrs (old: {
communityModules = pkgs.fetchhg {
url = "https://hg.prosody.im/prosody-modules";
rev = "e3a3a6c86a9f";
sha256 = "sha256-C2x6PCv0sYuj4/SroDOJLsNPzfeNCodYKbMqmNodFrk=";
};
src = pkgs.fetchhg {
url = "https://hg.prosody.im/trunk";
rev = "8a2f75e38eb2";
sha256 = "sha256-zMNp9+wQ/hvUVyxFl76DqCVzQUPP8GkNdstiTDkG8Hw=";
};
});
prosody-sasl2 = prosody-newer-community-modules.override {
withCommunityModules = [
"sasl2" "sasl2_fast" "sasl2_sm" "sasl2_bind2"
];
};
in pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
flutter pinnedJDK android.platform-tools dart # Flutter/Android 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
cairo cairo
clang clang
@@ -53,14 +91,19 @@
pkg-config pkg-config
xorg.libX11 xorg.libX11
xorg.xorgproto xorg.xorgproto
# For the scripts in ./scripts/
pythonEnv
# For integration testing against a local prosody server
prosody-sasl2
mkcert
]; ];
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_HOME = (toString ./.) + "/.android/sdk";
JAVA_HOME = pinnedJDK; JAVA_HOME = pinnedJDK;
ANDROID_AVD_HOME = (toString ./.) + "/.android/avd";
}; };
}); });
} }

6
integration_tests/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# Files and directories created by pub.
.dart_tool/
.packages
# Conventional directory for build output.
build/

View File

@@ -0,0 +1,5 @@
# Integration Tests
The included `./prosody.cfg.lua` config file must be used for integration testing.
Additionally, ensure that a user `testuser@localhost` with the password `abc123`
exists. Note that this currently requires prosody-trunk.

View File

@@ -0,0 +1 @@
include: ../analysis_options.yaml

View File

@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEAzCCAmugAwIBAgIQd61NPnP8++X7h8a+85C6DjANBgkqhkiG9w0BAQsFADBZ
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFzAVBgNVBAsMDmFsZXhh
bmRlckBtaWt1MR4wHAYDVQQDDBVta2NlcnQgYWxleGFuZGVyQG1pa3UwHhcNMjMw
NDAyMTM1ODIxWhcNMjUwNzAyMTM1ODIxWjBCMScwJQYDVQQKEx5ta2NlcnQgZGV2
ZWxvcG1lbnQgY2VydGlmaWNhdGUxFzAVBgNVBAsMDmFsZXhhbmRlckBtaWt1MIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1DElEXPY+VDQP7cSikK0ne0K
gDgorGYPG9R7lOeuPLHyFYYry78+hB037OT0BOyA2uTu1yrog0dI/4YGicPDIqXh
IgHfjV+4kMi5SgO7ECWOBmZFqTC3bBwvbNtoW40aFjYSFaOkm/nnfp+nalEJJZ/N
kSkD4gdT3pH1ClsovlI4BlsxeIoJtyGzxMidJVXDAqMNraLatzJBwnT3OEs93xTf
7Kd1KUpQp9OZFrGi15zv/n6tCmrcC3xMOVHuYkhW0UCTFmev7ZqbghQsQ9N9s0E6
kk9rUf9xtMNH4Af6+2YRkT1DAGQ6FkXl1nQdB5H5XRgOBl+3k9s8wUrxQvQddQID
AQABo14wXDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYD
VR0jBBgwFoAU54aUZ+dytAOBTsYIdGtSnjiig/gwFAYDVR0RBA0wC4IJbG9jYWxo
b3N0MA0GCSqGSIb3DQEBCwUAA4IBgQBU8p7Ua0Cs+lXlWmtCh2j+YF9R+dvc+3Iw
dYEzCmYd375uxPctyHXW0yYjyuH9WuYn0F7OicEFEeC2+exHND+/z0J2Zv5yu34r
SfgHVfvE/Vxisn9InYrUCVtfRwLDF3HgLyIlm8FVzIyiIANhpe6vJdqjEWTsiL2X
I6hoDf1xlRgEqUx+Wxl2IFWrg+1SPPGTQzDPImiRlz8d+9ZJ9v48vaV5+aITMvDP
Gfm/bnNXXd5Gf7nGwL8zFHiwLoYQ5AUYl0IfXYwFAXJ72+LjiRT33IOidVJF0gsQ
6k9cTsc4lIrt4FOzdchalbF1Eu2prieWoZxz0apG8OuUeAhaB+t8kT6swAkwvkLW
OnlSATm9Cls9Pc4XDHTbZlbMmwF2Jmukgz/l1vlTutt4ZgZwQkSEa9Qfoi9Zym0R
iKls1CgD49zguR/cFDKK3agvfv6Afw6HdgaS/WqcI/Ros7b+RCkbAlAG5gqr6BLQ
8RGyVjZSC4Mz/ddcnMEpRAnjuFJjhGA=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDUMSURc9j5UNA/
txKKQrSd7QqAOCisZg8b1HuU5648sfIVhivLvz6EHTfs5PQE7IDa5O7XKuiDR0j/
hgaJw8MipeEiAd+NX7iQyLlKA7sQJY4GZkWpMLdsHC9s22hbjRoWNhIVo6Sb+ed+
n6dqUQkln82RKQPiB1PekfUKWyi+UjgGWzF4igm3IbPEyJ0lVcMCow2totq3MkHC
dPc4Sz3fFN/sp3UpSlCn05kWsaLXnO/+fq0KatwLfEw5Ue5iSFbRQJMWZ6/tmpuC
FCxD032zQTqST2tR/3G0w0fgB/r7ZhGRPUMAZDoWReXWdB0HkfldGA4GX7eT2zzB
SvFC9B11AgMBAAECggEAYaj4yY6LFzxVjG2i79WBsYnOonK2bZpPa9ygwEjdTXwM
0lE9SPoNONsFyVca5EVBjP1+27MY7orZkxlJWxCpeAHmmzNHg5bBqIlpliIfb3AJ
bPKXLyaH1Q8n2K8m2bQYhI6ARktZ0Jv1KrcqY2lGj3V8NEovSlFbDX4ZzJlmKCly
d4Ia6eQ7f9AjgsOwpQGeCTF7WLaVDnch6D4JfCGrW08lFeaqogiBQczsOE3hcNSd
tEul21Z0CkC7Iiw28KdkApPINquo1VYdAcOvUCOXkwJfPC1gsJwK4O2jxfi9v5NF
uU1niK0/00b396pQKvXpkfViynexwzK0MZCoo3zuQQKBgQDzaZexcniQNDyWqN3C
oMe4V3rnxs+aO/lu8Ed3mng+Jf4vuarZlxNot7WRBMGT/T+b7/UIrqRJy50CYAPY
3RRR84tLg3UMwUWhDYsPucNc2icODBG4c+QWJ300W19r+J+iT8PwS9AbH2n094Rn
LCRYFrX5aMsgIH5uwuncKzweMQKBgQDfKj2i1ptC53aOcr1tMCFYcnMGtaAZ8u6+
cKSgnzKlTw/g0EYlGcETUnCyZe0oVYWp3y859FBXU0JMDmxu84aYEZNF6BwRVlpF
feQgtUFZHyf9MepQGhjIJ5El8n7jhh1bsBY18QbDFe6/GtqPx/mQEF7vE+wPFl9h
putwdv3OhQKBgGKPyi2/BVSW4kW7IPiTM+vP+GNrnFp+mHS0dKvYb4HyzmcyzhyH
UQOhB7Mt8thivmP9GQIn/TwoZ24zxLsGYhkA/dFY7Id6pyAcpMd8V7/8Ub4dYvuG
acASw1709MF6jeEiXVuqxxyEbtoTc5h3Rkwo/gx8w2tB3RAqepl9JD2xAoGAfVL3
ci8a2iOqTKza/Cp/T3BWcHonAuuOb5xKl3lPs84GmLXd7o/cAcHWUBk1aeU9Pvx7
RQyS4bd8D8I52sUf3N5h2mxS9tmLsGLWbhfcLvR0PJh/gaRmLmEp/imEYLm8WvU0
Q+6rYXs7rE6kVwJygBjxd0m003Q49FoM9gec2RECgYEA5SLAe2UmJSLIb0DKk27o
nSfARDSdi9N40vIjDFHmDRdKTOYicED/f7KqXnxVpvFxDdCvJ7xeC4V7vkaqiiwd
/oMLQq0GjmBxG/PNd1AFIWDydyH+JcY6U4XWIzIw92OKVYC/KMvd2f9orTfmDyAU
RsGMfgV90kCzouAZKy3yPmo=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,60 @@
admins = { }
plugin_paths = {}
modules_enabled = {
-- Generally required
"disco"; -- Service discovery
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
-- Not essential, but recommended
"blocklist"; -- Allow users to block communications with other users
"bookmarks"; -- Synchronise the list of open rooms between clients
"carbons"; -- Keep multiple online clients in sync
"dialback"; -- Support for verifying remote servers using DNS
"limits"; -- Enable bandwidth limiting for XMPP connections
"pep"; -- Allow users to store public and private data in their account
"private"; -- Legacy account storage mechanism (XEP-0049)
"smacks"; -- Stream management and resumption (XEP-0198)
"vcard4"; -- User profiles (stored in PEP)
"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
-- Nice to have
"csi_simple"; -- Simple but effective traffic optimizations for mobile devices
"invites"; -- Create and manage invites
"invites_adhoc"; -- Allow admins/users to create invitations via their client
"invites_register"; -- Allows invited users to create accounts
"ping"; -- Replies to XMPP pings with pongs
"register"; -- Allow users to register on this server using a client and change passwords
"time"; -- Let others know the time here on this server
"uptime"; -- Report how long server has been running
"version"; -- Replies to server version requests
-- SASL2
"sasl2";
"sasl2_sm";
"sasl2_fast";
"sasl2_bind2";
}
s2s_secure_auth = false
-- Authentication
authentication = "internal_plain"
-- Storage
storage = "internal"
data_path = "/tmp/prosody-data/"
log = {
debug = "*console";
}
pidfile = "/tmp/prosody.pid"
component_ports = { 8888 }
component_interfaces = { '127.0.0.1' }
VirtualHost "localhost"
Component "component.localhost"
component_secret = "abc123"

View File

@@ -0,0 +1,16 @@
name: integration_tests
description: A sample command-line application.
version: 1.0.0
environment:
sdk: '>=2.18.0 <3.0.0'
dependencies:
logging: ^1.0.2
moxxmpp: 0.3.0
moxxmpp_socket_tcp: 0.3.0
dev_dependencies:
lints: ^2.0.0
test: ^1.16.0
very_good_analysis: ^3.0.1

View File

@@ -0,0 +1,42 @@
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
import 'package:test/test.dart';
class TestingTCPSocketWrapper extends TCPSocketWrapper {
@override
bool onBadCertificate(dynamic certificate, String domain) {
return true;
}
}
void main() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print(
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
);
});
test('Test connecting to prosody as a component', () async {
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ComponentToServerNegotiator(),
TestingTCPSocketWrapper(),
)..connectionSettings = ConnectionSettings(
jid: JID.fromString('component.localhost'),
password: 'abc123',
host: '127.0.0.1',
port: 8888,
);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<bool>(), true);
});
}

View File

@@ -0,0 +1,74 @@
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
import 'package:test/test.dart';
class TestingTCPSocketWrapper extends TCPSocketWrapper {
@override
bool onBadCertificate(dynamic certificate, String domain) {
return true;
}
}
void main() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print(
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
);
});
test('Test authenticating against Prosody with SASL2, Bind2, and FAST',
() async {
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
TestingTCPSocketWrapper(),
)..connectionSettings = ConnectionSettings(
jid: JID.fromString('testuser@localhost'),
password: 'abc123',
host: '127.0.0.1',
port: 5222,
);
final csi = CSIManager();
await csi.setInactive(sendNonza: false);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
FASTSaslNegotiator(),
Bind2Negotiator(),
StartTlsNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<bool>(), true);
expect(
conn.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!.state,
NegotiatorState.done,
);
expect(
conn
.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!
.fastToken !=
null,
true,
);
});
}

35
nix/moxxmpp-docs.nix Normal file
View File

@@ -0,0 +1,35 @@
{
stdenv
, pubCache
, dart
, lib
}:
stdenv.mkDerivation {
pname = "moxxmpp-docs";
version = "0.2.0";
PUB_CACHE = "${pubCache}";
src = "${./..}/packages/moxxmpp";
buildPhase = ''
runHook preBuild
(
set -x
echo $PUB_CACHE
${dart}/bin/dart pub get --no-precompile --offline
)
runHook postBuild
'';
installPhase = ''
runHook preInstall
${dart}/bin/dart doc -o $out
runHook postInstall
'';
}

730
nix/moxxmpp.lock Normal file
View File

@@ -0,0 +1,730 @@
packages:
_fe_analyzer_shared:
archive_url: https://pub.dartlang.org/packages/_fe_analyzer_shared/versions/50.0.0.tar.gz
dependency: transitive
description:
name: _fe_analyzer_shared
url: https://pub.dartlang.org
sha256: 1hyd5pmjcfyvfwhsc0wq6k0229abmqq5zn95g31hh42bklb2gci5
source: hosted
version: 50.0.0
analyzer:
archive_url: https://pub.dartlang.org/packages/analyzer/versions/5.2.0.tar.gz
dependency: transitive
description:
name: analyzer
url: https://pub.dartlang.org
sha256: 0niy5b3w39aywpjpw5a84pxdilhh3zzv1c22x8ywml756pybmj4r
source: hosted
version: 5.2.0
args:
archive_url: https://pub.dartlang.org/packages/args/versions/2.3.1.tar.gz
dependency: transitive
description:
name: args
url: https://pub.dartlang.org
sha256: 0c78zkzg2d2kzw1qrpiyrj1qvm4pr0yhnzapbqk347m780ha408g
source: hosted
version: 2.3.1
async:
archive_url: https://pub.dartlang.org/packages/async/versions/2.10.0.tar.gz
dependency: transitive
description:
name: async
url: https://pub.dartlang.org
sha256: 00hhylamsjcqmcbxlsrfimri63gb384l31r9mqvacn6c6bvk4yfx
source: hosted
version: 2.10.0
boolean_selector:
archive_url: https://pub.dartlang.org/packages/boolean_selector/versions/2.1.1.tar.gz
dependency: transitive
description:
name: boolean_selector
url: https://pub.dartlang.org
sha256: 0hxq8072hb89q9s91xlz9fvrjxfy7hw6jkdwkph5dp77df841kmj
source: hosted
version: 2.1.1
build:
archive_url: https://pub.dartlang.org/packages/build/versions/2.3.1.tar.gz
dependency: transitive
description:
name: build
url: https://pub.dartlang.org
sha256: 1x6nkii6kqy6y7ck0151yfhc9lp2nvbhznnhdi2mxr8afk6jxigd
source: hosted
version: 2.3.1
build_config:
archive_url: https://pub.dartlang.org/packages/build_config/versions/1.1.1.tar.gz
dependency: transitive
description:
name: build_config
url: https://pub.dartlang.org
sha256: 092rrbhbdy9fk50jqb1fwj1sfk415fi43irvsd0hk5w90gn8vazj
source: hosted
version: 1.1.1
build_daemon:
archive_url: https://pub.dartlang.org/packages/build_daemon/versions/3.1.0.tar.gz
dependency: transitive
description:
name: build_daemon
url: https://pub.dartlang.org
sha256: 0b6hnwjc3gi5g7cnpy8xyiqigcrs0xp51c7y7v1pqn9v75g25w6j
source: hosted
version: 3.1.0
build_resolvers:
archive_url: https://pub.dartlang.org/packages/build_resolvers/versions/2.1.0.tar.gz
dependency: transitive
description:
name: build_resolvers
url: https://pub.dartlang.org
sha256: 0fnrisgq6rnvbqsf8v43hb11kr1qq6azrxbsvx3wwimd37nxx8m5
source: hosted
version: 2.1.0
build_runner:
archive_url: https://pub.dartlang.org/packages/build_runner/versions/2.3.2.tar.gz
dependency: direct dev
description:
name: build_runner
url: https://pub.dartlang.org
sha256: 0246bxl9rxgil55fhfzi7csd9a56blj9s1j1z79717hiyzsr60x6
source: hosted
version: 2.3.2
build_runner_core:
archive_url: https://pub.dartlang.org/packages/build_runner_core/versions/7.2.7.tar.gz
dependency: transitive
description:
name: build_runner_core
url: https://pub.dartlang.org
sha256: 0bpil0fw0dag3vbnin9p945ymi7xjgkiy7jrq9j52plljf7cnf5z
source: hosted
version: 7.2.7
built_collection:
archive_url: https://pub.dartlang.org/packages/built_collection/versions/5.1.1.tar.gz
dependency: transitive
description:
name: built_collection
url: https://pub.dartlang.org
sha256: 0bqjahxr42q84w91nhv3n4cr580l3s3ffx3vgzyyypgqnrck0hv3
source: hosted
version: 5.1.1
built_value:
archive_url: https://pub.dartlang.org/packages/built_value/versions/8.4.2.tar.gz
dependency: transitive
description:
name: built_value
url: https://pub.dartlang.org
sha256: 0sslr4258snvcj8qhbdk6wapka174als0viyxddwqlnhs7dlci8i
source: hosted
version: 8.4.2
checked_yaml:
archive_url: https://pub.dartlang.org/packages/checked_yaml/versions/2.0.1.tar.gz
dependency: transitive
description:
name: checked_yaml
url: https://pub.dartlang.org
sha256: 1gf7ankc5jb7mk17br87ajv05pfg6vb8nf35ay6c35w8jp70ra7k
source: hosted
version: 2.0.1
code_builder:
archive_url: https://pub.dartlang.org/packages/code_builder/versions/4.3.0.tar.gz
dependency: transitive
description:
name: code_builder
url: https://pub.dartlang.org
sha256: 1vl9dl23yd0zjw52ndrazijs6dw83fg1rvyb2gfdpd6n1lj9nbhg
source: hosted
version: 4.3.0
collection:
archive_url: https://pub.dartlang.org/packages/collection/versions/1.17.0.tar.gz
dependency: direct main
description:
name: collection
url: https://pub.dartlang.org
sha256: 1iyl3v3j7mj3sxjf63b1kc182fwrwd04mjp5x2i61hic8ihfw545
source: hosted
version: 1.17.0
convert:
archive_url: https://pub.dartlang.org/packages/convert/versions/3.1.1.tar.gz
dependency: transitive
description:
name: convert
url: https://pub.dartlang.org
sha256: 0adsigjk3l1c31i6k91p28dqyjlgwiqrs4lky5djrm2scf8k6cri
source: hosted
version: 3.1.1
coverage:
archive_url: https://pub.dartlang.org/packages/coverage/versions/1.6.1.tar.gz
dependency: transitive
description:
name: coverage
url: https://pub.dartlang.org
sha256: 0akbg1yp2h4vprc8r9xvrpgvp5d26h7m80h5sbzgr5dlis1bcw0d
source: hosted
version: 1.6.1
crypto:
archive_url: https://pub.dartlang.org/packages/crypto/versions/3.0.2.tar.gz
dependency: transitive
description:
name: crypto
url: https://pub.dartlang.org
sha256: 1kjfb8fvdxazmv9ps2iqdhb8kcr31115h0nwn6v4xmr71k8jb8ds
source: hosted
version: 3.0.2
cryptography:
archive_url: https://pub.dartlang.org/packages/cryptography/versions/2.0.5.tar.gz
dependency: direct main
description:
name: cryptography
url: https://pub.dartlang.org
sha256: 0jqph45d9lbhdakprnb84c3qhk4aq05hhb1pmn8w23yhl41ypijs
source: hosted
version: 2.0.5
dart_style:
archive_url: https://pub.dartlang.org/packages/dart_style/versions/2.2.4.tar.gz
dependency: transitive
description:
name: dart_style
url: https://pub.dartlang.org
sha256: 01wg15kalbjlh4i3xbawc9zk8yrk28qhak7xp7mlwn2syhdckn7v
source: hosted
version: 2.2.4
file:
archive_url: https://pub.dartlang.org/packages/file/versions/6.1.4.tar.gz
dependency: transitive
description:
name: file
url: https://pub.dartlang.org
sha256: 0ajcfblf8d4dicp1sgzkbrhd0b0v0d8wl70jsnf5drjck3p3ppk7
source: hosted
version: 6.1.4
fixnum:
archive_url: https://pub.dartlang.org/packages/fixnum/versions/1.0.1.tar.gz
dependency: transitive
description:
name: fixnum
url: https://pub.dartlang.org
sha256: 1m8cdfqp9d6w1cik3fwz9bai1wf9j11rjv2z0zlv7ich87q9kkjk
source: hosted
version: 1.0.1
freezed:
archive_url: https://pub.dartlang.org/packages/freezed/versions/2.1.1.tar.gz
dependency: direct main
description:
name: freezed
url: https://pub.dartlang.org
sha256: 1i9s4djf4vlz56zqn8brcck3n7sk07qay23wmaan991cqydd10iq
source: hosted
version: 2.1.1
freezed_annotation:
archive_url: https://pub.dartlang.org/packages/freezed_annotation/versions/2.1.0.tar.gz
dependency: direct main
description:
name: freezed_annotation
url: https://pub.dartlang.org
sha256: 0ym120dh1lpfnb68gxh1finm8p9l445q5x10aw8269y469b9k9z3
source: hosted
version: 2.1.0
frontend_server_client:
archive_url: https://pub.dartlang.org/packages/frontend_server_client/versions/3.1.0.tar.gz
dependency: transitive
description:
name: frontend_server_client
url: https://pub.dartlang.org
sha256: 0nv4avkv2if9hdcfzckz36f3mclv7vxchivrg8j3miaqhnjvv4bj
source: hosted
version: 3.1.0
glob:
archive_url: https://pub.dartlang.org/packages/glob/versions/2.1.0.tar.gz
dependency: transitive
description:
name: glob
url: https://pub.dartlang.org
sha256: 0a6gbwsbz6rkg35dkff0zv88rvcflqdmda90hdfpn7jp1z1w9rhs
source: hosted
version: 2.1.0
graphs:
archive_url: https://pub.dartlang.org/packages/graphs/versions/2.2.0.tar.gz
dependency: transitive
description:
name: graphs
url: https://pub.dartlang.org
sha256: 0cr6dgs1a7ln2ir5gd0kiwpn787lk4dwhqfjv8876hkkr1rv80m9
source: hosted
version: 2.2.0
hex:
archive_url: https://pub.dartlang.org/packages/hex/versions/0.2.0.tar.gz
dependency: direct main
description:
name: hex
url: https://pub.dartlang.org
sha256: 19w3f90mdiy06a6kf8hlwc4jn4cxixkj106kc3g3bis27ar7smkh
source: hosted
version: 0.2.0
http_multi_server:
archive_url: https://pub.dartlang.org/packages/http_multi_server/versions/3.2.1.tar.gz
dependency: transitive
description:
name: http_multi_server
url: https://pub.dartlang.org
sha256: 1zdcm04z85jahb2hs7qs85rh974kw49hffhy9cn1gfda3077dvql
source: hosted
version: 3.2.1
http_parser:
archive_url: https://pub.dartlang.org/packages/http_parser/versions/4.0.2.tar.gz
dependency: transitive
description:
name: http_parser
url: https://pub.dartlang.org
sha256: 027c4sjkhkkx3sk1aqs6s4djb87syi9h521qpm1bf21bq3gga5jd
source: hosted
version: 4.0.2
io:
archive_url: https://pub.dartlang.org/packages/io/versions/1.0.3.tar.gz
dependency: transitive
description:
name: io
url: https://pub.dartlang.org
sha256: 1bp5l8hkrp6fjj7zw9af51hxyp52sjspc5558lq0lmi453l0czni
source: hosted
version: 1.0.3
js:
archive_url: https://pub.dartlang.org/packages/js/versions/0.6.5.tar.gz
dependency: transitive
description:
name: js
url: https://pub.dartlang.org
sha256: 13fbxgyg1v6bmzvxamg6494vk3923fn3mgxj6f4y476aqwk99n50
source: hosted
version: 0.6.5
json_annotation:
archive_url: https://pub.dartlang.org/packages/json_annotation/versions/4.7.0.tar.gz
dependency: transitive
description:
name: json_annotation
url: https://pub.dartlang.org
sha256: 1p9nvn33psx2zbalhyqjw8gr4agd76jj5jq0fdz0i584c7l77bby
source: hosted
version: 4.7.0
json_serializable:
archive_url: https://pub.dartlang.org/packages/json_serializable/versions/6.5.4.tar.gz
dependency: direct main
description:
name: json_serializable
url: https://pub.dartlang.org
sha256: 04d7laaxrbiybcgbv3y223hy8d6n9f84h5lv9sv79zd9ffzkb2hg
source: hosted
version: 6.5.4
logging:
archive_url: https://pub.dartlang.org/packages/logging/versions/1.0.2.tar.gz
dependency: direct main
description:
name: logging
url: https://pub.dartlang.org
sha256: 0hl1mjh662c44ci7z60x92i0jsyqg1zm6k6fc89n9pdcxsqdpwfs
source: hosted
version: 1.0.2
matcher:
archive_url: https://pub.dartlang.org/packages/matcher/versions/0.12.13.tar.gz
dependency: transitive
description:
name: matcher
url: https://pub.dartlang.org
sha256: 0pjgc38clnjbv124n8bh724db1wcc4kk125j7dxl0icz7clvm0p0
source: hosted
version: 0.12.13
meta:
archive_url: https://pub.dartlang.org/packages/meta/versions/1.8.0.tar.gz
dependency: direct main
description:
name: meta
url: https://pub.dartlang.org
sha256: 01kqdd25nln5a219pr94s66p27m0kpqz0wpmwnm24kdy3ngif1v5
source: hosted
version: 1.8.0
mime:
archive_url: https://pub.dartlang.org/packages/mime/versions/1.0.2.tar.gz
dependency: transitive
description:
name: mime
url: https://pub.dartlang.org
sha256: 1dr3qikzvp10q1saka7azki5gk2kkf2v7k9wfqjsyxmza2zlv896
source: hosted
version: 1.0.2
moxlib:
archive_url: https://git.polynom.me/api/packages/moxxy/pub/api/packages/moxlib/files/0.1.5.tar.gz
dependency: direct main
description:
name: moxlib
url: https://git.polynom.me/api/packages/Moxxy/pub/
sha256: 1j52xglpwy8c7dbylc3f6vrh0p52xhhwqs4h0qcqk8c1rvjn5czq
source: hosted
version: 0.1.5
node_preamble:
archive_url: https://pub.dartlang.org/packages/node_preamble/versions/2.0.1.tar.gz
dependency: transitive
description:
name: node_preamble
url: https://pub.dartlang.org
sha256: 0i0gfc2yqa09182vc01lj47qpq98kfm9m8h4n8c5fby0mjd0lvyx
source: hosted
version: 2.0.1
omemo_dart:
archive_url: https://git.polynom.me/api/packages/PapaTutuWawa/pub/api/packages/omemo_dart/files/0.4.3.tar.gz
dependency: direct main
description:
name: omemo_dart
url: https://git.polynom.me/api/packages/PapaTutuWawa/pub/
sha256: 09x3jqa11hjdjp31nxnz91j6jssbc2f8a1lh44fmkc0d79hs8bbi
source: hosted
version: 0.4.3
package_config:
archive_url: https://pub.dartlang.org/packages/package_config/versions/2.1.0.tar.gz
dependency: transitive
description:
name: package_config
url: https://pub.dartlang.org
sha256: 1d4l0i4cby344zj45f5shrg2pkw1i1jn03kx0qqh0l7gh1ha7bpc
source: hosted
version: 2.1.0
path:
archive_url: https://pub.dartlang.org/packages/path/versions/1.8.2.tar.gz
dependency: transitive
description:
name: path
url: https://pub.dartlang.org
sha256: 16ggdh29ciy7h8sdshhwmxn6dd12sfbykf2j82c56iwhhlljq181
source: hosted
version: 1.8.2
pedantic:
archive_url: https://pub.dartlang.org/packages/pedantic/versions/1.11.1.tar.gz
dependency: transitive
description:
name: pedantic
url: https://pub.dartlang.org
sha256: 10ch0h3hi6cfwiz2ihfkh6m36m75c0m7fd0wwqaqggffsj2dn8ad
source: hosted
version: 1.11.1
petitparser:
archive_url: https://pub.dartlang.org/packages/petitparser/versions/5.1.0.tar.gz
dependency: transitive
description:
name: petitparser
url: https://pub.dartlang.org
sha256: 1pqqqqiy9ald24qsi24q9qrr0zphgpsrnrv9rlx4vwr6xak7d8c0
source: hosted
version: 5.1.0
pinenacl:
archive_url: https://pub.dartlang.org/packages/pinenacl/versions/0.5.1.tar.gz
dependency: transitive
description:
name: pinenacl
url: https://pub.dartlang.org
sha256: 0didjgva658z90hbcmhd0y8w1b8v86dp6gabfhylnw1aixl47cxg
source: hosted
version: 0.5.1
pool:
archive_url: https://pub.dartlang.org/packages/pool/versions/1.5.1.tar.gz
dependency: transitive
description:
name: pool
url: https://pub.dartlang.org
sha256: 0wmzs46hjszv3ayhr1p5l7xza7q9rkg2q9z4swmhdqmhlz3c50x4
source: hosted
version: 1.5.1
pub_semver:
archive_url: https://pub.dartlang.org/packages/pub_semver/versions/2.1.2.tar.gz
dependency: transitive
description:
name: pub_semver
url: https://pub.dartlang.org
sha256: 1vsj5c1f2dza4l5zmjix4zh65lp8gsg6pw01h57pijx2id0g4bwi
source: hosted
version: 2.1.2
pubspec_parse:
archive_url: https://pub.dartlang.org/packages/pubspec_parse/versions/1.2.1.tar.gz
dependency: transitive
description:
name: pubspec_parse
url: https://pub.dartlang.org
sha256: 19dmr9k4wsqjnhlzp1lbrw8dv7a1gnwmr8l5j9zlw407rmfg20d1
source: hosted
version: 1.2.1
random_string:
archive_url: https://pub.dartlang.org/packages/random_string/versions/2.3.1.tar.gz
dependency: direct main
description:
name: random_string
url: https://pub.dartlang.org
sha256: 11cjiv75sgldvk3x7w6j77lgi08r6737wm94m3ylabylsr6zdyff
source: hosted
version: 2.3.1
saslprep:
archive_url: https://pub.dartlang.org/packages/saslprep/versions/1.0.2.tar.gz
dependency: direct main
description:
name: saslprep
url: https://pub.dartlang.org
sha256: 04lss0xvm6p801p8306jdxg7k0b28kr6n65dz2f57dkca237kcw7
source: hosted
version: 1.0.2
shelf:
archive_url: https://pub.dartlang.org/packages/shelf/versions/1.4.0.tar.gz
dependency: transitive
description:
name: shelf
url: https://pub.dartlang.org
sha256: 0x2xl7glrnq0hdxpy2i94a4wxbdrd6dm46hvhzgjn8alsm8z0wz1
source: hosted
version: 1.4.0
shelf_packages_handler:
archive_url: https://pub.dartlang.org/packages/shelf_packages_handler/versions/3.0.1.tar.gz
dependency: transitive
description:
name: shelf_packages_handler
url: https://pub.dartlang.org
sha256: 199rbdbifj46lg3iynznnsbs8zr4dfcw0s7wan8v73nvpqvli82q
source: hosted
version: 3.0.1
shelf_static:
archive_url: https://pub.dartlang.org/packages/shelf_static/versions/1.1.1.tar.gz
dependency: transitive
description:
name: shelf_static
url: https://pub.dartlang.org
sha256: 1kqbaslz7bna9lldda3ibrjg0gczbzlwgm9cic8shg0bnl0v3s34
source: hosted
version: 1.1.1
shelf_web_socket:
archive_url: https://pub.dartlang.org/packages/shelf_web_socket/versions/1.0.3.tar.gz
dependency: transitive
description:
name: shelf_web_socket
url: https://pub.dartlang.org
sha256: 0rr87nx2wdf9alippxiidqlgi82fbprnsarr1jswg9qin0yy4jpn
source: hosted
version: 1.0.3
source_gen:
archive_url: https://pub.dartlang.org/packages/source_gen/versions/1.2.6.tar.gz
dependency: transitive
description:
name: source_gen
url: https://pub.dartlang.org
sha256: 1kxgx782lzpjhv736h0pz3lnxpcgiy05h0ysy0q77gix8q09i1hz
source: hosted
version: 1.2.6
source_helper:
archive_url: https://pub.dartlang.org/packages/source_helper/versions/1.3.3.tar.gz
dependency: transitive
description:
name: source_helper
url: https://pub.dartlang.org
sha256: 044kzmzlfpx93s4raz5avijahizmvai0zvl0lbm4wi93ynhdp1pd
source: hosted
version: 1.3.3
source_map_stack_trace:
archive_url: https://pub.dartlang.org/packages/source_map_stack_trace/versions/2.1.1.tar.gz
dependency: transitive
description:
name: source_map_stack_trace
url: https://pub.dartlang.org
sha256: 0b5d4c5n5qd3j8n10gp1khhr508wfl3819bhk6xnl34qxz8n032k
source: hosted
version: 2.1.1
source_maps:
archive_url: https://pub.dartlang.org/packages/source_maps/versions/0.10.11.tar.gz
dependency: transitive
description:
name: source_maps
url: https://pub.dartlang.org
sha256: 18ixrlz3l2alk3hp0884qj0mcgzhxmjpg6nq0n1200pfy62pc4z6
source: hosted
version: 0.10.11
source_span:
archive_url: https://pub.dartlang.org/packages/source_span/versions/1.9.1.tar.gz
dependency: transitive
description:
name: source_span
url: https://pub.dartlang.org
sha256: 1lq4sy7lw15qsv9cijf6l48p16qr19r7njzwr4pxn8vv1kh6rb86
source: hosted
version: 1.9.1
stack_trace:
archive_url: https://pub.dartlang.org/packages/stack_trace/versions/1.11.0.tar.gz
dependency: transitive
description:
name: stack_trace
url: https://pub.dartlang.org
sha256: 0bggqvvpkrfvqz24bnir4959k0c45azc3zivk4lyv3mvba6092na
source: hosted
version: 1.11.0
stream_channel:
archive_url: https://pub.dartlang.org/packages/stream_channel/versions/2.1.1.tar.gz
dependency: transitive
description:
name: stream_channel
url: https://pub.dartlang.org
sha256: 054by84c60yxphr3qgg6f82gg6d22a54aqjp265anlm8dwz1ji32
source: hosted
version: 2.1.1
stream_transform:
archive_url: https://pub.dartlang.org/packages/stream_transform/versions/2.1.0.tar.gz
dependency: transitive
description:
name: stream_transform
url: https://pub.dartlang.org
sha256: 0jq6767v9ds17i2nd6mdd9i0f7nvsgg3dz74d0v54x66axjgr0gp
source: hosted
version: 2.1.0
string_scanner:
archive_url: https://pub.dartlang.org/packages/string_scanner/versions/1.2.0.tar.gz
dependency: transitive
description:
name: string_scanner
url: https://pub.dartlang.org
sha256: 0p1r0v2923avwfg03rk0pmc6f21m0zxpcx6i57xygd25k6hdfi00
source: hosted
version: 1.2.0
synchronized:
archive_url: https://pub.dartlang.org/packages/synchronized/versions/3.0.0%2B2.tar.gz
dependency: direct main
description:
name: synchronized
url: https://pub.dartlang.org
sha256: 1j6108cq1hbcqpwhk9sah8q3gcidd7222bzhha2nk9syxhzqy82i
source: hosted
version: 3.0.0+2
term_glyph:
archive_url: https://pub.dartlang.org/packages/term_glyph/versions/1.2.1.tar.gz
dependency: transitive
description:
name: term_glyph
url: https://pub.dartlang.org
sha256: 1x8nspxaccls0sxjamp703yp55yxdvhj6wg21lzwd296i9rwlxh9
source: hosted
version: 1.2.1
test:
archive_url: https://pub.dartlang.org/packages/test/versions/1.22.0.tar.gz
dependency: direct dev
description:
name: test
url: https://pub.dartlang.org
sha256: 08kimbjvkdw3bkj7za36p3yqdr8dnlb5v30c250kvdncb7k09h4x
source: hosted
version: 1.22.0
test_api:
archive_url: https://pub.dartlang.org/packages/test_api/versions/0.4.16.tar.gz
dependency: transitive
description:
name: test_api
url: https://pub.dartlang.org
sha256: 0mfyjpqkkmaqdh7xygrydx12591wq9ll816f61n80dc6rmkdx7px
source: hosted
version: 0.4.16
test_core:
archive_url: https://pub.dartlang.org/packages/test_core/versions/0.4.20.tar.gz
dependency: transitive
description:
name: test_core
url: https://pub.dartlang.org
sha256: 1r8dnvkxxvh55z1c8lrsja1m0dkf5i4lgwwqixcx0mqvxx5w3005
source: hosted
version: 0.4.20
timing:
archive_url: https://pub.dartlang.org/packages/timing/versions/1.0.0.tar.gz
dependency: transitive
description:
name: timing
url: https://pub.dartlang.org
sha256: 0a02znvy0fbzr0n4ai67pp8in7w6m768aynkk1kp5lnmgy17ppsg
source: hosted
version: 1.0.0
typed_data:
archive_url: https://pub.dartlang.org/packages/typed_data/versions/1.3.1.tar.gz
dependency: transitive
description:
name: typed_data
url: https://pub.dartlang.org
sha256: 1x402bvyzdmdvmyqhyfamjxf54p9j8sa8ns2n5dwsdhnfqbw859g
source: hosted
version: 1.3.1
unorm_dart:
archive_url: https://pub.dartlang.org/packages/unorm_dart/versions/0.2.0.tar.gz
dependency: transitive
description:
name: unorm_dart
url: https://pub.dartlang.org
sha256: 05kyk2764yz14pzgx00i7h5b1lzh8kjqnxspfzyf8z920bcgbz0v
source: hosted
version: 0.2.0
uuid:
archive_url: https://pub.dartlang.org/packages/uuid/versions/3.0.5.tar.gz
dependency: direct main
description:
name: uuid
url: https://pub.dartlang.org
sha256: 12lsynr07lw9848jknmzxvzn3ia12xdj07iiva0vg0qjvpq7ladg
source: hosted
version: 3.0.5
very_good_analysis:
archive_url: https://pub.dartlang.org/packages/very_good_analysis/versions/3.1.0.tar.gz
dependency: direct dev
description:
name: very_good_analysis
url: https://pub.dartlang.org
sha256: 1p2dh8aahbqyyqfzbsxswafgxnmxgisjq2xfp008skyh7imk6sz4
source: hosted
version: 3.1.0
vm_service:
archive_url: https://pub.dartlang.org/packages/vm_service/versions/9.4.0.tar.gz
dependency: transitive
description:
name: vm_service
url: https://pub.dartlang.org
sha256: 05xaxaxzyfls6jklw1hzws2jmina1cjk10gbl7a63djh1ghnzjb5
source: hosted
version: 9.4.0
watcher:
archive_url: https://pub.dartlang.org/packages/watcher/versions/1.0.2.tar.gz
dependency: transitive
description:
name: watcher
url: https://pub.dartlang.org
sha256: 1sk7gvwa7s0h4l652qrgbh7l8wyqc6nr6lki8m4rj55720p0fnyg
source: hosted
version: 1.0.2
web_socket_channel:
archive_url: https://pub.dartlang.org/packages/web_socket_channel/versions/2.2.0.tar.gz
dependency: transitive
description:
name: web_socket_channel
url: https://pub.dartlang.org
sha256: 147amn05v1f1a1grxjr7yzgshrczjwijwiywggsv6dgic8kxyj5a
source: hosted
version: 2.2.0
webkit_inspection_protocol:
archive_url: https://pub.dartlang.org/packages/webkit_inspection_protocol/versions/1.2.0.tar.gz
dependency: transitive
description:
name: webkit_inspection_protocol
url: https://pub.dartlang.org
sha256: 0z400dzw7gf68a3wm95xi2mf461iigkyq6x69xgi7qs3fvpmn3hx
source: hosted
version: 1.2.0
xml:
archive_url: https://pub.dartlang.org/packages/xml/versions/6.2.0.tar.gz
dependency: direct main
description:
name: xml
url: https://pub.dartlang.org
sha256: 0jwknkfcnb5svg6r01xjsj0aiw06mlx54pgay1ymaaqm2mjhyz01
source: hosted
version: 6.2.0
yaml:
archive_url: https://pub.dartlang.org/packages/yaml/versions/3.1.1.tar.gz
dependency: transitive
description:
name: yaml
url: https://pub.dartlang.org
sha256: 0mqqmzn3c9rr38b5xm312fz1vyp6vb36lm477r9hak77bxzpp0iw
source: hosted
version: 3.1.1

814
nix/pubcache.moxxmpp.nix Normal file
View File

@@ -0,0 +1,814 @@
{fetchzip, runCommand} : rec {
_fe_analyzer_shared = fetchzip {
sha256 = "1hyd5pmjcfyvfwhsc0wq6k0229abmqq5zn95g31hh42bklb2gci5";
url = "https://pub.dartlang.org/packages/_fe_analyzer_shared/versions/50.0.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
analyzer = fetchzip {
sha256 = "0niy5b3w39aywpjpw5a84pxdilhh3zzv1c22x8ywml756pybmj4r";
url = "https://pub.dartlang.org/packages/analyzer/versions/5.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
args = fetchzip {
sha256 = "0c78zkzg2d2kzw1qrpiyrj1qvm4pr0yhnzapbqk347m780ha408g";
url = "https://pub.dartlang.org/packages/args/versions/2.3.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
async = fetchzip {
sha256 = "00hhylamsjcqmcbxlsrfimri63gb384l31r9mqvacn6c6bvk4yfx";
url = "https://pub.dartlang.org/packages/async/versions/2.10.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
boolean_selector = fetchzip {
sha256 = "0hxq8072hb89q9s91xlz9fvrjxfy7hw6jkdwkph5dp77df841kmj";
url = "https://pub.dartlang.org/packages/boolean_selector/versions/2.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
build = fetchzip {
sha256 = "1x6nkii6kqy6y7ck0151yfhc9lp2nvbhznnhdi2mxr8afk6jxigd";
url = "https://pub.dartlang.org/packages/build/versions/2.3.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
build_config = fetchzip {
sha256 = "092rrbhbdy9fk50jqb1fwj1sfk415fi43irvsd0hk5w90gn8vazj";
url = "https://pub.dartlang.org/packages/build_config/versions/1.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
build_daemon = fetchzip {
sha256 = "0b6hnwjc3gi5g7cnpy8xyiqigcrs0xp51c7y7v1pqn9v75g25w6j";
url = "https://pub.dartlang.org/packages/build_daemon/versions/3.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
build_resolvers = fetchzip {
sha256 = "0fnrisgq6rnvbqsf8v43hb11kr1qq6azrxbsvx3wwimd37nxx8m5";
url = "https://pub.dartlang.org/packages/build_resolvers/versions/2.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
build_runner = fetchzip {
sha256 = "0246bxl9rxgil55fhfzi7csd9a56blj9s1j1z79717hiyzsr60x6";
url = "https://pub.dartlang.org/packages/build_runner/versions/2.3.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
build_runner_core = fetchzip {
sha256 = "0bpil0fw0dag3vbnin9p945ymi7xjgkiy7jrq9j52plljf7cnf5z";
url = "https://pub.dartlang.org/packages/build_runner_core/versions/7.2.7.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
built_collection = fetchzip {
sha256 = "0bqjahxr42q84w91nhv3n4cr580l3s3ffx3vgzyyypgqnrck0hv3";
url = "https://pub.dartlang.org/packages/built_collection/versions/5.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
built_value = fetchzip {
sha256 = "0sslr4258snvcj8qhbdk6wapka174als0viyxddwqlnhs7dlci8i";
url = "https://pub.dartlang.org/packages/built_value/versions/8.4.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
checked_yaml = fetchzip {
sha256 = "1gf7ankc5jb7mk17br87ajv05pfg6vb8nf35ay6c35w8jp70ra7k";
url = "https://pub.dartlang.org/packages/checked_yaml/versions/2.0.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
code_builder = fetchzip {
sha256 = "1vl9dl23yd0zjw52ndrazijs6dw83fg1rvyb2gfdpd6n1lj9nbhg";
url = "https://pub.dartlang.org/packages/code_builder/versions/4.3.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
collection = fetchzip {
sha256 = "1iyl3v3j7mj3sxjf63b1kc182fwrwd04mjp5x2i61hic8ihfw545";
url = "https://pub.dartlang.org/packages/collection/versions/1.17.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
convert = fetchzip {
sha256 = "0adsigjk3l1c31i6k91p28dqyjlgwiqrs4lky5djrm2scf8k6cri";
url = "https://pub.dartlang.org/packages/convert/versions/3.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
coverage = fetchzip {
sha256 = "0akbg1yp2h4vprc8r9xvrpgvp5d26h7m80h5sbzgr5dlis1bcw0d";
url = "https://pub.dartlang.org/packages/coverage/versions/1.6.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
crypto = fetchzip {
sha256 = "1kjfb8fvdxazmv9ps2iqdhb8kcr31115h0nwn6v4xmr71k8jb8ds";
url = "https://pub.dartlang.org/packages/crypto/versions/3.0.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
cryptography = fetchzip {
sha256 = "0jqph45d9lbhdakprnb84c3qhk4aq05hhb1pmn8w23yhl41ypijs";
url = "https://pub.dartlang.org/packages/cryptography/versions/2.0.5.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
dart_style = fetchzip {
sha256 = "01wg15kalbjlh4i3xbawc9zk8yrk28qhak7xp7mlwn2syhdckn7v";
url = "https://pub.dartlang.org/packages/dart_style/versions/2.2.4.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
file = fetchzip {
sha256 = "0ajcfblf8d4dicp1sgzkbrhd0b0v0d8wl70jsnf5drjck3p3ppk7";
url = "https://pub.dartlang.org/packages/file/versions/6.1.4.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
fixnum = fetchzip {
sha256 = "1m8cdfqp9d6w1cik3fwz9bai1wf9j11rjv2z0zlv7ich87q9kkjk";
url = "https://pub.dartlang.org/packages/fixnum/versions/1.0.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
freezed = fetchzip {
sha256 = "1i9s4djf4vlz56zqn8brcck3n7sk07qay23wmaan991cqydd10iq";
url = "https://pub.dartlang.org/packages/freezed/versions/2.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
freezed_annotation = fetchzip {
sha256 = "0ym120dh1lpfnb68gxh1finm8p9l445q5x10aw8269y469b9k9z3";
url = "https://pub.dartlang.org/packages/freezed_annotation/versions/2.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
frontend_server_client = fetchzip {
sha256 = "0nv4avkv2if9hdcfzckz36f3mclv7vxchivrg8j3miaqhnjvv4bj";
url = "https://pub.dartlang.org/packages/frontend_server_client/versions/3.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
glob = fetchzip {
sha256 = "0a6gbwsbz6rkg35dkff0zv88rvcflqdmda90hdfpn7jp1z1w9rhs";
url = "https://pub.dartlang.org/packages/glob/versions/2.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
graphs = fetchzip {
sha256 = "0cr6dgs1a7ln2ir5gd0kiwpn787lk4dwhqfjv8876hkkr1rv80m9";
url = "https://pub.dartlang.org/packages/graphs/versions/2.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
hex = fetchzip {
sha256 = "19w3f90mdiy06a6kf8hlwc4jn4cxixkj106kc3g3bis27ar7smkh";
url = "https://pub.dartlang.org/packages/hex/versions/0.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
http_multi_server = fetchzip {
sha256 = "1zdcm04z85jahb2hs7qs85rh974kw49hffhy9cn1gfda3077dvql";
url = "https://pub.dartlang.org/packages/http_multi_server/versions/3.2.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
http_parser = fetchzip {
sha256 = "027c4sjkhkkx3sk1aqs6s4djb87syi9h521qpm1bf21bq3gga5jd";
url = "https://pub.dartlang.org/packages/http_parser/versions/4.0.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
io = fetchzip {
sha256 = "1bp5l8hkrp6fjj7zw9af51hxyp52sjspc5558lq0lmi453l0czni";
url = "https://pub.dartlang.org/packages/io/versions/1.0.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
js = fetchzip {
sha256 = "13fbxgyg1v6bmzvxamg6494vk3923fn3mgxj6f4y476aqwk99n50";
url = "https://pub.dartlang.org/packages/js/versions/0.6.5.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
json_annotation = fetchzip {
sha256 = "1p9nvn33psx2zbalhyqjw8gr4agd76jj5jq0fdz0i584c7l77bby";
url = "https://pub.dartlang.org/packages/json_annotation/versions/4.7.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
json_serializable = fetchzip {
sha256 = "04d7laaxrbiybcgbv3y223hy8d6n9f84h5lv9sv79zd9ffzkb2hg";
url = "https://pub.dartlang.org/packages/json_serializable/versions/6.5.4.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
logging = fetchzip {
sha256 = "0hl1mjh662c44ci7z60x92i0jsyqg1zm6k6fc89n9pdcxsqdpwfs";
url = "https://pub.dartlang.org/packages/logging/versions/1.0.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
matcher = fetchzip {
sha256 = "0pjgc38clnjbv124n8bh724db1wcc4kk125j7dxl0icz7clvm0p0";
url = "https://pub.dartlang.org/packages/matcher/versions/0.12.13.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
meta = fetchzip {
sha256 = "01kqdd25nln5a219pr94s66p27m0kpqz0wpmwnm24kdy3ngif1v5";
url = "https://pub.dartlang.org/packages/meta/versions/1.8.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
mime = fetchzip {
sha256 = "1dr3qikzvp10q1saka7azki5gk2kkf2v7k9wfqjsyxmza2zlv896";
url = "https://pub.dartlang.org/packages/mime/versions/1.0.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
moxlib = fetchzip {
sha256 = "1j52xglpwy8c7dbylc3f6vrh0p52xhhwqs4h0qcqk8c1rvjn5czq";
url = "https://git.polynom.me/api/packages/moxxy/pub/api/packages/moxlib/files/0.1.5.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
node_preamble = fetchzip {
sha256 = "0i0gfc2yqa09182vc01lj47qpq98kfm9m8h4n8c5fby0mjd0lvyx";
url = "https://pub.dartlang.org/packages/node_preamble/versions/2.0.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
omemo_dart = fetchzip {
sha256 = "09x3jqa11hjdjp31nxnz91j6jssbc2f8a1lh44fmkc0d79hs8bbi";
url = "https://git.polynom.me/api/packages/PapaTutuWawa/pub/api/packages/omemo_dart/files/0.4.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
package_config = fetchzip {
sha256 = "1d4l0i4cby344zj45f5shrg2pkw1i1jn03kx0qqh0l7gh1ha7bpc";
url = "https://pub.dartlang.org/packages/package_config/versions/2.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
path = fetchzip {
sha256 = "16ggdh29ciy7h8sdshhwmxn6dd12sfbykf2j82c56iwhhlljq181";
url = "https://pub.dartlang.org/packages/path/versions/1.8.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
pedantic = fetchzip {
sha256 = "10ch0h3hi6cfwiz2ihfkh6m36m75c0m7fd0wwqaqggffsj2dn8ad";
url = "https://pub.dartlang.org/packages/pedantic/versions/1.11.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
petitparser = fetchzip {
sha256 = "1pqqqqiy9ald24qsi24q9qrr0zphgpsrnrv9rlx4vwr6xak7d8c0";
url = "https://pub.dartlang.org/packages/petitparser/versions/5.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
pinenacl = fetchzip {
sha256 = "0didjgva658z90hbcmhd0y8w1b8v86dp6gabfhylnw1aixl47cxg";
url = "https://pub.dartlang.org/packages/pinenacl/versions/0.5.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
pool = fetchzip {
sha256 = "0wmzs46hjszv3ayhr1p5l7xza7q9rkg2q9z4swmhdqmhlz3c50x4";
url = "https://pub.dartlang.org/packages/pool/versions/1.5.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
pub_semver = fetchzip {
sha256 = "1vsj5c1f2dza4l5zmjix4zh65lp8gsg6pw01h57pijx2id0g4bwi";
url = "https://pub.dartlang.org/packages/pub_semver/versions/2.1.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
pubspec_parse = fetchzip {
sha256 = "19dmr9k4wsqjnhlzp1lbrw8dv7a1gnwmr8l5j9zlw407rmfg20d1";
url = "https://pub.dartlang.org/packages/pubspec_parse/versions/1.2.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
random_string = fetchzip {
sha256 = "11cjiv75sgldvk3x7w6j77lgi08r6737wm94m3ylabylsr6zdyff";
url = "https://pub.dartlang.org/packages/random_string/versions/2.3.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
saslprep = fetchzip {
sha256 = "04lss0xvm6p801p8306jdxg7k0b28kr6n65dz2f57dkca237kcw7";
url = "https://pub.dartlang.org/packages/saslprep/versions/1.0.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
shelf = fetchzip {
sha256 = "0x2xl7glrnq0hdxpy2i94a4wxbdrd6dm46hvhzgjn8alsm8z0wz1";
url = "https://pub.dartlang.org/packages/shelf/versions/1.4.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
shelf_packages_handler = fetchzip {
sha256 = "199rbdbifj46lg3iynznnsbs8zr4dfcw0s7wan8v73nvpqvli82q";
url = "https://pub.dartlang.org/packages/shelf_packages_handler/versions/3.0.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
shelf_static = fetchzip {
sha256 = "1kqbaslz7bna9lldda3ibrjg0gczbzlwgm9cic8shg0bnl0v3s34";
url = "https://pub.dartlang.org/packages/shelf_static/versions/1.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
shelf_web_socket = fetchzip {
sha256 = "0rr87nx2wdf9alippxiidqlgi82fbprnsarr1jswg9qin0yy4jpn";
url = "https://pub.dartlang.org/packages/shelf_web_socket/versions/1.0.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
source_gen = fetchzip {
sha256 = "1kxgx782lzpjhv736h0pz3lnxpcgiy05h0ysy0q77gix8q09i1hz";
url = "https://pub.dartlang.org/packages/source_gen/versions/1.2.6.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
source_helper = fetchzip {
sha256 = "044kzmzlfpx93s4raz5avijahizmvai0zvl0lbm4wi93ynhdp1pd";
url = "https://pub.dartlang.org/packages/source_helper/versions/1.3.3.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
source_map_stack_trace = fetchzip {
sha256 = "0b5d4c5n5qd3j8n10gp1khhr508wfl3819bhk6xnl34qxz8n032k";
url = "https://pub.dartlang.org/packages/source_map_stack_trace/versions/2.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
source_maps = fetchzip {
sha256 = "18ixrlz3l2alk3hp0884qj0mcgzhxmjpg6nq0n1200pfy62pc4z6";
url = "https://pub.dartlang.org/packages/source_maps/versions/0.10.11.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
source_span = fetchzip {
sha256 = "1lq4sy7lw15qsv9cijf6l48p16qr19r7njzwr4pxn8vv1kh6rb86";
url = "https://pub.dartlang.org/packages/source_span/versions/1.9.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
stack_trace = fetchzip {
sha256 = "0bggqvvpkrfvqz24bnir4959k0c45azc3zivk4lyv3mvba6092na";
url = "https://pub.dartlang.org/packages/stack_trace/versions/1.11.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
stream_channel = fetchzip {
sha256 = "054by84c60yxphr3qgg6f82gg6d22a54aqjp265anlm8dwz1ji32";
url = "https://pub.dartlang.org/packages/stream_channel/versions/2.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
stream_transform = fetchzip {
sha256 = "0jq6767v9ds17i2nd6mdd9i0f7nvsgg3dz74d0v54x66axjgr0gp";
url = "https://pub.dartlang.org/packages/stream_transform/versions/2.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
string_scanner = fetchzip {
sha256 = "0p1r0v2923avwfg03rk0pmc6f21m0zxpcx6i57xygd25k6hdfi00";
url = "https://pub.dartlang.org/packages/string_scanner/versions/1.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
synchronized = fetchzip {
sha256 = "1j6108cq1hbcqpwhk9sah8q3gcidd7222bzhha2nk9syxhzqy82i";
url = "https://pub.dartlang.org/packages/synchronized/versions/3.0.0%2B2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
term_glyph = fetchzip {
sha256 = "1x8nspxaccls0sxjamp703yp55yxdvhj6wg21lzwd296i9rwlxh9";
url = "https://pub.dartlang.org/packages/term_glyph/versions/1.2.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
test = fetchzip {
sha256 = "08kimbjvkdw3bkj7za36p3yqdr8dnlb5v30c250kvdncb7k09h4x";
url = "https://pub.dartlang.org/packages/test/versions/1.22.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
test_api = fetchzip {
sha256 = "0mfyjpqkkmaqdh7xygrydx12591wq9ll816f61n80dc6rmkdx7px";
url = "https://pub.dartlang.org/packages/test_api/versions/0.4.16.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
test_core = fetchzip {
sha256 = "1r8dnvkxxvh55z1c8lrsja1m0dkf5i4lgwwqixcx0mqvxx5w3005";
url = "https://pub.dartlang.org/packages/test_core/versions/0.4.20.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
timing = fetchzip {
sha256 = "0a02znvy0fbzr0n4ai67pp8in7w6m768aynkk1kp5lnmgy17ppsg";
url = "https://pub.dartlang.org/packages/timing/versions/1.0.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
typed_data = fetchzip {
sha256 = "1x402bvyzdmdvmyqhyfamjxf54p9j8sa8ns2n5dwsdhnfqbw859g";
url = "https://pub.dartlang.org/packages/typed_data/versions/1.3.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
unorm_dart = fetchzip {
sha256 = "05kyk2764yz14pzgx00i7h5b1lzh8kjqnxspfzyf8z920bcgbz0v";
url = "https://pub.dartlang.org/packages/unorm_dart/versions/0.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
uuid = fetchzip {
sha256 = "12lsynr07lw9848jknmzxvzn3ia12xdj07iiva0vg0qjvpq7ladg";
url = "https://pub.dartlang.org/packages/uuid/versions/3.0.5.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
very_good_analysis = fetchzip {
sha256 = "1p2dh8aahbqyyqfzbsxswafgxnmxgisjq2xfp008skyh7imk6sz4";
url = "https://pub.dartlang.org/packages/very_good_analysis/versions/3.1.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
vm_service = fetchzip {
sha256 = "05xaxaxzyfls6jklw1hzws2jmina1cjk10gbl7a63djh1ghnzjb5";
url = "https://pub.dartlang.org/packages/vm_service/versions/9.4.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
watcher = fetchzip {
sha256 = "1sk7gvwa7s0h4l652qrgbh7l8wyqc6nr6lki8m4rj55720p0fnyg";
url = "https://pub.dartlang.org/packages/watcher/versions/1.0.2.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
web_socket_channel = fetchzip {
sha256 = "147amn05v1f1a1grxjr7yzgshrczjwijwiywggsv6dgic8kxyj5a";
url = "https://pub.dartlang.org/packages/web_socket_channel/versions/2.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
webkit_inspection_protocol = fetchzip {
sha256 = "0z400dzw7gf68a3wm95xi2mf461iigkyq6x69xgi7qs3fvpmn3hx";
url = "https://pub.dartlang.org/packages/webkit_inspection_protocol/versions/1.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
xml = fetchzip {
sha256 = "0jwknkfcnb5svg6r01xjsj0aiw06mlx54pgay1ymaaqm2mjhyz01";
url = "https://pub.dartlang.org/packages/xml/versions/6.2.0.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
yaml = fetchzip {
sha256 = "0mqqmzn3c9rr38b5xm312fz1vyp6vb36lm477r9hak77bxzpp0iw";
url = "https://pub.dartlang.org/packages/yaml/versions/3.1.1.tar.gz";
stripRoot = false;
extension = "tar.gz";
};
pubCache = runCommand "moxxmpp-pub-cache" {} ''
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${_fe_analyzer_shared} $out/hosted/pub.dartlang.org/_fe_analyzer_shared-50.0.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${analyzer} $out/hosted/pub.dartlang.org/analyzer-5.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${args} $out/hosted/pub.dartlang.org/args-2.3.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${async} $out/hosted/pub.dartlang.org/async-2.10.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${boolean_selector} $out/hosted/pub.dartlang.org/boolean_selector-2.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build} $out/hosted/pub.dartlang.org/build-2.3.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_config} $out/hosted/pub.dartlang.org/build_config-1.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_daemon} $out/hosted/pub.dartlang.org/build_daemon-3.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_resolvers} $out/hosted/pub.dartlang.org/build_resolvers-2.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_runner} $out/hosted/pub.dartlang.org/build_runner-2.3.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${build_runner_core} $out/hosted/pub.dartlang.org/build_runner_core-7.2.7
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${built_collection} $out/hosted/pub.dartlang.org/built_collection-5.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${built_value} $out/hosted/pub.dartlang.org/built_value-8.4.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${checked_yaml} $out/hosted/pub.dartlang.org/checked_yaml-2.0.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${code_builder} $out/hosted/pub.dartlang.org/code_builder-4.3.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${collection} $out/hosted/pub.dartlang.org/collection-1.17.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${convert} $out/hosted/pub.dartlang.org/convert-3.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${coverage} $out/hosted/pub.dartlang.org/coverage-1.6.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${crypto} $out/hosted/pub.dartlang.org/crypto-3.0.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${cryptography} $out/hosted/pub.dartlang.org/cryptography-2.0.5
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${dart_style} $out/hosted/pub.dartlang.org/dart_style-2.2.4
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${file} $out/hosted/pub.dartlang.org/file-6.1.4
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${fixnum} $out/hosted/pub.dartlang.org/fixnum-1.0.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${freezed} $out/hosted/pub.dartlang.org/freezed-2.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${freezed_annotation} $out/hosted/pub.dartlang.org/freezed_annotation-2.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${frontend_server_client} $out/hosted/pub.dartlang.org/frontend_server_client-3.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${glob} $out/hosted/pub.dartlang.org/glob-2.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${graphs} $out/hosted/pub.dartlang.org/graphs-2.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${hex} $out/hosted/pub.dartlang.org/hex-0.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${http_multi_server} $out/hosted/pub.dartlang.org/http_multi_server-3.2.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${http_parser} $out/hosted/pub.dartlang.org/http_parser-4.0.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${io} $out/hosted/pub.dartlang.org/io-1.0.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${js} $out/hosted/pub.dartlang.org/js-0.6.5
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${json_annotation} $out/hosted/pub.dartlang.org/json_annotation-4.7.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${json_serializable} $out/hosted/pub.dartlang.org/json_serializable-6.5.4
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${logging} $out/hosted/pub.dartlang.org/logging-1.0.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${matcher} $out/hosted/pub.dartlang.org/matcher-0.12.13
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${meta} $out/hosted/pub.dartlang.org/meta-1.8.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${mime} $out/hosted/pub.dartlang.org/mime-1.0.2
mkdir -p $out/hosted/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
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${node_preamble} $out/hosted/pub.dartlang.org/node_preamble-2.0.1
mkdir -p $out/hosted/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
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${package_config} $out/hosted/pub.dartlang.org/package_config-2.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${path} $out/hosted/pub.dartlang.org/path-1.8.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pedantic} $out/hosted/pub.dartlang.org/pedantic-1.11.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${petitparser} $out/hosted/pub.dartlang.org/petitparser-5.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pinenacl} $out/hosted/pub.dartlang.org/pinenacl-0.5.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pool} $out/hosted/pub.dartlang.org/pool-1.5.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pub_semver} $out/hosted/pub.dartlang.org/pub_semver-2.1.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${pubspec_parse} $out/hosted/pub.dartlang.org/pubspec_parse-1.2.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${random_string} $out/hosted/pub.dartlang.org/random_string-2.3.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${saslprep} $out/hosted/pub.dartlang.org/saslprep-1.0.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${shelf} $out/hosted/pub.dartlang.org/shelf-1.4.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${shelf_packages_handler} $out/hosted/pub.dartlang.org/shelf_packages_handler-3.0.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${shelf_static} $out/hosted/pub.dartlang.org/shelf_static-1.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${shelf_web_socket} $out/hosted/pub.dartlang.org/shelf_web_socket-1.0.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_gen} $out/hosted/pub.dartlang.org/source_gen-1.2.6
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_helper} $out/hosted/pub.dartlang.org/source_helper-1.3.3
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_map_stack_trace} $out/hosted/pub.dartlang.org/source_map_stack_trace-2.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_maps} $out/hosted/pub.dartlang.org/source_maps-0.10.11
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${source_span} $out/hosted/pub.dartlang.org/source_span-1.9.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${stack_trace} $out/hosted/pub.dartlang.org/stack_trace-1.11.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${stream_channel} $out/hosted/pub.dartlang.org/stream_channel-2.1.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${stream_transform} $out/hosted/pub.dartlang.org/stream_transform-2.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${string_scanner} $out/hosted/pub.dartlang.org/string_scanner-1.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${synchronized} $out/hosted/pub.dartlang.org/synchronized-3.0.0+2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${term_glyph} $out/hosted/pub.dartlang.org/term_glyph-1.2.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${test} $out/hosted/pub.dartlang.org/test-1.22.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${test_api} $out/hosted/pub.dartlang.org/test_api-0.4.16
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${test_core} $out/hosted/pub.dartlang.org/test_core-0.4.20
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${timing} $out/hosted/pub.dartlang.org/timing-1.0.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${typed_data} $out/hosted/pub.dartlang.org/typed_data-1.3.1
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${unorm_dart} $out/hosted/pub.dartlang.org/unorm_dart-0.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${uuid} $out/hosted/pub.dartlang.org/uuid-3.0.5
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${very_good_analysis} $out/hosted/pub.dartlang.org/very_good_analysis-3.1.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${vm_service} $out/hosted/pub.dartlang.org/vm_service-9.4.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${watcher} $out/hosted/pub.dartlang.org/watcher-1.0.2
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${web_socket_channel} $out/hosted/pub.dartlang.org/web_socket_channel-2.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${webkit_inspection_protocol} $out/hosted/pub.dartlang.org/webkit_inspection_protocol-1.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${xml} $out/hosted/pub.dartlang.org/xml-6.2.0
mkdir -p $out/hosted/pub.dartlang.org
ln -s ${yaml} $out/hosted/pub.dartlang.org/yaml-3.1.1
'';
}

View File

@@ -0,0 +1 @@
pubspec_overrides.yaml

View File

@@ -1,3 +1,58 @@
## 0.3.0
- **BREAKING**: Removed `connectAwaitable` and merged it with `connect`.
- **BREAKING**: Removed `allowPlainAuth` from `ConnectionSettings`. If you don't want to use SASL PLAIN, don't register the negotiator. If you want to only conditionally use SASL PLAIN, extend the `SaslPlainNegotiator` and override its `matchesFeature` method to only call the super method when SASL PLAIN should be used.
- **BREAKING**: The user avatar's `subscribe` and `unsubscribe` no longer subscribe to the `:data` PubSub nodes
- Renamed `ResourceBindingSuccessEvent` to `ResourceBoundEvent`
- **BREAKING**: Removed `isFeatureSupported` from the manager attributes. The managers now all have a method `isFeatureSupported` that works the same
- The `PresenceManager` is now optional
- **BREAKING**: Removed `setConnectionSettings` and `getConnectionSettings`. Just directly acces the `connectionSettings` field.
- Implement XEP-0114 for implementing components
- **BREAKING**: Remove `useDirectTLS` from `ConnectionSettings`
## 0.1.6+1
- **FIX**: Fix LMC not working.
## 0.1.6
- **FEAT**: Implement XEP-0308.
## 0.1.5
- **FEAT**: Message events now contain the stanza error, if available.
## 0.1.4
- **FIX**: Only stanza-id required 'sid:0' support.
- **FEAT**: Implement parsing and sending of retractions.
## 0.1.3+1
- **FIX**: Expose the error classes.
## 0.1.3
- **REFACTOR**: Replace MayFail by Result.
- **FIX**: Remove the old Results API.
- **FEAT**: Rework how the negotiator system works.
## 0.1.2+3
- **FIX**: SASL SCRAM-SHA-{256,512} should now work.
## 0.1.2+2
- **FIX**: Fix reconnections when the connection is awaited.
## 0.1.2+1
- **FIX**: A certificate rejection does not crash the connection.
## 0.1.2
- **FEAT**: Remove Moxxy specific strings.
## 0.1.1 ## 0.1.1
- **REFACTOR**: Move packages into packages/. - **REFACTOR**: Move packages into packages/.

View File

@@ -2,6 +2,24 @@
A pure-Dart XMPP library written for Moxxy. A pure-Dart XMPP library written for Moxxy.
## Usage
Include the following as a dependency in your pubspec file:
```
moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.3.0
```
You can find the documentation [here](https://moxxy.org/developers/docs/moxxmpp/).
## License ## License
See `./LICENSE`. See `./LICENSE`.
## Support
If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/assets/repo/kofi.png" height="36" style="height: 36px; border: 0px;"></img>](https://ko-fi.com/papatutuwawa)

View File

@@ -1 +1 @@
include: ../analysis_options.yaml include: ../../analysis_options.yaml

View File

@@ -1,7 +1,13 @@
library moxxmpp; library moxxmpp;
export 'package:moxxmpp/src/connection.dart'; export 'package:moxxmpp/src/connection.dart';
export 'package:moxxmpp/src/connection_errors.dart';
export 'package:moxxmpp/src/connectivity.dart';
export 'package:moxxmpp/src/errors.dart';
export 'package:moxxmpp/src/events.dart'; export 'package:moxxmpp/src/events.dart';
export 'package:moxxmpp/src/handlers/base.dart';
export 'package:moxxmpp/src/handlers/client.dart';
export 'package:moxxmpp/src/handlers/component.dart';
export 'package:moxxmpp/src/iq.dart'; export 'package:moxxmpp/src/iq.dart';
export 'package:moxxmpp/src/jid.dart'; export 'package:moxxmpp/src/jid.dart';
export 'package:moxxmpp/src/managers/attributes.dart'; export 'package:moxxmpp/src/managers/attributes.dart';
@@ -15,24 +21,27 @@ export 'package:moxxmpp/src/namespaces.dart';
export 'package:moxxmpp/src/negotiators/manager.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/negotiators/resource_binding.dart';
export 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
export 'package:moxxmpp/src/negotiators/sasl/plain.dart';
export 'package:moxxmpp/src/negotiators/sasl/scram.dart';
export 'package:moxxmpp/src/negotiators/starttls.dart';
export 'package:moxxmpp/src/ping.dart'; export 'package:moxxmpp/src/ping.dart';
export 'package:moxxmpp/src/presence.dart'; export 'package:moxxmpp/src/presence.dart';
export 'package:moxxmpp/src/reconnect.dart'; export 'package:moxxmpp/src/reconnect.dart';
export 'package:moxxmpp/src/rfcs/rfc_2782.dart'; export 'package:moxxmpp/src/rfcs/rfc_2782.dart';
export 'package:moxxmpp/src/rfcs/rfc_4790.dart'; export 'package:moxxmpp/src/rfcs/rfc_4790.dart';
export 'package:moxxmpp/src/roster.dart'; export 'package:moxxmpp/src/rfcs/rfc_6120/resource_binding.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/negotiator.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/plain.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/scram.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/starttls.dart';
export 'package:moxxmpp/src/roster/errors.dart';
export 'package:moxxmpp/src/roster/roster.dart';
export 'package:moxxmpp/src/roster/state.dart';
export 'package:moxxmpp/src/settings.dart'; 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/error.dart'; export 'package:moxxmpp/src/types/result.dart';
export 'package:moxxmpp/src/types/resultv2.dart';
export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
export 'package:moxxmpp/src/xeps/staging/fast.dart';
export 'package:moxxmpp/src/xeps/staging/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';
@@ -57,11 +66,13 @@ export 'package:moxxmpp/src/xeps/xep_0203.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';
export 'package:moxxmpp/src/xeps/xep_0308.dart';
export 'package:moxxmpp/src/xeps/xep_0333.dart'; export 'package:moxxmpp/src/xeps/xep_0333.dart';
export 'package:moxxmpp/src/xeps/xep_0334.dart'; export 'package:moxxmpp/src/xeps/xep_0334.dart';
export 'package:moxxmpp/src/xeps/xep_0352.dart'; export 'package:moxxmpp/src/xeps/xep_0352.dart';
export 'package:moxxmpp/src/xeps/xep_0359.dart'; export 'package:moxxmpp/src/xeps/xep_0359.dart';
export 'package:moxxmpp/src/xeps/xep_0363.dart'; export 'package:moxxmpp/src/xeps/xep_0363/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0363/xep_0363.dart';
export 'package:moxxmpp/src/xeps/xep_0380.dart'; export 'package:moxxmpp/src/xeps/xep_0380.dart';
export 'package:moxxmpp/src/xeps/xep_0384/crypto.dart'; export 'package:moxxmpp/src/xeps/xep_0384/crypto.dart';
export 'package:moxxmpp/src/xeps/xep_0384/errors.dart'; export 'package:moxxmpp/src/xeps/xep_0384/errors.dart';
@@ -69,8 +80,16 @@ export 'package:moxxmpp/src/xeps/xep_0384/helpers.dart';
export 'package:moxxmpp/src/xeps/xep_0384/types.dart'; export 'package:moxxmpp/src/xeps/xep_0384/types.dart';
export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart'; export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart';
export 'package:moxxmpp/src/xeps/xep_0385.dart'; export 'package:moxxmpp/src/xeps/xep_0385.dart';
export 'package:moxxmpp/src/xeps/xep_0386.dart';
export 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
export 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
export 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
export 'package:moxxmpp/src/xeps/xep_0414.dart'; export 'package:moxxmpp/src/xeps/xep_0414.dart';
export 'package:moxxmpp/src/xeps/xep_0424.dart';
export 'package:moxxmpp/src/xeps/xep_0444.dart';
export 'package:moxxmpp/src/xeps/xep_0446.dart'; export 'package:moxxmpp/src/xeps/xep_0446.dart';
export 'package:moxxmpp/src/xeps/xep_0447.dart'; 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_0461.dart'; export 'package:moxxmpp/src/xeps/xep_0461.dart';

View File

@@ -0,0 +1,94 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:synchronized/synchronized.dart';
/// A surrogate key for awaiting stanzas.
@immutable
class _StanzaSurrogateKey {
const _StanzaSurrogateKey(this.sentTo, this.id, this.tag);
/// The JID the original stanza was sent to. We expect the result to come from the
/// same JID.
final String sentTo;
/// The ID of the original stanza. We expect the result to have the same ID.
final String id;
/// The tag name of the stanza.
final String tag;
@override
int get hashCode => sentTo.hashCode ^ id.hashCode ^ tag.hashCode;
@override
bool operator ==(Object other) {
return other is _StanzaSurrogateKey &&
other.sentTo == sentTo &&
other.id == id &&
other.tag == tag;
}
}
/// 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.
///
/// That means that when sending ```<iq to="example@some.server.example" id="abc123" />```,
/// the response stanza must be from "example@some.server.example", have id "abc123" and
/// be an iq stanza.
///
/// This class also handles some "edge cases" of RFC 6120, like an empty "from" attribute.
class StanzaAwaiter {
/// The pending stanzas, identified by their surrogate key.
final Map<_StanzaSurrogateKey, Completer<XMLNode>> _pending = {};
/// The critical section for accessing [StanzaAwaiter._pending].
final Lock _lock = Lock();
/// Register a stanza as pending.
/// [to] is the value of the stanza's "to" attribute.
/// [id] is the value of the stanza's "id" attribute.
/// [tag] is the stanza's tag name.
///
/// Returns a future that might resolve to the response to the stanza.
Future<Future<XMLNode>> addPending(String to, String id, String tag) async {
final completer = await _lock.synchronized(() {
final completer = Completer<XMLNode>();
_pending[_StanzaSurrogateKey(to, id, tag)] = completer;
return completer;
});
return completer.future;
}
/// Checks if the stanza [stanza] is being awaited. [bareJid] is the bare JID of
/// the connection.
/// If [stanza] is awaited, resolves the future and returns true. If not, returns
/// false.
Future<bool> onData(XMLNode stanza, JID bareJid) async {
assert(bareJid.isBare(), 'bareJid must be bare');
final id = stanza.attributes['id'] as String?;
if (id == null) return false;
final key = _StanzaSurrogateKey(
// Section 8.1.2.1 § 3 of RFC 6120 says that an empty "from" indicates that the
// attribute is implicitly from our own bare JID.
stanza.attributes['from'] as String? ?? bareJid.toString(),
id,
stanza.tag,
);
return _lock.synchronized(() {
final completer = _pending[key];
if (completer != null) {
_pending.remove(key);
completer.complete(stanza);
return true;
}
return false;
});
}
}

View File

@@ -1,24 +1,76 @@
import 'dart:async'; import 'dart:async';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
import 'package:xml/xml_events.dart'; import 'package:xml/xml_events.dart';
class XmlStreamBuffer extends StreamTransformerBase<String, XMLNode> { /// A result object for XmlStreamBuffer.
abstract class XmlStreamBufferObject {}
XmlStreamBuffer() : _streamController = StreamController(), _decoder = const XmlNodeDecoder(); /// A complete XML element returned by the stream buffer.
final StreamController<XMLNode> _streamController; class XmlStreamBufferElement extends XmlStreamBufferObject {
final XmlNodeDecoder _decoder; XmlStreamBufferElement(this.node);
/// The actual [XMLNode].
final XMLNode node;
}
/// Just the stream header of a new XML stream.
class XmlStreamBufferHeader extends XmlStreamBufferObject {
XmlStreamBufferHeader(this.attributes);
/// The headers of the stream header.
final Map<String, String> attributes;
}
/// A buffer to put between a socket's input and a full XML stream.
class XmlStreamBuffer
extends StreamTransformerBase<String, XmlStreamBufferObject> {
final StreamController<XmlStreamBufferObject> _streamController =
StreamController<XmlStreamBufferObject>();
@override @override
Stream<XMLNode> bind(Stream<String> stream) { Stream<XmlStreamBufferObject> bind(Stream<String> stream) {
stream.toXmlEvents().selectSubtreeEvents((event) { final events = stream.toXmlEvents().asBroadcastStream();
events.transform(
StreamTransformer<List<XmlEvent>, XmlStartElementEvent>.fromHandlers(
handleData: (events, sink) {
for (final event in events) {
if (event is! XmlStartElementEvent) {
continue;
}
if (event.name != 'stream:stream') {
continue;
}
sink.add(event);
}
},
),
).listen((event) {
_streamController.add(
XmlStreamBufferHeader(
Map<String, String>.fromEntries(
event.attributes.map((attr) {
return MapEntry(attr.name, attr.value);
}),
),
),
);
});
events
.selectSubtreeEvents((event) {
return event.qualifiedName != 'stream:stream'; return event.qualifiedName != 'stream:stream';
}).transform(_decoder).listen((nodes) { })
.transform(const XmlNodeDecoder())
.listen((nodes) {
for (final node in nodes) { for (final node in nodes) {
if (node.nodeType == XmlNodeType.ELEMENT) { if (node.nodeType == XmlNodeType.ELEMENT) {
_streamController.add(XMLNode.fromXmlElement(node as XmlElement)); _streamController.add(
XmlStreamBufferElement(
XMLNode.fromXmlElement(node as XmlElement),
),
);
} }
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
/// The reason a call to `XmppConnection.connect` failed.
abstract class XmppConnectionError extends XmppError {}
/// Returned by `XmppConnection.connect` when a negotiator returned an unrecoverable
/// error. Only returned when waitUntilLogin is true.
class NegotiatorReturnedError extends XmppConnectionError {
NegotiatorReturnedError(this.error);
@override
bool isRecoverable() => error.isRecoverable();
/// The error returned by the negotiator.
final NegotiatorError error;
}
class StreamFailureError extends XmppConnectionError {
StreamFailureError(this.error);
@override
bool isRecoverable() => error.isRecoverable();
/// The error that causes a connection failure.
final XmppError error;
}
/// Returned by `XmppConnection.connect` when no connection could
/// be established.
class NoConnectionPossibleError extends XmppConnectionError {
@override
bool isRecoverable() => true;
}
/// Returned if no matching authentication mechanism has been presented
class NoMatchingAuthenticationMechanismAvailableError
extends XmppConnectionError {
@override
bool isRecoverable() => false;
}
/// Returned if no negotiator was picked, even though negotiations are not done
/// yet.
class NoAuthenticatorAvailableError extends XmppConnectionError {
@override
bool isRecoverable() => false;
}
/// Returned by the negotiation handler if unexpected data has been received
class UnexpectedDataError extends XmppConnectionError {
@override
bool isRecoverable() => false;
}
/// Returned by the ComponentToServerNegotiator if the handshake is not successful.
class InvalidHandshakeCredentialsError extends XmppConnectionError {
@override
bool isRecoverable() => false;
}

View File

@@ -0,0 +1,18 @@
/// This manager class is responsible to tell the moxxmpp XmppConnection
/// when a connection can be established or not, regarding the network availability.
abstract class ConnectivityManager {
/// Returns true if a network connection is available. If not, returns false.
Future<bool> hasConnection();
/// Returns a future that resolves once we have a network connection.
Future<void> waitForConnection();
}
/// An implementation of [ConnectivityManager] that is always connected.
class AlwaysConnectedConnectivityManager extends ConnectivityManager {
@override
Future<bool> hasConnection() async => true;
@override
Future<void> waitForConnection() async {}
}

View File

@@ -0,0 +1,37 @@
import 'package:moxxmpp/src/socket.dart';
/// An internal error class
// ignore: one_member_abstracts
abstract class XmppError {
/// Return true if we can recover from the error by attempting a reconnection.
bool isRecoverable();
}
/// Returned if we could not establish a TCP connection
/// to the server.
class NoConnectionError extends XmppError {
@override
bool isRecoverable() => true;
}
/// Returned if a socket error occured
class SocketError extends XmppError {
SocketError(this.event);
final XmppSocketErrorEvent event;
@override
bool isRecoverable() => true;
}
/// Returned if we time out
class TimeoutError extends XmppError {
@override
bool isRecoverable() => true;
}
/// Returned if we received a stream error
class StreamError extends XmppError {
// TODO(PapaTutuWawa): Be more precise
@override
bool isRecoverable() => true;
}

View File

@@ -1,13 +1,18 @@
import 'package:moxxmpp/src/connection.dart'; import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/jid.dart'; 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/stanza.dart'; import 'package:moxxmpp/src/stanza.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_0066.dart';
import 'package:moxxmpp/src/xeps/xep_0085.dart'; import 'package:moxxmpp/src/xeps/xep_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart'; import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0385.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_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart';
@@ -21,6 +26,12 @@ class ConnectionStateChangedEvent extends XmppEvent {
final XmppConnectionState before; final XmppConnectionState before;
final XmppConnectionState state; final XmppConnectionState state;
final bool resumed; final bool resumed;
/// Indicates whether the connection state switched from a not connected state to a
/// connected state.
bool get connectionEstablished =>
before != XmppConnectionState.connected &&
state == XmppConnectionState.connected;
} }
/// Triggered when we encounter a stream error. /// Triggered when we encounter a stream error.
@@ -38,9 +49,6 @@ class AuthenticationFailedEvent extends XmppEvent {
/// Triggered after the SASL authentication has succeeded. /// Triggered after the SASL authentication has succeeded.
class AuthenticationSuccessEvent extends XmppEvent {} class AuthenticationSuccessEvent extends XmppEvent {}
/// Triggered when we want to ping the connection open
class SendPingEvent extends XmppEvent {}
/// Triggered when the stream resumption was successful /// Triggered when the stream resumption was successful
class StreamResumedEvent extends XmppEvent { class StreamResumedEvent extends XmppEvent {
StreamResumedEvent({required this.h}); StreamResumedEvent({required this.h});
@@ -50,6 +58,22 @@ class StreamResumedEvent extends XmppEvent {
/// Triggered when stream resumption failed /// Triggered when stream resumption failed
class StreamResumeFailedEvent extends XmppEvent {} class StreamResumeFailedEvent extends XmppEvent {}
/// Triggered when the roster has been modified
class RosterUpdatedEvent extends XmppEvent {
RosterUpdatedEvent(this.removed, this.modified, this.added);
/// A list of bare JIDs that are removed from the roster
final List<String> removed;
/// A list of XmppRosterItems that are modified. Can be correlated with one's cache
/// using the jid attribute.
final List<XmppRosterItem> modified;
/// A list of XmppRosterItems that are added to the roster.
final List<XmppRosterItem> added;
}
/// Triggered when a message is received
class MessageEvent extends XmppEvent { class MessageEvent extends XmppEvent {
MessageEvent({ MessageEvent({
required this.body, required this.body,
@@ -62,6 +86,7 @@ class MessageEvent extends XmppEvent {
required this.isMarkable, required this.isMarkable,
required this.encrypted, required this.encrypted,
required this.other, required this.other,
this.error,
this.type, this.type,
this.oob, this.oob,
this.sfs, this.sfs,
@@ -71,7 +96,13 @@ class MessageEvent extends XmppEvent {
this.fun, this.fun,
this.funReplacement, this.funReplacement,
this.funCancellation, this.funCancellation,
this.messageRetraction,
this.messageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
}); });
final StanzaError? error;
final String body; final String body;
final JID fromJid; final JID fromJid;
final JID toJid; final JID toJid;
@@ -90,6 +121,11 @@ class MessageEvent extends XmppEvent {
final String? funReplacement; final String? funReplacement;
final String? funCancellation; final String? funCancellation;
final bool encrypted; final bool encrypted;
final MessageRetractionData? messageRetraction;
final String? messageCorrectionId;
final MessageReactions? messageReactions;
final List<MessageProcessingHint>? messageProcessingHints;
final String? stickerPackId;
final Map<String, dynamic> other; final Map<String, dynamic> other;
} }
@@ -124,8 +160,10 @@ class StreamManagementEnabledEvent extends XmppEvent {
} }
/// Triggered when we bound a resource /// Triggered when we bound a resource
class ResourceBindingSuccessEvent extends XmppEvent { class ResourceBoundEvent extends XmppEvent {
ResourceBindingSuccessEvent({ required this.resource }); ResourceBoundEvent(this.resource);
/// The resource that was just bound.
final String resource; final String resource;
} }
@@ -155,7 +193,11 @@ class SubscriptionRequestReceivedEvent extends XmppEvent {
/// Triggered when we receive a new or updated avatar /// Triggered when we receive a new or updated avatar
class AvatarUpdatedEvent extends XmppEvent { class AvatarUpdatedEvent extends XmppEvent {
AvatarUpdatedEvent({ required this.jid, required this.base64, required this.hash }); AvatarUpdatedEvent({
required this.jid,
required this.base64,
required this.hash,
});
final String jid; final String jid;
final String base64; final String base64;
final String hash; final String hash;
@@ -204,3 +246,15 @@ class OmemoDeviceListUpdatedEvent extends XmppEvent {
final JID jid; final JID jid;
final List<int> deviceList; final List<int> deviceList;
} }
/// Triggered when a reconnection is not performed due to a non-recoverable
/// error.
class NonRecoverableErrorEvent extends XmppEvent {
NonRecoverableErrorEvent(this.error);
/// The error in question.
final XmppError error;
}
/// Triggered when the stream negotiations are done.
class StreamNegotiationsDoneEvent extends XmppEvent {}

View File

@@ -0,0 +1,121 @@
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/buffer.dart';
import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/settings.dart';
import 'package:moxxmpp/src/stringxml.dart';
/// A callback for when the [NegotiationsHandler] is done.
typedef NegotiationsDoneCallback = Future<void> Function();
/// A callback for the case that an error occurs while negotiating.
typedef ErrorCallback = Future<void> Function(XmppError);
/// Return true if the current connection is authenticated. If not, return false.
typedef IsAuthenticatedFunction = bool Function();
/// Send a nonza on the stream
typedef SendNonzaFunction = void Function(XMLNode);
/// Returns the connection settings.
typedef GetConnectionSettingsFunction = ConnectionSettings Function();
/// This class implements the stream feature negotiation for XmppConnection.
abstract class NegotiationsHandler {
@protected
late final Logger log;
/// Map of all negotiators registered against the handler.
@protected
final Map<String, XmppFeatureNegotiatorBase> negotiators = {};
/// Function that is called once the negotiator is done with its stream negotiations.
@protected
late final NegotiationsDoneCallback onNegotiationsDone;
/// XmppConnection's handleError method.
@protected
late final ErrorCallback handleError;
/// Returns true if the connection is authenticated. If not, returns false.
@protected
late final IsAuthenticatedFunction isAuthenticated;
/// Send a nonza over the stream.
@protected
late final SendNonzaFunction sendNonza;
/// Get the connection's settings.
@protected
late final GetConnectionSettingsFunction getConnectionSettings;
/// The id included in the last stream header.
@protected
String? streamId;
/// Set the id of the last stream header.
void setStreamHeaderId(String? id) {
streamId = id;
}
/// Returns, if registered, a negotiator with id [id].
T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) =>
negotiators[id] as T?;
/// Register the parameters as the corresponding methods in this class. Also
/// initializes the logger.
void register(
NegotiationsDoneCallback onNegotiationsDone,
ErrorCallback handleError,
IsAuthenticatedFunction isAuthenticated,
SendNonzaFunction sendNonza,
GetConnectionSettingsFunction getConnectionSettings,
) {
this.onNegotiationsDone = onNegotiationsDone;
this.handleError = handleError;
this.isAuthenticated = isAuthenticated;
this.sendNonza = sendNonza;
this.getConnectionSettings = getConnectionSettings;
log = Logger(toString());
}
/// Registers the negotiator [negotiator] against this negotiations handler.
void registerNegotiator(XmppFeatureNegotiatorBase negotiator);
/// Sends the stream header.
void sendStreamHeader();
/// Runs the post-register callback of all negotiators.
Future<void> runPostRegisterCallback() async {
for (final negotiator in negotiators.values) {
await negotiator.postRegisterCallback();
}
}
Future<void> sendEventToNegotiators(XmppEvent event) async {
for (final negotiator in negotiators.values) {
await negotiator.onXmppEvent(event);
}
}
/// Remove [feature] from the stream features we are currently negotiating.
void removeNegotiatingFeature(String feature) {}
/// Resets all registered negotiators and the negotiation handler.
@mustCallSuper
void reset() {
streamId = null;
for (final negotiator in negotiators.values) {
negotiator.reset();
}
}
/// Called whenever the stream buffer outputs a new event [event].
Future<void> negotiate(XmlStreamBufferObject event) async {
if (event is XmlStreamBufferHeader) {
streamId = event.attributes['id'];
}
}
}

View File

@@ -0,0 +1,223 @@
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/buffer.dart';
import 'package:moxxmpp/src/connection_errors.dart';
import 'package:moxxmpp/src/handlers/base.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
/// "Nonza" describing the XMPP stream header of a client-to-server connection.
class ClientStreamHeaderNonza extends XMLNode {
ClientStreamHeaderNonza(JID jid)
: super(
tag: 'stream:stream',
attributes: <String, String>{
'xmlns': stanzaXmlns,
'version': '1.0',
'xmlns:stream': streamXmlns,
'to': jid.domain,
'from': jid.toBare().toString(),
'xml:lang': 'en',
},
closeTag: false,
);
}
/// This class implements the stream feature negotiation for usage in client to server
/// connections.
class ClientToServerNegotiator extends NegotiationsHandler {
ClientToServerNegotiator() : super();
/// Cached list of stream features.
final List<XMLNode> _streamFeatures = List.empty(growable: true);
/// The currently active negotiator.
XmppFeatureNegotiatorBase? _currentNegotiator;
@override
void registerNegotiator(XmppFeatureNegotiatorBase negotiator) {
negotiators[negotiator.id] = negotiator;
}
@override
void reset() {
super.reset();
// Prevent leaking the last active negotiator
_currentNegotiator = null;
}
@override
void removeNegotiatingFeature(String feature) {
_streamFeatures.removeWhere((node) {
return node.attributes['xmlns'] == feature;
});
}
@override
void sendStreamHeader() {
sendNonza(
XMLNode(
tag: 'xml',
attributes: {'version': '1.0'},
closeTag: false,
isDeclaration: true,
children: [
ClientStreamHeaderNonza(getConnectionSettings().jid),
],
),
);
}
/// Returns true if all mandatory features in [features] have been negotiated.
/// Otherwise returns false.
bool _isMandatoryNegotiationDone(List<XMLNode> features) {
return features.every((XMLNode feature) {
return feature.firstTag('required') == null &&
feature.tag != 'mechanisms';
});
}
/// Returns true if we can still negotiate. Returns false if no negotiator is
/// matching and ready.
bool _isNegotiationPossible(List<XMLNode> features) {
return getNextNegotiator(features, log: false) != null;
}
/// Returns the next negotiator that matches [features]. Returns null if none can be
/// picked. If [log] is true, then the list of matching negotiators will be logged.
@visibleForTesting
XmppFeatureNegotiatorBase? getNextNegotiator(
List<XMLNode> features, {
bool log = true,
}) {
final matchingNegotiators =
negotiators.values.where((XmppFeatureNegotiatorBase negotiator) {
return negotiator.state == NegotiatorState.ready &&
negotiator.matchesFeature(features);
}).toList()
..sort((a, b) => b.priority.compareTo(a.priority));
if (log) {
this.log.finest(
'List of matching negotiators: ${matchingNegotiators.map((a) => a.id)}',
);
}
if (matchingNegotiators.isEmpty) return null;
return matchingNegotiators.first;
}
Future<void> _executeCurrentNegotiator(XMLNode nonza) async {
// If we don't have a negotiator, get one
_currentNegotiator ??= getNextNegotiator(_streamFeatures);
if (_currentNegotiator == null &&
_isMandatoryNegotiationDone(_streamFeatures) &&
!_isNegotiationPossible(_streamFeatures)) {
log.finest('Negotiations done!');
await onNegotiationsDone();
return;
}
// If we don't have a next negotiator, we have to bail
if (_currentNegotiator == null &&
!_isMandatoryNegotiationDone(_streamFeatures) &&
!_isNegotiationPossible(_streamFeatures)) {
// We failed before authenticating
if (!isAuthenticated()) {
log.severe('No negotiator could be picked while unauthenticated');
await handleError(NoMatchingAuthenticationMechanismAvailableError());
return;
} else {
log.severe(
'No negotiator could be picked while negotiations are not done',
);
await handleError(NoAuthenticatorAvailableError());
return;
}
}
final result = await _currentNegotiator!.negotiate(nonza);
if (result.isType<NegotiatorError>()) {
log.severe('Negotiator returned an error');
await handleError(result.get<NegotiatorError>());
return;
}
final state = result.get<NegotiatorState>();
_currentNegotiator!.state = state;
switch (state) {
case NegotiatorState.ready:
return;
case NegotiatorState.done:
if (_currentNegotiator!.sendStreamHeaderWhenDone) {
_currentNegotiator = null;
_streamFeatures.clear();
sendStreamHeader();
} else {
removeNegotiatingFeature(_currentNegotiator!.negotiatingXmlns);
_currentNegotiator = null;
if (_isMandatoryNegotiationDone(_streamFeatures) &&
!_isNegotiationPossible(_streamFeatures)) {
log.finest('Negotiations done!');
await onNegotiationsDone();
} else {
_currentNegotiator = getNextNegotiator(_streamFeatures);
log.finest('Chose ${_currentNegotiator!.id} as next negotiator');
final fakeStanza = XMLNode(
tag: 'stream:features',
children: _streamFeatures,
);
await _executeCurrentNegotiator(fakeStanza);
}
}
break;
case NegotiatorState.retryLater:
log.finest('Negotiator wants to continue later. Picking new one...');
_currentNegotiator!.state = NegotiatorState.ready;
if (_isMandatoryNegotiationDone(_streamFeatures) &&
!_isNegotiationPossible(_streamFeatures)) {
log.finest('Negotiations done!');
await onNegotiationsDone();
} else {
log.finest('Picking new negotiator...');
_currentNegotiator = getNextNegotiator(_streamFeatures);
log.finest('Chose $_currentNegotiator as next negotiator');
final fakeStanza = XMLNode(
tag: 'stream:features',
children: _streamFeatures,
);
await _executeCurrentNegotiator(fakeStanza);
}
break;
case NegotiatorState.skipRest:
log.finest(
'Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!',
);
await onNegotiationsDone();
break;
}
}
@override
Future<void> negotiate(XmlStreamBufferObject event) async {
if (event is XmlStreamBufferElement) {
if (event.node.tag == 'stream:features') {
// Store the received stream features
_streamFeatures
..clear()
..addAll(event.node.children);
}
await _executeCurrentNegotiator(event.node);
}
}
}

View File

@@ -0,0 +1,117 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:hex/hex.dart';
import 'package:moxxmpp/src/buffer.dart';
import 'package:moxxmpp/src/connection_errors.dart';
import 'package:moxxmpp/src/handlers/base.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
/// Nonza describing the XMPP stream header.
class ComponentStreamHeaderNonza extends XMLNode {
ComponentStreamHeaderNonza(JID jid)
: assert(jid.isBare(), 'Component JID must be bare'),
super(
tag: 'stream:stream',
attributes: <String, String>{
'xmlns': componentAcceptXmlns,
'xmlns:stream': streamXmlns,
'to': jid.domain,
},
closeTag: false,
);
}
/// The states the ComponentToServerNegotiator can be in.
enum ComponentToServerState {
/// No data has been sent or received yet
idle,
/// Handshake has been sent
handshakeSent,
}
/// The ComponentToServerNegotiator is a NegotiationsHandler that allows writing
/// components that adhere to XEP-0114.
class ComponentToServerNegotiator extends NegotiationsHandler {
ComponentToServerNegotiator();
/// The state the negotiation handler is currently in
ComponentToServerState _state = ComponentToServerState.idle;
@override
void registerNegotiator(XmppFeatureNegotiatorBase negotiator) {}
@override
void sendStreamHeader() {
sendNonza(
XMLNode(
tag: 'xml',
attributes: {'version': '1.0'},
closeTag: false,
isDeclaration: true,
children: [
ComponentStreamHeaderNonza(getConnectionSettings().jid),
],
),
);
}
Future<String> _computeHandshake(String id) async {
final secret = getConnectionSettings().password;
return HEX.encode(
(await Sha1().hash(utf8.encode('$streamId$secret'))).bytes,
);
}
@override
Future<void> negotiate(XmlStreamBufferObject event) async {
switch (_state) {
case ComponentToServerState.idle:
if (event is XmlStreamBufferHeader) {
streamId = event.attributes['id'];
assert(
streamId != null,
'The server must respond with a stream header that contains an id',
);
_state = ComponentToServerState.handshakeSent;
sendNonza(
XMLNode(
tag: 'handshake',
text: await _computeHandshake(streamId!),
),
);
} else {
log.severe('Unexpected data received');
await handleError(UnexpectedDataError());
}
break;
case ComponentToServerState.handshakeSent:
if (event is XmlStreamBufferElement) {
if (event.node.tag == 'handshake' &&
event.node.children.isEmpty &&
event.node.attributes.isEmpty) {
log.info('Successfully authenticated as component');
await onNegotiationsDone();
} else {
log.warning('Handshake failed');
await handleError(InvalidHandshakeCredentialsError());
}
} else {
log.severe('Unexpected data received');
await handleError(UnexpectedDataError());
}
break;
}
}
@override
void reset() {
_state = ComponentToServerState.idle;
super.reset();
}
}

View File

@@ -1,10 +1,31 @@
import 'package:moxxmpp/src/connection.dart'; import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stanza.dart';
bool handleUnhandledStanza(XmppConnection conn, Stanza stanza) { /// Bounce a stanza if it was not handled by any manager. [conn] is the connection object
if (stanza.type != 'error' && stanza.type != 'result') { /// to use for sending the stanza. [data] is the StanzaHandlerData of the unhandled
conn.sendStanza(stanza.errorReply('cancel', 'feature-not-implemented')); /// stanza.
} Future<void> handleUnhandledStanza(
XmppConnection conn,
StanzaHandlerData data,
) async {
if (data.stanza.type != 'error' && data.stanza.type != 'result') {
final stanza = data.stanza.copyWith(
to: data.stanza.from,
from: data.stanza.to,
type: 'error',
children: [
buildErrorElement(
'cancel',
'feature-not-implemented',
),
],
);
return true; await conn.sendStanza(
stanza,
awaitable: false,
forceEncryption: data.encrypted,
);
}
} }

View File

@@ -1,83 +1,79 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
/// Represents a Jabber ID in parsed form.
@immutable @immutable
class JID { class JID {
const JID(this.local, this.domain, this.resource); const JID(this.local, this.domain, this.resource);
/// Parses the string [jid] into a JID instance.
factory JID.fromString(String jid) { factory JID.fromString(String jid) {
// 0: Parsing either the local or domain part // Algorithm taken from here: https://blog.samwhited.com/2021/02/xmpp-addresses/
// 1: Parsing the domain part var localPart = '';
// 2: Parsing the resource var domainPart = '';
var state = 0; var resourcePart = '';
var buffer = '';
var local_ = '';
var domain_ = '';
var resource_ = '';
for (var i = 0; i < jid.length; i++) { final slashParts = jid.split('/');
final c = jid[i]; if (slashParts.length == 1) {
final eol = i == jid.length - 1; resourcePart = '';
switch (state) {
case 0: {
if (c == '@') {
local_ = buffer;
buffer = '';
state = 1;
} else if (c == '/') {
domain_ = buffer;
buffer = '';
state = 2;
} else if (eol) {
domain_ = buffer + c;
} else { } else {
buffer += c; resourcePart = slashParts.sublist(1).join('/');
}
}
break;
case 1: {
if (c == '/') {
domain_ = buffer;
buffer = '';
state = 2;
} else if (eol) {
domain_ = buffer;
if (c != ' ') { assert(
domain_ = domain_ + c; resourcePart.isNotEmpty,
} 'Resource part cannot be there and empty',
} else if (c != ' ') { );
buffer += c;
}
}
break;
case 2: {
if (eol) {
resource_ = buffer;
if (c != ' ') {
resource_ = resource_ + c;
}
} else if (c != ''){
buffer += c;
}
}
}
} }
return JID(local_, domain_, resource_); final atParts = slashParts.first.split('@');
if (atParts.length == 1) {
localPart = '';
domainPart = atParts.first;
} else {
localPart = atParts.first;
domainPart = atParts.sublist(1).join('@');
assert(localPart.isNotEmpty, 'Local part cannot be there and empty');
}
return JID(
localPart,
domainPart.endsWith('.')
? domainPart.substring(0, domainPart.length - 1)
: domainPart,
resourcePart,
);
} }
final String local; final String local;
final String domain; final String domain;
final String resource; final String resource;
/// Returns true if the JID is bare.
bool isBare() => resource.isEmpty; bool isBare() => resource.isEmpty;
/// Returns true if the JID is full.
bool isFull() => resource.isNotEmpty; bool isFull() => resource.isNotEmpty;
JID toBare() => JID(local, domain, ''); /// Converts the JID into a bare JID.
JID toBare() {
if (isBare()) return this;
return JID(local, domain, '');
}
/// 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);
/// 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]
/// is optionally set to true, then [other] MUST be bare. Otherwise, false is returned.
bool bareCompare(JID other, {bool ensureBare = false}) {
if (ensureBare && !other.isBare()) return false;
return local == other.local && domain == other.domain;
}
/// Converts to JID instance into its string representation of
/// localpart@domainpart/resource.
@override @override
String toString() { String toString() {
var result = ''; var result = '';
@@ -97,7 +93,9 @@ class JID {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is JID) { if (other is JID) {
return other.local == local && other.domain == domain && other.resource == resource; return other.local == local &&
other.domain == domain &&
other.resource == resource;
} }
return false; return false;

View File

@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:moxxmpp/src/connection.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';
@@ -11,21 +10,27 @@ import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
class XmppManagerAttributes { class XmppManagerAttributes {
XmppManagerAttributes({ XmppManagerAttributes({
required this.sendStanza, required this.sendStanza,
required this.sendNonza, required this.sendNonza,
required this.getManagerById, required this.getManagerById,
required this.sendEvent, required this.sendEvent,
required this.getConnectionSettings, required this.getConnectionSettings,
required this.isFeatureSupported,
required this.getFullJID, required this.getFullJID,
required this.getSocket, required this.getSocket,
required this.getConnection, required this.getConnection,
required this.getNegotiatorById, required this.getNegotiatorById,
}); });
/// Send a stanza whose response can be awaited. /// Send a stanza whose response can be awaited.
final Future<XMLNode> Function(Stanza stanza, { StanzaFromType addFrom, bool addId, bool awaitable, bool encrypted}) sendStanza; final Future<XMLNode> Function(
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;
@@ -39,9 +44,6 @@ class XmppManagerAttributes {
/// (Maybe) Get a Manager attached to the connection by its Id. /// (Maybe) Get a Manager attached to the connection by its Id.
final T? Function<T extends XmppManagerBase>(String) getManagerById; final T? Function<T extends XmppManagerBase>(String) getManagerById;
/// Returns true if a server feature is supported
final bool Function(String) isFeatureSupported;
/// Returns the full JID of the current account /// Returns the full JID of the current account
final JID Function() getFullJID; final JID Function() getFullJID;
@@ -51,5 +53,6 @@ class XmppManagerAttributes {
/// Return the [XmppConnection] the manager is registered against. /// Return the [XmppConnection] the manager is registered against.
final XmppConnection Function() getConnection; final XmppConnection Function() getConnection;
final T? Function<T extends XmppFeatureNegotiatorBase>(String) getNegotiatorById; final T? Function<T extends XmppFeatureNegotiatorBase>(String)
getNegotiatorById;
} }

View File

@@ -1,17 +1,29 @@
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/managers/attributes.dart'; import 'package:moxxmpp/src/managers/attributes.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/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
abstract class XmppManagerBase { abstract class XmppManagerBase {
XmppManagerBase(this.id);
late final XmppManagerAttributes _managerAttributes; late final XmppManagerAttributes _managerAttributes;
late final Logger _log; late final Logger _log;
/// Flag indicating that the post registration callback has been called once.
bool initialized = false;
/// Registers the callbacks from XmppConnection with the manager /// Registers the callbacks from XmppConnection with the manager
void register(XmppManagerAttributes attributes) { void register(XmppManagerAttributes attributes) {
_managerAttributes = attributes; _managerAttributes = attributes;
_log = Logger(getName()); _log = Logger(name);
} }
/// Returns the attributes that are registered with the manager. /// Returns the attributes that are registered with the manager.
@@ -20,29 +32,65 @@ abstract class XmppManagerBase {
return _managerAttributes; return _managerAttributes;
} }
/// Resolves to true when the server supports the disco feature [xmlns]. Resolves
/// to false when either the disco request fails or the server does not
/// support [xmlns].
/// Note that this function requires a registered DiscoManager.
@protected
Future<bool> isFeatureSupported(String xmlns) async {
final dm = _managerAttributes.getManagerById<DiscoManager>(discoManager);
assert(
dm != null,
'The DiscoManager must be registered for isFeatureSupported to work',
);
final result = await dm!.discoInfoQuery(
_managerAttributes.getConnectionSettings().jid.domain,
shouldEncrypt: false,
);
if (result.isType<DiscoError>()) {
return false;
}
return result.get<DiscoInfo>().features.contains(xmlns);
}
/// Return the StanzaHandlers associated with this manager that deal with stanzas we /// Return the StanzaHandlers associated with this manager that deal with stanzas we
/// send. These are run before the stanza is sent. /// send. These are run before the stanza is sent. The higher the value of the
/// handler's priority, the earlier it is run.
List<StanzaHandler> getOutgoingPreStanzaHandlers() => []; List<StanzaHandler> getOutgoingPreStanzaHandlers() => [];
/// Return the StanzaHandlers associated with this manager that deal with stanzas we /// Return the StanzaHandlers associated with this manager that deal with stanzas we
/// send. These are run after the stanza is sent. /// send. These are run after the stanza is sent. The higher the value of the
/// handler's priority, the earlier it is run.
List<StanzaHandler> getOutgoingPostStanzaHandlers() => []; List<StanzaHandler> getOutgoingPostStanzaHandlers() => [];
/// Return the StanzaHandlers associated with this manager that deal with stanzas we /// Return the StanzaHandlers associated with this manager that deal with stanzas we
/// receive. /// receive. The higher the value of the
/// handler's priority, the earlier it is run.
List<StanzaHandler> getIncomingStanzaHandlers() => []; List<StanzaHandler> getIncomingStanzaHandlers() => [];
/// Return the NonzaHandlers associated with this manager. /// Return the StanzaHandlers associated with this manager that deal with stanza handlers
/// that have to run before the main ones run. This is useful, for example, for OMEMO
/// as we have to decrypt the stanza before we do anything else. The higher the value
/// of the handler's priority, the earlier it is run.
List<StanzaHandler> getIncomingPreStanzaHandlers() => [];
/// Return the NonzaHandlers associated with this manager. The higher the value of the
/// handler's priority, the earlier it is run.
List<NonzaHandler> getNonzaHandlers() => []; List<NonzaHandler> getNonzaHandlers() => [];
/// 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() => [];
/// Return the Id (akin to xmlns) of this manager. /// Return a list of identities that should be included in a disco response.
String getId(); List<Identity> getDiscoIdentities() => [];
/// Return a name that will be used for logging. /// Return the Id (akin to xmlns) of this manager.
String getName(); final String id;
/// The name of the manager.
String get name => toString();
/// Return the logger for this manager. /// Return the logger for this manager.
Logger get logger => _log; Logger get logger => _log;
@@ -53,20 +101,73 @@ abstract class XmppManagerBase {
/// Returns true if the XEP is supported on the server. If not, returns false /// Returns true if the XEP is supported on the server. If not, returns false
Future<bool> isSupported(); Future<bool> isSupported();
/// Called after the registration of all managers against the XmppConnection is done.
/// This method is only called once during the entire lifetime of it.
@mustCallSuper
Future<void> postRegisterCallback() async {
initialized = true;
final disco = getAttributes().getManagerById<DiscoManager>(discoManager);
if (disco != null) {
if (getDiscoFeatures().isNotEmpty) {
disco.addFeatures(getDiscoFeatures());
}
if (getDiscoIdentities().isNotEmpty) {
disco.addIdentities(getDiscoIdentities());
}
}
}
/// Runs all NonzaHandlers of this Manager which match the nonza. Resolves to true if /// Runs all NonzaHandlers of this Manager which match the nonza. Resolves to true if
/// the nonza has been handled by one of the handlers. Resolves to false otherwise. /// the nonza has been handled by one of the handlers. Resolves to false otherwise.
Future<bool> runNonzaHandlers(XMLNode nonza) async { Future<bool> runNonzaHandlers(XMLNode nonza) async {
var handled = false; var handled = false;
await Future.forEach( await Future.forEach(getNonzaHandlers(), (NonzaHandler handler) async {
getNonzaHandlers(),
(NonzaHandler handler) async {
if (handler.matches(nonza)) { if (handler.matches(nonza)) {
handled = true; handled = true;
await handler.callback(nonza); await handler.callback(nonza);
} }
} });
);
return handled; return handled;
} }
/// Returns true, if the current stream negotiations resulted in a new stream. Useful
/// for plugins to reset their cache in case of a new stream.
/// The value only makes sense after receiving a StreamNegotiationsDoneEvent.
Future<bool> isNewStream() async {
final sm =
getAttributes().getManagerById<StreamManagementManager>(smManager);
return sm?.streamResumed == false;
}
/// Sends a reply of the stanza in [data] with [type]. Replaces the original stanza's
/// children with [children].
///
/// Note that this function currently only accepts IQ stanzas.
Future<void> reply(
StanzaHandlerData data,
String type,
List<XMLNode> children,
) async {
assert(
data.stanza.tag == 'iq',
'Reply makes little sense for non-IQ stanzas',
);
final stanza = data.stanza.copyWith(
to: data.stanza.from,
from: data.stanza.to,
type: type,
children: children,
);
await getAttributes().sendStanza(
stanza,
awaitable: false,
forceEncryption: data.encrypted,
);
}
} }

View File

@@ -6,6 +6,8 @@ import 'package:moxxmpp/src/xeps/xep_0203.dart';
import 'package:moxxmpp/src/xeps/xep_0359.dart'; import 'package:moxxmpp/src/xeps/xep_0359.dart';
import 'package:moxxmpp/src/xeps/xep_0380.dart'; import 'package:moxxmpp/src/xeps/xep_0380.dart';
import 'package:moxxmpp/src/xeps/xep_0385.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_0446.dart';
import 'package:moxxmpp/src/xeps/xep_0447.dart'; import 'package:moxxmpp/src/xeps/xep_0447.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart'; import 'package:moxxmpp/src/xeps/xep_0461.dart';
@@ -25,8 +27,7 @@ class StanzaHandlerData with _$StanzaHandlerData {
dynamic cancelReason, dynamic cancelReason,
// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
// necessary, e.g. with Message Carbons or OMEMO // necessary, e.g. with Message Carbons or OMEMO
Stanza stanza, Stanza stanza, {
{
// Whether the stanza is retransmitted. Only useful in the context of outgoing // Whether the stanza is retransmitted. Only useful in the context of outgoing
// stanza handlers. MUST NOT be overwritten. // stanza handlers. MUST NOT be overwritten.
@Default(false) bool retransmitted, @Default(false) bool retransmitted,
@@ -48,6 +49,11 @@ class StanzaHandlerData with _$StanzaHandlerData {
String? funCancellation, String? funCancellation,
// Whether the stanza was received encrypted // Whether the stanza was received encrypted
@Default(false) bool encrypted, @Default(false) 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.
@Default(false) bool forceEncryption,
// The stated type of encryption used, if any was used // The stated type of encryption used, if any was used
ExplicitEncryptionType? encryptionType, ExplicitEncryptionType? encryptionType,
// Delayed Delivery // Delayed Delivery
@@ -55,6 +61,14 @@ class StanzaHandlerData with _$StanzaHandlerData {
// This is for stanza handlers that are not part of the XMPP library but still need // This is for stanza handlers that are not part of the XMPP library but still need
// pass data around. // pass data around.
@Default(<String, dynamic>{}) Map<String, dynamic> other, @Default(<String, dynamic>{}) Map<String, dynamic> other,
} // If non-null, then it indicates the origin Id of the message that should be
) = _StanzaHandlerData; // retracted
MessageRetractionData? messageRetraction,
// If non-null, then the message is a correction for the specified stanza Id
String? lastMessageCorrectionSid,
// Reactions data
MessageReactions? messageReactions,
// The Id of the sticker pack this sticker belongs to
String? stickerPackId,
}) = _StanzaHandlerData;
} }

View File

@@ -48,13 +48,27 @@ mixin _$StanzaHandlerData {
String? get funCancellation => String? get funCancellation =>
throw _privateConstructorUsedError; // Whether the stanza was received encrypted throw _privateConstructorUsedError; // Whether the stanza was received encrypted
bool get 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 throw _privateConstructorUsedError; // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType => ExplicitEncryptionType? get encryptionType =>
throw _privateConstructorUsedError; // Delayed Delivery throw _privateConstructorUsedError; // Delayed Delivery
DelayedDelivery? get delayedDelivery => DelayedDelivery? get delayedDelivery =>
throw _privateConstructorUsedError; // This is for stanza handlers that are not part of the XMPP library but still need throw _privateConstructorUsedError; // This is for stanza handlers that are not part of the XMPP library but still need
// pass data around. // pass data around.
Map<String, dynamic> get other => throw _privateConstructorUsedError; 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) @JsonKey(ignore: true)
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith => $StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
@@ -85,9 +99,14 @@ abstract class $StanzaHandlerDataCopyWith<$Res> {
String? funReplacement, String? funReplacement,
String? funCancellation, String? funCancellation,
bool encrypted, bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType, ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other}); Map<String, dynamic> other,
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
} }
/// @nodoc /// @nodoc
@@ -119,9 +138,14 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
Object? funReplacement = freezed, Object? funReplacement = freezed,
Object? funCancellation = freezed, Object? funCancellation = freezed,
Object? encrypted = freezed, Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed, Object? encryptionType = freezed,
Object? delayedDelivery = freezed, Object? delayedDelivery = freezed,
Object? other = freezed, Object? other = freezed,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
done: done == freezed done: done == freezed
@@ -196,6 +220,10 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
? _value.encrypted ? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable : encrypted // ignore: cast_nullable_to_non_nullable
as bool, as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed encryptionType: encryptionType == freezed
? _value.encryptionType ? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable : encryptionType // ignore: cast_nullable_to_non_nullable
@@ -208,6 +236,22 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
? _value.other ? _value.other
: other // ignore: cast_nullable_to_non_nullable : other // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>, 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?,
)); ));
} }
} }
@@ -238,9 +282,14 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res>
String? funReplacement, String? funReplacement,
String? funCancellation, String? funCancellation,
bool encrypted, bool encrypted,
bool forceEncryption,
ExplicitEncryptionType? encryptionType, ExplicitEncryptionType? encryptionType,
DelayedDelivery? delayedDelivery, DelayedDelivery? delayedDelivery,
Map<String, dynamic> other}); Map<String, dynamic> other,
MessageRetractionData? messageRetraction,
String? lastMessageCorrectionSid,
MessageReactions? messageReactions,
String? stickerPackId});
} }
/// @nodoc /// @nodoc
@@ -274,9 +323,14 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
Object? funReplacement = freezed, Object? funReplacement = freezed,
Object? funCancellation = freezed, Object? funCancellation = freezed,
Object? encrypted = freezed, Object? encrypted = freezed,
Object? forceEncryption = freezed,
Object? encryptionType = freezed, Object? encryptionType = freezed,
Object? delayedDelivery = freezed, Object? delayedDelivery = freezed,
Object? other = freezed, Object? other = freezed,
Object? messageRetraction = freezed,
Object? lastMessageCorrectionSid = freezed,
Object? messageReactions = freezed,
Object? stickerPackId = freezed,
}) { }) {
return _then(_$_StanzaHandlerData( return _then(_$_StanzaHandlerData(
done == freezed done == freezed
@@ -351,6 +405,10 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
? _value.encrypted ? _value.encrypted
: encrypted // ignore: cast_nullable_to_non_nullable : encrypted // ignore: cast_nullable_to_non_nullable
as bool, as bool,
forceEncryption: forceEncryption == freezed
? _value.forceEncryption
: forceEncryption // ignore: cast_nullable_to_non_nullable
as bool,
encryptionType: encryptionType == freezed encryptionType: encryptionType == freezed
? _value.encryptionType ? _value.encryptionType
: encryptionType // ignore: cast_nullable_to_non_nullable : encryptionType // ignore: cast_nullable_to_non_nullable
@@ -363,6 +421,22 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
? _value._other ? _value._other
: other // ignore: cast_nullable_to_non_nullable : other // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>, 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?,
)); ));
} }
} }
@@ -385,9 +459,14 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
this.funReplacement, this.funReplacement,
this.funCancellation, this.funCancellation,
this.encrypted = false, this.encrypted = false,
this.forceEncryption = false,
this.encryptionType, this.encryptionType,
this.delayedDelivery, this.delayedDelivery,
final Map<String, dynamic> other = const <String, dynamic>{}}) final Map<String, dynamic> other = const <String, dynamic>{},
this.messageRetraction,
this.lastMessageCorrectionSid,
this.messageReactions,
this.stickerPackId})
: _other = other; : _other = other;
// Indicates to the runner that processing is now done. This means that all // Indicates to the runner that processing is now done. This means that all
@@ -445,6 +524,13 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
@override @override
@JsonKey() @JsonKey()
final bool encrypted; 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 // The stated type of encryption used, if any was used
@override @override
final ExplicitEncryptionType? encryptionType; final ExplicitEncryptionType? encryptionType;
@@ -463,9 +549,23 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
return EqualUnmodifiableMapView(_other); 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 @override
String toString() { String toString() {
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other)'; 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 @override
@@ -497,11 +597,21 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.funCancellation, funCancellation) && .equals(other.funCancellation, funCancellation) &&
const DeepCollectionEquality().equals(other.encrypted, encrypted) && const DeepCollectionEquality().equals(other.encrypted, encrypted) &&
const DeepCollectionEquality()
.equals(other.forceEncryption, forceEncryption) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.encryptionType, encryptionType) && .equals(other.encryptionType, encryptionType) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.delayedDelivery, delayedDelivery) && .equals(other.delayedDelivery, delayedDelivery) &&
const DeepCollectionEquality().equals(other._other, this._other)); 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 @override
@@ -525,9 +635,14 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
const DeepCollectionEquality().hash(funReplacement), const DeepCollectionEquality().hash(funReplacement),
const DeepCollectionEquality().hash(funCancellation), const DeepCollectionEquality().hash(funCancellation),
const DeepCollectionEquality().hash(encrypted), const DeepCollectionEquality().hash(encrypted),
const DeepCollectionEquality().hash(forceEncryption),
const DeepCollectionEquality().hash(encryptionType), const DeepCollectionEquality().hash(encryptionType),
const DeepCollectionEquality().hash(delayedDelivery), const DeepCollectionEquality().hash(delayedDelivery),
const DeepCollectionEquality().hash(_other) const DeepCollectionEquality().hash(_other),
const DeepCollectionEquality().hash(messageRetraction),
const DeepCollectionEquality().hash(lastMessageCorrectionSid),
const DeepCollectionEquality().hash(messageReactions),
const DeepCollectionEquality().hash(stickerPackId)
]); ]);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@@ -554,9 +669,14 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
final String? funReplacement, final String? funReplacement,
final String? funCancellation, final String? funCancellation,
final bool encrypted, final bool encrypted,
final bool forceEncryption,
final ExplicitEncryptionType? encryptionType, final ExplicitEncryptionType? encryptionType,
final DelayedDelivery? delayedDelivery, final DelayedDelivery? delayedDelivery,
final Map<String, dynamic> other}) = _$_StanzaHandlerData; 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 @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. // pre-processing is done and no other handlers should be consulted.
@@ -599,6 +719,11 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
String? get funCancellation; String? get funCancellation;
@override // Whether the stanza was received encrypted @override // Whether the stanza was received encrypted
bool get 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 @override // The stated type of encryption used, if any was used
ExplicitEncryptionType? get encryptionType; ExplicitEncryptionType? get encryptionType;
@override // Delayed Delivery @override // Delayed Delivery
@@ -606,6 +731,15 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
@override // This is for stanza handlers that are not part of the XMPP library but still need @override // This is for stanza handlers that are not part of the XMPP library but still need
// pass data around. // pass data around.
Map<String, dynamic> get other; 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 @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith => _$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>

View File

@@ -5,7 +5,6 @@ import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
abstract class Handler { abstract class Handler {
const Handler(this.matchStanzas, {this.nonzaTag, this.nonzaXmlns}); const Handler(this.matchStanzas, {this.nonzaTag, this.nonzaXmlns});
final String? nonzaTag; final String? nonzaTag;
final String? nonzaXmlns; final String? nonzaXmlns;
@@ -20,7 +19,8 @@ abstract class Handler {
} }
if (nonzaXmlns != null && nonzaTag != null) { if (nonzaXmlns != null && nonzaTag != null) {
matches = (node.attributes['xmlns'] ?? '') == nonzaXmlns! && node.tag == nonzaTag!; matches = (node.attributes['xmlns'] ?? '') == nonzaXmlns! &&
node.tag == nonzaTag!;
} }
if (matchStanzas && nonzaTag == null) { if (matchStanzas && nonzaTag == null) {
@@ -32,7 +32,6 @@ abstract class Handler {
} }
class NonzaHandler extends Handler { class NonzaHandler extends Handler {
NonzaHandler({ NonzaHandler({
required this.callback, required this.callback,
String? nonzaTag, String? nonzaTag,
@@ -46,7 +45,6 @@ class NonzaHandler extends Handler {
} }
class StanzaHandler extends Handler { class StanzaHandler extends Handler {
StanzaHandler({ StanzaHandler({
required this.callback, required this.callback,
this.tagXmlns, this.tagXmlns,
@@ -78,7 +76,9 @@ class StanzaHandler extends Handler {
} else if (tagXmlns != null) { } else if (tagXmlns != null) {
return listContains( return listContains(
node.children, node.children,
(XMLNode node_) => node_.attributes.containsKey('xmlns') && node_.attributes['xmlns'] == tagXmlns, (XMLNode node_) =>
node_.attributes.containsKey('xmlns') &&
node_.attributes['xmlns'] == tagXmlns,
); );
} }
@@ -90,4 +90,5 @@ class StanzaHandler extends Handler {
} }
} }
int stanzaHandlerSortComparator(StanzaHandler a, StanzaHandler b) => b.priority.compareTo(a.priority); int stanzaHandlerSortComparator(StanzaHandler a, StanzaHandler b) =>
b.priority.compareTo(a.priority);

View File

@@ -1,26 +1,33 @@
const smManager = 'im.moxxy.streammangementmanager'; const smManager = 'org.moxxmpp.streammangementmanager';
const discoManager = 'im.moxxy.discomanager'; const discoManager = 'org.moxxmpp.discomanager';
const messageManager = 'im.moxxy.messagemanager'; const messageManager = 'org.moxxmpp.messagemanager';
const rosterManager = 'im.moxxy.rostermanager'; const rosterManager = 'org.moxxmpp.rostermanager';
const presenceManager = 'im.moxxy.presencemanager'; const presenceManager = 'org.moxxmpp.presencemanager';
const csiManager = 'im.moxxy.csimanager'; const csiManager = 'org.moxxmpp.csimanager';
const carbonsManager = 'im.moxxy.carbonsmanager'; const carbonsManager = 'org.moxxmpp.carbonsmanager';
const vcardManager = 'im.moxxy.vcardmanager'; const vcardManager = 'org.moxxmpp.vcardmanager';
const pubsubManager = 'im.moxxy.pubsubmanager'; const pubsubManager = 'org.moxxmpp.pubsubmanager';
const userAvatarManager = 'im.moxxy.useravatarmanager'; const userAvatarManager = 'org.moxxmpp.useravatarmanager';
const stableIdManager = 'im.moxxy.stableidmanager'; const stableIdManager = 'org.moxxmpp.stableidmanager';
const simsManager = 'im.moxxy.simsmanager'; const simsManager = 'org.moxxmpp.simsmanager';
const messageDeliveryReceiptManager = 'im.moxxy.messagedeliveryreceiptmanager'; const messageDeliveryReceiptManager =
const chatMarkerManager = 'im.moxxy.chatmarkermanager'; 'org.moxxmpp.messagedeliveryreceiptmanager';
const oobManager = 'im.moxxy.oobmanager'; const chatMarkerManager = 'org.moxxmpp.chatmarkermanager';
const sfsManager = 'im.moxxy.sfsmanager'; const oobManager = 'org.moxxmpp.oobmanager';
const messageRepliesManager = 'im.moxxy.messagerepliesmanager'; const sfsManager = 'org.moxxmpp.sfsmanager';
const blockingManager = 'im.moxxy.blockingmanager'; const messageRepliesManager = 'org.moxxmpp.messagerepliesmanager';
const httpFileUploadManager = 'im.moxxy.httpfileuploadmanager'; const blockingManager = 'org.moxxmpp.blockingmanager';
const chatStateManager = 'im.moxxy.chatstatemanager'; const httpFileUploadManager = 'org.moxxmpp.httpfileuploadmanager';
const pingManager = 'im.moxxy.ping'; const chatStateManager = 'org.moxxmpp.chatstatemanager';
const fileUploadNotificationManager = 'im.moxxy.fileuploadnotificationmanager'; const pingManager = 'org.moxxmpp.ping';
const omemoManager = 'org.moxxy.omemomanager'; const fileUploadNotificationManager =
const emeManager = 'org.moxxy.ememanager'; 'org.moxxmpp.fileuploadnotificationmanager';
const cryptographicHashManager = 'org.moxxy.cryptographichashmanager'; const omemoManager = 'org.moxxmpp.omemomanager';
const delayedDeliveryManager = 'org.moxxy.delayeddeliverymanager'; const emeManager = 'org.moxxmpp.ememanager';
const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager';
const delayedDeliveryManager = 'org.moxxmpp.delayeddeliverymanager';
const messageRetractionManager = 'org.moxxmpp.messageretractionmanager';
const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager';
const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager';
const stickersManager = 'org.moxxmpp.stickersmanager';
const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities';

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';
@@ -11,14 +12,23 @@ import 'package:moxxmpp/src/xeps/staging/file_upload_notification.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_0085.dart';
import 'package:moxxmpp/src/xeps/xep_0184.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_0333.dart';
import 'package:moxxmpp/src/xeps/xep_0334.dart';
import 'package:moxxmpp/src/xeps/xep_0359.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_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_0448.dart';
import 'package:moxxmpp/src/xeps/xep_0461.dart';
/// Data used to build a message stanza.
///
/// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be
/// added. This is recommended when sharing files but may cause issues when the message
/// stanza should include a SFS element without any fallbacks.
class MessageDetails { class MessageDetails {
const MessageDetails({ const MessageDetails({
required this.to, required this.to,
this.body, this.body,
@@ -35,6 +45,12 @@ class MessageDetails {
this.funReplacement, this.funReplacement,
this.funCancellation, this.funCancellation,
this.shouldEncrypt = false, this.shouldEncrypt = false,
this.messageRetraction,
this.lastMessageCorrectionId,
this.messageReactions,
this.messageProcessingHints,
this.stickerPackId,
this.setOOBFallbackBody = true,
}); });
final String to; final String to;
final String? body; final String? body;
@@ -51,14 +67,16 @@ class MessageDetails {
final String? funReplacement; final String? funReplacement;
final String? funCancellation; final String? funCancellation;
final bool shouldEncrypt; final bool shouldEncrypt;
final MessageRetractionData? messageRetraction;
final String? lastMessageCorrectionId;
final MessageReactions? messageReactions;
final String? stickerPackId;
final List<MessageProcessingHint>? messageProcessingHints;
final bool setOOBFallbackBody;
} }
class MessageManager extends XmppManagerBase { class MessageManager extends XmppManagerBase {
@override MessageManager() : super(messageManager);
String getId() => messageManager;
@override
String getName() => 'MessageManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -72,11 +90,21 @@ class MessageManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(Stanza _, StanzaHandlerData state) async { Future<StanzaHandlerData> _onMessage(
Stanza _,
StanzaHandlerData state,
) async {
final message = state.stanza; final message = state.stanza;
final body = message.firstTag('body'); final body = message.firstTag('body');
getAttributes().sendEvent(MessageEvent( final hints = List<MessageProcessingHint>.empty(growable: true);
for (final element
in message.findTagsByXmlns(messageProcessingHintsXmlns)) {
hints.add(messageProcessingHintFromXml(element));
}
getAttributes().sendEvent(
MessageEvent(
body: body != null ? body.innerText() : '', body: body != null ? body.innerText() : '',
fromJid: JID.fromString(message.attributes['from']! as String), fromJid: JID.fromString(message.attributes['from']! as String),
toJid: JID.fromString(message.attributes['to']! as String), toJid: JID.fromString(message.attributes['to']! as String),
@@ -95,8 +123,15 @@ class MessageManager extends XmppManagerBase {
funReplacement: state.funReplacement, funReplacement: state.funReplacement,
funCancellation: state.funCancellation, funCancellation: state.funCancellation,
encrypted: state.encrypted, encrypted: state.encrypted,
messageRetraction: state.messageRetraction,
messageCorrectionId: state.lastMessageCorrectionSid,
messageReactions: state.messageReactions,
messageProcessingHints: hints.isEmpty ? null : hints,
stickerPackId: state.stickerPackId,
other: state.other, other: state.other,
),); error: StanzaError.fromStanza(message),
),
);
return state.copyWith(done: true); return state.copyWith(done: true);
} }
@@ -107,6 +142,14 @@ class MessageManager extends XmppManagerBase {
/// element to this id. If originId is non-null, then it will create an "origin-id" /// element to this id. If originId is non-null, then it will create an "origin-id"
/// child in the message stanza and set its id to originId. /// child in the message stanza and set its id to originId.
void sendMessage(MessageDetails details) { void sendMessage(MessageDetails details) {
assert(
implies(
details.quoteBody != null,
details.quoteFrom != null && details.quoteId != null,
),
'When quoting a message, then quoteFrom and quoteId must also be non-null',
);
final stanza = Stanza.message( final stanza = Stanza.message(
to: details.to, to: details.to,
type: 'chat', type: 'chat',
@@ -115,35 +158,30 @@ class MessageManager extends XmppManagerBase {
); );
if (details.quoteBody != null) { if (details.quoteBody != null) {
final fallback = '&gt; ${details.quoteBody!}'; final quote = QuoteData.fromBodies(details.quoteBody!, details.body!);
stanza stanza
..addChild( ..addChild(
XMLNode(tag: 'body', text: '$fallback\n${details.body}'), XMLNode(tag: 'body', text: quote.body),
) )
..addChild( ..addChild(
XMLNode.xmlns( XMLNode.xmlns(
tag: 'reply', tag: 'reply',
xmlns: replyXmlns, xmlns: replyXmlns,
attributes: { attributes: {'to': details.quoteFrom!, 'id': details.quoteId!},
'to': details.quoteFrom!,
'id': details.quoteId!
},
), ),
) )
..addChild( ..addChild(
XMLNode.xmlns( XMLNode.xmlns(
tag: 'fallback', tag: 'fallback',
xmlns: fallbackXmlns, xmlns: fallbackXmlns,
attributes: { attributes: {'for': replyXmlns},
'for': replyXmlns
},
children: [ children: [
XMLNode( XMLNode(
tag: 'body', tag: 'body',
attributes: <String, String>{ attributes: <String, String>{
'start': '0', 'start': '0',
'end': '${fallback.length}' 'end': '${quote.fallbackLength}',
}, },
) )
], ],
@@ -151,7 +189,7 @@ class MessageManager extends XmppManagerBase {
); );
} else { } else {
var body = details.body; var body = details.body;
if (details.sfs != null) { if (details.sfs != null && details.setOOBFallbackBody) {
// TODO(Unknown): Maybe find a better solution // TODO(Unknown): Maybe find a better solution
final firstSource = details.sfs!.sources.first; final firstSource = details.sfs!.sources.first;
if (firstSource is StatelessFileSharingUrlSource) { if (firstSource is StatelessFileSharingUrlSource) {
@@ -159,12 +197,16 @@ class MessageManager extends XmppManagerBase {
} else if (firstSource is StatelessFileSharingEncryptedSource) { } else if (firstSource is StatelessFileSharingEncryptedSource) {
body = firstSource.source.url; body = firstSource.source.url;
} }
} else if (details.messageRetraction?.fallback != null) {
body = details.messageRetraction!.fallback;
} }
if (body != null) {
stanza.addChild( stanza.addChild(
XMLNode(tag: 'body', text: body), XMLNode(tag: 'body', text: body),
); );
} }
}
if (details.requestDeliveryReceipt) { if (details.requestDeliveryReceipt) {
stanza.addChild(makeMessageDeliveryRequest()); stanza.addChild(makeMessageDeliveryRequest());
@@ -180,7 +222,8 @@ class MessageManager extends XmppManagerBase {
stanza.addChild(details.sfs!.toXML()); stanza.addChild(details.sfs!.toXML());
final source = details.sfs!.sources.first; final source = details.sfs!.sources.first;
if (source is StatelessFileSharingUrlSource) { if (source is StatelessFileSharingUrlSource &&
details.setOOBFallbackBody) {
// SFS recommends OOB as a fallback // SFS recommends OOB as a fallback
stanza.addChild(constructOOBNode(OOBData(url: source.url))); stanza.addChild(constructOOBNode(OOBData(url: source.url)));
} }
@@ -189,7 +232,10 @@ class MessageManager extends XmppManagerBase {
if (details.chatState != null) { if (details.chatState != null) {
stanza.addChild( stanza.addChild(
// TODO(Unknown): Move this into xep_0085.dart // TODO(Unknown): Move this into xep_0085.dart
XMLNode.xmlns(tag: chatStateToString(details.chatState!), xmlns: chatStateXmlns), XMLNode.xmlns(
tag: chatStateToString(details.chatState!),
xmlns: chatStateXmlns,
),
); );
} }
@@ -217,6 +263,63 @@ class MessageManager extends XmppManagerBase {
); );
} }
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); getAttributes().sendStanza(stanza, awaitable: false);
} }
} }

View File

@@ -28,9 +28,11 @@ const vCardTempUpdate = 'vcard-temp:x:update';
const pubsubXmlns = 'http://jabber.org/protocol/pubsub'; const pubsubXmlns = 'http://jabber.org/protocol/pubsub';
const pubsubEventXmlns = 'http://jabber.org/protocol/pubsub#event'; const pubsubEventXmlns = 'http://jabber.org/protocol/pubsub#event';
const pubsubOwnerXmlns = 'http://jabber.org/protocol/pubsub#owner'; const pubsubOwnerXmlns = 'http://jabber.org/protocol/pubsub#owner';
const pubsubPublishOptionsXmlns = 'http://jabber.org/protocol/pubsub#publish-options'; const pubsubPublishOptionsXmlns =
'http://jabber.org/protocol/pubsub#publish-options';
const pubsubNodeConfigMax = 'http://jabber.org/protocol/pubsub#config-node-max'; const pubsubNodeConfigMax = 'http://jabber.org/protocol/pubsub#config-node-max';
const pubsubNodeConfigMultiItems = 'http://jabber.org/protocol/pubsub#multi-items'; const pubsubNodeConfigMultiItems =
'http://jabber.org/protocol/pubsub#multi-items';
// XEP-0066 // XEP-0066
const oobDataXmlns = 'jabber:x:oob'; const oobDataXmlns = 'jabber:x:oob';
@@ -42,6 +44,9 @@ const userAvatarMetadataXmlns = 'urn:xmpp:avatar:metadata';
// XEP-0085 // XEP-0085
const chatStateXmlns = 'http://jabber.org/protocol/chatstates'; const chatStateXmlns = 'http://jabber.org/protocol/chatstates';
// XEP-0114
const componentAcceptXmlns = 'jabber:component:accept';
// XEP-0115 // XEP-0115
const capsXmlns = 'http://jabber.org/protocol/caps'; const capsXmlns = 'http://jabber.org/protocol/caps';
@@ -76,6 +81,9 @@ const hashSha3512 = 'sha3-512';
const hashBlake2b256 = 'blake2b-256'; const hashBlake2b256 = 'blake2b-256';
const hashBlake2b512 = 'blake2b-512'; const hashBlake2b512 = 'blake2b-512';
// XEP-0308
const lmcXmlns = 'urn:xmpp:message-correct:0';
// XEP-0333 // XEP-0333
const chatMarkersXmlns = 'urn:xmpp:chat-markers:0'; const chatMarkersXmlns = 'urn:xmpp:chat-markers:0';
@@ -111,9 +119,27 @@ const omemoBundlesXmlns = 'urn:xmpp:omemo:2:bundles';
// XEP-0385 // XEP-0385
const simsXmlns = 'urn:xmpp:sims:1'; const simsXmlns = 'urn:xmpp:sims:1';
// XEP-0386
const bind2Xmlns = 'urn:xmpp:bind:0';
// XEP-0388
const sasl2Xmlns = 'urn:xmpp:sasl:2';
// XEP-0420 // XEP-0420
const sceXmlns = 'urn:xmpp:sce:1'; const sceXmlns = 'urn:xmpp:sce:1';
// XEP-0422
const fasteningXmlns = 'urn:xmpp:fasten:0';
// XEP-0424
const messageRetractionXmlns = 'urn:xmpp:message-retract:0';
// XEP-0428
const fallbackIndicationXmlns = 'urn:xmpp:fallback:0';
// XEP-0444
const messageReactionsXmlns = 'urn:xmpp:reactions:0';
// XEP-0446 // XEP-0446
const fileMetadataXmlns = 'urn:xmpp:file:metadata:0'; const fileMetadataXmlns = 'urn:xmpp:file:metadata:0';
@@ -122,13 +148,21 @@ const sfsXmlns = 'urn:xmpp:sfs:0';
// XEP-0448 // XEP-0448
const sfsEncryptionXmlns = 'urn:xmpp:esfs:0'; const sfsEncryptionXmlns = 'urn:xmpp:esfs:0';
const sfsEncryptionAes128GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-128-gcm-nopadding:0'; const sfsEncryptionAes128GcmNoPaddingXmlns =
const sfsEncryptionAes256GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-256-gcm-nopadding:0'; 'urn:xmpp:ciphers:aes-128-gcm-nopadding:0';
const sfsEncryptionAes256GcmNoPaddingXmlns =
'urn:xmpp:ciphers:aes-256-gcm-nopadding:0';
const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0'; const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0';
// XEP-0449
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 fallbackXmlns = 'urn:xmpp:feature-fallback:0';
// ??? // ???
const urlDataXmlns = 'http://jabber.org/protocol/url-data'; const urlDataXmlns = 'http://jabber.org/protocol/url-data';
// XEP-XXXX
const fastXmlns = 'urn:xmpp:fast:0';

View File

@@ -1,9 +1,13 @@
const saslPlainNegotiator = 'im.moxxy.sasl.plain'; const saslPlainNegotiator = 'im.moxxmpp.sasl.plain';
const saslScramSha1Negotiator = 'im.moxxy.sasl.scram.sha1'; const saslScramSha1Negotiator = 'im.moxxmpp.sasl.scram.sha1';
const saslScramSha256Negotiator = 'im.moxxy.sasl.scram.sha256'; const saslScramSha256Negotiator = 'im.moxxmpp.sasl.scram.sha256';
const saslScramSha512Negotiator = 'im.moxxy.sasl.scram.sha512'; const saslScramSha512Negotiator = 'im.moxxmpp.sasl.scram.sha512';
const csiNegotiator = 'im.moxxy.xeps.csi'; const csiNegotiator = 'im.moxxmpp.xeps.csi';
const rosterNegotiator = 'im.moxxy.core.roster'; const rosterNegotiator = 'im.moxxmpp.core.roster';
const resourceBindingNegotiator = 'im.moxxy.core.resource'; const resourceBindingNegotiator = 'im.moxxmpp.core.resource';
const streamManagementNegotiator = 'im.moxxy.xeps.sm'; const streamManagementNegotiator = 'im.moxxmpp.xeps.sm';
const startTlsNegotiator = 'im.moxxy.core.starttls'; const startTlsNegotiator = 'im.moxxmpp.core.starttls';
const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2';
const bind2Negotiator = 'org.moxxmpp.bind2';
const saslFASTNegotiator = 'org.moxxmpp.sasl.fast';
const carbonsNegotiator = 'org.moxxmpp.bind2.carbons';

View File

@@ -1,10 +1,14 @@
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/errors.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/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 {
@@ -14,17 +18,18 @@ enum NegotiatorState {
done, done,
// Cancel the current attempt but we are not done // Cancel the current attempt but we are not done
retryLater, retryLater,
// The negotiator is in an error state
error,
// Skip the rest of the negotiation and assume the stream ready. Only use this when // Skip the rest of the negotiation and assume the stream ready. Only use this when
// using stream restoration XEPs, like Stream Management. // using stream restoration XEPs, like Stream Management.
skipRest, skipRest,
} }
class NegotiatorAttributes { /// A base class for all errors that may occur during feature negotiation
abstract class NegotiatorError extends XmppError {}
class NegotiatorAttributes {
const NegotiatorAttributes( const NegotiatorAttributes(
this.sendNonza, this.sendNonza,
this.getConnection,
this.getConnectionSettings, this.getConnectionSettings,
this.sendEvent, this.sendEvent,
this.getNegotiatorById, this.getNegotiatorById,
@@ -32,29 +37,59 @@ class NegotiatorAttributes {
this.getFullJID, this.getFullJID,
this.getSocket, this.getSocket,
this.isAuthenticated, this.isAuthenticated,
this.setAuthenticated,
this.setResource,
this.removeNegotiatingFeature,
); );
/// Sends the nonza nonza and optionally redacts it in logs if redact is not null. /// Sends the nonza nonza and optionally redacts it in logs if redact is not null.
final void Function(XMLNode nonza, {String? redact}) sendNonza; final void Function(XMLNode nonza) sendNonza;
/// Returns the connection settings. /// Returns the connection settings.
final ConnectionSettings Function() getConnectionSettings; final ConnectionSettings Function() getConnectionSettings;
/// Send an event event to the connection's event bus
/// Returns the connection object.
final XmppConnection Function() getConnection;
/// Send an event event to the connection's event bus.
final Future<void> Function(XmppEvent event) sendEvent; final Future<void> Function(XmppEvent event) sendEvent;
/// Returns the negotiator with id id of the connection or null. /// Returns the negotiator with id id of the connection or null.
final T? Function<T extends XmppFeatureNegotiatorBase>(String) getNegotiatorById; final T? Function<T extends XmppFeatureNegotiatorBase>(String)
getNegotiatorById;
/// Returns the manager with id id of the connection or null. /// Returns the manager with id id of the connection or null.
final T? Function<T extends XmppManagerBase>(String) getManagerById; final T? Function<T extends XmppManagerBase>(String) getManagerById;
/// Returns the full JID of the current account /// Returns the full JID of the current account
final JID Function() getFullJID; final JID Function() getFullJID;
/// Returns the socket the negotiator is attached to /// Returns the socket the negotiator is attached to
final BaseSocketWrapper Function() getSocket; final BaseSocketWrapper Function() getSocket;
/// Returns true if the stream is authenticated. Returns false if not. /// Returns true if the stream is authenticated. Returns false if not.
final bool Function() isAuthenticated; final bool Function() isAuthenticated;
/// Sets the resource of the connection. If triggerEvent is true, then a
/// [ResourceBoundEvent] is triggered.
final void Function(String, {bool triggerEvent}) setResource;
/// Sets the authentication state of the connection to true.
final void Function() setAuthenticated;
/// Remove a stream feature from our internal cache. This is useful for when you
/// negotiated a feature for another negotiator, like SASL2.
final void Function(String) removeNegotiatingFeature;
} }
abstract class XmppFeatureNegotiatorBase { abstract class XmppFeatureNegotiatorBase {
XmppFeatureNegotiatorBase(
this.priority,
this.sendStreamHeaderWhenDone,
this.negotiatingXmlns,
this.id,
) : state = NegotiatorState.ready;
XmppFeatureNegotiatorBase(this.priority, this.sendStreamHeaderWhenDone, this.negotiatingXmlns, this.id)
: state = NegotiatorState.ready;
/// The priority regarding other negotiators. The higher, the earlier will the /// The priority regarding other negotiators. The higher, the earlier will the
/// negotiator be used /// negotiator be used
final int priority; final int priority;
@@ -85,9 +120,13 @@ abstract class XmppFeatureNegotiatorBase {
return firstWhereOrNull( return firstWhereOrNull(
features, features,
(XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns, (XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns,
) != null; ) !=
null;
} }
/// Called when an event is triggered in the [XmppConnection].
Future<void> onXmppEvent(XmppEvent event) async {}
/// Called with the currently received nonza [nonza] when the negotiator is active. /// Called with the currently received nonza [nonza] when the negotiator is active.
/// If the negotiator is just elected to be the next one, then [nonza] is equal to /// If the negotiator is just elected to be the next one, then [nonza] is equal to
/// the <stream:features /> nonza. /// the <stream:features /> nonza.
@@ -97,12 +136,17 @@ abstract class XmppFeatureNegotiatorBase {
/// must switch some internal state to prevent getting matched immediately again. /// must switch some internal state to prevent getting matched immediately again.
/// If ready is returned, then the negotiator indicates that it is not done with /// If ready is returned, then the negotiator indicates that it is not done with
/// negotiation. /// negotiation.
Future<void> negotiate(XMLNode nonza); Future<Result<NegotiatorState, NegotiatorError>> negotiate(XMLNode nonza);
/// Reset the negotiator to a state that negotation can happen again. /// Reset the negotiator to a state that negotation can happen again.
void reset() { void reset() {
state = NegotiatorState.ready; state = NegotiatorState.ready;
} }
@protected
NegotiatorAttributes get attributes => _attributes; NegotiatorAttributes get attributes => _attributes;
/// Run after all negotiators are registered. Useful for registering callbacks against
/// other negotiators. By default this function does nothing.
Future<void> postRegisterCallback() async {}
} }

View File

@@ -1,66 +0,0 @@
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:uuid/uuid.dart';
class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
ResourceBindingNegotiator() : _requestSent = false, super(0, false, bindXmlns, resourceBindingNegotiator);
bool _requestSent;
@override
bool matchesFeature(List<XMLNode> features) {
final sm = attributes.getManagerById<StreamManagementManager>(smManager);
if (sm != null) {
return super.matchesFeature(features) && !sm.streamResumed && attributes.isAuthenticated();
}
return super.matchesFeature(features) && attributes.isAuthenticated();
}
@override
Future<void> negotiate(XMLNode nonza) async {
if (!_requestSent) {
final stanza = XMLNode.xmlns(
tag: 'iq',
xmlns: stanzaXmlns,
attributes: {
'type': 'set',
'id': const Uuid().v4(),
},
children: [
XMLNode.xmlns(
tag: 'bind',
xmlns: bindXmlns,
),
],
);
_requestSent = true;
attributes.sendNonza(stanza);
} else {
if (nonza.tag != 'iq' || nonza.attributes['type'] != 'result') {
state = NegotiatorState.error;
return;
}
final bind = nonza.firstTag('bind')!;
final jid = bind.firstTag('jid')!;
final resource = jid.innerText().split('/')[1];
await attributes.sendEvent(ResourceBindingSuccessEvent(resource: resource));
state = NegotiatorState.done;
}
}
@override
void reset() {
_requestSent = false;
super.reset();
}
}

View File

@@ -1,46 +0,0 @@
enum ParserState {
variableName,
variableValue
}
/// Parse a string like "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL" into
/// { "n": "user", "r": "fyko+d2lbbFgONRv9qkxdawL"}.
Map<String, String> parseKeyValue(String keyValueString) {
var state = ParserState.variableName;
var name = '';
var value = '';
final values = <String, String>{};
for (var i = 0; i < keyValueString.length; i++) {
final char = keyValueString[i];
switch (state) {
case ParserState.variableName: {
if (char == '=') {
state = ParserState.variableValue;
} else if (char == ',') {
name = '';
} else {
name += char;
}
}
break;
case ParserState.variableValue: {
if (char == ',' || i == keyValueString.length - 1) {
if (char != ',') {
value += char;
}
values[name] = value;
value = '';
name = '';
state = ParserState.variableName;
} else {
value += char;
}
}
break;
}
}
return values;
}

View File

@@ -1,13 +0,0 @@
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart';
class SaslAuthNonza extends XMLNode {
SaslAuthNonza(String mechanism, String body) : super(
tag: 'auth',
attributes: <String, String>{
'xmlns': saslXmlns,
'mechanism': mechanism ,
},
text: body,
);
}

View File

@@ -1,73 +0,0 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
import 'package:moxxmpp/src/stringxml.dart';
class SaslPlainAuthNonza extends SaslAuthNonza {
SaslPlainAuthNonza(String username, String password) : super(
'PLAIN', base64.encode(utf8.encode('\u0000$username\u0000$password')),
);
}
class SaslPlainNegotiator extends SaslNegotiator {
SaslPlainNegotiator()
: _authSent = false,
_log = Logger('SaslPlainNegotiator'),
super(0, saslPlainNegotiator, 'PLAIN');
bool _authSent;
final Logger _log;
@override
bool matchesFeature(List<XMLNode> features) {
if (!attributes.getConnectionSettings().allowPlainAuth) return false;
if (super.matchesFeature(features)) {
if (!attributes.getSocket().isSecure()) {
_log.warning('Refusing to match SASL feature due to unsecured connection');
return false;
}
return true;
}
return false;
}
@override
Future<void> negotiate(XMLNode nonza) async {
if (!_authSent) {
final settings = attributes.getConnectionSettings();
attributes.sendNonza(
SaslPlainAuthNonza(settings.jid.local, settings.password),
redact: SaslPlainAuthNonza('******', '******').toXml(),
);
_authSent = true;
} else {
final tag = nonza.tag;
if (tag == 'success') {
await attributes.sendEvent(AuthenticationSuccessEvent());
state = NegotiatorState.done;
} else {
// We assume it's a <failure/>
final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error));
state = NegotiatorState.error;
}
}
}
@override
void reset() {
_authSent = false;
super.reset();
}
}

View File

@@ -1,259 +0,0 @@
import 'dart:convert';
import 'dart:math' show Random;
import 'package:cryptography/cryptography.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/kv.dart';
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:random_string/random_string.dart';
import 'package:saslprep/saslprep.dart';
// NOTE: Inspired by https://github.com/vukoye/xmpp_dart/blob/3b1a0588562b9e591488c99d834088391840911d/lib/src/features/sasl/ScramSaslHandler.dart
enum ScramHashType {
sha1,
sha256,
sha512
}
HashAlgorithm hashFromType(ScramHashType type) {
switch (type) {
case ScramHashType.sha1: return Sha1();
case ScramHashType.sha256: return Sha256();
case ScramHashType.sha512: return Sha512();
}
}
const scramSha1Mechanism = 'SCRAM-SHA-1';
const scramSha256Mechanism = 'SCRAM-SHA-256';
const scramSha512Mechanism = 'SCRAM-SHA-512';
String mechanismNameFromType(ScramHashType type) {
switch (type) {
case ScramHashType.sha1: return scramSha1Mechanism;
case ScramHashType.sha256: return scramSha256Mechanism;
case ScramHashType.sha512: return scramSha512Mechanism;
}
}
String namespaceFromType(ScramHashType type) {
switch (type) {
case ScramHashType.sha1: return saslScramSha1Negotiator;
case ScramHashType.sha256: return saslScramSha256Negotiator;
case ScramHashType.sha512: return saslScramSha512Negotiator;
}
}
class SaslScramAuthNonza extends SaslAuthNonza {
// This subclassing makes less sense here, but this is since the auth nonza here
// requires knowledge of the inner state of the Negotiator.
SaslScramAuthNonza({ required ScramHashType type, required String body }) : super(
mechanismNameFromType(type), body,
);
}
class SaslScramResponseNonza extends XMLNode {
SaslScramResponseNonza({ required String body }) : super(
tag: 'response',
attributes: <String, String>{
'xmlns': saslXmlns,
},
text: body,
);
}
enum ScramState {
preSent,
initialMessageSent,
challengeResponseSent,
error
}
const gs2Header = 'n,,';
class SaslScramNegotiator extends SaslNegotiator {
// NOTE: NEVER, and I mean, NEVER set clientNonce or initalMessageNoGS2. They are just there for testing
SaslScramNegotiator(
int priority,
this.initialMessageNoGS2,
this.clientNonce,
this.hashType,
) :
_hash = hashFromType(hashType),
_serverSignature = '',
_scramState = ScramState.preSent,
_log = Logger('SaslScramNegotiator(${mechanismNameFromType(hashType)})'),
super(priority, namespaceFromType(hashType), mechanismNameFromType(hashType));
String? clientNonce;
String initialMessageNoGS2;
final ScramHashType hashType;
final HashAlgorithm _hash;
String _serverSignature;
// The internal state for performing the negotiation
ScramState _scramState;
final Logger _log;
Future<List<int>> calculateSaltedPassword(String salt, int iterations) async {
final pbkdf2 = Pbkdf2(
macAlgorithm: Hmac(_hash),
iterations: iterations,
bits: 160, // NOTE: RFC says 20 octets => 20 octets * 8 bits/octet
);
final saltedPasswordRaw = await pbkdf2.deriveKey(
secretKey: SecretKey(
utf8.encode(Saslprep.saslprep(attributes.getConnectionSettings().password)),
),
nonce: base64.decode(salt),
);
return saltedPasswordRaw.extractBytes();
}
Future<List<int>> calculateClientKey(List<int> saltedPassword) async {
return (await Hmac(_hash).calculateMac(
utf8.encode('Client Key'), secretKey: SecretKey(saltedPassword),
)).bytes;
}
Future<List<int>> calculateClientSignature(String authMessage, List<int> storedKey) async {
return (await Hmac(_hash).calculateMac(
utf8.encode(authMessage),
secretKey: SecretKey(storedKey),
)).bytes;
}
Future<List<int>> calculateServerKey(List<int> saltedPassword) async {
return (await Hmac(_hash).calculateMac(
utf8.encode('Server Key'),
secretKey: SecretKey(saltedPassword),
)).bytes;
}
Future<List<int>> calculateServerSignature(String authMessage, List<int> serverKey) async {
return (await Hmac(_hash).calculateMac(
utf8.encode(authMessage),
secretKey: SecretKey(serverKey),
)).bytes;
}
List<int> calculateClientProof(List<int> clientKey, List<int> clientSignature) {
final clientProof = List<int>.filled(clientKey.length, 0);
for (var i = 0; i < clientKey.length; i++) {
clientProof[i] = clientKey[i] ^ clientSignature[i];
}
return clientProof;
}
Future<String> calculateChallengeResponse(String base64Challenge) async {
final challengeString = utf8.decode(base64.decode(base64Challenge));
final challenge = parseKeyValue(challengeString);
final clientFinalMessageBare = 'c=biws,r=${challenge['r']!}';
final saltedPassword = await calculateSaltedPassword(challenge['s']!, int.parse(challenge['i']!));
final clientKey = await calculateClientKey(saltedPassword);
final storedKey = (await _hash.hash(clientKey)).bytes;
final authMessage = '$initialMessageNoGS2,$challengeString,$clientFinalMessageBare';
final clientSignature = await calculateClientSignature(authMessage, storedKey);
final clientProof = calculateClientProof(clientKey, clientSignature);
final serverKey = await calculateServerKey(saltedPassword);
_serverSignature = base64.encode(await calculateServerSignature(authMessage, serverKey));
return '$clientFinalMessageBare,p=${base64.encode(clientProof)}';
}
@override
bool matchesFeature(List<XMLNode> features) {
if (super.matchesFeature(features)) {
if (!attributes.getSocket().isSecure()) {
_log.warning('Refusing to match SASL feature due to unsecured connection');
return false;
}
return true;
}
return false;
}
@override
Future<void> negotiate(XMLNode nonza) async {
switch (_scramState) {
case ScramState.preSent:
if (clientNonce == null || clientNonce == '') {
clientNonce = randomAlphaNumeric(40, provider: CoreRandomProvider.from(Random.secure()));
}
initialMessageNoGS2 = 'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce';
_scramState = ScramState.initialMessageSent;
attributes.sendNonza(
SaslScramAuthNonza(body: base64.encode(utf8.encode(gs2Header + initialMessageNoGS2)), type: hashType),
redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(),
);
break;
case ScramState.initialMessageSent:
if (nonza.tag != 'challenge') {
final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error));
state = NegotiatorState.error;
_scramState = ScramState.error;
return;
}
final challengeBase64 = nonza.innerText();
final response = await calculateChallengeResponse(challengeBase64);
final responseBase64 = base64.encode(utf8.encode(response));
_scramState = ScramState.challengeResponseSent;
attributes.sendNonza(
SaslScramResponseNonza(body: responseBase64),
redact: SaslScramResponseNonza(body: '******').toXml(),
);
return;
case ScramState.challengeResponseSent:
if (nonza.tag != 'success') {
// We assume it's a <failure />
final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error;
state = NegotiatorState.error;
return;
}
// NOTE: This assumes that the string is always "v=..." and contains no other parameters
final signature = parseKeyValue(utf8.decode(base64.decode(nonza.innerText())));
if (signature['v']! != _serverSignature) {
// TODO(Unknown): Notify of a signature mismatch
//final error = nonza.children.first.tag;
//attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error;
state = NegotiatorState.error;
return;
}
await attributes.sendEvent(AuthenticationSuccessEvent());
state = NegotiatorState.done;
return;
case ScramState.error:
state = NegotiatorState.error;
return;
}
}
@override
void reset() {
_scramState = ScramState.preSent;
super.reset();
}
}

View File

@@ -1,65 +0,0 @@
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
enum _StartTlsState {
ready,
requested
}
class StartTLSNonza extends XMLNode {
StartTLSNonza() : super.xmlns(
tag: 'starttls',
xmlns: startTlsXmlns,
);
}
class StartTlsNegotiator extends XmppFeatureNegotiatorBase {
StartTlsNegotiator()
: _state = _StartTlsState.ready,
_log = Logger('StartTlsNegotiator'),
super(10, true, startTlsXmlns, startTlsNegotiator);
_StartTlsState _state;
final Logger _log;
@override
Future<void> negotiate(XMLNode nonza) async {
switch (_state) {
case _StartTlsState.ready:
_log.fine('StartTLS is available. Performing StartTLS upgrade...');
_state = _StartTlsState.requested;
attributes.sendNonza(StartTLSNonza());
break;
case _StartTlsState.requested:
if (nonza.tag != 'proceed' || nonza.attributes['xmlns'] != startTlsXmlns) {
_log.severe('Failed to perform StartTLS negotiation');
state = NegotiatorState.error;
return;
}
_log.fine('Securing socket');
final result = await attributes.getSocket()
.secure(attributes.getConnectionSettings().jid.domain);
if (!result) {
_log.severe('Failed to secure stream');
state = NegotiatorState.error;
return;
}
_log.fine('Stream is now TLS secured');
state = NegotiatorState.done;
break;
}
}
@override
void reset() {
_state = _StartTlsState.ready;
super.reset();
}
}

View File

@@ -1,26 +1,62 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.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/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:synchronized/synchronized.dart';
/// This manager class is responsible to sending periodic pings, if required, using
/// either whitespaces or Stream Management. Keep in mind, that without
/// Stream Management, a stale connection cannot be detected.
class PingManager extends XmppManagerBase { class PingManager extends XmppManagerBase {
@override PingManager(this._pingDuration) : super(pingManager);
String getId() => pingManager;
@override /// The time between pings, when connected.
String getName() => 'PingManager'; final Duration _pingDuration;
/// The actual timer.
Timer? _pingTimer;
final Lock _timerLock = Lock();
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
void _logWarning() { void _logWarning() {
logger.warning('Cannot send keepalives as SM is not available, the socket disallows whitespace pings and does not manage its own keepalives. Cannot guarantee that the connection survives.'); logger.warning(
'Cannot send keepalives as SM is not available, the socket disallows whitespace pings and does not manage its own keepalives. Cannot guarantee that the connection survives.',
);
} }
@override /// Cancel a potentially scheduled ping timer. Can be overriden to cancel a custom timing mechanism.
Future<void> onXmppEvent(XmppEvent event) async { /// By default, cancels a [Timer.periodic] that was set up prior.
if (event is SendPingEvent) { @visibleForOverriding
logger.finest('Received ping event.'); Future<void> cancelPing() async {
await _timerLock.synchronized(() {
logger.finest('Cancelling timer');
_pingTimer?.cancel();
_pingTimer = null;
});
}
/// Schedule a ping to be sent after a given amount of time. Can be overriden for custom timing mechanisms.
/// By default, uses a [Timer.periodic] timer to trigger a ping.
/// NOTE: This function is called whenever the connection is re-established. Custom
/// implementations should thus guard against multiple timers being started.
@visibleForOverriding
Future<void> schedulePing() async {
await _timerLock.synchronized(() {
logger.finest('Scheduling new timer? ${_pingTimer != null}');
_pingTimer ??= Timer.periodic(
_pingDuration,
_sendPing,
);
});
}
Future<void> _sendPing(Timer _) async {
logger.finest('Attempting to send ping');
final attrs = getAttributes(); final attrs = getAttributes();
final socket = attrs.getSocket(); final socket = attrs.getSocket();
@@ -31,11 +67,14 @@ class PingManager extends XmppManagerBase {
final stream = attrs.getManagerById(smManager) as StreamManagementManager?; final stream = attrs.getManagerById(smManager) as StreamManagementManager?;
if (stream != null) { if (stream != null) {
if (stream.isStreamManagementEnabled() /*&& stream.getUnackedStanzaCount() > 0*/) { if (stream
.isStreamManagementEnabled() /*&& stream.getUnackedStanzaCount() > 0*/) {
logger.finest('Sending an ack ping as Stream Management is enabled'); logger.finest('Sending an ack ping as Stream Management is enabled');
stream.sendAckRequestPing(); stream.sendAckRequestPing();
} else if (attrs.getSocket().whitespacePingAllowed()) { } else if (attrs.getSocket().whitespacePingAllowed()) {
logger.finest('Sending a whitespace ping as Stream Management is not enabled'); logger.finest(
'Sending a whitespace ping as Stream Management is not enabled',
);
attrs.getConnection().sendWhitespacePing(); attrs.getConnection().sendWhitespacePing();
} else { } else {
_logWarning(); _logWarning();
@@ -48,5 +87,15 @@ class PingManager extends XmppManagerBase {
} }
} }
} }
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is ConnectionStateChangedEvent) {
if (event.connectionEstablished) {
await schedulePing();
} else {
await cancelPing();
}
}
} }
} }

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:moxxmpp/src/connection.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';
@@ -6,23 +7,24 @@ 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/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/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0115.dart';
import 'package:moxxmpp/src/xeps/xep_0414.dart';
/// 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
/// presence.
typedef PresencePreSendCallback = Future<List<XMLNode>> Function();
/// A mandatory manager that handles initial presence sending, sending of subscription
/// request management requests and triggers events for incoming presence stanzas.
class PresenceManager extends XmppManagerBase { class PresenceManager extends XmppManagerBase {
PresenceManager() : super(presenceManager);
PresenceManager() : _capabilityHash = null, super(); /// The list of pre-send callbacks.
String? _capabilityHash; final List<PresencePreSendCallback> _presenceCallbacks =
List.empty(growable: true);
@override
String getId() => presenceManager;
@override
String getName() => 'PresenceManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -38,66 +40,77 @@ class PresenceManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onPresence(Stanza presence, StanzaHandlerData state) async { /// Register the pre-send callback [callback].
void registerPreSendCallback(PresencePreSendCallback callback) {
_presenceCallbacks.add(callback);
}
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is StreamNegotiationsDoneEvent) {
// Send initial presence only when we have not resumed the stream
final sm = getAttributes().getNegotiatorById<StreamManagementNegotiator>(
streamManagementNegotiator,
);
final isResumed = sm?.isResumed ?? false;
if (!isResumed) {
unawaited(sendInitialPresence());
}
}
}
Future<StanzaHandlerData> _onPresence(
Stanza presence,
StanzaHandlerData state,
) async {
final attrs = getAttributes(); final attrs = getAttributes();
switch (presence.type) { switch (presence.type) {
case 'subscribe': case 'subscribe':
case 'subscribed': { case 'subscribed':
{
attrs.sendEvent( attrs.sendEvent(
SubscriptionRequestReceivedEvent(from: JID.fromString(presence.from!)), SubscriptionRequestReceivedEvent(
from: JID.fromString(presence.from!),
),
); );
return state.copyWith(done: true); return state.copyWith(done: true);
} }
default: break; default:
break;
} }
if (presence.from != null) { if (presence.from != null) {
logger.finest("Received presence from '${presence.from}'"); logger.finest("Received presence from '${presence.from}'");
getAttributes().sendEvent(PresenceReceivedEvent(JID.fromString(presence.from!), presence)); getAttributes().sendEvent(
PresenceReceivedEvent(JID.fromString(presence.from!), presence),
);
return state.copyWith(done: true); return state.copyWith(done: true);
} }
return state; return state;
} }
/// Returns the capability hash.
Future<String> getCapabilityHash() async {
final manager = getAttributes().getManagerById(discoManager)! as DiscoManager;
_capabilityHash ??= await calculateCapabilityHash(
DiscoInfo(
manager.getRegisteredDiscoFeatures(),
manager.getIdentities(),
[],
getAttributes().getFullJID(),
),
getHashByName('sha-1')!,
);
return _capabilityHash!;
}
/// Sends the initial presence to enable receiving messages. /// Sends the initial presence to enable receiving messages.
Future<void> sendInitialPresence() async { Future<void> sendInitialPresence() async {
final attrs = getAttributes(); final children = List<XMLNode>.from([
attrs.sendNonza(
Stanza.presence(
from: attrs.getFullJID().toString(),
children: [
XMLNode( XMLNode(
tag: 'show', tag: 'show',
text: 'chat', text: 'chat',
), ),
XMLNode.xmlns( ]);
tag: 'c',
xmlns: capsXmlns, for (final callback in _presenceCallbacks) {
attributes: { children.addAll(
'hash': 'sha-1', await callback(),
'node': 'http://moxxy.im', );
'ver': await getCapabilityHash() }
},
) final attrs = getAttributes();
], attrs.sendNonza(
Stanza.presence(
from: attrs.getFullJID().toString(),
children: children,
), ),
); );
} }

View File

@@ -4,37 +4,66 @@ import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
abstract class ReconnectionPolicy { /// A callback function to be called when the connection to the server has been lost.
typedef ConnectionLostCallback = Future<void> Function();
ReconnectionPolicy() /// A function that, when called, causes the XmppConnection to connect to the server, if
: _shouldAttemptReconnection = false, /// another reconnection is not already running.
_isReconnecting = false, typedef PerformReconnectFunction = Future<void> Function();
_isReconnectingLock = Lock();
abstract class ReconnectionPolicy {
/// Function provided by XmppConnection that allows the policy /// Function provided by XmppConnection that allows the policy
/// to perform a reconnection. /// to perform a reconnection.
Future<void> Function()? performReconnect; PerformReconnectFunction? performReconnect;
/// Function provided by XmppConnection that allows the policy
/// to say that we lost the connection. final Lock _lock = Lock();
void Function()? triggerConnectionLost;
/// Indicate if should try to reconnect.
bool _shouldAttemptReconnection;
/// Indicate if a reconnection attempt is currently running. /// Indicate if a reconnection attempt is currently running.
bool _isReconnecting; bool _isReconnecting = false;
/// And the corresponding lock
final Lock _isReconnectingLock; /// Indicate if should try to reconnect.
bool _shouldAttemptReconnection = false;
@protected
Future<bool> canTryReconnecting() async =>
_lock.synchronized(() => !_isReconnecting);
@protected
Future<bool> getIsReconnecting() async =>
_lock.synchronized(() => _isReconnecting);
Future<void> _resetIsReconnecting() async {
await _lock.synchronized(() {
_isReconnecting = false;
});
}
/// Called by XmppConnection to register the policy. /// Called by XmppConnection to register the policy.
void register(Future<void> Function() performReconnect, void Function() triggerConnectionLost) { void register(
PerformReconnectFunction performReconnect,
) {
this.performReconnect = performReconnect; this.performReconnect = performReconnect;
this.triggerConnectionLost = triggerConnectionLost;
unawaited(reset());
} }
/// In case the policy depends on some internal state, this state must be reset /// In case the policy depends on some internal state, this state must be reset
/// to an initial state when reset is called. In case timers run, they must be /// to an initial state when reset is called. In case timers run, they must be
/// terminated. /// terminated.
Future<void> reset(); @mustCallSuper
Future<void> reset() async {
await _resetIsReconnecting();
}
@mustCallSuper
Future<bool> canTriggerFailure() async {
return _lock.synchronized(() {
if (_shouldAttemptReconnection && !_isReconnecting) {
_isReconnecting = true;
return true;
}
return false;
});
}
/// Called by the XmppConnection when the reconnection failed. /// Called by the XmppConnection when the reconnection failed.
Future<void> onFailure() async {} Future<void> onFailure() async {}
@@ -42,99 +71,94 @@ abstract class ReconnectionPolicy {
/// Caled by the XmppConnection when the reconnection was successful. /// Caled by the XmppConnection when the reconnection was successful.
Future<void> onSuccess(); Future<void> onSuccess();
bool get shouldReconnect => _shouldAttemptReconnection; Future<bool> getShouldReconnect() async {
return _lock.synchronized(() => _shouldAttemptReconnection);
}
/// Set whether a reconnection attempt should be made. /// Set whether a reconnection attempt should be made.
void setShouldReconnect(bool value) { Future<void> setShouldReconnect(bool value) async {
_shouldAttemptReconnection = value; return _lock.synchronized(() => _shouldAttemptReconnection = value);
}
/// Returns true if the manager is currently triggering a reconnection. If not, returns
/// false.
Future<bool> isReconnectionRunning() async {
return _isReconnectingLock.synchronized(() => _isReconnecting);
}
/// Set the _isReconnecting state to [value].
@protected
Future<void> setIsReconnecting(bool value) async {
await _isReconnectingLock.synchronized(() async {
_isReconnecting = value;
});
}
@protected
Future<bool> testAndSetIsReconnecting() async {
return _isReconnectingLock.synchronized(() {
if (_isReconnecting) {
return false;
} else {
_isReconnecting = true;
return true;
}
});
} }
} }
/// A simple reconnection strategy: Make the reconnection delays exponentially longer /// A simple reconnection strategy: Make the reconnection delays exponentially longer
/// for every failed attempt. /// for every failed attempt.
class ExponentialBackoffReconnectionPolicy extends ReconnectionPolicy { /// NOTE: This ReconnectionPolicy may be broken
class RandomBackoffReconnectionPolicy extends ReconnectionPolicy {
ExponentialBackoffReconnectionPolicy() RandomBackoffReconnectionPolicy(
: _counter = 0, this._minBackoffTime,
_log = Logger('ExponentialBackoffReconnectionPolicy'), this._maxBackoffTime,
) : assert(
_minBackoffTime < _maxBackoffTime,
'_minBackoffTime must be smaller than _maxBackoffTime',
),
super(); super();
int _counter;
/// The maximum time in seconds that a backoff should be.
final int _maxBackoffTime;
/// The minimum time in seconds that a backoff should be.
final int _minBackoffTime;
/// Backoff timer.
Timer? _timer; Timer? _timer;
final Logger _log;
/// Logger.
final Logger _log = Logger('RandomBackoffReconnectionPolicy');
final Lock _timerLock = Lock();
/// Called when the backoff expired /// Called when the backoff expired
Future<void> _onTimerElapsed() async { @visibleForTesting
final isReconnecting = await isReconnectionRunning(); Future<void> onTimerElapsed() async {
if (shouldReconnect) { _log.fine('Timer elapsed. Waiting for lock...');
if (!isReconnecting) { await _timerLock.synchronized(() async {
if (!(await getIsReconnecting())) {
return;
}
if (!(await getShouldReconnect())) {
_log.fine(
'Should not reconnect. Stopping here.',
);
return;
}
_log.fine('Triggering reconnect');
_timer?.cancel();
_timer = null;
await performReconnect!(); await performReconnect!();
} else { });
// Should never happen.
_log.fine('Backoff timer expired but reconnection is running, so doing nothing.');
}
}
} }
@override @override
Future<void> reset() async { Future<void> reset() async {
_log.finest('Resetting internal state'); _log.finest('Resetting internal state');
_counter = 0; _timer?.cancel();
await setIsReconnecting(false);
if (_timer != null) {
_timer!.cancel();
_timer = null; _timer = null;
} await super.reset();
} }
@override @override
Future<void> onFailure() async { Future<void> onFailure() async {
_log.finest('Failure occured. Starting exponential backoff'); final seconds =
_counter++; Random().nextInt(_maxBackoffTime - _minBackoffTime) + _minBackoffTime;
await setIsReconnecting(true); _log.finest('Failure occured. Starting random backoff with ${seconds}s');
_timer?.cancel();
if (_timer != null) { _timer = Timer(Duration(seconds: seconds), onTimerElapsed);
_timer!.cancel();
}
// Wait at max 80 seconds.
final seconds = min(pow(2, _counter).toInt(), 80);
_timer = Timer(Duration(seconds: seconds), _onTimerElapsed);
} }
@override @override
Future<void> onSuccess() async { Future<void> onSuccess() async {
await reset(); await reset();
} }
@visibleForTesting
bool isTimerRunning() => _timer != null;
} }
/// A stub reconnection policy for tests /// A stub reconnection policy for tests.
@visibleForTesting @visibleForTesting
class TestingReconnectionPolicy extends ReconnectionPolicy { class TestingReconnectionPolicy extends ReconnectionPolicy {
TestingReconnectionPolicy() : super(); TestingReconnectionPolicy() : super();
@@ -146,5 +170,29 @@ class TestingReconnectionPolicy extends ReconnectionPolicy {
Future<void> onFailure() async {} Future<void> onFailure() async {}
@override @override
Future<void> reset() async {} Future<void> reset() async {
await super.reset();
}
}
/// A reconnection policy for tests that waits a constant number of seconds before
/// attempting a reconnection.
@visibleForTesting
class TestingSleepReconnectionPolicy extends ReconnectionPolicy {
TestingSleepReconnectionPolicy(this._sleepAmount) : super();
final int _sleepAmount;
@override
Future<void> onSuccess() async {}
@override
Future<void> onFailure() async {
await Future<void>.delayed(Duration(seconds: _sleepAmount));
await performReconnect!();
}
@override
Future<void> reset() async {
await super.reset();
}
} }

View File

@@ -24,3 +24,28 @@ int ioctetSortComparator(String a, String b) {
return 1; return 1;
} }
int ioctetSortComparatorRaw(List<int> a, List<int> b) {
if (a.isEmpty && b.isEmpty) {
return 0;
}
if (a.isEmpty && b.isNotEmpty) {
return -1;
}
if (a.isNotEmpty && b.isEmpty) {
return 1;
}
if (a[0] == b[0]) {
return ioctetSortComparatorRaw(a.sublist(1), b.sublist(1));
}
// TODO(Unknown): Is this correct?
if (a[0] < b[0]) {
return -1;
}
return 1;
}

View File

@@ -0,0 +1,84 @@
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:uuid/uuid.dart';
class ResourceBindingFailedError extends NegotiatorError {
@override
bool isRecoverable() => true;
}
/// A negotiator that implements resource binding against a random server-provided
/// resource.
class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
ResourceBindingNegotiator()
: super(0, false, bindXmlns, resourceBindingNegotiator);
/// Flag indicating the state of the negotiator:
/// - True: We sent a binding request
/// - False: We have not yet sent the binding request
bool _requestSent = false;
@override
bool matchesFeature(List<XMLNode> features) {
final sm = attributes.getManagerById<StreamManagementManager>(smManager);
if (sm != null) {
return super.matchesFeature(features) &&
!sm.streamResumed &&
attributes.isAuthenticated() &&
attributes.getConnection().resource.isEmpty;
}
return super.matchesFeature(features) &&
attributes.isAuthenticated() &&
attributes.getConnection().resource.isEmpty;
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
if (!_requestSent) {
final stanza = XMLNode.xmlns(
tag: 'iq',
xmlns: stanzaXmlns,
attributes: {
'type': 'set',
'id': const Uuid().v4(),
},
children: [
XMLNode.xmlns(
tag: 'bind',
xmlns: bindXmlns,
),
],
);
_requestSent = true;
attributes.sendNonza(stanza);
return const Result(NegotiatorState.ready);
} else {
if (nonza.tag != 'iq' || nonza.attributes['type'] != 'result') {
return Result(ResourceBindingFailedError());
}
final bind = nonza.firstTag('bind')!;
final rawJid = bind.firstTag('jid')!.innerText();
final resource = JID.fromString(rawJid).resource;
attributes.setResource(resource);
return const Result(NegotiatorState.done);
}
}
@override
void reset() {
_requestSent = false;
super.reset();
}
}

View File

@@ -0,0 +1,53 @@
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
abstract class SaslError extends NegotiatorError {
static SaslError fromFailure(XMLNode failure) {
XMLNode? error;
for (final child in failure.children) {
if (child.tag == 'text') continue;
error = child;
break;
}
switch (error?.tag) {
case 'credentials-expired':
return SaslCredentialsExpiredError();
case 'not-authorized':
return SaslNotAuthorizedError();
case 'account-disabled':
return SaslAccountDisabledError();
}
return SaslUnspecifiedError();
}
}
/// Triggered when the server returned us a <not-authorized /> failure during SASL
/// (https://xmpp.org/rfcs/rfc6120.html#sasl-errors-not-authorized).
class SaslNotAuthorizedError extends SaslError {
@override
bool isRecoverable() => false;
}
/// Triggered when the server returned us a <credentials-expired /> failure during SASL
/// (https://xmpp.org/rfcs/rfc6120.html#sasl-errors-credentials-expired).
class SaslCredentialsExpiredError extends SaslError {
@override
bool isRecoverable() => false;
}
/// Triggered when the server returned us a <account-disabled /> failure during SASL
/// (https://xmpp.org/rfcs/rfc6120.html#sasl-errors-account-disabled).
class SaslAccountDisabledError extends SaslError {
@override
bool isRecoverable() => false;
}
/// An unspecified SASL error, i.e. everything not matched by any more precise erorr
/// class.
class SaslUnspecifiedError extends SaslError {
@override
bool isRecoverable() => true;
}

View File

@@ -0,0 +1,45 @@
enum ParserState { variableName, variableValue }
/// Parse a string like "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL" into
/// { "n": "user", "r": "fyko+d2lbbFgONRv9qkxdawL"}.
Map<String, String> parseKeyValue(String keyValueString) {
var state = ParserState.variableName;
var name = '';
var value = '';
final values = <String, String>{};
for (var i = 0; i < keyValueString.length; i++) {
final char = keyValueString[i];
switch (state) {
case ParserState.variableName:
{
if (char == '=') {
state = ParserState.variableValue;
} else if (char == ',') {
name = '';
} else {
name += char;
}
}
break;
case ParserState.variableValue:
{
if (char == ',' || i == keyValueString.length - 1) {
if (char != ',') {
value += char;
}
values[name] = value;
value = '';
name = '';
state = ParserState.variableName;
} else {
value += char;
}
}
break;
}
}
return values;
}

View File

@@ -4,8 +4,9 @@ import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
abstract class SaslNegotiator extends XmppFeatureNegotiatorBase { abstract class SaslNegotiator extends XmppFeatureNegotiatorBase {
SaslNegotiator(int priority, String id, this.mechanismName)
: super(priority, true, saslXmlns, id);
SaslNegotiator(int priority, String id, this.mechanismName) : super(priority, true, saslXmlns, id);
/// The name inside the <mechanism /> element /// The name inside the <mechanism /> element
final String mechanismName; final String mechanismName;
@@ -22,6 +23,7 @@ abstract class SaslNegotiator extends XmppFeatureNegotiatorBase {
return firstWhereOrNull( return firstWhereOrNull(
mechanisms.children, mechanisms.children,
(XMLNode mechanism) => mechanism.text == mechanismName, (XMLNode mechanism) => mechanism.text == mechanismName,
) != null; ) !=
null;
} }
} }

View File

@@ -0,0 +1,14 @@
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart';
class SaslAuthNonza extends XMLNode {
SaslAuthNonza(String mechanism, String body)
: super(
tag: 'auth',
attributes: <String, String>{
'xmlns': saslXmlns,
'mechanism': mechanism,
},
text: body,
);
}

View File

@@ -0,0 +1,110 @@
import 'dart:convert';
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
import 'package:saslprep/saslprep.dart';
class SaslPlainAuthNonza extends SaslAuthNonza {
SaslPlainAuthNonza(String data)
: super(
'PLAIN',
data,
);
}
class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
SaslPlainNegotiator()
: _authSent = false,
_log = Logger('SaslPlainNegotiator'),
super(0, saslPlainNegotiator, 'PLAIN');
bool _authSent;
final Logger _log;
@override
bool matchesFeature(List<XMLNode> features) {
if (super.matchesFeature(features)) {
if (!attributes.getSocket().isSecure()) {
_log.warning(
'Refusing to match SASL feature due to unsecured connection',
);
return false;
}
return true;
}
return false;
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
if (!_authSent) {
final data = await getRawStep('');
attributes.sendNonza(
SaslPlainAuthNonza(data),
);
_authSent = true;
return const Result(NegotiatorState.ready);
} else {
final tag = nonza.tag;
if (tag == 'success') {
attributes.setAuthenticated();
return const Result(NegotiatorState.done);
} else {
// We assume it's a <failure/>
final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error));
return Result(
SaslError.fromFailure(nonza),
);
}
}
}
@override
void reset() {
_authSent = false;
super.reset();
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerSaslNegotiator(this);
}
@override
Future<String> getRawStep(String input) async {
final settings = attributes.getConnectionSettings();
final prep = Saslprep.saslprep(settings.password);
return base64.encode(
utf8.encode('\u0000${settings.jid.local}\u0000$prep'),
);
}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
state = NegotiatorState.done;
return const Result(true);
}
@override
Future<void> onSasl2Failure(XMLNode response) async {}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
return [];
}
}

View File

@@ -0,0 +1,377 @@
import 'dart:convert';
import 'dart:math' show Random;
import 'package:cryptography/cryptography.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
import 'package:random_string/random_string.dart';
import 'package:saslprep/saslprep.dart';
abstract class SaslScramError extends NegotiatorError {}
class NoAdditionalDataError extends SaslScramError {
@override
bool isRecoverable() => false;
}
class InvalidServerSignatureError extends SaslScramError {
@override
bool isRecoverable() => false;
}
// NOTE: Inspired by https://github.com/vukoye/xmpp_dart/blob/3b1a0588562b9e591488c99d834088391840911d/lib/src/features/sasl/ScramSaslHandler.dart
enum ScramHashType { sha1, sha256, sha512 }
HashAlgorithm hashFromType(ScramHashType type) {
switch (type) {
case ScramHashType.sha1:
return Sha1();
case ScramHashType.sha256:
return Sha256();
case ScramHashType.sha512:
return Sha512();
}
}
int pbkdfBitsFromHash(ScramHashType type) {
switch (type) {
// NOTE: SHA1 is 20 octets long => 20 octets * 8 bits/octet
case ScramHashType.sha1:
return 160;
// NOTE: SHA256 is 32 octets long => 32 octets * 8 bits/octet
case ScramHashType.sha256:
return 256;
// NOTE: SHA512 is 64 octets long => 64 octets * 8 bits/octet
case ScramHashType.sha512:
return 512;
}
}
const scramSha1Mechanism = 'SCRAM-SHA-1';
const scramSha256Mechanism = 'SCRAM-SHA-256';
const scramSha512Mechanism = 'SCRAM-SHA-512';
String mechanismNameFromType(ScramHashType type) {
switch (type) {
case ScramHashType.sha1:
return scramSha1Mechanism;
case ScramHashType.sha256:
return scramSha256Mechanism;
case ScramHashType.sha512:
return scramSha512Mechanism;
}
}
String namespaceFromType(ScramHashType type) {
switch (type) {
case ScramHashType.sha1:
return saslScramSha1Negotiator;
case ScramHashType.sha256:
return saslScramSha256Negotiator;
case ScramHashType.sha512:
return saslScramSha512Negotiator;
}
}
class SaslScramAuthNonza extends SaslAuthNonza {
// This subclassing makes less sense here, but this is since the auth nonza here
// requires knowledge of the inner state of the Negotiator.
SaslScramAuthNonza({required ScramHashType type, required String body})
: super(
mechanismNameFromType(type),
body,
);
}
class SaslScramResponseNonza extends XMLNode {
SaslScramResponseNonza({required String body})
: super(
tag: 'response',
attributes: <String, String>{
'xmlns': saslXmlns,
},
text: body,
);
}
enum ScramState { preSent, initialMessageSent, challengeResponseSent, error }
const gs2Header = 'n,,';
class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
// NOTE: NEVER, and I mean, NEVER set clientNonce or initalMessageNoGS2. They are just there for testing
SaslScramNegotiator(
int priority,
this.initialMessageNoGS2,
this.clientNonce,
this.hashType,
) : _hash = hashFromType(hashType),
_serverSignature = '',
_scramState = ScramState.preSent,
_log =
Logger('SaslScramNegotiator(${mechanismNameFromType(hashType)})'),
super(
priority,
namespaceFromType(hashType),
mechanismNameFromType(hashType),
);
String? clientNonce;
String initialMessageNoGS2;
final ScramHashType hashType;
final HashAlgorithm _hash;
String _serverSignature;
// The internal state for performing the negotiation
ScramState _scramState;
final Logger _log;
Future<List<int>> calculateSaltedPassword(String salt, int iterations) async {
final pbkdf2 = Pbkdf2(
macAlgorithm: Hmac(_hash),
iterations: iterations,
bits: pbkdfBitsFromHash(hashType),
);
final saltedPasswordRaw = await pbkdf2.deriveKey(
secretKey: SecretKey(
utf8.encode(
Saslprep.saslprep(attributes.getConnectionSettings().password),
),
),
nonce: base64.decode(salt),
);
return saltedPasswordRaw.extractBytes();
}
Future<List<int>> calculateClientKey(List<int> saltedPassword) async {
return (await Hmac(_hash).calculateMac(
utf8.encode('Client Key'),
secretKey: SecretKey(saltedPassword),
))
.bytes;
}
Future<List<int>> calculateClientSignature(
String authMessage,
List<int> storedKey,
) async {
return (await Hmac(_hash).calculateMac(
utf8.encode(authMessage),
secretKey: SecretKey(storedKey),
))
.bytes;
}
Future<List<int>> calculateServerKey(List<int> saltedPassword) async {
return (await Hmac(_hash).calculateMac(
utf8.encode('Server Key'),
secretKey: SecretKey(saltedPassword),
))
.bytes;
}
Future<List<int>> calculateServerSignature(
String authMessage,
List<int> serverKey,
) async {
return (await Hmac(_hash).calculateMac(
utf8.encode(authMessage),
secretKey: SecretKey(serverKey),
))
.bytes;
}
List<int> calculateClientProof(
List<int> clientKey,
List<int> clientSignature,
) {
final clientProof = List<int>.filled(clientKey.length, 0);
for (var i = 0; i < clientKey.length; i++) {
clientProof[i] = clientKey[i] ^ clientSignature[i];
}
return clientProof;
}
Future<String> calculateChallengeResponse(String base64Challenge) async {
final challengeString = utf8.decode(base64.decode(base64Challenge));
final challenge = parseKeyValue(challengeString);
final clientFinalMessageBare = 'c=biws,r=${challenge['r']!}';
final saltedPassword = await calculateSaltedPassword(
challenge['s']!,
int.parse(challenge['i']!),
);
final clientKey = await calculateClientKey(saltedPassword);
final storedKey = (await _hash.hash(clientKey)).bytes;
final authMessage =
'$initialMessageNoGS2,$challengeString,$clientFinalMessageBare';
final clientSignature =
await calculateClientSignature(authMessage, storedKey);
final clientProof = calculateClientProof(clientKey, clientSignature);
final serverKey = await calculateServerKey(saltedPassword);
_serverSignature =
base64.encode(await calculateServerSignature(authMessage, serverKey));
return '$clientFinalMessageBare,p=${base64.encode(clientProof)}';
}
@override
bool matchesFeature(List<XMLNode> features) {
if (super.matchesFeature(features)) {
if (!attributes.getSocket().isSecure()) {
_log.warning(
'Refusing to match SASL feature due to unsecured connection',
);
return false;
}
return true;
}
return false;
}
bool _checkSignature(String base64Signature) {
final signature =
parseKeyValue(utf8.decode(base64.decode(base64Signature)));
return signature['v']! == _serverSignature;
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
switch (_scramState) {
case ScramState.preSent:
attributes.sendNonza(
SaslScramAuthNonza(
body: await getRawStep(''),
type: hashType,
),
);
return const Result(NegotiatorState.ready);
case ScramState.initialMessageSent:
if (nonza.tag != 'challenge') {
final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error;
return Result(
SaslError.fromFailure(nonza),
);
}
attributes.sendNonza(
SaslScramResponseNonza(body: await getRawStep(nonza.innerText())),
);
return const Result(NegotiatorState.ready);
case ScramState.challengeResponseSent:
if (nonza.tag != 'success') {
// We assume it's a <failure />
final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error;
return Result(
SaslError.fromFailure(nonza),
);
}
if (!_checkSignature(nonza.innerText())) {
// TODO(Unknown): Notify of a signature mismatch
//final error = nonza.children.first.tag;
//attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error;
return Result(
SaslError.fromFailure(nonza),
);
}
attributes.setAuthenticated();
return const Result(NegotiatorState.done);
case ScramState.error:
return Result(
SaslError.fromFailure(nonza),
);
}
}
@override
void reset() {
_scramState = ScramState.preSent;
super.reset();
}
@override
Future<String> getRawStep(String input) async {
switch (_scramState) {
case ScramState.preSent:
if (clientNonce == null || clientNonce == '') {
clientNonce = randomAlphaNumeric(
40,
provider: CoreRandomProvider.from(Random.secure()),
);
}
initialMessageNoGS2 =
'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce';
_scramState = ScramState.initialMessageSent;
return base64.encode(utf8.encode(gs2Header + initialMessageNoGS2));
case ScramState.initialMessageSent:
final challengeBase64 = input;
final response = await calculateChallengeResponse(challengeBase64);
final responseBase64 = base64.encode(utf8.encode(response));
_scramState = ScramState.challengeResponseSent;
return responseBase64;
case ScramState.challengeResponseSent:
case ScramState.error:
return '';
}
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerSaslNegotiator(this);
}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
return [];
}
@override
Future<void> onSasl2Failure(XMLNode response) async {}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
// When we're done with SASL2, check the additional data to verify the server
// signature.
state = NegotiatorState.done;
final additionalData = response.firstTag('additional-data');
if (additionalData == null) {
return Result(NoAdditionalDataError());
}
if (!_checkSignature(additionalData.innerText())) {
return Result(InvalidServerSignatureError());
}
return const Result(true);
}
}

View File

@@ -0,0 +1,70 @@
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
enum _StartTlsState { ready, requested }
class StartTLSFailedError extends NegotiatorError {
@override
bool isRecoverable() => true;
}
class StartTLSNonza extends XMLNode {
StartTLSNonza()
: super.xmlns(
tag: 'starttls',
xmlns: startTlsXmlns,
);
}
/// A negotiator implementing StartTLS.
class StartTlsNegotiator extends XmppFeatureNegotiatorBase {
StartTlsNegotiator() : super(10, true, startTlsXmlns, startTlsNegotiator);
/// The state of the negotiator.
_StartTlsState _state = _StartTlsState.ready;
/// Logger.
final Logger _log = Logger('StartTlsNegotiator');
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
switch (_state) {
case _StartTlsState.ready:
_log.fine('StartTLS is available. Performing StartTLS upgrade...');
_state = _StartTlsState.requested;
attributes.sendNonza(StartTLSNonza());
return const Result(NegotiatorState.ready);
case _StartTlsState.requested:
if (nonza.tag != 'proceed' ||
nonza.attributes['xmlns'] != startTlsXmlns) {
_log.severe('Failed to perform StartTLS negotiation');
return Result(StartTLSFailedError());
}
_log.fine('Securing socket');
final result = await attributes
.getSocket()
.secure(attributes.getConnectionSettings().jid.domain);
if (!result) {
_log.severe('Failed to secure stream');
return Result(StartTLSFailedError());
}
_log.fine('Stream is now TLS secured');
return const Result(NegotiatorState.done);
}
}
@override
void reset() {
_state = _StartTlsState.ready;
super.reset();
}
}

View File

@@ -0,0 +1,7 @@
abstract class RosterError {}
/// Returned when the server's response did not contain a <query /> element
class NoQueryError extends RosterError {}
/// Unspecified error
class UnknownError extends RosterError {}

View File

@@ -1,5 +1,8 @@
import 'package:moxxmpp/src/events.dart'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:meta/meta.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/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,57 +10,88 @@ 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/roster/errors.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/error.dart'; import 'package:moxxmpp/src/types/result.dart';
const rosterErrorNoQuery = 1;
const rosterErrorNonResult = 2;
@immutable
class XmppRosterItem { class XmppRosterItem {
const XmppRosterItem({
XmppRosterItem({ required this.jid, required this.subscription, this.ask, this.name, this.groups = const [] }); required this.jid,
required this.subscription,
this.ask,
this.name,
this.groups = const [],
});
final String jid; final String jid;
final String? name; final String? name;
final String subscription; final String subscription;
final String? ask; final String? ask;
final List<String> groups; final List<String> groups;
@override
bool operator ==(Object other) {
return other is XmppRosterItem &&
other.jid == jid &&
other.name == name &&
other.subscription == subscription &&
other.ask == ask &&
const ListEquality<String>().equals(other.groups, groups);
} }
enum RosterRemovalResult { @override
okay, int get hashCode =>
error, jid.hashCode ^
itemNotFound name.hashCode ^
subscription.hashCode ^
ask.hashCode ^
groups.hashCode;
@override
String toString() {
return 'XmppRosterItem('
'jid: $jid, '
'name: $name, '
'subscription: $subscription, '
'ask: $ask, '
'groups: $groups)';
} }
}
enum RosterRemovalResult { okay, error, itemNotFound }
class RosterRequestResult { class RosterRequestResult {
RosterRequestResult(this.items, this.ver);
RosterRequestResult({ required this.items, this.ver });
List<XmppRosterItem> items; List<XmppRosterItem> items;
String? ver; String? ver;
} }
class RosterPushEvent extends XmppEvent { class RosterPushResult {
RosterPushResult(this.item, this.ver);
RosterPushEvent({ required this.item, this.ver });
final XmppRosterItem item; final XmppRosterItem item;
final String? ver; final String? ver;
} }
/// A Stub feature negotiator for finding out whether roster versioning is supported. /// A Stub feature negotiator for finding out whether roster versioning is supported.
class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase { class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase {
RosterFeatureNegotiator() : _supported = false, super(11, false, rosterVersioningXmlns, rosterNegotiator); RosterFeatureNegotiator()
: _supported = false,
super(11, false, rosterVersioningXmlns, rosterNegotiator);
/// True if rosterVersioning is supported. False otherwise. /// True if rosterVersioning is supported. False otherwise.
bool _supported; bool _supported;
bool get isSupported => _supported; bool get isSupported => _supported;
@override @override
Future<void> negotiate(XMLNode nonza) async { Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
// negotiate is only called when the negotiator matched, meaning the server // negotiate is only called when the negotiator matched, meaning the server
// advertises roster versioning. // advertises roster versioning.
_supported = true; _supported = true;
state = NegotiatorState.done; return const Result(NegotiatorState.done);
} }
@override @override
@@ -70,15 +104,16 @@ class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase {
/// This manager requires a RosterFeatureNegotiator to be registered. /// This manager requires a RosterFeatureNegotiator to be registered.
class RosterManager extends XmppManagerBase { class RosterManager extends XmppManagerBase {
RosterManager(this._stateManager) : super(rosterManager);
RosterManager() : _rosterVersion = null, super(); /// The class managing the entire roster state.
String? _rosterVersion; final BaseRosterStateManager _stateManager;
@override @override
String getId() => rosterManager; void register(XmppManagerAttributes attributes) {
super.register(attributes);
@override _stateManager.register(attributes.sendEvent);
String getName() => 'RosterManager'; }
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -93,17 +128,10 @@ class RosterManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
/// Override-able functions Future<StanzaHandlerData> _onRosterPush(
Future<void> commitLastRosterVersion(String version) async {} Stanza stanza,
Future<void> loadLastRosterVersion() async {} StanzaHandlerData state,
) async {
void setRosterVersion(String ver) {
assert(_rosterVersion == null, 'A roster version must not be empty');
_rosterVersion = ver;
}
Future<StanzaHandlerData> _onRosterPush(Stanza stanza, StanzaHandlerData state) async {
final attrs = getAttributes(); final attrs = getAttributes();
final from = stanza.attributes['from'] as String?; final from = stanza.attributes['from'] as String?;
final selfJid = attrs.getConnectionSettings().jid; final selfJid = attrs.getConnectionSettings().jid;
@@ -114,11 +142,14 @@ class RosterManager extends XmppManagerBase {
// - empty, i.e. not set // - empty, i.e. not set
// - a full JID of our own // - a full JID of our own
if (from != null && JID.fromString(from).toBare() != selfJid) { if (from != null && JID.fromString(from).toBare() != selfJid) {
logger.warning('Roster push invalid! Unexpected from attribute: ${stanza.toXml()}'); logger.warning(
'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}',
);
return state.copyWith(done: true); return state.copyWith(done: true);
} }
final query = stanza.firstTag('query', xmlns: rosterXmlns)!; final query = stanza.firstTag('query', xmlns: rosterXmlns)!;
logger.fine('Roster push: ${query.toXml()}');
final item = query.firstTag('item'); final item = query.firstTag('item');
if (item == null) { if (item == null) {
@@ -126,95 +157,106 @@ class RosterManager extends XmppManagerBase {
return state.copyWith(done: true); return state.copyWith(done: true);
} }
if (query.attributes['ver'] != null) { unawaited(
final ver = query.attributes['ver']! as String; _stateManager.handleRosterPush(
await commitLastRosterVersion(ver); RosterPushResult(
_rosterVersion = ver; XmppRosterItem(
}
attrs.sendEvent(RosterPushEvent(
item: XmppRosterItem(
jid: item.attributes['jid']! as String, jid: item.attributes['jid']! as String,
subscription: item.attributes['subscription']! as String, subscription: item.attributes['subscription']! as String,
ask: item.attributes['ask'] as String?, ask: item.attributes['ask'] as String?,
name: item.attributes['name'] as String?, name: item.attributes['name'] as String?,
), ),
ver: query.attributes['ver'] as String?, query.attributes['ver'] as String?,
),); ),
await attrs.sendStanza(stanza.reply()); ),
);
await reply(
state,
'result',
[],
);
return state.copyWith(done: true); return state.copyWith(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.
Future<MayFail<RosterRequestResult>> _handleRosterResponse(XMLNode? query) async { Future<Result<RosterRequestResult, RosterError>> _handleRosterResponse(
XMLNode? query,
) async {
final List<XmppRosterItem> items; final List<XmppRosterItem> items;
String? rosterVersion;
if (query != null) { if (query != null) {
items = query.children.map((item) => XmppRosterItem( items = query.children
.map(
(item) => XmppRosterItem(
name: item.attributes['name'] as String?, name: item.attributes['name'] as String?,
jid: item.attributes['jid']! as String, jid: item.attributes['jid']! as String,
subscription: item.attributes['subscription']! as String, subscription: item.attributes['subscription']! as String,
ask: item.attributes['ask'] as String?, ask: item.attributes['ask'] as String?,
groups: item.findTags('group').map((groupNode) => groupNode.innerText()).toList(), groups: item
),).toList(); .findTags('group')
.map((groupNode) => groupNode.innerText())
if (query.attributes['ver'] != null) { .toList(),
final ver_ = query.attributes['ver']! as String;
await commitLastRosterVersion(ver_);
_rosterVersion = ver_;
}
} else {
logger.warning('Server response to roster request without roster versioning does not contain a <query /> element, while the type is not error. This violates RFC6121');
return MayFail.failure(rosterErrorNoQuery);
}
final ver = query.attributes['ver'] as String?;
if (ver != null) {
_rosterVersion = ver;
await commitLastRosterVersion(ver);
}
return MayFail.success(
RosterRequestResult(
items: items,
ver: ver,
), ),
)
.toList();
rosterVersion = query.attributes['ver'] as String?;
} else {
logger.warning(
'Server response to roster request without roster versioning does not contain a <query /> element, while the type is not error. This violates RFC6121',
);
return Result(NoQueryError());
}
final result = RosterRequestResult(
items,
rosterVersion,
); );
unawaited(
_stateManager.handleRosterFetch(result),
);
return Result(result);
} }
/// Requests the roster following RFC 6121 without using roster versioning. /// Requests the roster following RFC 6121.
Future<MayFail<RosterRequestResult>> requestRoster() async { Future<Result<RosterRequestResult, RosterError>> requestRoster() async {
final attrs = getAttributes(); final attrs = getAttributes();
final query = XMLNode.xmlns(
tag: 'query',
xmlns: rosterXmlns,
);
final rosterVersion = await _stateManager.getRosterVersion();
if (rosterVersion != null && rosterVersioningAvailable()) {
query.attributes['ver'] = rosterVersion;
}
final response = await attrs.sendStanza( final response = await attrs.sendStanza(
Stanza.iq( Stanza.iq(
type: 'get', type: 'get',
children: [ children: [
XMLNode.xmlns( query,
tag: 'query',
xmlns: rosterXmlns,
)
], ],
), ),
); );
if (response.attributes['type'] != 'result') { if (response.attributes['type'] != 'result') {
logger.warning('Error requesting roster without roster versioning: ${response.toXml()}'); logger.warning('Error requesting roster: ${response.toXml()}');
return MayFail.failure(rosterErrorNonResult); return Result(UnknownError());
} }
final query = response.firstTag('query', xmlns: rosterXmlns); final responseQuery = response.firstTag('query', xmlns: rosterXmlns);
return _handleRosterResponse(query); return _handleRosterResponse(responseQuery);
} }
/// 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
/// advertises urn:xmpp:features:rosterver in the stream features. /// advertises urn:xmpp:features:rosterver in the stream features.
Future<MayFail<RosterRequestResult?>> requestRosterPushes() async { Future<Result<RosterRequestResult?, RosterError>>
if (_rosterVersion == null) { requestRosterPushes() async {
await loadLastRosterVersion();
}
final attrs = getAttributes(); final attrs = getAttributes();
final result = await attrs.sendStanza( final result = await attrs.sendStanza(
Stanza.iq( Stanza.iq(
@@ -224,7 +266,7 @@ class RosterManager extends XmppManagerBase {
tag: 'query', tag: 'query',
xmlns: rosterXmlns, xmlns: rosterXmlns,
attributes: { attributes: {
'ver': _rosterVersion ?? '' 'ver': await _stateManager.getRosterVersion() ?? '',
}, },
) )
], ],
@@ -233,7 +275,7 @@ class RosterManager extends XmppManagerBase {
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()}');
return MayFail.failure(rosterErrorNonResult); return Result(UnknownError());
} }
final query = result.firstTag('query', xmlns: rosterXmlns); final query = result.firstTag('query', xmlns: rosterXmlns);
@@ -241,12 +283,18 @@ class RosterManager extends XmppManagerBase {
} }
bool rosterVersioningAvailable() { bool rosterVersioningAvailable() {
return getAttributes().getNegotiatorById<RosterFeatureNegotiator>(rosterNegotiator)!.isSupported; return getAttributes()
.getNegotiatorById<RosterFeatureNegotiator>(rosterNegotiator)!
.isSupported;
} }
/// Attempts to add [jid] with a title of [title] and groups [groups] to the roster. /// Attempts to add [jid] with a title of [title] and groups [groups] to the roster.
/// Returns true if the process was successful, false otherwise. /// Returns true if the process was successful, false otherwise.
Future<bool> addToRoster(String jid, String title, { List<String>? groups }) async { Future<bool> addToRoster(
String jid,
String title, {
List<String>? groups,
}) async {
final attrs = getAttributes(); final attrs = getAttributes();
final response = await attrs.sendStanza( final response = await attrs.sendStanza(
Stanza.iq( Stanza.iq(
@@ -260,9 +308,13 @@ class RosterManager extends XmppManagerBase {
tag: 'item', tag: 'item',
attributes: <String, String>{ attributes: <String, String>{
'jid': jid, 'jid': jid,
...title == jid.split('@')[0] ? <String, String>{} : <String, String>{ 'name': title } ...title == jid.split('@')[0]
? <String, String>{}
: <String, String>{'name': title}
}, },
children: (groups ?? []).map((group) => XMLNode(tag: 'group', text: group)).toList(), children: (groups ?? [])
.map((group) => XMLNode(tag: 'group', text: group))
.toList(),
) )
], ],
) )

View File

@@ -0,0 +1,235 @@
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/roster/roster.dart';
import 'package:synchronized/synchronized.dart';
class _RosterProcessTriple {
const _RosterProcessTriple(this.removed, this.modified, this.added);
final String? removed;
final XmppRosterItem? modified;
final XmppRosterItem? added;
}
class RosterCacheLoadResult {
const RosterCacheLoadResult(this.version, this.roster);
final String? version;
final List<XmppRosterItem> roster;
}
/// This class manages the roster state in order to correctly process and persist
/// roster pushes and facilitate roster versioning requests.
abstract class BaseRosterStateManager {
/// The cached version of the roster. If null, then it has not been loaded yet.
List<XmppRosterItem>? _currentRoster;
/// The cached version of the roster version.
String? _currentVersion;
/// A critical section locking both _currentRoster and _currentVersion.
final Lock _lock = Lock();
/// A function to send an XmppEvent to moxxmpp's main event bus
late void Function(XmppEvent) _sendEvent;
/// Overrideable function
/// Loads the old cached version of the roster and optionally that roster version
/// from persistent storage into a RosterCacheLoadResult object.
Future<RosterCacheLoadResult> loadRosterCache();
/// Overrideable function
/// Commits the roster data to persistent storage.
///
/// [version] is the roster version string. If none was provided, then this value
/// is null.
///
/// [removed] is a (possibly empty) list of bare JIDs that are removed from the
/// roster.
///
/// [modified] is a (possibly empty) list of XmppRosterItems that are modified. Correlation with
/// the cache is done using its jid attribute.
///
/// [added] is a (possibly empty) list of XmppRosterItems that are added by the
/// roster push or roster fetch request.
Future<void> commitRoster(
String? version,
List<String> removed,
List<XmppRosterItem> modified,
List<XmppRosterItem> added,
);
/// Internal function. Registers functions from the RosterManger against this
/// instance.
void register(void Function(XmppEvent) sendEvent) {
_sendEvent = sendEvent;
}
/// Load and cache or return the cached roster version.
Future<String?> getRosterVersion() async {
return _lock.synchronized(() async {
await _loadRosterCache();
return _currentVersion;
});
}
/// A wrapper around _commitRoster that also sends an event to moxxmpp's event
/// bus.
Future<void> _commitRoster(
String? version,
List<String> removed,
List<XmppRosterItem> modified,
List<XmppRosterItem> added,
) async {
_sendEvent(
RosterUpdatedEvent(
removed,
modified,
added,
),
);
await commitRoster(version, removed, modified, added);
}
/// Loads the cached roster data into memory, if that has not already happened.
/// NOTE: Must be called from within the _lock critical section.
Future<void> _loadRosterCache() async {
if (_currentRoster == null) {
final result = await loadRosterCache();
_currentRoster = result.roster;
_currentVersion = result.version;
}
}
/// Processes only single XmppRosterItem [item].
/// NOTE: Requires to be called from within the _lock critical section.
_RosterProcessTriple _handleRosterItem(XmppRosterItem item) {
if (item.subscription == 'remove') {
// The item has been removed
_currentRoster!.removeWhere((i) => i.jid == item.jid);
return _RosterProcessTriple(
item.jid,
null,
null,
);
}
final index = _currentRoster!.indexWhere((i) => i.jid == item.jid);
if (index == -1) {
// The item does not exist
_currentRoster!.add(item);
return _RosterProcessTriple(
null,
null,
item,
);
} else if (_currentRoster![index] != item) {
// The item is updated
_currentRoster![index] = item;
return _RosterProcessTriple(
null,
item,
null,
);
}
// Item has not been modified or added
return const _RosterProcessTriple(
null,
null,
null,
);
}
/// Handles a roster push from the RosterManager.
Future<void> handleRosterPush(RosterPushResult event) async {
await _lock.synchronized(() async {
await _loadRosterCache();
_currentVersion = event.ver;
final result = _handleRosterItem(event.item);
if (result.removed != null) {
return _commitRoster(
_currentVersion,
[result.removed!],
[],
[],
);
} else if (result.modified != null) {
return _commitRoster(
_currentVersion,
[],
[result.modified!],
[],
);
} else if (result.added != null) {
return _commitRoster(
_currentVersion,
[],
[],
[result.added!],
);
}
});
}
/// Handles the result from a roster fetch.
Future<void> handleRosterFetch(RosterRequestResult result) async {
await _lock.synchronized(() async {
final removed = List<String>.empty(growable: true);
final modified = List<XmppRosterItem>.empty(growable: true);
final added = List<XmppRosterItem>.empty(growable: true);
await _loadRosterCache();
_currentVersion = result.ver;
for (final item in result.items) {
final result = _handleRosterItem(item);
if (result.removed != null) removed.add(result.removed!);
if (result.modified != null) modified.add(result.modified!);
if (result.added != null) added.add(result.added!);
}
await _commitRoster(
_currentVersion,
removed,
modified,
added,
);
});
}
@visibleForTesting
List<XmppRosterItem> getRosterItems() => _currentRoster!;
}
@visibleForTesting
class TestingRosterStateManager extends BaseRosterStateManager {
TestingRosterStateManager(
this.initialRosterVersion,
this.initialRoster,
);
final String? initialRosterVersion;
final List<XmppRosterItem> initialRoster;
int loadCount = 0;
@override
Future<RosterCacheLoadResult> loadRosterCache() async {
loadCount++;
return RosterCacheLoadResult(
initialRosterVersion,
initialRoster,
);
}
@override
Future<void> commitRoster(
String? version,
List<String> removed,
List<XmppRosterItem> modified,
List<XmppRosterItem> added,
) async {}
}

View File

@@ -1,6 +1 @@
enum RoutingState { enum RoutingState { error, preConnection, negotiating, handleStanzas }
error,
preConnection,
negotiating,
handleStanzas
}

View File

@@ -1,10 +1,22 @@
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
class ConnectionSettings { class ConnectionSettings {
ConnectionSettings({
required this.jid,
required this.password,
this.host,
this.port,
});
ConnectionSettings({ required this.jid, required this.password, required this.useDirectTLS, required this.allowPlainAuth }); /// The JID to authenticate as.
final JID jid; final JID jid;
/// The password to use during authentication.
final String password; final String password;
final bool useDirectTLS;
final bool allowPlainAuth; /// The host to connect to. Skips DNS resolution if specified.
final String? host;
/// The port to connect to. Skips DNS resolution if specified.
final int? port;
} }

View File

@@ -5,13 +5,17 @@ abstract class XmppSocketEvent {}
/// Triggered by the socket when an error occurs. /// Triggered by the socket when an error occurs.
class XmppSocketErrorEvent extends XmppSocketEvent { class XmppSocketErrorEvent extends XmppSocketEvent {
XmppSocketErrorEvent(this.error); XmppSocketErrorEvent(this.error);
final Object error; final Object error;
} }
/// Triggered when the socket is closed /// Triggered when the socket is closed
class XmppSocketClosureEvent extends XmppSocketEvent {} class XmppSocketClosureEvent extends XmppSocketEvent {
XmppSocketClosureEvent(this.expected);
/// Indicate that the socket did not close unexpectedly.
final bool expected;
}
/// This class is the base for a socket that XmppConnection can use. /// This class is the base for a socket that XmppConnection can use.
abstract class BaseSocketWrapper { abstract class BaseSocketWrapper {
@@ -26,9 +30,8 @@ abstract class BaseSocketWrapper {
/// reused by calling [this.connect] again. /// reused by calling [this.connect] again.
void close(); void close();
/// Write [data] into the socket. If [redact] is not null, then [redact] will be /// Write [data] into the socket.
/// logged instead of [data]. void write(String data);
void write(String data, { String? redact });
/// This must connect to [host]:[port] and initialize the streams accordingly. /// This must connect to [host]:[port] and initialize the streams accordingly.
/// [domain] is the domain that TLS should be validated against, in case the Socket /// [domain] is the domain that TLS should be validated against, in case the Socket

View File

@@ -1,61 +1,107 @@
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
/// A simple description of the <error /> element that may be inside a stanza
class StanzaError {
StanzaError(this.type, this.error);
String type;
String error;
/// Returns a StanzaError if [stanza] contains a <error /> element. If not, returns
/// null.
static StanzaError? fromStanza(Stanza stanza) {
final error = stanza.firstTag('error');
if (error == null) return null;
final stanzaError = error.firstTagByXmlns(fullStanzaXmlns);
if (stanzaError == null) return null;
return StanzaError(
error.attributes['type']! as String,
stanzaError.tag,
);
}
}
class Stanza extends XMLNode { class Stanza extends XMLNode {
// ignore: use_super_parameters // ignore: use_super_parameters
Stanza({ this.to, this.from, this.type, this.id, List<XMLNode> children = const [], required String tag, Map<String, String> attributes = const {} }) : super( Stanza({
this.to,
this.from,
this.type,
this.id,
List<XMLNode> children = const [],
required String tag,
Map<String, String> attributes = const {},
}) : super(
tag: tag, tag: tag,
attributes: <String, dynamic>{ attributes: <String, dynamic>{
...attributes, ...attributes,
...type != null ? <String, dynamic>{ 'type': type } : <String, dynamic>{}, ...type != null
? <String, dynamic>{'type': type}
: <String, dynamic>{},
...id != null ? <String, dynamic>{'id': id} : <String, dynamic>{}, ...id != null ? <String, dynamic>{'id': id} : <String, dynamic>{},
...to != null ? <String, dynamic>{'to': to} : <String, dynamic>{}, ...to != null ? <String, dynamic>{'to': to} : <String, dynamic>{},
...from != null ? <String, dynamic>{ 'from': from } : <String, dynamic>{}, ...from != null
? <String, dynamic>{'from': from}
: <String, dynamic>{},
'xmlns': stanzaXmlns 'xmlns': stanzaXmlns
}, },
children: children, children: children,
); );
factory Stanza.iq({ String? to, String? from, String? type, String? id, List<XMLNode> children = const [], Map<String, String>? attributes = const {} }) { factory Stanza.iq({
String? to,
String? from,
String? type,
String? id,
List<XMLNode> children = const [],
Map<String, String>? attributes = const {},
}) {
return Stanza( return Stanza(
tag: 'iq', tag: 'iq',
from: from, from: from,
to: to, to: to,
id: id, id: id,
type: type, type: type,
attributes: <String, String>{ attributes: <String, String>{...attributes!, 'xmlns': stanzaXmlns},
...attributes!,
'xmlns': stanzaXmlns
},
children: children, children: children,
); );
} }
factory Stanza.presence({ String? to, String? from, String? type, String? id, List<XMLNode> children = const [], Map<String, String>? attributes = const {} }) { factory Stanza.presence({
String? to,
String? from,
String? type,
String? id,
List<XMLNode> children = const [],
Map<String, String>? attributes = const {},
}) {
return Stanza( return Stanza(
tag: 'presence', tag: 'presence',
from: from, from: from,
to: to, to: to,
id: id, id: id,
type: type, type: type,
attributes: <String, String>{ attributes: <String, String>{...attributes!, 'xmlns': stanzaXmlns},
...attributes!,
'xmlns': stanzaXmlns
},
children: children, children: children,
); );
} }
factory Stanza.message({ String? to, String? from, String? type, String? id, List<XMLNode> children = const [], Map<String, String>? attributes = const {} }) { factory Stanza.message({
String? to,
String? from,
String? type,
String? id,
List<XMLNode> children = const [],
Map<String, String>? attributes = const {},
}) {
return Stanza( return Stanza(
tag: 'message', tag: 'message',
from: from, from: from,
to: to, to: to,
id: id, id: id,
type: type, type: type,
attributes: <String, String>{ attributes: <String, String>{...attributes!, 'xmlns': stanzaXmlns},
...attributes!,
'xmlns': stanzaXmlns
},
children: children, children: children,
); );
} }
@@ -70,8 +116,8 @@ class Stanza extends XMLNode {
children: node.children, children: node.children,
// TODO(Unknown): Remove to, from, id, and type // TODO(Unknown): Remove to, from, id, and type
// TODO(Unknown): Not sure if this is the correct way to approach this // TODO(Unknown): Not sure if this is the correct way to approach this
attributes: node.attributes attributes:
.map<String, String>((String key, dynamic value) { node.attributes.map<String, String>((String key, dynamic value) {
return MapEntry(key, value.toString()); return MapEntry(key, value.toString());
}), }),
); );
@@ -82,7 +128,13 @@ class Stanza extends XMLNode {
String? type; String? type;
String? id; String? id;
Stanza copyWith({ String? id, String? from, String? to, String? type, List<XMLNode>? children }) { Stanza copyWith({
String? id,
String? from,
String? to,
String? type,
List<XMLNode>? children,
}) {
return Stanza( return Stanza(
tag: tag, tag: tag,
to: to ?? this.to, to: to ?? this.to,
@@ -92,40 +144,29 @@ class Stanza extends XMLNode {
children: children ?? this.children, children: children ?? this.children,
); );
} }
Stanza reply({ List<XMLNode> children = const [] }) {
return copyWith(
from: attributes['to'] as String?,
to: attributes['from'] as String?,
type: tag == 'iq' ? 'result' : attributes['type'] as String?,
children: children,
);
} }
Stanza errorReply(String type, String condition, { String? text }) { /// Build an <error /> element with a child <[condition] type="[type]" />. If [text]
return copyWith( /// is not null, then the condition element will contain a <text /> element with [text]
from: attributes['to'] as String?, /// as the body.
to: attributes['from'] as String?, XMLNode buildErrorElement(String type, String condition, {String? text}) {
type: 'error', return XMLNode(
children: [
XMLNode(
tag: 'error', tag: 'error',
attributes: <String, dynamic>{'type': type}, attributes: <String, dynamic>{'type': type},
children: [ children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: condition, tag: condition,
xmlns: fullStanzaXmlns, xmlns: fullStanzaXmlns,
children: text != null ?[ children: text != null
? [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'text', tag: 'text',
xmlns: fullStanzaXmlns, xmlns: fullStanzaXmlns,
text: text, text: text,
) )
] : [], ]
) : [],
], ),
)
], ],
); );
} }
}

View File

@@ -1,7 +1,6 @@
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
class XMLNode { class XMLNode {
XMLNode({ XMLNode({
required this.tag, required this.tag,
this.attributes = const <String, dynamic>{}, this.attributes = const <String, dynamic>{},
@@ -17,7 +16,9 @@ class XMLNode {
this.children = const [], this.children = const [],
this.closeTag = true, this.closeTag = true,
this.text, this.text,
}) : attributes = <String, String>{ 'xmlns': xmlns, ...attributes }, isDeclaration = false; }) : attributes = <String, String>{'xmlns': xmlns, ...attributes},
isDeclaration = false;
/// Because this API is better ;) /// Because this API is better ;)
/// Don't use in production. Just for testing /// Don't use in production. Just for testing
factory XMLNode.fromXmlElement(XmlElement element) { factory XMLNode.fromXmlElement(XmlElement element) {
@@ -37,10 +38,12 @@ class XMLNode {
return XMLNode( return XMLNode(
tag: element.name.qualified, tag: element.name.qualified,
attributes: attributes, attributes: attributes,
children: element.childElements.toList().map(XMLNode.fromXmlElement).toList(), children:
element.childElements.toList().map(XMLNode.fromXmlElement).toList(),
); );
} }
} }
/// Just for testing purposes /// Just for testing purposes
factory XMLNode.fromString(String str) { factory XMLNode.fromString(String str) {
return XMLNode.fromXmlElement( return XMLNode.fromXmlElement(
@@ -63,7 +66,10 @@ class XMLNode {
String renderAttributes() { String renderAttributes() {
return attributes.keys.map((String key) { return attributes.keys.map((String key) {
final dynamic value = attributes[key]; final dynamic value = attributes[key];
assert(value is String || value is int, 'XML values must either be string or int'); assert(
value is String || value is int,
'XML values must either be string or int',
);
if (value is String) { if (value is String) {
return "$key='$value'"; return "$key='$value'";
} else { } else {
@@ -124,13 +130,22 @@ class XMLNode {
/// Returns all children whose tag is equal to [tag]. /// Returns all children whose tag is equal to [tag].
List<XMLNode> findTags(String tag, {String? xmlns}) { List<XMLNode> findTags(String tag, {String? xmlns}) {
return children.where((element) { return children.where((element) {
final xmlnsMatches = xmlns != null ? element.attributes['xmlns'] == xmlns : true; final xmlnsMatches =
xmlns != null ? element.attributes['xmlns'] == xmlns : true;
return element.tag == tag && xmlnsMatches; return element.tag == tag && xmlnsMatches;
}).toList(); }).toList();
} }
List<XMLNode> findTagsByXmlns(String xmlns) {
return children
.where((element) => element.attributes['xmlns'] == xmlns)
.toList();
}
/// Returns the inner text of the node. If none is set, returns the "". /// Returns the inner text of the node. If none is set, returns the "".
String innerText() { String innerText() {
return text ?? ''; return text ?? '';
} }
String? get xmlns => attributes['xmlns'] as String?;
} }

View File

@@ -1,19 +0,0 @@
/// A wrapper class that can be used to indicate that a function may return a valid
/// instance of [T] but may also fail.
/// The way [MayFail] is intended to be used to to have function specific - or application
/// specific - error codes that can be either handled by code or be translated into a
/// localised error message for the user.
class MayFail<T> {
MayFail({ this.result, this.errorCode });
MayFail.success(this.result);
MayFail.failure(this.errorCode);
T? result;
int? errorCode;
bool isError() => result == null && errorCode != null;
T getValue() => result!;
int getErrorCode() => errorCode!;
}

View File

@@ -1,13 +1,16 @@
/// Class that is supposed to by used with a state type S and a value type V. class Result<T, V> {
/// The state indicates if an action was successful or not, while the value const Result(this._data)
/// type indicates the return value, i.e. a result in a computation or the : assert(
/// actual error description. _data is T || _data is V,
class Result<S, V> { 'Invalid data type: Must be either $T or $V',
);
final dynamic _data;
Result(S state, V value) : _state = state, _value = value; bool isType<S>() => _data is S;
final S _state;
final V _value;
S getState() => _state; S get<S>() {
V getValue() => _value; assert(_data is S, 'Data is not $S');
return _data as S;
}
} }

View File

@@ -1,13 +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,56 @@
import 'dart:async';
import 'dart:collection';
import 'package:meta/meta.dart';
import 'package:synchronized/synchronized.dart';
/// A job to be submitted to an [AsyncQueue].
typedef AsyncQueueJob = Future<void> Function();
/// A (hopefully) async-safe queue that attempts to force
/// in-order execution of its jobs.
class AsyncQueue {
/// The lock for accessing [AsyncQueue._lock] and [AsyncQueue._running].
final Lock _lock = Lock();
/// The actual job queue.
final Queue<AsyncQueueJob> _queue = Queue<AsyncQueueJob>();
/// Indicates whether we are currently executing a job.
bool _running = false;
@visibleForTesting
Queue<AsyncQueueJob> get queue => _queue;
@visibleForTesting
bool get isRunning => _running;
/// Adds a job [job] to the queue.
Future<void> addJob(AsyncQueueJob job) async {
await _lock.synchronized(() {
_queue.add(job);
if (!_running && _queue.isNotEmpty) {
_running = true;
unawaited(_popJob());
}
});
}
Future<void> clear() async {
await _lock.synchronized(_queue.clear);
}
Future<void> _popJob() async {
final job = _queue.removeFirst();
final future = job();
await future;
await _lock.synchronized(() {
if (_queue.isNotEmpty) {
unawaited(_popJob());
} else {
_running = false;
}
});
}
}

View File

@@ -0,0 +1,67 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:synchronized/synchronized.dart';
/// This class allows for multiple asynchronous code places to wait on the
/// same computation of type [V], indentified by a key of type [K].
class WaitForTracker<K, V> {
/// The mapping of key -> Completer for the pending tasks.
final Map<K, List<Completer<V>>> _tracker = {};
/// The lock for accessing _tracker.
final Lock _lock = Lock();
/// Wait for a task with key [key]. If there was no such task already
/// present, returns null. If one or more tasks were already present, returns
/// a future that will resolve to the result of the first task.
Future<Future<V>?> waitFor(K key) async {
final result = await _lock.synchronized(() {
if (_tracker.containsKey(key)) {
// The task already exists. Just append outselves
final completer = Completer<V>();
_tracker[key]!.add(completer);
return completer;
}
// The task does not exist yet
_tracker[key] = List<Completer<V>>.empty(growable: true);
return null;
});
return result?.future;
}
/// Resolve a task with key [key] to [value].
Future<void> resolve(K key, V value) async {
await _lock.synchronized(() {
if (!_tracker.containsKey(key)) return;
for (final completer in _tracker[key]!) {
completer.complete(value);
}
_tracker.remove(key);
});
}
Future<void> resolveAll(V value) async {
await _lock.synchronized(() {
for (final key in _tracker.keys) {
for (final completer in _tracker[key]!) {
completer.complete(value);
}
}
});
}
/// Remove all tasks from the tracker.
Future<void> clear() async {
await _lock.synchronized(_tracker.clear);
}
@visibleForTesting
bool hasTasksRunning() => _tracker.isNotEmpty;
@visibleForTesting
List<Completer<V>> getRunningTasks(K key) => _tracker[key]!;
}

View File

@@ -8,17 +8,20 @@ const blurhashThumbnailType = '$fileThumbnailsXmlns:blurhash';
abstract class Thumbnail {} abstract class Thumbnail {}
class BlurhashThumbnail extends Thumbnail { class BlurhashThumbnail extends Thumbnail {
BlurhashThumbnail(this.hash); BlurhashThumbnail(this.hash);
final String hash; final String hash;
} }
Thumbnail? parseFileThumbnailElement(XMLNode node) { Thumbnail? parseFileThumbnailElement(XMLNode node) {
assert(node.attributes['xmlns'] == fileThumbnailsXmlns, 'Invalid element xmlns'); assert(
node.attributes['xmlns'] == fileThumbnailsXmlns,
'Invalid element xmlns',
);
assert(node.tag == 'file-thumbnail', 'Invalid element name'); assert(node.tag == 'file-thumbnail', 'Invalid element name');
switch (node.attributes['type']!) { switch (node.attributes['type']!) {
case blurhashThumbnailType: { case blurhashThumbnailType:
{
final hash = node.firstTag('blurhash')!.innerText(); final hash = node.firstTag('blurhash')!.innerText();
return BlurhashThumbnail(hash); return BlurhashThumbnail(hash);
} }

View File

@@ -0,0 +1,167 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
/// This event is triggered whenever a new FAST token is received.
class NewFASTTokenReceivedEvent extends XmppEvent {
NewFASTTokenReceivedEvent(this.token);
/// The token.
final FASTToken token;
}
/// This event is triggered whenever a new FAST token is invalidated because it's
/// invalid.
class InvalidateFASTTokenEvent extends XmppEvent {
InvalidateFASTTokenEvent();
}
/// The description of a token for FAST authentication.
class FASTToken {
const FASTToken(
this.token,
this.expiry,
);
factory FASTToken.fromXml(XMLNode token) {
assert(
token.tag == 'token',
'Token can only be deserialised from a <token /> element',
);
assert(
token.xmlns == fastXmlns,
'Token can only be deserialised from a <token /> element',
);
return FASTToken(
token.attributes['token']! as String,
token.attributes['expiry']! as String,
);
}
/// The actual token.
final String token;
/// The token's expiry.
final String expiry;
}
// TODO(Unknown): Implement multiple hash functions, similar to how we do SCRAM
class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator {
FASTSaslNegotiator() : super(20, saslFASTNegotiator, 'HT-SHA-256-NONE');
final Logger _log = Logger('FASTSaslNegotiator');
/// The token, if non-null, to use for authentication.
FASTToken? fastToken;
@override
bool matchesFeature(List<XMLNode> features) {
if (fastToken == null) {
return false;
}
if (super.matchesFeature(features)) {
if (!attributes.getSocket().isSecure()) {
_log.warning(
'Refusing to match SASL feature due to unsecured connection',
);
return false;
}
return true;
}
return false;
}
@override
bool canInlineFeature(List<XMLNode> features) {
return features.firstWhereOrNull(
(child) => child.tag == 'fast' && child.xmlns == fastXmlns,
) !=
null;
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
// TODO(Unknown): Is FAST supposed to work without SASL2?
return const Result(NegotiatorState.done);
}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
final token = response.firstTag('token', xmlns: fastXmlns);
if (token != null) {
fastToken = FASTToken.fromXml(token);
await attributes.sendEvent(
NewFASTTokenReceivedEvent(fastToken!),
);
}
state = NegotiatorState.done;
return const Result(true);
}
@override
Future<void> onSasl2Failure(XMLNode response) async {
fastToken = null;
await attributes.sendEvent(
InvalidateFASTTokenEvent(),
);
}
@override
bool shouldRetrySasl() => true;
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
if (fastToken != null && pickedForSasl2) {
// Specify that we are using a token
return [
// As we don't do TLS 0-RTT, we don't have to specify `count`.
XMLNode.xmlns(
tag: 'fast',
xmlns: fastXmlns,
),
];
}
// Only request a new token when we don't already have one and we are not picked
// for SASL
if (!pickedForSasl2) {
return [
XMLNode.xmlns(
tag: 'request-token',
xmlns: fastXmlns,
attributes: {
'mechanism': 'HT-SHA-256-NONE',
},
),
];
} else {
return [];
}
}
@override
Future<String> getRawStep(String input) async {
return fastToken!.token;
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerSaslNegotiator(this);
}
}

View File

@@ -11,13 +11,7 @@ import 'package:moxxmpp/src/xeps/xep_0446.dart';
const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0'; const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0';
class FileUploadNotificationManager extends XmppManagerBase { class FileUploadNotificationManager extends XmppManagerBase {
FileUploadNotificationManager() : super(); FileUploadNotificationManager() : super(fileUploadNotificationManager);
@override
String getId() => fileUploadNotificationManager;
@override
String getName() => 'FileUploadNotificationManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -47,8 +41,12 @@ class FileUploadNotificationManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onFileUploadNotificationReceived(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onFileUploadNotificationReceived(
final funElement = message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!; Stanza message,
StanzaHandlerData state,
) async {
final funElement =
message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith( return state.copyWith(
fun: FileMetadataData.fromXML( fun: FileMetadataData.fromXML(
funElement.firstTag('file', xmlns: fileMetadataXmlns)!, funElement.firstTag('file', xmlns: fileMetadataXmlns)!,
@@ -56,15 +54,23 @@ class FileUploadNotificationManager extends XmppManagerBase {
); );
} }
Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived(
final element = message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!; Stanza message,
StanzaHandlerData state,
) async {
final element =
message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith( return state.copyWith(
funReplacement: element.attributes['id']! as String, funReplacement: element.attributes['id']! as String,
); );
} }
Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived(
final element = message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!; Stanza message,
StanzaHandlerData state,
) async {
final element =
message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!;
return state.copyWith( return state.copyWith(
funCancellation: element.attributes['id']! as String, funCancellation: element.attributes['id']! as String,
); );

View File

@@ -3,7 +3,6 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
class DataFormOption { class DataFormOption {
const DataFormOption({required this.value, this.label}); const DataFormOption({required this.value, this.label});
final String? label; final String? label;
final String value; final String value;
@@ -11,7 +10,9 @@ class DataFormOption {
XMLNode toXml() { XMLNode toXml() {
return XMLNode( return XMLNode(
tag: 'option', tag: 'option',
attributes: label != null ? <String, dynamic>{ 'label': label } : <String, dynamic>{}, attributes: label != null
? <String, dynamic>{'label': label}
: <String, dynamic>{},
children: [ children: [
XMLNode( XMLNode(
tag: 'value', tag: 'value',
@@ -23,7 +24,6 @@ class DataFormOption {
} }
class DataFormField { class DataFormField {
const DataFormField({ const DataFormField({
required this.options, required this.options,
required this.values, required this.values,
@@ -45,9 +45,13 @@ class DataFormField {
return XMLNode( return XMLNode(
tag: 'field', tag: 'field',
attributes: <String, dynamic>{ attributes: <String, dynamic>{
...varAttr != null ? <String, dynamic>{ 'var': varAttr } : <String, dynamic>{}, ...varAttr != null
? <String, dynamic>{'var': varAttr}
: <String, dynamic>{},
...type != null ? <String, dynamic>{'type': type} : <String, dynamic>{}, ...type != null ? <String, dynamic>{'type': type} : <String, dynamic>{},
...label != null ? <String, dynamic>{ 'label': label } : <String, dynamic>{} ...label != null
? <String, dynamic>{'label': label}
: <String, dynamic>{}
}, },
children: [ children: [
...description != null ? [XMLNode(tag: 'desc', text: description)] : [], ...description != null ? [XMLNode(tag: 'desc', text: description)] : [],
@@ -60,7 +64,6 @@ class DataFormField {
} }
class DataForm { class DataForm {
const DataForm({ const DataForm({
required this.type, required this.type,
required this.instructions, required this.instructions,
@@ -84,18 +87,18 @@ class DataForm {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'x', tag: 'x',
xmlns: dataFormsXmlns, xmlns: dataFormsXmlns,
attributes: { attributes: {'type': type},
'type': type
},
children: [ children: [
...instructions.map((i) => XMLNode(tag: 'instruction', text: i)), ...instructions.map((i) => XMLNode(tag: 'instruction', text: i)),
...title != null ? [XMLNode(tag: 'title', text: title)] : [], ...title != null ? [XMLNode(tag: 'title', text: title)] : [],
...fields.map((field) => field.toXml()), ...fields.map((field) => field.toXml()),
...reported.map((report) => report.toXml()), ...reported.map((report) => report.toXml()),
...items.map((item) => XMLNode( ...items.map(
(item) => XMLNode(
tag: 'item', tag: 'item',
children: item.map((i) => i.toXml()).toList(), children: item.map((i) => i.toXml()).toList(),
),), ),
),
], ],
); );
} }
@@ -131,10 +134,19 @@ DataForm parseDataForm(XMLNode x) {
final type = x.attributes['type']! as String; final type = x.attributes['type']! as String;
final title = x.firstTag('title')?.innerText(); final title = x.firstTag('title')?.innerText();
final instructions = x.findTags('instructions').map((i) => i.innerText()).toList(); final instructions =
x.findTags('instructions').map((i) => i.innerText()).toList();
final fields = x.findTags('field').map(_parseDataFormField).toList(); final fields = x.findTags('field').map(_parseDataFormField).toList();
final reported = x.firstTag('reported')?.findTags('field').map((i) => _parseDataFormField(i.firstTag('field')!)).toList() ?? []; final reported = x
final items = x.findTags('item').map((i) => i.findTags('field').map(_parseDataFormField).toList()).toList(); .firstTag('reported')
?.findTags('field')
.map((i) => _parseDataFormField(i.firstTag('field')!))
.toList() ??
[];
final items = x
.findTags('item')
.map((i) => i.findTags('field').map(_parseDataFormField).toList())
.toList();
return DataForm( return DataForm(
type: type, type: type,

View File

@@ -0,0 +1,21 @@
import 'package:meta/meta.dart';
@internal
@immutable
class DiscoCacheKey {
const DiscoCacheKey(this.jid, this.node);
/// The JID we're requesting disco data from.
final String jid;
/// Optionally the node we are requesting from.
final String? node;
@override
bool operator ==(Object other) {
return other is DiscoCacheKey && jid == other.jid && node == other.node;
}
@override
int get hashCode => jid.hashCode ^ node.hashCode;
}

View File

@@ -5,21 +5,29 @@ 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(String entity, String? node) {
return Stanza.iq(to: entity, type: 'get', children: [ return Stanza.iq(
to: entity,
type: 'get',
children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoInfoXmlns, xmlns: discoInfoXmlns,
attributes: node != null ? {'node': node} : {}, attributes: node != null ? {'node': node} : {},
) )
],); ],
);
} }
Stanza buildDiscoItemsQueryStanza(String entity, {String? node}) { Stanza buildDiscoItemsQueryStanza(String entity, {String? node}) {
return Stanza.iq(to: entity, type: 'get', children: [ return Stanza.iq(
to: entity,
type: 'get',
children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoItemsXmlns, xmlns: discoItemsXmlns,
attributes: node != null ? {'node': node} : {}, attributes: node != null ? {'node': node} : {},
) )
],); ],
);
} }

View File

@@ -1,10 +1,16 @@
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0004.dart'; import 'package:moxxmpp/src/xeps/xep_0004.dart';
class Identity { class Identity {
const Identity({
const Identity({ required this.category, required this.type, this.name, this.lang }); required this.category,
required this.type,
this.name,
this.lang,
});
final String category; final String category;
final String type; final String type;
final String? name; final String? name;
@@ -17,30 +23,109 @@ class Identity {
'category': category, 'category': category,
'type': type, 'type': type,
'name': name, 'name': name,
...lang == null ? <String, dynamic>{} : <String, dynamic>{ 'xml:lang': lang } ...lang == null
? <String, dynamic>{}
: <String, dynamic>{'xml:lang': lang}
}, },
); );
} }
} }
@immutable
class DiscoInfo { class DiscoInfo {
const DiscoInfo( const DiscoInfo(
this.features, this.features,
this.identities, this.identities,
this.extendedInfo, this.extendedInfo,
this.node,
this.jid, this.jid,
); );
factory DiscoInfo.fromQuery(XMLNode query, JID jid) {
final features = List<String>.empty(growable: true);
final identities = List<Identity>.empty(growable: true);
final extendedInfo = List<DataForm>.empty(growable: true);
for (final element in query.children) {
if (element.tag == 'feature') {
features.add(element.attributes['var']! as String);
} else if (element.tag == 'identity') {
identities.add(
Identity(
category: element.attributes['category']! as String,
type: element.attributes['type']! as String,
name: element.attributes['name'] as String?,
),
);
} else if (element.tag == 'x' &&
element.attributes['xmlns'] == dataFormsXmlns) {
extendedInfo.add(
parseDataForm(element),
);
}
}
return DiscoInfo(
features,
identities,
extendedInfo,
query.attributes['node'] as String?,
jid,
);
}
final List<String> features; final List<String> features;
final List<Identity> identities; final List<Identity> identities;
final List<DataForm> extendedInfo; final List<DataForm> extendedInfo;
final JID jid; final String? node;
final JID? jid;
XMLNode toXml() {
return XMLNode.xmlns(
tag: 'query',
xmlns: discoInfoXmlns,
attributes: node != null
? <String, String>{
'node': node!,
}
: <String, String>{},
children: [
...identities.map((identity) => identity.toXMLNode()),
...features.map(
(feature) => XMLNode(
tag: 'feature',
attributes: {
'var': feature,
},
),
),
if (extendedInfo.isNotEmpty) ...extendedInfo.map((ei) => ei.toXml()),
],
);
}
} }
@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 String jid;
final String? node; final String? node;
final String? name; final String? name;
XMLNode toXml() {
final attributes = {
'jid': jid,
};
if (node != null) {
attributes['node'] = node!;
}
if (name != null) {
attributes['name'] = name!;
}
return XMLNode(
tag: 'node',
attributes: attributes,
);
}
} }

View File

@@ -1,5 +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: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,62 +8,74 @@ 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/namespaces.dart';
import 'package:moxxmpp/src/presence.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/resultv2.dart'; import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0004.dart'; import 'package:moxxmpp/src/util/wait.dart';
import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/helpers.dart'; import 'package:moxxmpp/src/xeps/xep_0030/helpers.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_0115.dart'; import 'package:moxxmpp/src/xeps/xep_0115.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
@immutable /// Callback that is called when a disco#info requests is received on a given node.
class DiscoCacheKey { typedef DiscoInfoRequestCallback = Future<DiscoInfo> Function();
const DiscoCacheKey(this.jid, this.node); /// Callback that is called when a disco#items requests is received on a given node.
final String jid; typedef DiscoItemsRequestCallback = Future<List<DiscoItem>> Function();
final String? node;
@override
bool operator ==(Object other) {
return other is DiscoCacheKey && jid == other.jid && node == other.node;
}
@override
int get hashCode => jid.hashCode ^ node.hashCode;
}
/// This manager implements XEP-0030 by providing a way of performing disco#info and
/// disco#items requests and answering those requests.
/// A caching mechanism is also provided.
class DiscoManager extends XmppManagerBase { class DiscoManager extends XmppManagerBase {
/// [identities] is a list of disco identities that should be added by default
/// to a disco#info response.
DiscoManager(List<Identity> identities)
: _identities = List<Identity>.from(identities),
super(discoManager);
DiscoManager()
: _features = List.empty(growable: true),
_capHashCache = {},
_capHashInfoCache = {},
_discoInfoCache = {},
_runningInfoQueries = {},
_cacheLock = Lock(),
super();
/// Our features /// Our features
final List<String> _features; final List<String> _features = List.empty(growable: true);
// Map full JID to Capability hashes /// Disco identities that we advertise
final Map<String, CapabilityHashInfo> _capHashCache; final List<Identity> _identities;
// Map capability hash to the disco info
final Map<String, DiscoInfo> _capHashInfoCache; /// Map full JID to Capability hashes
// Map full JID to Disco Info final Map<String, CapabilityHashInfo> _capHashCache = {};
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache;
// Mapping the full JID to a list of running requests /// Map capability hash to the disco info
final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries; final Map<String, DiscoInfo> _capHashInfoCache = {};
// Cache lock
final Lock _cacheLock; /// Map full JID to Disco Info
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
/// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>>
_discoInfoTracker = WaitForTracker();
/// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, List<DiscoItem>>>
_discoItemsTracker = WaitForTracker();
/// Cache lock
final Lock _cacheLock = Lock();
/// disco#info callbacks: node -> Callback
final Map<String, DiscoInfoRequestCallback> _discoInfoCallbacks = {};
/// disco#items callbacks: node -> Callback
final Map<String, DiscoItemsRequestCallback> _discoItemsCallbacks = {};
/// The list of identities that are registered.
List<Identity> get identities => _identities;
/// The list of disco features that are registered.
List<String> get features => _features;
@visibleForTesting @visibleForTesting
bool hasInfoQueriesRunning() => _runningInfoQueries.isNotEmpty; WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>>
get infoTracker => _discoInfoTracker;
@visibleForTesting
List<Completer<Result<DiscoError, DiscoInfo>>> getRunningInfoQueries(DiscoCacheKey key) => _runningInfoQueries[key]!;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -80,12 +93,6 @@ class DiscoManager extends XmppManagerBase {
), ),
]; ];
@override
String getId() => discoManager;
@override
String getName() => 'DiscoManager';
@override @override
List<String> getDiscoFeatures() => [discoInfoXmlns, discoItemsXmlns]; List<String> getDiscoFeatures() => [discoInfoXmlns, discoItemsXmlns];
@@ -96,7 +103,21 @@ class DiscoManager extends XmppManagerBase {
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is PresenceReceivedEvent) { if (event is PresenceReceivedEvent) {
await _onPresence(event.jid, event.presence); await _onPresence(event.jid, event.presence);
} else if (event is StreamResumeFailedEvent) { } 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;
// Cancel all waiting requests
await _discoInfoTracker.resolveAll(
Result<DiscoError, DiscoInfo>(UnknownDiscoError()),
);
await _discoItemsTracker.resolveAll(
Result<DiscoError, List<DiscoItem>>(UnknownDiscoError()),
);
await _cacheLock.synchronized(() async { await _cacheLock.synchronized(() async {
// Clear the cache // Clear the cache
_discoInfoCache.clear(); _discoInfoCache.clear();
@@ -104,9 +125,19 @@ class DiscoManager extends XmppManagerBase {
} }
} }
/// Register a callback [callback] for a disco#info query on [node].
void registerInfoCallback(String node, DiscoInfoRequestCallback callback) {
_discoInfoCallbacks[node] = callback;
}
/// Register a callback [callback] for a disco#items query on [node].
void registerItemsCallback(String node, DiscoItemsRequestCallback callback) {
_discoItemsCallbacks[node] = callback;
}
/// 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 addDiscoFeatures(List<String> features) { void addFeatures(List<String> features) {
for (final feat in features) { for (final feat in features) {
if (!_features.contains(feat)) { if (!_features.contains(feat)) {
_features.add(feat); _features.add(feat);
@@ -114,6 +145,16 @@ class DiscoManager extends XmppManagerBase {
} }
} }
/// Adds a list of identities to the possible disco info response.
/// This function only adds features that are not already present in the disco features.
void addIdentities(List<Identity> identities) {
for (final identity in identities) {
if (!_identities.contains(identity)) {
_identities.add(identity);
}
}
}
Future<void> _onPresence(JID from, Stanza presence) async { Future<void> _onPresence(JID from, Stanza presence) async {
final c = presence.firstTag('c', xmlns: capsXmlns); final c = presence.firstTag('c', xmlns: capsXmlns);
if (c == null) return; if (c == null) return;
@@ -134,8 +175,11 @@ class DiscoManager extends XmppManagerBase {
if (cached) return; if (cached) return;
// Request the cap hash // Request the cap hash
logger.finest("Received capability hash we don't know about. Requesting it..."); logger.finest(
final result = await discoInfoQuery(from.toString(), node: '${info.node}#${info.ver}'); "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; if (result.isType<DiscoError>()) return;
await _cacheLock.synchronized(() async { await _cacheLock.synchronized(() async {
@@ -144,178 +188,130 @@ class DiscoManager extends XmppManagerBase {
}); });
} }
/// Returns the list of disco features registered. /// Returns the [DiscoInfo] object that would be used as the response to a disco#info
List<String> getRegisteredDiscoFeatures() => _features; /// query against our bare JID with no node. The results node attribute is set
/// to [node].
DiscoInfo getDiscoInfo(String? node) {
return DiscoInfo(
_features,
_identities,
const [],
node,
null,
);
}
/// May be overriden. Specifies the identities which will be returned in a disco info response. Future<StanzaHandlerData> _onDiscoInfoRequest(
List<Identity> getIdentities() => const [ Identity(category: 'client', type: 'pc', name: 'moxxmpp', lang: 'en') ]; Stanza stanza,
StanzaHandlerData state,
Future<StanzaHandlerData> _onDiscoInfoRequest(Stanza stanza, StanzaHandlerData state) async { ) async {
if (stanza.type != 'get') return state; if (stanza.type != 'get') return state;
final presence = getAttributes().getManagerById(presenceManager)! as PresenceManager; final query = stanza.firstTag('query', xmlns: discoInfoXmlns)!;
final query = stanza.firstTag('query')!;
final node = query.attributes['node'] as String?; final node = query.attributes['node'] as String?;
final capHash = await presence.getCapabilityHash();
final isCapabilityNode = node == 'http://moxxy.im#$capHash';
if (!isCapabilityNode && node != null) { if (_discoInfoCallbacks.containsKey(node)) {
await getAttributes().sendStanza(Stanza.iq( // We can now assume that node != null
to: stanza.from, final result = await _discoInfoCallbacks[node]!();
from: stanza.to, await reply(
id: stanza.id, state,
type: 'error', 'result',
children: [ [
XMLNode.xmlns( result.toXml(),
tag: 'query',
// TODO(PapaTutuWawa): Why are we copying the xmlns?
xmlns: query.attributes['xmlns']! as String,
attributes: <String, String>{
'node': node
},
),
XMLNode(
tag: 'error',
attributes: <String, String>{
'type': 'cancel'
},
children: [
XMLNode.xmlns(
tag: 'not-allowed',
xmlns: fullStanzaXmlns,
)
], ],
)
],
)
,);
return state.copyWith(done: true);
}
await getAttributes().sendStanza(stanza.reply(
children: [
XMLNode.xmlns(
tag: 'query',
xmlns: discoInfoXmlns,
attributes: {
...!isCapabilityNode ? {} : {
'node': 'http://moxxy.im#$capHash'
}
},
children: [
...getIdentities().map((identity) => identity.toXMLNode()),
..._features.map((feat) {
return XMLNode(
tag: 'feature',
attributes: <String, dynamic>{ 'var': feat },
); );
}),
],
),
],
),);
return state.copyWith(done: true); return state.copyWith(done: true);
} }
Future<StanzaHandlerData> _onDiscoItemsRequest(Stanza stanza, StanzaHandlerData state) async { await reply(
state,
'result',
[
getDiscoInfo(node).toXml(),
],
);
return state.copyWith(done: true);
}
Future<StanzaHandlerData> _onDiscoItemsRequest(
Stanza stanza,
StanzaHandlerData state,
) async {
if (stanza.type != 'get') return state; if (stanza.type != 'get') return state;
final query = stanza.firstTag('query')!; final query = stanza.firstTag('query', xmlns: discoItemsXmlns)!;
if (query.attributes['node'] != null) { final node = query.attributes['node'] as String?;
// TODO(Unknown): Handle the node we specified for XEP-0115 if (_discoItemsCallbacks.containsKey(node)) {
await getAttributes().sendStanza( final result = await _discoItemsCallbacks[node]!();
Stanza.iq( await reply(
to: stanza.from, state,
from: stanza.to, 'result',
id: stanza.id, [
type: 'error',
children: [
XMLNode.xmlns(
tag: 'query',
// TODO(PapaTutuWawa): Why copy the xmlns?
xmlns: query.attributes['xmlns']! as String,
attributes: <String, String>{
'node': query.attributes['node']! as String,
},
),
XMLNode(
tag: 'error',
attributes: <String, dynamic>{
'type': 'cancel'
},
children: [
XMLNode.xmlns(
tag: 'not-allowed',
xmlns: fullStanzaXmlns,
),
],
),
],
),
);
return state.copyWith(done: true);
}
await getAttributes().sendStanza(
stanza.reply(
children: [
XMLNode.xmlns( XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoItemsXmlns, xmlns: discoItemsXmlns,
attributes: <String, String>{
'node': node!,
},
children: result.map((item) => item.toXml()).toList(),
), ),
], ],
),
); );
return state.copyWith(done: true); return state.copyWith(done: true);
} }
Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async { return state;
return _cacheLock.synchronized(() async {
// Complete all futures
for (final completer in _runningInfoQueries[key]!) {
completer.complete(result);
} }
Future<void> _exitDiscoInfoCriticalSection(
DiscoCacheKey key,
Result<DiscoError, DiscoInfo> result,
) 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>()) {
_discoInfoCache[key] = result.get<DiscoInfo>(); _discoInfoCache[key] = result.get<DiscoInfo>();
} }
// Remove from the request cache
_runningInfoQueries.remove(key);
}); });
await _discoInfoTracker.resolve(key, result);
} }
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node]. /// Sends a disco info query to the (full) jid [entity], optionally with node=[node].
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node}) async { Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(
String entity, {
String? node,
bool shouldEncrypt = true,
}) async {
final cacheKey = DiscoCacheKey(entity, node); final cacheKey = DiscoCacheKey(entity, node);
DiscoInfo? info; DiscoInfo? info;
Completer<Result<DiscoError, DiscoInfo>>? completer; final ffuture = await _cacheLock
await _cacheLock.synchronized(() async { .synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>(
() 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;
} else { } else {
// Is a request running? return _discoInfoTracker.waitFor(cacheKey);
if (_runningInfoQueries.containsKey(cacheKey)) {
completer = Completer();
_runningInfoQueries[cacheKey]!.add(completer!);
} else {
_runningInfoQueries[cacheKey] = List.from(<Completer<DiscoInfo?>>[]);
}
} }
}); });
if (info != null) { if (info != null) {
return Result<DiscoError, DiscoInfo>(info); return Result<DiscoError, DiscoInfo>(info);
} else if (completer != null) { } else {
return completer!.future; final future = await ffuture;
if (future != null) {
return future;
}
} }
final stanza = await getAttributes().sendStanza( final stanza = await getAttributes().sendStanza(
buildDiscoInfoQueryStanza(entity, node), buildDiscoInfoQueryStanza(entity, node),
encrypted: !shouldEncrypt,
); );
final query = stanza.firstTag('query'); final query = stanza.firstTag('query');
if (query == null) { if (query == null) {
@@ -324,34 +320,17 @@ class DiscoManager extends XmppManagerBase {
return result; return result;
} }
final error = stanza.firstTag('error'); if (stanza.attributes['type'] == 'error') {
if (error != null && stanza.attributes['type'] == 'error') { //final error = stanza.firstTag('error');
final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError()); final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError());
await _exitDiscoInfoCriticalSection(cacheKey, result); await _exitDiscoInfoCriticalSection(cacheKey, result);
return result; return result;
} }
final features = List<String>.empty(growable: true);
final identities = List<Identity>.empty(growable: true);
for (final element in query.children) {
if (element.tag == 'feature') {
features.add(element.attributes['var']! as String);
} else if (element.tag == 'identity') {
identities.add(Identity(
category: element.attributes['category']! as String,
type: element.attributes['type']! as String,
name: element.attributes['name'] as String?,
),);
}
}
final result = Result<DiscoError, DiscoInfo>( final result = Result<DiscoError, DiscoInfo>(
DiscoInfo( DiscoInfo.fromQuery(
features, query,
identities, JID.fromString(entity),
query.findTags('x', xmlns: dataFormsXmlns).map(parseDataForm).toList(),
JID.fromString(stanza.attributes['from']! as String),
), ),
); );
await _exitDiscoInfoCriticalSection(cacheKey, result); await _exitDiscoInfoCriticalSection(cacheKey, result);
@@ -359,30 +338,61 @@ class DiscoManager extends XmppManagerBase {
} }
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node]. /// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, { String? node }) async { Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(
final stanza = await getAttributes() String entity, {
.sendStanza(buildDiscoItemsQueryStanza(entity, node: node)) as Stanza; String? node,
bool shouldEncrypt = true,
final query = stanza.firstTag('query'); }) async {
if (query == null) return Result(InvalidResponseDiscoError()); final key = DiscoCacheKey(entity, node);
final future = await _discoItemsTracker.waitFor(key);
final error = stanza.firstTag('error'); if (future != null) {
if (error != null && stanza.type == 'error') { return future;
//print("Disco Items error: " + error.toXml());
return Result(ErrorResponseDiscoError());
} }
final items = query.findTags('item').map((node) => DiscoItem( final stanza = await getAttributes().sendStanza(
buildDiscoItemsQueryStanza(entity, node: node),
encrypted: !shouldEncrypt,
) as Stanza;
final query = stanza.firstTag('query');
if (query == null) {
final result =
Result<DiscoError, List<DiscoItem>>(InvalidResponseDiscoError());
await _discoItemsTracker.resolve(key, 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
.findTags('item')
.map(
(node) => DiscoItem(
jid: node.attributes['jid']! as String, jid: 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?,
),).toList(); ),
)
.toList();
return Result(items); final result = Result<DiscoError, List<DiscoItem>>(items);
await _discoItemsTracker.resolve(key, result);
return result;
} }
/// Queries information about a jid based on its node and capability hash. /// Queries information about a jid based on its node and capability hash.
Future<Result<DiscoError, DiscoInfo>> discoInfoCapHashQuery(String jid, String node, String ver) async { Future<Result<DiscoError, DiscoInfo>> discoInfoCapHashQuery(
String jid,
String node,
String ver,
) async {
return discoInfoQuery(jid, node: '$node#$ver'); return discoInfoQuery(jid, node: '$node#$ver');
} }

View File

@@ -7,15 +7,20 @@ 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 {}
class UnknownVCardError extends VCardError {}
class InvalidVCardError extends VCardError {}
class VCardPhoto { class VCardPhoto {
const VCardPhoto({this.binval}); const VCardPhoto({this.binval});
final String? binval; final String? binval;
} }
class VCard { class VCard {
const VCard({this.nickname, this.url, this.photo}); const VCard({this.nickname, this.url, this.photo});
final String? nickname; final String? nickname;
final String? url; final String? url;
@@ -23,15 +28,8 @@ class VCard {
} }
class VCardManager extends XmppManagerBase { class VCardManager extends XmppManagerBase {
VCardManager() : super(vcardManager);
VCardManager() : _lastHash = {}, super(); final Map<String, String> _lastHash = {};
final Map<String, String> _lastHash;
@override
String getId() => vcardManager;
@override
String getName() => 'vCardManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -51,7 +49,10 @@ class VCardManager extends XmppManagerBase {
_lastHash[jid] = hash; _lastHash[jid] = hash;
} }
Future<StanzaHandlerData> _onPresence(Stanza presence, StanzaHandlerData state) async { Future<StanzaHandlerData> _onPresence(
Stanza presence,
StanzaHandlerData state,
) async {
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();
@@ -59,12 +60,18 @@ class VCardManager extends XmppManagerBase {
final lastHash = _lastHash[from]; final lastHash = _lastHash[from];
if (lastHash != hash) { if (lastHash != hash) {
_lastHash[from] = hash; _lastHash[from] = hash;
final vcard = await requestVCard(from); final vcardResult = await requestVCard(from);
if (vcard != null) { if (vcardResult.isType<VCard>()) {
final binval = vcard.photo?.binval; final binval = vcardResult.get<VCard>().photo?.binval;
if (binval != null) { if (binval != null) {
getAttributes().sendEvent(AvatarUpdatedEvent(jid: from, base64: binval, hash: hash)); getAttributes().sendEvent(
AvatarUpdatedEvent(
jid: from,
base64: binval,
hash: hash,
),
);
} else { } else {
logger.warning('No avatar data found'); logger.warning('No avatar data found');
} }
@@ -95,7 +102,7 @@ class VCardManager extends XmppManagerBase {
); );
} }
Future<VCard?> requestVCard(String jid) async { Future<Result<VCardError, VCard>> requestVCard(String jid) async {
final result = await getAttributes().sendStanza( final result = await getAttributes().sendStanza(
Stanza.iq( Stanza.iq(
to: jid, to: jid,
@@ -107,12 +114,17 @@ class VCardManager extends XmppManagerBase {
) )
], ],
), ),
encrypted: true,
); );
if (result.attributes['type'] != 'result') return null; if (result.attributes['type'] != 'result') {
return Result(UnknownVCardError());
}
final vcard = result.firstTag('vCard', xmlns: vCardTempXmlns); final vcard = result.firstTag('vCard', xmlns: vCardTempXmlns);
if (vcard == null) return null; if (vcard == null) {
return Result(UnknownVCardError());
}
return _parseVCard(vcard); return Result(_parseVCard(vcard));
} }
} }

View File

@@ -1,3 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:meta/meta.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 +9,7 @@ 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/resultv2.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';
@@ -16,7 +18,6 @@ import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0060/helpers.dart'; import 'package:moxxmpp/src/xeps/xep_0060/helpers.dart';
class PubSubPublishOptions { class PubSubPublishOptions {
const PubSubPublishOptions({ const PubSubPublishOptions({
this.accessModel, this.accessModel,
this.maxItems, this.maxItems,
@@ -38,30 +39,37 @@ class PubSubPublishOptions {
varAttr: 'FORM_TYPE', varAttr: 'FORM_TYPE',
type: 'hidden', type: 'hidden',
), ),
...accessModel != null ? [ ...accessModel != null
? [
DataFormField( DataFormField(
options: [], options: [],
isRequired: false, isRequired: false,
values: [accessModel!], values: [accessModel!],
varAttr: 'pubsub#access_model', varAttr: 'pubsub#access_model',
) )
] : [], ]
...maxItems != null ? [ : [],
...maxItems != null
? [
DataFormField( DataFormField(
options: [], options: [],
isRequired: false, isRequired: false,
values: [maxItems!], values: [maxItems!],
varAttr: 'pubsub#max_items', varAttr: 'pubsub#max_items',
), ),
] : [], ]
: [],
], ],
).toXml(); ).toXml();
} }
} }
class PubSubItem { class PubSubItem {
const PubSubItem({
const PubSubItem({ required this.id, required this.node, required this.payload }); required this.id,
required this.node,
required this.payload,
});
final String id; final String id;
final String node; final String node;
final XMLNode payload; final XMLNode payload;
@@ -71,11 +79,7 @@ class PubSubItem {
} }
class PubSubManager extends XmppManagerBase { class PubSubManager extends XmppManagerBase {
@override PubSubManager() : super(pubsubManager);
String getId() => pubsubManager;
@override
String getName() => 'PubsubManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -90,20 +94,25 @@ class PubSubManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onPubsubMessage(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onPubsubMessage(
Stanza message,
StanzaHandlerData state,
) async {
logger.finest('Received PubSub event'); logger.finest('Received PubSub event');
final event = message.firstTag('event', xmlns: pubsubEventXmlns)!; final event = message.firstTag('event', xmlns: pubsubEventXmlns)!;
final items = event.firstTag('items')!; final items = event.firstTag('items')!;
final item = items.firstTag('item')!; final item = items.firstTag('item')!;
getAttributes().sendEvent(PubSubNotificationEvent( getAttributes().sendEvent(
PubSubNotificationEvent(
item: PubSubItem( item: PubSubItem(
id: item.attributes['id']! as String, id: item.attributes['id']! as String,
node: items.attributes['node']! as String, node: items.attributes['node']! as String,
payload: item.children[0], payload: item.children[0],
), ),
from: message.attributes['from']! as String, from: message.attributes['from']! as String,
),); ),
);
return state.copyWith(done: true); return state.copyWith(done: true);
} }
@@ -113,7 +122,9 @@ class PubSubManager extends XmppManagerBase {
final response = await dm.discoItemsQuery(jid, node: node); final response = await dm.discoItemsQuery(jid, node: node);
var count = 0; var count = 0;
if (response.isType<DiscoError>()) { if (response.isType<DiscoError>()) {
logger.warning('_getNodeItemCount: disco#items query failed. Assuming no items.'); logger.warning(
'_getNodeItemCount: disco#items query failed. Assuming no items.',
);
} else { } else {
count = response.get<List<DiscoItem>>().length; count = response.get<List<DiscoItem>>().length;
} }
@@ -121,19 +132,30 @@ class PubSubManager extends XmppManagerBase {
return count; return count;
} }
Future<PubSubPublishOptions> _preprocessPublishOptions(String jid, String node, PubSubPublishOptions options) async { // TODO(PapaTutuWawa): This should return a Result<T> in case we cannot proceed
// with the requested configuration.
@visibleForTesting
Future<PubSubPublishOptions> preprocessPublishOptions(
String jid,
String node,
PubSubPublishOptions options,
) async {
if (options.maxItems != null) { if (options.maxItems != null) {
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!; final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
final result = await dm.discoInfoQuery(jid); final result = await dm.discoInfoQuery(jid);
if (result.isType<DiscoError>()) { if (result.isType<DiscoError>()) {
if (options.maxItems == 'max') { if (options.maxItems == 'max') {
logger.severe('disco#info query failed and options.maxItems is set to "max".'); logger.severe(
'disco#info query failed and options.maxItems is set to "max".',
);
return options; return options;
} }
} }
final nodeMultiItemsSupported = result.isType<DiscoInfo>() && result.get<DiscoInfo>().features.contains(pubsubNodeConfigMultiItems); final nodeMultiItemsSupported = result.isType<DiscoInfo>() &&
final nodeMaxSupported = result.isType<DiscoInfo>() && result.get<DiscoInfo>().features.contains(pubsubNodeConfigMax); result.get<DiscoInfo>().features.contains(pubsubNodeConfigMultiItems);
final nodeMaxSupported = result.isType<DiscoInfo>() &&
result.get<DiscoInfo>().features.contains(pubsubNodeConfigMax);
if (options.maxItems != null && !nodeMultiItemsSupported) { if (options.maxItems != null && !nodeMultiItemsSupported) {
// TODO(PapaTutuWawa): Here, we need to admit defeat // TODO(PapaTutuWawa): Here, we need to admit defeat
logger.finest('PubSub host does not support multi-items!'); logger.finest('PubSub host does not support multi-items!');
@@ -142,7 +164,9 @@ class PubSubManager extends XmppManagerBase {
accessModel: options.accessModel, accessModel: options.accessModel,
); );
} else if (options.maxItems == 'max' && !nodeMaxSupported) { } else if (options.maxItems == 'max' && !nodeMaxSupported) {
logger.finest('PubSub host does not support node-config-max. Working around it'); logger.finest(
'PubSub host does not support node-config-max. Working around it',
);
final count = await _getNodeItemCount(jid, node) + 1; final count = await _getNodeItemCount(jid, node) + 1;
return PubSubPublishOptions( return PubSubPublishOptions(
@@ -179,13 +203,19 @@ class PubSubManager extends XmppManagerBase {
), ),
); );
if (result.attributes['type'] != 'result') return Result(UnknownPubSubError()); if (result.attributes['type'] != 'result') {
return Result(UnknownPubSubError());
}
final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns);
if (pubsub == null) return Result(UnknownPubSubError()); if (pubsub == null) {
return Result(UnknownPubSubError());
}
final subscription = pubsub.firstTag('subscription'); final subscription = pubsub.firstTag('subscription');
if (subscription == null) return Result(UnknownPubSubError()); if (subscription == null) {
return Result(UnknownPubSubError());
}
return Result(subscription.attributes['subscription'] == 'subscribed'); return Result(subscription.attributes['subscription'] == 'subscribed');
} }
@@ -214,13 +244,19 @@ class PubSubManager extends XmppManagerBase {
), ),
); );
if (result.attributes['type'] != 'result') return Result(UnknownPubSubError()); if (result.attributes['type'] != 'result') {
return Result(UnknownPubSubError());
}
final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns);
if (pubsub == null) return Result(UnknownPubSubError()); if (pubsub == null) {
return Result(UnknownPubSubError());
}
final subscription = pubsub.firstTag('subscription'); final subscription = pubsub.firstTag('subscription');
if (subscription == null) return Result(UnknownPubSubError()); if (subscription == null) {
return Result(UnknownPubSubError());
}
return Result(subscription.attributes['subscription'] == 'none'); return Result(subscription.attributes['subscription'] == 'none');
} }
@@ -233,8 +269,7 @@ class PubSubManager extends XmppManagerBase {
XMLNode payload, { XMLNode payload, {
String? id, String? id,
PubSubPublishOptions? options, PubSubPublishOptions? options,
} }) async {
) async {
return _publish( return _publish(
jid, jid,
node, node,
@@ -252,11 +287,10 @@ class PubSubManager extends XmppManagerBase {
PubSubPublishOptions? options, PubSubPublishOptions? options,
// Should, if publishing fails, try to reconfigure and publish again? // Should, if publishing fails, try to reconfigure and publish again?
bool tryConfigureAndPublish = true, bool tryConfigureAndPublish = true,
} }) async {
) async {
PubSubPublishOptions? pubOptions; PubSubPublishOptions? pubOptions;
if (options != null) { if (options != null) {
pubOptions = await _preprocessPublishOptions(jid, node, options); pubOptions = await preprocessPublishOptions(jid, node, options);
} }
final result = await getAttributes().sendStanza( final result = await getAttributes().sendStanza(
@@ -274,17 +308,18 @@ class PubSubManager extends XmppManagerBase {
children: [ children: [
XMLNode( XMLNode(
tag: 'item', tag: 'item',
attributes: id != null ? <String, String>{ 'id': id } : <String, String>{}, attributes: id != null
? <String, String>{'id': id}
: <String, String>{},
children: [payload], children: [payload],
) )
], ],
), ),
...options != null ? [ if (pubOptions != null)
XMLNode( XMLNode(
tag: 'publish-options', tag: 'publish-options',
children: [options.toXml()], children: [pubOptions.toXml()],
), ),
] : [],
], ],
) )
], ],
@@ -308,10 +343,16 @@ class PubSubManager extends XmppManagerBase {
options: options, options: options,
tryConfigureAndPublish: false, tryConfigureAndPublish: false,
); );
if (publishResult.isType<PubSubError>()) return publishResult; if (publishResult.isType<PubSubError>()) {
} else if (error is EjabberdMaxItemsError && tryConfigureAndPublish && options != null) { return publishResult;
}
} else if (error is EjabberdMaxItemsError &&
tryConfigureAndPublish &&
options != null) {
// TODO(Unknown): Remove once ejabberd fixes the bug. See errors.dart for more info. // TODO(Unknown): Remove once ejabberd fixes the bug. See errors.dart for more info.
logger.warning('Publish failed due to the server rejecting the usage of "max" for "max_items" in publish options. Configuring...'); logger.warning(
'Publish failed due to the server rejecting the usage of "max" for "max_items" in publish options. Configuring...',
);
final count = await _getNodeItemCount(jid, node) + 1; final count = await _getNodeItemCount(jid, node) + 1;
return publish( return publish(
jid, jid,
@@ -329,20 +370,31 @@ class PubSubManager extends XmppManagerBase {
} }
final pubsubElement = result.firstTag('pubsub', xmlns: pubsubXmlns); final pubsubElement = result.firstTag('pubsub', xmlns: pubsubXmlns);
if (pubsubElement == null) return Result(MalformedResponseError()); if (pubsubElement == null) {
return Result(MalformedResponseError());
}
final publishElement = pubsubElement.firstTag('publish'); final publishElement = pubsubElement.firstTag('publish');
if (publishElement == null) return Result(MalformedResponseError()); if (publishElement == null) {
return Result(MalformedResponseError());
}
final item = publishElement.firstTag('item'); final item = publishElement.firstTag('item');
if (item == null) return Result(MalformedResponseError()); if (item == null) {
return Result(MalformedResponseError());
}
if (id != null) return Result(item.attributes['id'] == id); if (id != null) {
return Result(item.attributes['id'] == id);
}
return const Result(true); return const Result(true);
} }
Future<Result<PubSubError, List<PubSubItem>>> getItems(String jid, String node) async { Future<Result<PubSubError, List<PubSubItem>>> getItems(
String jid,
String node,
) async {
final result = await getAttributes().sendStanza( final result = await getAttributes().sendStanza(
Stanza.iq( Stanza.iq(
type: 'get', type: 'get',
@@ -359,26 +411,31 @@ class PubSubManager extends XmppManagerBase {
), ),
); );
if (result.attributes['type'] != 'result') return Result(getPubSubError(result)); if (result.attributes['type'] != 'result') {
return Result(getPubSubError(result));
}
final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns);
if (pubsub == null) return Result(getPubSubError(result)); if (pubsub == null) {
return Result(getPubSubError(result));
}
final items = pubsub final items = pubsub.firstTag('items')!.children.map((item) {
.firstTag('items')!
.children.map((item) {
return PubSubItem( return PubSubItem(
id: item.attributes['id']! as String, id: item.attributes['id']! as String,
payload: item.children[0], payload: item.children[0],
node: node, node: node,
); );
}) }).toList();
.toList();
return Result(items); return Result(items);
} }
Future<Result<PubSubError, PubSubItem>> getItem(String jid, String node, String id) async { Future<Result<PubSubError, PubSubItem>> getItem(
String jid,
String node,
String id,
) async {
final result = await getAttributes().sendStanza( final result = await getAttributes().sendStanza(
Stanza.iq( Stanza.iq(
type: 'get', type: 'get',
@@ -404,7 +461,9 @@ class PubSubManager extends XmppManagerBase {
), ),
); );
if (result.attributes['type'] != 'result') return Result(getPubSubError(result)); if (result.attributes['type'] != 'result') {
return Result(getPubSubError(result));
}
final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns); final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns);
if (pubsub == null) return Result(getPubSubError(result)); if (pubsub == null) return Result(getPubSubError(result));
@@ -421,7 +480,11 @@ class PubSubManager extends XmppManagerBase {
return Result(item); return Result(item);
} }
Future<Result<PubSubError, bool>> configure(String jid, String node, PubSubPublishOptions options) async { Future<Result<PubSubError, bool>> configure(
String jid,
String node,
PubSubPublishOptions options,
) async {
final attrs = getAttributes(); final attrs = getAttributes();
// Request the form // Request the form
@@ -445,7 +508,9 @@ class PubSubManager extends XmppManagerBase {
], ],
), ),
); );
if (form.attributes['type'] != 'result') return Result(getPubSubError(form)); if (form.attributes['type'] != 'result') {
return Result(getPubSubError(form));
}
final submit = await attrs.sendStanza( final submit = await attrs.sendStanza(
Stanza.iq( Stanza.iq(
@@ -470,7 +535,9 @@ class PubSubManager extends XmppManagerBase {
], ],
), ),
); );
if (submit.attributes['type'] != 'result') return Result(getPubSubError(form)); if (submit.attributes['type'] != 'result') {
return Result(getPubSubError(form));
}
return const Result(true); return const Result(true);
} }
@@ -505,7 +572,11 @@ class PubSubManager extends XmppManagerBase {
return const Result(true); return const Result(true);
} }
Future<Result<PubSubError, bool>> retract(JID host, String node, String itemId) async { Future<Result<PubSubError, bool>> retract(
JID host,
String node,
String itemId,
) async {
final request = await getAttributes().sendStanza( final request = await getAttributes().sendStanza(
Stanza.iq( Stanza.iq(
type: 'set', type: 'set',

View File

@@ -8,7 +8,6 @@ import 'package:moxxmpp/src/stringxml.dart';
/// A data class representing the jabber:x:oob tag. /// A data class representing the jabber:x:oob tag.
class OOBData { class OOBData {
const OOBData({this.url, this.desc}); const OOBData({this.url, this.desc});
final String? url; final String? url;
final String? desc; final String? desc;
@@ -32,11 +31,7 @@ XMLNode constructOOBNode(OOBData data) {
} }
class OOBManager extends XmppManagerBase { class OOBManager extends XmppManagerBase {
@override OOBManager() : super(oobManager);
String getName() => 'OOBName';
@override
String getId() => oobManager;
@override @override
List<String> getDiscoFeatures() => [oobDataXmlns]; List<String> getDiscoFeatures() => [oobDataXmlns];
@@ -56,7 +51,10 @@ class OOBManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onMessage(
Stanza message,
StanzaHandlerData state,
) async {
final x = message.firstTag('x', xmlns: oobDataXmlns)!; final x = message.firstTag('x', xmlns: oobDataXmlns)!;
final url = x.firstTag('url'); final url = x.firstTag('url');
final desc = x.firstTag('desc'); final desc = x.firstTag('desc');

View File

@@ -3,21 +3,24 @@ import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/namespaces.dart'; import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart'; import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
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';
class UserAvatar { abstract class AvatarError {}
class UnknownAvatarError extends AvatarError {}
class UserAvatar {
const UserAvatar({required this.base64, required this.hash}); const UserAvatar({required this.base64, required this.hash});
final String base64; final String base64;
final String hash; final String hash;
} }
class UserAvatarMetadata { class UserAvatarMetadata {
const UserAvatarMetadata( const UserAvatarMetadata(
this.id, this.id,
this.length, this.length,
@@ -25,30 +28,41 @@ class UserAvatarMetadata {
this.height, this.height,
this.mime, this.mime,
); );
/// The amount of bytes in the file /// 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 MIME type of the avatar
final String mime; final String mime;
} }
/// NOTE: This class requires a PubSubManager /// NOTE: This class requires a PubSubManager
class UserAvatarManager extends XmppManagerBase { class UserAvatarManager extends XmppManagerBase {
@override UserAvatarManager() : super(userAvatarManager);
String getId() => userAvatarManager;
@override PubSubManager _getPubSubManager() =>
String getName() => 'UserAvatarManager'; getAttributes().getManagerById(pubsubManager)! as PubSubManager;
PubSubManager _getPubSubManager() => getAttributes().getManagerById(pubsubManager)! as PubSubManager;
@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.payload.tag != 'data' ||
event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) {
logger.warning(
'Received avatar update from ${event.from} but the payload is invalid. Ignoring...',
);
return;
}
getAttributes().sendEvent( getAttributes().sendEvent(
AvatarUpdatedEvent( AvatarUpdatedEvent(
jid: event.from, jid: event.from,
@@ -65,27 +79,31 @@ 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
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, UserAvatar>> getUserAvatar(String jid) async {
Future<UserAvatar?> getUserAvatar(String jid) async {
final pubsub = _getPubSubManager(); final pubsub = _getPubSubManager();
final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns); final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns);
if (resultsRaw.isType<PubSubError>()) return null; if (resultsRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
final results = resultsRaw.get<List<PubSubItem>>(); final results = resultsRaw.get<List<PubSubItem>>();
if (results.isEmpty) return null; if (results.isEmpty) return Result(UnknownAvatarError());
final item = results[0]; final item = results[0];
return UserAvatar( return Result(
UserAvatar(
base64: item.payload.innerText(), base64: item.payload.innerText(),
hash: item.id, hash: item.id,
),
); );
} }
/// Publish the avatar data, [base64], on the pubsub node using [hash] as /// Publish the avatar data, [base64], on the pubsub node using [hash] as
/// the item id. [hash] must be the SHA-1 hash of the image data, while /// the item id. [hash] must be the SHA-1 hash of the image data, while
/// [base64] must be the base64-encoded version of the image data. /// [base64] must be the base64-encoded version of the image data.
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, bool>> publishUserAvatar(
Future<bool> publishUserAvatar(String base64, String hash, bool public) async { String base64,
String hash,
bool public,
) async {
final pubsub = _getPubSubManager(); final pubsub = _getPubSubManager();
final result = await pubsub.publish( final result = await pubsub.publish(
getAttributes().getFullJID().toBare().toString(), getAttributes().getFullJID().toBare().toString(),
@@ -101,14 +119,18 @@ class UserAvatarManager extends XmppManagerBase {
), ),
); );
return !result.isType<PubSubError>(); if (result.isType<PubSubError>()) return Result(UnknownAvatarError());
return const Result(true);
} }
/// Publish avatar metadata [metadata] to the User Avatar's metadata node. If [public] /// Publish avatar metadata [metadata] to the User Avatar's metadata node. If [public]
/// is true, then the node will be set to an 'open' access model. If [public] is false, /// is true, then the node will be set to an 'open' access model. If [public] is false,
/// then the node will be set to an 'roster' access model. /// then the node will be set to an 'roster' access model.
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, bool>> publishUserAvatarMetadata(
Future<bool> publishUserAvatarMetadata(UserAvatarMetadata metadata, bool public) async { UserAvatarMetadata metadata,
bool public,
) async {
final pubsub = _getPubSubManager(); final pubsub = _getPubSubManager();
final result = await pubsub.publish( final result = await pubsub.publish(
getAttributes().getFullJID().toBare().toString(), getAttributes().getFullJID().toBare().toString(),
@@ -135,39 +157,39 @@ class UserAvatarManager extends XmppManagerBase {
), ),
); );
return result.isType<PubSubError>(); if (result.isType<PubSubError>()) return Result(UnknownAvatarError());
return const Result(true);
} }
/// Subscribe the data and metadata node of [jid]. /// Subscribe the data and metadata node of [jid].
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, bool>> subscribe(String jid) async {
Future<bool> subscribe(String jid) async {
await _getPubSubManager().subscribe(jid, userAvatarDataXmlns);
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return true; return const Result(true);
} }
/// Unsubscribe the data and metadata node of [jid]. /// Unsubscribe the data and metadata node of [jid].
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, bool>> unsubscribe(String jid) async {
Future<bool> unsubscribe(String jid) async {
await _getPubSubManager().unsubscribe(jid, userAvatarDataXmlns);
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns); await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
return true; return const Result(true);
} }
/// Returns the PubSub Id of an avatar after doing a disco#items query. /// 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 /// Note that this assumes that there is only one (1) item published on
/// the node. /// the node.
// TODO(Unknown): Migrate to Resultsv2 Future<Result<AvatarError, String>> getAvatarId(String jid) async {
Future<String?> getAvatarId(String jid) async {
final disco = getAttributes().getManagerById(discoManager)! as DiscoManager; final disco = getAttributes().getManagerById(discoManager)! as DiscoManager;
final response = await disco.discoItemsQuery(jid, node: userAvatarDataXmlns); final response = await disco.discoItemsQuery(
if (response.isType<DiscoError>()) return null; jid,
node: userAvatarDataXmlns,
shouldEncrypt: false,
);
if (response.isType<DiscoError>()) return Result(UnknownAvatarError());
final items = response.get<List<DiscoItem>>(); final items = response.get<List<DiscoItem>>();
if (items.isEmpty) return null; if (items.isEmpty) return Result(UnknownAvatarError());
return items.first.name; return Result(items.first.name);
} }
} }

View File

@@ -6,48 +6,45 @@ 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 ChatState { enum ChatState { active, composing, paused, inactive, gone }
active,
composing,
paused,
inactive,
gone
}
ChatState chatStateFromString(String raw) { ChatState chatStateFromString(String raw) {
switch (raw) { switch (raw) {
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: { default:
{
return ChatState.gone; return ChatState.gone;
} }
} }
} }
String chatStateToString(ChatState state) => state.toString().split('.').last; String chatStateToString(ChatState state) => state.toString().split('.').last;
class ChatStateManager extends XmppManagerBase { class ChatStateManager extends XmppManagerBase {
ChatStateManager() : super(chatStateManager);
@override @override
List<String> getDiscoFeatures() => [chatStateXmlns]; List<String> getDiscoFeatures() => [chatStateXmlns];
@override
String getName() => 'ChatStateManager';
@override
String getId() => chatStateManager;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
@@ -62,32 +59,41 @@ class ChatStateManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onChatStateReceived(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onChatStateReceived(
Stanza message,
StanzaHandlerData state,
) async {
final element = state.stanza.firstTagByXmlns(chatStateXmlns)!; final element = state.stanza.firstTagByXmlns(chatStateXmlns)!;
ChatState? chatState; ChatState? chatState;
switch (element.tag) { switch (element.tag) {
case 'active': { case 'active':
{
chatState = ChatState.active; chatState = ChatState.active;
} }
break; break;
case 'composing': { case 'composing':
{
chatState = ChatState.composing; chatState = ChatState.composing;
} }
break; break;
case 'paused': { case 'paused':
{
chatState = ChatState.paused; chatState = ChatState.paused;
} }
break; break;
case 'inactive': { case 'inactive':
{
chatState = ChatState.inactive; chatState = ChatState.inactive;
} }
break; break;
case 'gone': { case 'gone':
{
chatState = ChatState.gone; chatState = ChatState.gone;
} }
break; break;
default: { default:
{
logger.warning("Received invalid chat state '${element.tag}'"); logger.warning("Received invalid chat state '${element.tag}'");
} }
} }
@@ -97,7 +103,11 @@ class ChatStateManager extends XmppManagerBase {
/// 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(ChatState state, String to, { String messageType = 'chat' }) { void sendChatState(
ChatState state,
String to, {
String messageType = 'chat',
}) {
final tagName = state.toString().split('.').last; final tagName = state.toString().split('.').last;
getAttributes().sendStanza( getAttributes().sendStanza(

View File

@@ -1,10 +1,18 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/presence.dart';
import 'package:moxxmpp/src/rfcs/rfc_4790.dart'; import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
import 'package:moxxmpp/src/stringxml.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_0414.dart';
@immutable
class CapabilityHashInfo { class CapabilityHashInfo {
const CapabilityHashInfo(this.ver, this.node, this.hash); const CapabilityHashInfo(this.ver, this.node, this.hash);
final String ver; final String ver;
final String node; final String node;
@@ -13,10 +21,16 @@ class CapabilityHashInfo {
/// 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(DiscoInfo info, HashAlgorithm algorithm) async { Future<String> calculateCapabilityHash(
DiscoInfo info,
HashAlgorithm algorithm,
) async {
final buffer = StringBuffer(); final buffer = StringBuffer();
final identitiesSorted = info.identities final identitiesSorted = info.identities
.map((Identity i) => '${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}') .map(
(Identity i) =>
'${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}',
)
.toList(); .toList();
// ignore: cascade_invocations // ignore: cascade_invocations
identitiesSorted.sort(ioctetSortComparator); identitiesSorted.sort(ioctetSortComparator);
@@ -28,7 +42,8 @@ Future<String> calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm)
if (info.extendedInfo.isNotEmpty) { if (info.extendedInfo.isNotEmpty) {
final sortedExt = info.extendedInfo final sortedExt = info.extendedInfo
..sort((a, b) => ioctetSortComparator( ..sort(
(a, b) => ioctetSortComparator(
a.getFieldByVar('FORM_TYPE')!.values.first, a.getFieldByVar('FORM_TYPE')!.values.first,
b.getFieldByVar('FORM_TYPE')!.values.first, b.getFieldByVar('FORM_TYPE')!.values.first,
), ),
@@ -37,7 +52,9 @@ Future<String> calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm)
for (final ext in sortedExt) { for (final ext in sortedExt) {
buffer.write('${ext.getFieldByVar("FORM_TYPE")!.values.first}<'); buffer.write('${ext.getFieldByVar("FORM_TYPE")!.values.first}<');
final sortedFields = ext.fields..sort((a, b) => ioctetSortComparator( final sortedFields = ext.fields
..sort(
(a, b) => ioctetSortComparator(
a.varAttr!, a.varAttr!,
b.varAttr!, b.varAttr!,
), ),
@@ -55,5 +72,84 @@ Future<String> calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm)
} }
} }
return base64.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes); return base64
.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes);
}
/// A manager implementing the advertising of XEP-0115. It responds to the
/// disco#info requests on the specified node with the information provided by
/// the DiscoManager.
/// NOTE: This manager requires that the DiscoManager is also registered.
class EntityCapabilitiesManager extends XmppManagerBase {
EntityCapabilitiesManager(this._capabilityHashBase)
: super(entityCapabilitiesManager);
/// The string that is both the node under which we advertise the disco info
/// and the base for the actual node on which we respond to disco#info requests.
final String _capabilityHashBase;
/// The cached capability hash.
String? _capabilityHash;
@override
Future<bool> isSupported() async => true;
@override
List<String> getDiscoFeatures() => [capsXmlns];
/// Computes, if required, the capability hash of the data provided by
/// the DiscoManager.
Future<String> getCapabilityHash() async {
_capabilityHash ??= await calculateCapabilityHash(
getAttributes()
.getManagerById<DiscoManager>(discoManager)!
.getDiscoInfo(null),
getHashByName('sha-1')!,
);
return _capabilityHash!;
}
Future<String> _getNode() async {
final hash = await getCapabilityHash();
return '$_capabilityHashBase#$hash';
}
Future<DiscoInfo> _onInfoQuery() async {
return getAttributes()
.getManagerById<DiscoManager>(discoManager)!
.getDiscoInfo(await _getNode());
}
Future<List<XMLNode>> _prePresenceSent() async {
return [
XMLNode.xmlns(
tag: 'c',
xmlns: capsXmlns,
attributes: {
'hash': 'sha-1',
'node': _capabilityHashBase,
'ver': await getCapabilityHash(),
},
),
];
}
@override
Future<void> postRegisterCallback() async {
await super.postRegisterCallback();
getAttributes()
.getManagerById<DiscoManager>(discoManager)!
.registerInfoCallback(
await _getNode(),
_onInfoQuery,
);
getAttributes()
.getManagerById<PresenceManager>(presenceManager)!
.registerPreSendCallback(
_prePresenceSent,
);
}
} }

View File

@@ -24,15 +24,11 @@ XMLNode makeMessageDeliveryResponse(String id) {
} }
class MessageDeliveryReceiptManager extends XmppManagerBase { class MessageDeliveryReceiptManager extends XmppManagerBase {
MessageDeliveryReceiptManager() : super(messageDeliveryReceiptManager);
@override @override
List<String> getDiscoFeatures() => [deliveryXmlns]; List<String> getDiscoFeatures() => [deliveryXmlns];
@override
String getName() => 'MessageDeliveryReceiptManager';
@override
String getId() => messageDeliveryReceiptManager;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
StanzaHandler( StanzaHandler(
@@ -56,15 +52,24 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onDeliveryRequestReceived(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onDeliveryRequestReceived(
Stanza message,
StanzaHandlerData state,
) async {
return state.copyWith(deliveryReceiptRequested: true); return state.copyWith(deliveryReceiptRequested: true);
} }
Future<StanzaHandlerData> _onDeliveryReceiptReceived(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onDeliveryReceiptReceived(
Stanza message,
StanzaHandlerData state,
) 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'].contains(item.tag)) { if (!['origin-id', 'stanza-id', 'delay', 'store', 'received']
logger.info("Won't handle stanza as delivery receipt because we found an '${item.tag}' element"); .contains(item.tag)) {
logger.info(
"Won't handle stanza as delivery receipt because we found an '${item.tag}' element",
);
return state.copyWith(done: true); return state.copyWith(done: true);
} }

View File

@@ -9,16 +9,10 @@ import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart'; import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
class BlockingManager extends XmppManagerBase { class BlockingManager extends XmppManagerBase {
BlockingManager() : _supported = false, _gotSupported = false, super(); BlockingManager() : super(blockingManager);
bool _supported; bool _supported = false;
bool _gotSupported; bool _gotSupported = false;
@override
String getId() => blockingManager;
@override
String getName() => 'BlockingManager';
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -52,25 +46,37 @@ class BlockingManager extends XmppManagerBase {
@override @override
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is StreamResumeFailedEvent) { if (event is StreamNegotiationsDoneEvent) {
final newStream = await isNewStream();
if (newStream) {
_gotSupported = false; _gotSupported = false;
_supported = false; _supported = false;
} }
} }
}
Future<StanzaHandlerData> _blockPush(Stanza iq, StanzaHandlerData state) async { Future<StanzaHandlerData> _blockPush(
Stanza iq,
StanzaHandlerData state,
) async {
final block = iq.firstTag('block', xmlns: blockingXmlns)!; final block = iq.firstTag('block', xmlns: blockingXmlns)!;
getAttributes().sendEvent( getAttributes().sendEvent(
BlocklistBlockPushEvent( BlocklistBlockPushEvent(
items: block.findTags('item').map((i) => i.attributes['jid']! as String).toList(), items: block
.findTags('item')
.map((i) => i.attributes['jid']! as String)
.toList(),
), ),
); );
return state.copyWith(done: true); return state.copyWith(done: true);
} }
Future<StanzaHandlerData> _unblockPush(Stanza iq, StanzaHandlerData state) async { Future<StanzaHandlerData> _unblockPush(
Stanza iq,
StanzaHandlerData state,
) async {
final unblock = iq.firstTag('unblock', xmlns: blockingXmlns)!; final unblock = iq.firstTag('unblock', xmlns: blockingXmlns)!;
final items = unblock.findTags('item'); final items = unblock.findTags('item');
@@ -97,14 +103,12 @@ class BlockingManager extends XmppManagerBase {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'block', tag: 'block',
xmlns: blockingXmlns, xmlns: blockingXmlns,
children: items children: items.map((item) {
.map((item) {
return XMLNode( return XMLNode(
tag: 'item', tag: 'item',
attributes: <String, String>{'jid': item}, attributes: <String, String>{'jid': item},
); );
}) }).toList(),
.toList(),
) )
], ],
), ),
@@ -139,10 +143,14 @@ class BlockingManager extends XmppManagerBase {
XMLNode.xmlns( XMLNode.xmlns(
tag: 'unblock', tag: 'unblock',
xmlns: blockingXmlns, xmlns: blockingXmlns,
children: items.map((item) => XMLNode( children: items
.map(
(item) => XMLNode(
tag: 'item', tag: 'item',
attributes: <String, String>{'jid': item}, attributes: <String, String>{'jid': item},
),).toList(), ),
)
.toList(),
) )
], ],
), ),
@@ -165,6 +173,9 @@ class BlockingManager extends XmppManagerBase {
); );
final blocklist = result.firstTag('blocklist', xmlns: blockingXmlns)!; final blocklist = result.firstTag('blocklist', xmlns: blockingXmlns)!;
return blocklist.findTags('item').map((item) => item.attributes['jid']! as String).toList(); return blocklist
.findTags('item')
.map((item) => item.attributes['jid']! as String)
.toList();
} }
} }

View File

@@ -1,14 +1,20 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.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';
import 'package:moxxmpp/src/xeps/xep_0352.dart'; import 'package:moxxmpp/src/xeps/xep_0352.dart';
import 'package:moxxmpp/src/xeps/xep_0386.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
enum _StreamManagementNegotiatorState { enum _StreamManagementNegotiatorState {
// We have not done anything yet // We have not done anything yet
@@ -22,27 +28,71 @@ enum _StreamManagementNegotiatorState {
/// NOTE: The stream management negotiator requires that loadState has been called on the /// NOTE: The stream management negotiator requires that loadState has been called on the
/// StreamManagementManager at least once before connecting, if stream resumption /// StreamManagementManager at least once before connecting, if stream resumption
/// is wanted. /// is wanted.
class StreamManagementNegotiator extends XmppFeatureNegotiatorBase { class StreamManagementNegotiator extends Sasl2FeatureNegotiator
implements Bind2FeatureNegotiatorInterface {
StreamManagementNegotiator() StreamManagementNegotiator()
: _state = _StreamManagementNegotiatorState.ready, : super(10, false, smXmlns, streamManagementNegotiator);
_supported = false,
_resumeFailed = false,
_isResumed = false,
_log = Logger('StreamManagementNegotiator'),
super(10, false, smXmlns, streamManagementNegotiator);
_StreamManagementNegotiatorState _state;
bool _resumeFailed;
bool _isResumed;
final Logger _log; /// Stream Management negotiation state.
_StreamManagementNegotiatorState _state =
_StreamManagementNegotiatorState.ready;
/// Flag indicating whether the resume failed (true) or succeeded (false).
bool _resumeFailed = false;
bool get resumeFailed => _resumeFailed;
/// Flag indicating whether the current stream is resumed (true) or not (false).
bool _isResumed = false;
bool get isResumed => _isResumed;
/// Flag indicating that stream enablement failed
bool _streamEnablementFailed = false;
bool get streamEnablementFailed => _streamEnablementFailed;
/// Logger
final Logger _log = Logger('StreamManagementNegotiator');
/// True if Stream Management is supported on this stream. /// True if Stream Management is supported on this stream.
bool _supported; bool _supported = false;
bool get isSupported => _supported; bool get isSupported => _supported;
/// True if the current stream is resumed. False if not. /// True if we requested stream enablement inline
bool get isResumed => _isResumed; bool _inlineStreamEnablementRequested = false;
/// Cached resource for stream resumption
String _resource = '';
@visibleForTesting
void setResource(String resource) {
_resource = resource;
}
@override
bool canInlineFeature(List<XMLNode> features) {
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
// We do not check here for authentication as enabling/resuming happens inline
// with the authentication.
if (sm.state.streamResumptionId != null && !_resumeFailed) {
// We can try to resume the stream or enable the stream
return features.firstWhereOrNull(
(child) => child.xmlns == smXmlns,
) !=
null;
} else {
// We can try to enable SM
return features.firstWhereOrNull(
(child) => child.tag == 'enable' && child.xmlns == smXmlns,
) !=
null;
}
}
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is ResourceBoundEvent) {
_resource = event.resource;
}
}
@override @override
bool matchesFeature(List<XMLNode> features) { bool matchesFeature(List<XMLNode> features) {
@@ -53,54 +103,13 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
return super.matchesFeature(features) && attributes.isAuthenticated(); return super.matchesFeature(features) && attributes.isAuthenticated();
} else { } else {
// We cannot do a stream resumption // We cannot do a stream resumption
final br = attributes.getNegotiatorById(resourceBindingNegotiator); return super.matchesFeature(features) &&
return super.matchesFeature(features) && br?.state == NegotiatorState.done && attributes.isAuthenticated(); attributes.getConnection().resource.isNotEmpty &&
attributes.isAuthenticated();
} }
} }
@override Future<void> _onStreamResumptionFailed() async {
Future<void> negotiate(XMLNode nonza) async {
// negotiate is only called when we matched the stream feature, so we know
// that the server advertises it.
_supported = true;
switch (_state) {
case _StreamManagementNegotiatorState.ready:
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
final srid = sm.state.streamResumptionId;
final h = sm.state.s2c;
// Attempt stream resumption first
if (srid != null) {
_log.finest('Found stream resumption Id. Attempting to perform stream resumption');
_state = _StreamManagementNegotiatorState.resumeRequested;
attributes.sendNonza(StreamManagementResumeNonza(srid, h));
} else {
_log.finest('Attempting to enable stream management');
_state = _StreamManagementNegotiatorState.enableRequested;
attributes.sendNonza(StreamManagementEnableNonza());
}
break;
case _StreamManagementNegotiatorState.resumeRequested:
if (nonza.tag == 'resumed') {
_log.finest('Stream Management resumption successful');
assert(attributes.getFullJID().resource != '', 'Resume only works when we already have a resource bound and know about it');
final csi = attributes.getManagerById(csiManager) as CSIManager?;
if (csi != null) {
csi.restoreCSIState();
}
final h = int.parse(nonza.attributes['h']! as String);
await attributes.sendEvent(StreamResumedEvent(h: h));
_resumeFailed = false;
_isResumed = true;
state = NegotiatorState.skipRest;
} else {
// We assume it is <failed />
_log.info('Stream resumption failed. Expected <resumed />, got ${nonza.tag}, Proceeding with new stream...');
await attributes.sendEvent(StreamResumeFailedEvent()); await attributes.sendEvent(StreamResumeFailedEvent());
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!; final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
@@ -113,15 +122,31 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
_resumeFailed = true; _resumeFailed = true;
_isResumed = false; _isResumed = false;
_state = _StreamManagementNegotiatorState.ready; _state = _StreamManagementNegotiatorState.ready;
state = NegotiatorState.retryLater;
} }
break;
case _StreamManagementNegotiatorState.enableRequested:
if (nonza.tag == 'enabled') {
_log.finest('Stream Management enabled');
final id = nonza.attributes['id'] as String?; Future<void> _onStreamResumptionSuccessful(XMLNode resumed) async {
if (id != null && ['true', '1'].contains(nonza.attributes['resume'])) { assert(resumed.tag == 'resumed', 'The correct element must be passed');
final h = int.parse(resumed.attributes['h']! as String);
await attributes.sendEvent(StreamResumedEvent(h: h));
_resumeFailed = false;
_isResumed = true;
if (attributes.getConnection().resource.isEmpty && _resource.isNotEmpty) {
attributes.setResource(_resource);
} else if (attributes.getConnection().resource.isNotEmpty &&
_resource.isEmpty) {
_resource = attributes.getConnection().resource;
}
}
Future<void> _onStreamEnablementSuccessful(XMLNode enabled) async {
assert(enabled.tag == 'enabled', 'The correct element must be used');
assert(enabled.xmlns == smXmlns, 'The correct element must be used');
final id = enabled.attributes['id'] as String?;
if (id != null && ['true', '1'].contains(enabled.attributes['resume'])) {
_log.info('Stream Resumption available'); _log.info('Stream Resumption available');
} }
@@ -129,18 +154,80 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
StreamManagementEnabledEvent( StreamManagementEnabledEvent(
resource: attributes.getFullJID().resource, resource: attributes.getFullJID().resource,
id: id, id: id,
location: nonza.attributes['location'] as String?, location: enabled.attributes['location'] as String?,
), ),
); );
}
state = NegotiatorState.done; void _onStreamEnablementFailed() {
_streamEnablementFailed = true;
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
// negotiate is only called when we matched the stream feature, so we know
// that the server advertises it.
_supported = true;
switch (_state) {
case _StreamManagementNegotiatorState.ready:
final sm =
attributes.getManagerById<StreamManagementManager>(smManager)!;
final srid = sm.state.streamResumptionId;
final h = sm.state.s2c;
// Attempt stream resumption first
if (srid != null) {
_log.finest(
'Found stream resumption Id. Attempting to perform stream resumption',
);
_state = _StreamManagementNegotiatorState.resumeRequested;
attributes.sendNonza(StreamManagementResumeNonza(srid, h));
} else {
_log.finest('Attempting to enable stream management');
_state = _StreamManagementNegotiatorState.enableRequested;
attributes.sendNonza(StreamManagementEnableNonza());
}
return const Result(NegotiatorState.ready);
case _StreamManagementNegotiatorState.resumeRequested:
if (nonza.tag == 'resumed') {
_log.finest('Stream Management resumption successful');
assert(
attributes.getFullJID().resource != '',
'Resume only works when we already have a resource bound and know about it',
);
final csi = attributes.getManagerById(csiManager) as CSIManager?;
if (csi != null) {
csi.restoreCSIState();
}
await _onStreamResumptionSuccessful(nonza);
return const Result(NegotiatorState.skipRest);
} else {
// We assume it is <failed />
_log.info(
'Stream resumption failed. Expected <resumed />, got ${nonza.tag}, Proceeding with new stream...',
);
await _onStreamResumptionFailed();
return const Result(NegotiatorState.retryLater);
}
case _StreamManagementNegotiatorState.enableRequested:
if (nonza.tag == 'enabled') {
_log.finest('Stream Management enabled');
await _onStreamEnablementSuccessful(nonza);
return const Result(NegotiatorState.done);
} else { } else {
// We assume a <failed /> // We assume a <failed />
_log.warning('Stream Management enablement failed'); _log.warning('Stream Management enablement failed');
state = NegotiatorState.done; _onStreamEnablementFailed();
return const Result(NegotiatorState.done);
} }
break;
} }
} }
@@ -150,7 +237,97 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
_supported = false; _supported = false;
_resumeFailed = false; _resumeFailed = false;
_isResumed = false; _isResumed = false;
_inlineStreamEnablementRequested = false;
_streamEnablementFailed = false;
super.reset(); super.reset();
} }
@override
Future<List<XMLNode>> onBind2FeaturesReceived(
List<String> bind2Features,
) async {
if (!bind2Features.contains(smXmlns)) {
return [];
}
_inlineStreamEnablementRequested = true;
return [
StreamManagementEnableNonza(),
];
}
@override
Future<void> onBind2Success(XMLNode response) async {}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
final inline = sasl2Features.firstTag('inline')!;
final resume = inline.firstTag('resume', xmlns: smXmlns);
if (resume == null) {
return [];
}
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
final srid = sm.state.streamResumptionId;
final h = sm.state.s2c;
if (srid == null) {
_log.finest('No srid');
return [];
}
return [
StreamManagementResumeNonza(
srid,
h,
),
];
}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
final enabled = response
.firstTag('bound', xmlns: bind2Xmlns)
?.firstTag('enabled', xmlns: smXmlns);
final resumed = response.firstTag('resumed', xmlns: smXmlns);
// We can only enable or resume->fail->enable. Thus, we check for enablement first
// and then exit.
if (_inlineStreamEnablementRequested) {
if (enabled != null) {
_log.finest('Inline stream enablement successful');
await _onStreamEnablementSuccessful(enabled);
return const Result(true);
} else {
_log.warning('Inline stream enablement failed');
_onStreamEnablementFailed();
}
}
if (resumed == null) {
_log.warning('Inline stream resumption failed');
await _onStreamResumptionFailed();
state = NegotiatorState.done;
return const Result(true);
}
_log.finest('Inline stream resumption successful');
await _onStreamResumptionSuccessful(resumed);
state = NegotiatorState.skipRest;
attributes.removeNegotiatingFeature(smXmlns);
attributes.removeNegotiatingFeature(bindXmlns);
return const Result(true);
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerNegotiator(this);
attributes
.getNegotiatorById<Bind2Negotiator>(bind2Negotiator)
?.registerNegotiator(this);
}
} }

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