18 Commits

Author SHA1 Message Date
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
103 changed files with 5375 additions and 1807 deletions

3
.gitignore vendored
View File

@@ -13,3 +13,6 @@ pubspec.lock
# Omit pubspec override files generated by melos # Omit pubspec override files generated by melos
**/pubspec_overrides.yaml **/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)+(,(meta|tests|style|docs|xep|core))*\)|release): [A-Z0-9].*$
[body-trailing-whitespace]
[body-first-line-empty]

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

@@ -7,6 +7,8 @@ moxxmpp is a XMPP library written purely in Dart for usage in Moxxy.
This package contains the actual XMPP code that is platform-independent. This package contains the actual XMPP code that is platform-independent.
Documentation is available [here](https://moxxy.org/developers/docs/moxxmpp/).
### [moxxmpp_socket_tcp](./packages/moxxmpp_socket_tcp) ### [moxxmpp_socket_tcp](./packages/moxxmpp_socket_tcp)
`moxxmpp_socket_tcp` contains the implementation of the `BaseSocketWrapper` class that `moxxmpp_socket_tcp` contains the implementation of the `BaseSocketWrapper` class that

View File

@@ -11,12 +11,14 @@ class ExampleTcpSocketWrapper extends TCPSocketWrapper {
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,22 +57,29 @@ 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(),
// The below causes the app to crash.
//ExampleTcpSocketWrapper(),
// In a production app, the below should be false.
TCPSocketWrapper(true),
); );
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(),
MessageManager(), MessageManager(),
PresenceManager('http://moxxmpp.example'), PresenceManager(),
]) ])
..registerFeatureNegotiators([ ..registerFeatureNegotiators([
ResourceBindingNegotiator(), ResourceBindingNegotiator(),
@@ -85,15 +95,45 @@ class _MyHomePageState extends State<MyHomePage> {
} }
Future<void> _buttonPressed() async { Future<void> _buttonPressed() async {
if (connected) {
await connection.disconnect();
setState(() {
connected = false;
});
return;
}
setState(() {
loading = true;
});
connection.setConnectionSettings( connection.setConnectionSettings(
ConnectionSettings( ConnectionSettings(
jid: JID.fromString(jidController.text), jid: JID.fromString(jidController.text),
password: passwordController.text, password: passwordController.text,
useDirectTLS: true, useDirectTLS: true,
allowPlainAuth: false, // If `allowPlainAuth` is `false`, connecting to some
// servers will cause apps to hang, and never connect.
// The hang is a bug that will be fixed, so when it is,
// allowPlainAuth should be set to false.
allowPlainAuth: true,
), ),
); );
await connection.connect(); final result = await connection.connectAwaitable();
setState(() {
connected = result.success;
loading = false;
});
if (result.error != null) {
logger.severe(result.error);
if (context.mounted) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Error'),
content: Text(result.error.toString()),
),
);
}
}
} }
@override @override
@@ -101,20 +141,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,
@@ -122,10 +166,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),
), ),
); );
} }

12
flake.lock generated
View File

@@ -17,16 +17,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1667610399, "lastModified": 1676076353,
"narHash": "sha256-XZd0f4ZWAY0QOoUSdiNWj/eFiKb4B9CJPtl9uO9SYY4=", "narHash": "sha256-mdUtE8Tp40cZETwcq5tCwwLqkJVV1ULJQ5GKRtbshag=",
"owner": "NixOS", "owner": "AtaraxiaSjel",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "1dd8696f96db47156e1424a49578fe7dd4ce99a4", "rev": "5deb99bdccbbb97e7562dee4ba8a3ee3021688e6",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "AtaraxiaSjel",
"ref": "nixpkgs-unstable", "ref": "update/flutter",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }

View File

@@ -1,7 +1,7 @@
{ {
description = "moxxmpp"; description = "moxxmpp";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:AtaraxiaSjel/nixpkgs/update/flutter";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };
@@ -30,14 +30,29 @@
useGoogleTVAddOns = false; useGoogleTVAddOns = false;
}; };
pinnedJDK = pkgs.jdk; pinnedJDK = pkgs.jdk;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
pyyaml
requests
]);
moxxmppPubCache = import ./nix/pubcache.moxxmpp.nix {
inherit (pkgs) fetchzip runCommand;
};
in { in {
packages = {
moxxmppDartDocs = pkgs.callPackage ./nix/moxxmpp-docs.nix {
inherit (moxxmppPubCache) pubCache;
};
};
devShell = pkgs.mkShell { devShell = 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,6 +68,9 @@
pkg-config pkg-config
xorg.libX11 xorg.libX11
xorg.xorgproto xorg.xorgproto
# For the scripts in ./scripts/
pythonEnv
]; ];
CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include"; CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include";

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

@@ -9,9 +9,11 @@ Include the following as a dependency in your pubspec file:
``` ```
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.1.6+1 version: 0.2.0
``` ```
You can find the documentation [here](https://moxxy.org/developers/docs/moxxmpp/).
## License ## License
See `./LICENSE`. See `./LICENSE`.

View File

@@ -23,7 +23,7 @@ class _StanzaSurrogateKey {
int get hashCode => sentTo.hashCode ^ id.hashCode ^ tag.hashCode; int get hashCode => sentTo.hashCode ^ id.hashCode ^ tag.hashCode;
@override @override
bool operator==(Object other) { bool operator ==(Object other) {
return other is _StanzaSurrogateKey && return other is _StanzaSurrogateKey &&
other.sentTo == sentTo && other.sentTo == sentTo &&
other.id == id && other.id == id &&

View File

@@ -6,16 +6,21 @@ import 'package:xml/xml.dart';
import 'package:xml/xml_events.dart'; import 'package:xml/xml_events.dart';
class XmlStreamBuffer extends StreamTransformerBase<String, XMLNode> { class XmlStreamBuffer extends StreamTransformerBase<String, XMLNode> {
XmlStreamBuffer()
XmlStreamBuffer() : _streamController = StreamController(), _decoder = const XmlNodeDecoder(); : _streamController = StreamController(),
_decoder = const XmlNodeDecoder();
final StreamController<XMLNode> _streamController; final StreamController<XMLNode> _streamController;
final XmlNodeDecoder _decoder; final XmlNodeDecoder _decoder;
@override @override
Stream<XMLNode> bind(Stream<String> stream) { Stream<XMLNode> bind(Stream<String> stream) {
stream.toXmlEvents().selectSubtreeEvents((event) { stream
.toXmlEvents()
.selectSubtreeEvents((event) {
return event.qualifiedName != 'stream:stream'; return event.qualifiedName != 'stream:stream';
}).transform(_decoder).listen((nodes) { })
.transform(_decoder)
.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(XMLNode.fromXmlElement(node as XmlElement));

View File

@@ -61,7 +61,8 @@ enum StanzaFromType {
/// Nonza describing the XMPP stream header. /// Nonza describing the XMPP stream header.
class StreamHeaderNonza extends XMLNode { class StreamHeaderNonza extends XMLNode {
StreamHeaderNonza(String serverDomain) : super( StreamHeaderNonza(String serverDomain)
: super(
tag: 'stream:stream', tag: 'stream:stream',
attributes: <String, String>{ attributes: <String, String>{
'xmlns': stanzaXmlns, 'xmlns': stanzaXmlns,
@@ -77,11 +78,9 @@ class StreamHeaderNonza extends XMLNode {
/// The result of an awaited connection. /// The result of an awaited connection.
class XmppConnectionResult { class XmppConnectionResult {
const XmppConnectionResult( const XmppConnectionResult(
this.success, this.success, {
{
this.error, this.error,
} });
);
/// True if the connection was successful. False if it failed for any reason. /// True if the connection was successful. False if it failed for any reason.
final bool success; final bool success;
@@ -96,12 +95,10 @@ class XmppConnection {
XmppConnection( XmppConnection(
ReconnectionPolicy reconnectionPolicy, ReconnectionPolicy reconnectionPolicy,
ConnectivityManager connectivityManager, ConnectivityManager connectivityManager,
this._socket, this._socket, {
{
this.connectionPingDuration = const Duration(minutes: 3), this.connectionPingDuration = const Duration(minutes: 3),
this.connectingTimeout = const Duration(minutes: 2), this.connectingTimeout = const Duration(minutes: 2),
} }) : _reconnectionPolicy = reconnectionPolicy,
) : _reconnectionPolicy = reconnectionPolicy,
_connectivityManager = connectivityManager { _connectivityManager = connectivityManager {
// Allow the reconnection policy to perform reconnections by itself // Allow the reconnection policy to perform reconnections by itself
_reconnectionPolicy.register( _reconnectionPolicy.register(
@@ -115,7 +112,6 @@ class XmppConnection {
_socket.getEventStream().listen(_handleSocketEvent); _socket.getEventStream().listen(_handleSocketEvent);
} }
/// The state that the connection is currently in /// The state that the connection is currently in
XmppConnectionState _connectionState = XmppConnectionState.notConnected; XmppConnectionState _connectionState = XmppConnectionState.notConnected;
@@ -139,11 +135,16 @@ class XmppConnection {
final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter(); final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter();
/// Sorted list of handlers that we call or incoming and outgoing stanzas /// Sorted list of handlers that we call or incoming and outgoing stanzas
final List<StanzaHandler> _incomingStanzaHandlers = List.empty(growable: true); final List<StanzaHandler> _incomingStanzaHandlers =
final List<StanzaHandler> _incomingPreStanzaHandlers = List.empty(growable: true); List.empty(growable: true);
final List<StanzaHandler> _outgoingPreStanzaHandlers = List.empty(growable: true); final List<StanzaHandler> _incomingPreStanzaHandlers =
final List<StanzaHandler> _outgoingPostStanzaHandlers = List.empty(growable: true); List.empty(growable: true);
final StreamController<XmppEvent> _eventStreamController = StreamController.broadcast(); final List<StanzaHandler> _outgoingPreStanzaHandlers =
List.empty(growable: true);
final List<StanzaHandler> _outgoingPostStanzaHandlers =
List.empty(growable: true);
final StreamController<XmppEvent> _eventStreamController =
StreamController.broadcast();
final Map<String, XmppManagerBase> _xmppManagers = {}; final Map<String, XmppManagerBase> _xmppManagers = {};
/// Disco info we got after binding a resource (xmlns) /// Disco info we got after binding a resource (xmlns)
@@ -185,6 +186,7 @@ class XmppConnection {
final Map<String, XmppFeatureNegotiatorBase> _featureNegotiators = {}; final Map<String, XmppFeatureNegotiatorBase> _featureNegotiators = {};
XmppFeatureNegotiatorBase? _currentNegotiator; XmppFeatureNegotiatorBase? _currentNegotiator;
final List<XMLNode> _streamFeatures = List.empty(growable: true); final List<XMLNode> _streamFeatures = List.empty(growable: true);
/// Prevent data from being passed to _currentNegotiator.negotiator while the negotiator /// Prevent data from being passed to _currentNegotiator.negotiator while the negotiator
/// is still running. /// is still running.
final Lock _negotiationLock = Lock(); final Lock _negotiationLock = Lock();
@@ -200,7 +202,8 @@ class XmppConnection {
/// and does the following: /// and does the following:
/// - if _isConnectionRunning is false, set it to true and return false. /// - if _isConnectionRunning is false, set it to true and return false.
/// - if _isConnectionRunning is true, return true. /// - if _isConnectionRunning is true, return true.
Future<bool> _testAndSetIsConnectionRunning() async => _connectionRunningLock.synchronized(() { Future<bool> _testAndSetIsConnectionRunning() async =>
_connectionRunningLock.synchronized(() {
if (!_isConnectionRunning) { if (!_isConnectionRunning) {
_isConnectionRunning = true; _isConnectionRunning = true;
return false; return false;
@@ -211,7 +214,8 @@ class XmppConnection {
/// Enters the critical section for accessing [XmppConnection._isConnectionRunning] /// Enters the critical section for accessing [XmppConnection._isConnectionRunning]
/// and sets it to false. /// and sets it to false.
Future<void> _resetIsConnectionRunning() async => _connectionRunningLock.synchronized(() => _isConnectionRunning = false); Future<void> _resetIsConnectionRunning() async =>
_connectionRunningLock.synchronized(() => _isConnectionRunning = false);
ReconnectionPolicy get reconnectionPolicy => _reconnectionPolicy; ReconnectionPolicy get reconnectionPolicy => _reconnectionPolicy;
@@ -221,7 +225,8 @@ class XmppConnection {
/// Return the registered feature negotiator that has id [id]. Returns null if /// Return the registered feature negotiator that has id [id]. Returns null if
/// none can be found. /// none can be found.
T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) => _featureNegotiators[id] as T?; T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) =>
_featureNegotiators[id] as T?;
/// Registers a list of [XmppManagerBase] sub-classes as managers on this connection. /// Registers a list of [XmppManagerBase] sub-classes as managers on this connection.
Future<void> registerManagers(List<XmppManagerBase> managers) async { Future<void> registerManagers(List<XmppManagerBase> managers) async {
@@ -247,7 +252,8 @@ class XmppConnection {
_incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers()); _incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers());
_incomingPreStanzaHandlers.addAll(manager.getIncomingPreStanzaHandlers()); _incomingPreStanzaHandlers.addAll(manager.getIncomingPreStanzaHandlers());
_outgoingPreStanzaHandlers.addAll(manager.getOutgoingPreStanzaHandlers()); _outgoingPreStanzaHandlers.addAll(manager.getOutgoingPreStanzaHandlers());
_outgoingPostStanzaHandlers.addAll(manager.getOutgoingPostStanzaHandlers()); _outgoingPostStanzaHandlers
.addAll(manager.getOutgoingPostStanzaHandlers());
} }
// Sort them // Sort them
@@ -303,12 +309,16 @@ class XmppConnection {
} }
/// Returns the Manager with id [id] or null if such a manager is not registered. /// Returns the Manager with id [id] or null if such a manager is not registered.
T? getManagerById<T extends XmppManagerBase>(String id) => _xmppManagers[id] as T?; T? getManagerById<T extends XmppManagerBase>(String id) =>
_xmppManagers[id] as T?;
/// A [PresenceManager] is required, so have a wrapper for getting it. /// A [PresenceManager] is required, so have a wrapper for getting it.
/// Returns the registered [PresenceManager]. /// Returns the registered [PresenceManager].
PresenceManager getPresenceManager() { PresenceManager getPresenceManager() {
assert(_xmppManagers.containsKey(presenceManager), 'A PresenceManager is mandatory'); assert(
_xmppManagers.containsKey(presenceManager),
'A PresenceManager is mandatory',
);
return getManagerById(presenceManager)!; return getManagerById(presenceManager)!;
} }
@@ -316,7 +326,10 @@ class XmppConnection {
/// A [DiscoManager] is required so, have a wrapper for getting it. /// A [DiscoManager] is required so, have a wrapper for getting it.
/// Returns the registered [DiscoManager]. /// Returns the registered [DiscoManager].
DiscoManager getDiscoManager() { DiscoManager getDiscoManager() {
assert(_xmppManagers.containsKey(discoManager), 'A DiscoManager is mandatory'); assert(
_xmppManagers.containsKey(discoManager),
'A DiscoManager is mandatory',
);
return getManagerById(discoManager)!; return getManagerById(discoManager)!;
} }
@@ -324,7 +337,10 @@ class XmppConnection {
/// A [RosterManager] is required, so have a wrapper for getting it. /// A [RosterManager] is required, so have a wrapper for getting it.
/// Returns the registered [RosterManager]. /// Returns the registered [RosterManager].
RosterManager getRosterManager() { RosterManager getRosterManager() {
assert(_xmppManagers.containsKey(rosterManager), 'A RosterManager is mandatory'); assert(
_xmppManagers.containsKey(rosterManager),
'A RosterManager is mandatory',
);
return getManagerById(rosterManager)!; return getManagerById(rosterManager)!;
} }
@@ -352,7 +368,9 @@ class XmppConnection {
/// Attempts to reconnect to the server by following an exponential backoff. /// Attempts to reconnect to the server by following an exponential backoff.
Future<void> _attemptReconnection() async { Future<void> _attemptReconnection() async {
if (await _testAndSetIsConnectionRunning()) { if (await _testAndSetIsConnectionRunning()) {
_log.warning('_attemptReconnection is called but connection attempt is already running. Ignoring...'); _log.warning(
'_attemptReconnection is called but connection attempt is already running. Ignoring...',
);
return; return;
} }
@@ -378,8 +396,13 @@ class XmppConnection {
// the connection result is being awaited, don't attempt a reconnection but instead // the connection result is being awaited, don't attempt a reconnection but instead
// try to gracefully disconnect. // try to gracefully disconnect.
if (_connectionCompleter != null) { if (_connectionCompleter != null) {
_log.info('Not triggering reconnection since connection result is being awaited'); _log.info(
await _disconnect(triggeredByUser: false, state: XmppConnectionState.error); 'Not triggering reconnection since connection result is being awaited',
);
await _disconnect(
triggeredByUser: false,
state: XmppConnectionState.error,
);
_connectionCompleter?.complete( _connectionCompleter?.complete(
XmppConnectionResult( XmppConnectionResult(
false, false,
@@ -390,11 +413,20 @@ class XmppConnection {
return; return;
} }
if (await _connectivityManager.hasConnection()) { if (!error.isRecoverable()) {
// We cannot recover this error
_log.severe(
'Since a $error is not recoverable, not attempting a reconnection',
);
await _setConnectionState(XmppConnectionState.error); await _setConnectionState(XmppConnectionState.error);
} else { await _sendEvent(
await _setConnectionState(XmppConnectionState.notConnected); NonRecoverableErrorEvent(error),
);
return;
} }
// The error is recoverable
await _setConnectionState(XmppConnectionState.notConnected);
await _reconnectionPolicy.onFailure(); await _reconnectionPolicy.onFailure();
} }
@@ -404,10 +436,14 @@ class XmppConnection {
await handleError(SocketError(event)); await handleError(SocketError(event));
} else if (event is XmppSocketClosureEvent) { } else if (event is XmppSocketClosureEvent) {
if (!event.expected) { if (!event.expected) {
_log.fine('Received unexpected XmppSocketClosureEvent. Reconnecting...'); _log.fine(
'Received unexpected XmppSocketClosureEvent. Reconnecting...',
);
await handleError(SocketError(XmppSocketErrorEvent(event))); await handleError(SocketError(XmppSocketErrorEvent(event)));
} else { } else {
_log.fine('Received XmppSocketClosureEvent. No reconnection attempt since _socketClosureTriggersReconnect is false...'); _log.fine(
'Received XmppSocketClosureEvent. No reconnection attempt since _socketClosureTriggersReconnect is false...',
);
} }
} }
} }
@@ -424,7 +460,7 @@ class XmppConnection {
} }
/// Sends an [XMLNode] without any further processing to the server. /// Sends an [XMLNode] without any further processing to the server.
void sendRawXML(XMLNode node, { String? redact }) { void sendRawXML(XMLNode node, {String? redact}) {
final string = node.toXml(); final string = node.toXml();
_log.finest('==> $string'); _log.finest('==> $string');
_socket.write(string, redact: redact); _socket.write(string, redact: redact);
@@ -437,10 +473,8 @@ class XmppConnection {
/// Returns true if we can send data through the socket. /// Returns true if we can send data through the socket.
Future<bool> _canSendData() async { Future<bool> _canSendData() async {
return [ return [XmppConnectionState.connected, XmppConnectionState.connecting]
XmppConnectionState.connected, .contains(await getConnectionState());
XmppConnectionState.connecting
].contains(await getConnectionState());
} }
/// Sends a [stanza] to the server. If stream management is enabled, then keeping track /// Sends a [stanza] to the server. If stream management is enabled, then keeping track
@@ -452,25 +486,43 @@ class XmppConnection {
/// If addId is true, then an 'id' attribute will be added to the stanza if [stanza] has /// If addId is true, then an 'id' attribute will be added to the stanza if [stanza] has
/// none. /// none.
// TODO(Unknown): if addId = false, the function crashes. // TODO(Unknown): if addId = false, the function crashes.
Future<XMLNode> sendStanza(Stanza stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async { Future<XMLNode> sendStanza(
assert(implies(addId == false && stanza.id == null, !awaitable), 'Cannot await a stanza with no id'); Stanza stanza, {
StanzaFromType addFrom = StanzaFromType.full,
bool addId = true,
bool awaitable = true,
bool encrypted = false,
bool forceEncryption = false,
}) async {
assert(
implies(addId == false && stanza.id == null, !awaitable),
'Cannot await a stanza with no id',
);
// Add extra data in case it was not set // Add extra data in case it was not set
var stanza_ = stanza; var stanza_ = stanza;
if (addId && (stanza_.id == null || stanza_.id == '')) { if (addId && (stanza_.id == null || stanza_.id == '')) {
stanza_ = stanza.copyWith(id: generateId()); stanza_ = stanza.copyWith(id: generateId());
} }
if (addFrom != StanzaFromType.none && (stanza_.from == null || stanza_.from == '')) { if (addFrom != StanzaFromType.none &&
(stanza_.from == null || stanza_.from == '')) {
switch (addFrom) { switch (addFrom) {
case StanzaFromType.full: { case StanzaFromType.full:
stanza_ = stanza_.copyWith(from: _connectionSettings.jid.withResource(_resource).toString()); {
stanza_ = stanza_.copyWith(
from: _connectionSettings.jid.withResource(_resource).toString(),
);
} }
break; break;
case StanzaFromType.bare: { case StanzaFromType.bare:
stanza_ = stanza_.copyWith(from: _connectionSettings.jid.toBare().toString()); {
stanza_ = stanza_.copyWith(
from: _connectionSettings.jid.toBare().toString(),
);
} }
break; break;
case StanzaFromType.none: break; case StanzaFromType.none:
break;
} }
} }
@@ -497,16 +549,16 @@ class XmppConnection {
from: data.stanza.to, from: data.stanza.to,
attributes: <String, String>{ attributes: <String, String>{
'type': 'error', 'type': 'error',
...data.stanza.id != null ? { ...data.stanza.id != null
? {
'id': data.stanza.id!, 'id': data.stanza.id!,
} : {}, }
: {},
}, },
); );
} }
final prefix = data.encrypted ? final prefix = data.encrypted ? '(Encrypted) ' : '';
'(Encrypted) ' :
'';
_log.finest('==> $prefix${stanza_.toXml()}'); _log.finest('==> $prefix${stanza_.toXml()}');
final stanzaString = data.stanza.toXml(); final stanzaString = data.stanza.toXml();
@@ -576,7 +628,9 @@ class XmppConnection {
final oldState = _connectionState; final oldState = _connectionState;
_connectionState = state; _connectionState = state;
final sm = getNegotiatorById<StreamManagementNegotiator>(streamManagementNegotiator); final sm = getNegotiatorById<StreamManagementNegotiator>(
streamManagementNegotiator,
);
await _sendEvent( await _sendEvent(
ConnectionStateChangedEvent( ConnectionStateChangedEvent(
state, state,
@@ -587,7 +641,8 @@ class XmppConnection {
if (state == XmppConnectionState.connected) { if (state == XmppConnectionState.connected) {
_log.finest('Starting _pingConnectionTimer'); _log.finest('Starting _pingConnectionTimer');
_connectionPingTimer = Timer.periodic(connectionPingDuration, _pingConnectionOpen); _connectionPingTimer =
Timer.periodic(connectionPingDuration, _pingConnectionOpen);
// We are connected, so the timer can stop. // We are connected, so the timer can stop.
_destroyConnectingTimer(); _destroyConnectingTimer();
@@ -638,14 +693,20 @@ class XmppConnection {
_log.finest('_pingConnectionTimer: Connected. Triggering a ping event.'); _log.finest('_pingConnectionTimer: Connected. Triggering a ping event.');
unawaited(_sendEvent(SendPingEvent())); unawaited(_sendEvent(SendPingEvent()));
} else { } else {
_log.finest('_pingConnectionTimer: Not connected. Not triggering an event.'); _log.finest(
'_pingConnectionTimer: Not connected. Not triggering an event.',
);
} }
} }
/// Iterate over [handlers] and check if the handler matches [stanza]. If it does, /// Iterate over [handlers] and check if the handler matches [stanza]. If it does,
/// call its callback and end the processing if the callback returned true; continue /// call its callback and end the processing if the callback returned true; continue
/// if it returned false. /// if it returned false.
Future<StanzaHandlerData> _runStanzaHandlers(List<StanzaHandler> handlers, Stanza stanza, { StanzaHandlerData? initial }) async { Future<StanzaHandlerData> _runStanzaHandlers(
List<StanzaHandler> handlers,
Stanza stanza, {
StanzaHandlerData? initial,
}) async {
var state = initial ?? StanzaHandlerData(false, false, null, stanza); var state = initial ?? StanzaHandlerData(false, false, null, stanza);
for (final handler in handlers) { for (final handler in handlers) {
if (handler.matches(state.stanza)) { if (handler.matches(state.stanza)) {
@@ -657,19 +718,36 @@ class XmppConnection {
return state; return state;
} }
Future<StanzaHandlerData> _runIncomingStanzaHandlers(Stanza stanza, { StanzaHandlerData? initial }) async { Future<StanzaHandlerData> _runIncomingStanzaHandlers(
return _runStanzaHandlers(_incomingStanzaHandlers, stanza, initial: initial); Stanza stanza, {
StanzaHandlerData? initial,
}) async {
return _runStanzaHandlers(
_incomingStanzaHandlers,
stanza,
initial: initial,
);
} }
Future<StanzaHandlerData> _runIncomingPreStanzaHandlers(Stanza stanza) async { Future<StanzaHandlerData> _runIncomingPreStanzaHandlers(Stanza stanza) async {
return _runStanzaHandlers(_incomingPreStanzaHandlers, stanza); return _runStanzaHandlers(_incomingPreStanzaHandlers, stanza);
} }
Future<StanzaHandlerData> _runOutgoingPreStanzaHandlers(Stanza stanza, { StanzaHandlerData? initial }) async { Future<StanzaHandlerData> _runOutgoingPreStanzaHandlers(
return _runStanzaHandlers(_outgoingPreStanzaHandlers, stanza, initial: initial); Stanza stanza, {
StanzaHandlerData? initial,
}) async {
return _runStanzaHandlers(
_outgoingPreStanzaHandlers,
stanza,
initial: initial,
);
} }
Future<bool> _runOutgoingPostStanzaHandlers(Stanza stanza, { StanzaHandlerData? initial }) async { Future<bool> _runOutgoingPostStanzaHandlers(
Stanza stanza, {
StanzaHandlerData? initial,
}) async {
final data = await _runStanzaHandlers( final data = await _runStanzaHandlers(
_outgoingPostStanzaHandlers, _outgoingPostStanzaHandlers,
stanza, stanza,
@@ -685,14 +763,12 @@ class XmppConnection {
_log.finest('<== ${nonza.toXml()}'); _log.finest('<== ${nonza.toXml()}');
var nonzaHandled = false; var nonzaHandled = false;
await Future.forEach( await Future.forEach(_xmppManagers.values,
_xmppManagers.values,
(XmppManagerBase manager) async { (XmppManagerBase manager) async {
final handled = await manager.runNonzaHandlers(nonza); final handled = await manager.runNonzaHandlers(nonza);
if (!nonzaHandled && handled) nonzaHandled = true; if (!nonzaHandled && handled) nonzaHandled = true;
} });
);
if (!nonzaHandled) { if (!nonzaHandled) {
_log.warning('Unhandled nonza received: ${nonza.toXml()}'); _log.warning('Unhandled nonza received: ${nonza.toXml()}');
@@ -705,9 +781,10 @@ class XmppConnection {
// Run the incoming stanza handlers and bounce with an error if no manager handled // Run the incoming stanza handlers and bounce with an error if no manager handled
// it. // it.
final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza); final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza);
final prefix = incomingPreHandlers.encrypted && incomingPreHandlers.other['encryption_error'] == null ? final prefix = incomingPreHandlers.encrypted &&
'(Encrypted) ' : incomingPreHandlers.other['encryption_error'] == null
''; ? '(Encrypted) '
: '';
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}'); _log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
final awaited = await _stanzaAwaiter.onData( final awaited = await _stanzaAwaiter.onData(
@@ -738,11 +815,10 @@ class XmppConnection {
/// Returns true if all mandatory features in [features] have been negotiated. /// Returns true if all mandatory features in [features] have been negotiated.
/// Otherwise returns false. /// Otherwise returns false.
bool _isMandatoryNegotiationDone(List<XMLNode> features) { bool _isMandatoryNegotiationDone(List<XMLNode> features) {
return features.every( return features.every((XMLNode feature) {
(XMLNode feature) { return feature.firstTag('required') == null &&
return feature.firstTag('required') == null && feature.tag != 'mechanisms'; feature.tag != 'mechanisms';
} });
);
} }
/// Returns true if we can still negotiate. Returns false if no negotiator is /// Returns true if we can still negotiate. Returns false if no negotiator is
@@ -754,18 +830,21 @@ class XmppConnection {
/// Returns the next negotiator that matches [features]. Returns null if none can be /// 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. /// picked. If [log] is true, then the list of matching negotiators will be logged.
@visibleForTesting @visibleForTesting
XmppFeatureNegotiatorBase? getNextNegotiator(List<XMLNode> features, {bool log = true}) { XmppFeatureNegotiatorBase? getNextNegotiator(
List<XMLNode> features, {
bool log = true,
}) {
final matchingNegotiators = _featureNegotiators.values final matchingNegotiators = _featureNegotiators.values
.where( .where((XmppFeatureNegotiatorBase negotiator) {
(XmppFeatureNegotiatorBase negotiator) { return negotiator.state == NegotiatorState.ready &&
return negotiator.state == NegotiatorState.ready && negotiator.matchesFeature(features); negotiator.matchesFeature(features);
} }).toList()
)
.toList()
..sort((a, b) => b.priority.compareTo(a.priority)); ..sort((a, b) => b.priority.compareTo(a.priority));
if (log) { if (log) {
_log.finest('List of matching negotiators: ${matchingNegotiators.map((a) => a.id)}'); _log.finest(
'List of matching negotiators: ${matchingNegotiators.map((a) => a.id)}',
);
} }
if (matchingNegotiators.isEmpty) return null; if (matchingNegotiators.isEmpty) return null;
@@ -784,6 +863,10 @@ class XmppConnection {
_connectionCompleter?.complete(const XmppConnectionResult(true)); _connectionCompleter?.complete(const XmppConnectionResult(true));
_connectionCompleter = null; _connectionCompleter = null;
// Tell consumers of the event stream that we're done with stream feature
// negotiations
await _sendEvent(StreamNegotiationsDoneEvent());
// Send out initial presence // Send out initial presence
await getPresenceManager().sendInitialPresence(); await getPresenceManager().sendInitialPresence();
} }
@@ -791,7 +874,9 @@ class XmppConnection {
Future<void> _executeCurrentNegotiator(XMLNode nonza) async { Future<void> _executeCurrentNegotiator(XMLNode nonza) async {
// If we don't have a negotiator get one // If we don't have a negotiator get one
_currentNegotiator ??= getNextNegotiator(_streamFeatures); _currentNegotiator ??= getNextNegotiator(_streamFeatures);
if (_currentNegotiator == null && _isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) { if (_currentNegotiator == null &&
_isMandatoryNegotiationDone(_streamFeatures) &&
!_isNegotiationPossible(_streamFeatures)) {
_log.finest('Negotiations done!'); _log.finest('Negotiations done!');
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
await _onNegotiationsDone(); await _onNegotiationsDone();
@@ -809,20 +894,22 @@ class XmppConnection {
final state = result.get<NegotiatorState>(); final state = result.get<NegotiatorState>();
_currentNegotiator!.state = state; _currentNegotiator!.state = state;
switch (state) { switch (state) {
case NegotiatorState.ready: return; case NegotiatorState.ready:
return;
case NegotiatorState.done: case NegotiatorState.done:
if (_currentNegotiator!.sendStreamHeaderWhenDone) { if (_currentNegotiator!.sendStreamHeaderWhenDone) {
_currentNegotiator = null; _currentNegotiator = null;
_streamFeatures.clear(); _streamFeatures.clear();
_sendStreamHeader(); _sendStreamHeader();
} else { } else {
_streamFeatures _streamFeatures.removeWhere((node) {
.removeWhere((node) { return node.attributes['xmlns'] ==
return node.attributes['xmlns'] == _currentNegotiator!.negotiatingXmlns; _currentNegotiator!.negotiatingXmlns;
}); });
_currentNegotiator = null; _currentNegotiator = null;
if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) { if (_isMandatoryNegotiationDone(_streamFeatures) &&
!_isNegotiationPossible(_streamFeatures)) {
_log.finest('Negotiations done!'); _log.finest('Negotiations done!');
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
@@ -844,8 +931,8 @@ class XmppConnection {
_log.finest('Negotiator wants to continue later. Picking new one...'); _log.finest('Negotiator wants to continue later. Picking new one...');
_currentNegotiator!.state = NegotiatorState.ready; _currentNegotiator!.state = NegotiatorState.ready;
if (_isMandatoryNegotiationDone(_streamFeatures) &&
if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) { !_isNegotiationPossible(_streamFeatures)) {
_log.finest('Negotiations done!'); _log.finest('Negotiations done!');
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
@@ -863,7 +950,9 @@ class XmppConnection {
} }
break; break;
case NegotiatorState.skipRest: case NegotiatorState.skipRest:
_log.finest('Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!'); _log.finest(
'Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!',
);
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
@@ -930,13 +1019,17 @@ class XmppConnection {
// Specific event handling // Specific event handling
if (event is ResourceBindingSuccessEvent) { if (event is ResourceBindingSuccessEvent) {
_log.finest('Received ResourceBindingSuccessEvent. Setting _resource to ${event.resource}'); _log.finest(
'Received ResourceBindingSuccessEvent. Setting _resource to ${event.resource}',
);
setResource(event.resource); setResource(event.resource);
_log.finest('Resetting _serverFeatures'); _log.finest('Resetting _serverFeatures');
_serverFeatures.clear(); _serverFeatures.clear();
} else if (event is AuthenticationSuccessEvent) { } else if (event is AuthenticationSuccessEvent) {
_log.finest('Received AuthenticationSuccessEvent. Setting _isAuthenticated to true'); _log.finest(
'Received AuthenticationSuccessEvent. Setting _isAuthenticated to true',
);
_isAuthenticated = true; _isAuthenticated = true;
} }
@@ -952,9 +1045,7 @@ class XmppConnection {
_socket.write( _socket.write(
XMLNode( XMLNode(
tag: 'xml', tag: 'xml',
attributes: <String, String>{ attributes: <String, String>{'version': '1.0'},
'version': '1.0'
},
closeTag: false, closeTag: false,
isDeclaration: true, isDeclaration: true,
children: [ children: [
@@ -976,7 +1067,10 @@ class XmppConnection {
await _disconnect(state: XmppConnectionState.notConnected); await _disconnect(state: XmppConnectionState.notConnected);
} }
Future<void> _disconnect({required XmppConnectionState state, bool triggeredByUser = true}) async { Future<void> _disconnect({
required XmppConnectionState state,
bool triggeredByUser = true,
}) async {
await _reconnectionPolicy.setShouldReconnect(false); await _reconnectionPolicy.setShouldReconnect(false);
if (triggeredByUser) { if (triggeredByUser) {
@@ -1000,15 +1094,30 @@ class XmppConnection {
/// Make sure that all required managers are registered /// Make sure that all required managers are registered
void _runPreConnectionAssertions() { void _runPreConnectionAssertions() {
assert(_xmppManagers.containsKey(presenceManager), 'A PresenceManager is mandatory'); assert(
assert(_xmppManagers.containsKey(rosterManager), 'A RosterManager is mandatory'); _xmppManagers.containsKey(presenceManager),
assert(_xmppManagers.containsKey(discoManager), 'A DiscoManager is mandatory'); 'A PresenceManager is mandatory',
assert(_xmppManagers.containsKey(pingManager), 'A PingManager is mandatory'); );
assert(
_xmppManagers.containsKey(rosterManager),
'A RosterManager is mandatory',
);
assert(
_xmppManagers.containsKey(discoManager),
'A DiscoManager is mandatory',
);
assert(
_xmppManagers.containsKey(pingManager),
'A PingManager is mandatory',
);
} }
/// Like [connect] but the Future resolves when the resource binding is either done or /// Like [connect] but the Future resolves when the resource binding is either done or
/// SASL has failed. /// SASL has failed.
Future<XmppConnectionResult> connectAwaitable({ String? lastResource, bool waitForConnection = false }) async { Future<XmppConnectionResult> connectAwaitable({
String? lastResource,
bool waitForConnection = false,
}) async {
_runPreConnectionAssertions(); _runPreConnectionAssertions();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
_connectionCompleter = Completer(); _connectionCompleter = Completer();
@@ -1022,9 +1131,16 @@ class XmppConnection {
} }
/// Start the connection process using the provided connection settings. /// Start the connection process using the provided connection settings.
Future<void> connect({ String? lastResource, bool waitForConnection = false, bool shouldReconnect = true }) async { Future<void> connect({
if (_connectionState != XmppConnectionState.notConnected && _connectionState != XmppConnectionState.error) { String? lastResource,
_log.fine('Cancelling this connection attempt as one appears to be already running.'); bool waitForConnection = false,
bool shouldReconnect = true,
}) async {
if (_connectionState != XmppConnectionState.notConnected &&
_connectionState != XmppConnectionState.error) {
_log.fine(
'Cancelling this connection attempt as one appears to be already running.',
);
return; return;
} }

View File

@@ -1,20 +1,37 @@
import 'package:moxxmpp/src/socket.dart'; import 'package:moxxmpp/src/socket.dart';
/// An internal error class /// An internal error class
abstract class XmppError {} // 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 /// Returned if we could not establish a TCP connection
/// to the server. /// to the server.
class NoConnectionError extends XmppError {} class NoConnectionError extends XmppError {
@override
bool isRecoverable() => true;
}
/// Returned if a socket error occured /// Returned if a socket error occured
class SocketError extends XmppError { class SocketError extends XmppError {
SocketError(this.event); SocketError(this.event);
final XmppSocketErrorEvent event; final XmppSocketErrorEvent event;
@override
bool isRecoverable() => true;
} }
/// Returned if we time out /// Returned if we time out
class TimeoutError extends XmppError {} class TimeoutError extends XmppError {
@override
bool isRecoverable() => true;
}
/// Returned if we received a stream error /// Returned if we received a stream error
class StreamError extends XmppError {} class StreamError extends XmppError {
// TODO(PapaTutuWawa): Be more precise
@override
bool isRecoverable() => true;
}

View File

@@ -1,4 +1,5 @@
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/roster/roster.dart';
@@ -29,7 +30,7 @@ class ConnectionStateChangedEvent extends XmppEvent {
/// Triggered when we encounter a stream error. /// Triggered when we encounter a stream error.
class StreamErrorEvent extends XmppEvent { class StreamErrorEvent extends XmppEvent {
StreamErrorEvent({ required this.error }); StreamErrorEvent({required this.error});
final String error; final String error;
} }
@@ -47,7 +48,7 @@ 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});
final int h; final int h;
} }
@@ -127,7 +128,7 @@ class MessageEvent extends XmppEvent {
/// Triggered when a client responds to our delivery receipt request /// Triggered when a client responds to our delivery receipt request
class DeliveryReceiptReceivedEvent extends XmppEvent { class DeliveryReceiptReceivedEvent extends XmppEvent {
DeliveryReceiptReceivedEvent({ required this.from, required this.id }); DeliveryReceiptReceivedEvent({required this.from, required this.id});
final JID from; final JID from;
final String id; final String id;
} }
@@ -157,7 +158,7 @@ class StreamManagementEnabledEvent extends XmppEvent {
/// Triggered when we bound a resource /// Triggered when we bound a resource
class ResourceBindingSuccessEvent extends XmppEvent { class ResourceBindingSuccessEvent extends XmppEvent {
ResourceBindingSuccessEvent({ required this.resource }); ResourceBindingSuccessEvent({required this.resource});
final String resource; final String resource;
} }
@@ -181,13 +182,17 @@ class ServerItemDiscoEvent extends XmppEvent {
/// Triggered when we receive a subscription request /// Triggered when we receive a subscription request
class SubscriptionRequestReceivedEvent extends XmppEvent { class SubscriptionRequestReceivedEvent extends XmppEvent {
SubscriptionRequestReceivedEvent({ required this.from }); SubscriptionRequestReceivedEvent({required this.from});
final JID from; final JID from;
} }
/// 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;
@@ -195,7 +200,7 @@ class AvatarUpdatedEvent extends XmppEvent {
/// Triggered when a PubSub notification has been received /// Triggered when a PubSub notification has been received
class PubSubNotificationEvent extends XmppEvent { class PubSubNotificationEvent extends XmppEvent {
PubSubNotificationEvent({ required this.item, required this.from }); PubSubNotificationEvent({required this.item, required this.from});
final PubSubItem item; final PubSubItem item;
final String from; final String from;
} }
@@ -208,13 +213,13 @@ class StanzaAckedEvent extends XmppEvent {
/// Triggered when receiving a push of the blocklist /// Triggered when receiving a push of the blocklist
class BlocklistBlockPushEvent extends XmppEvent { class BlocklistBlockPushEvent extends XmppEvent {
BlocklistBlockPushEvent({ required this.items }); BlocklistBlockPushEvent({required this.items});
final List<String> items; final List<String> items;
} }
/// Triggered when receiving a push of the blocklist /// Triggered when receiving a push of the blocklist
class BlocklistUnblockPushEvent extends XmppEvent { class BlocklistUnblockPushEvent extends XmppEvent {
BlocklistUnblockPushEvent({ required this.items }); BlocklistUnblockPushEvent({required this.items});
final List<String> items; final List<String> items;
} }
@@ -236,3 +241,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

@@ -5,7 +5,10 @@ import 'package:moxxmpp/src/stanza.dart';
/// Bounce a stanza if it was not handled by any manager. [conn] is the connection object /// Bounce a stanza if it was not handled by any manager. [conn] is the connection object
/// to use for sending the stanza. [data] is the StanzaHandlerData of the unhandled /// to use for sending the stanza. [data] is the StanzaHandlerData of the unhandled
/// stanza. /// stanza.
Future<void> handleUnhandledStanza(XmppConnection conn, StanzaHandlerData data) async { Future<void> handleUnhandledStanza(
XmppConnection conn,
StanzaHandlerData data,
) async {
if (data.stanza.type != 'error' && data.stanza.type != 'result') { if (data.stanza.type != 'error' && data.stanza.type != 'result') {
final stanza = data.stanza.copyWith( final stanza = data.stanza.copyWith(
to: data.stanza.from, to: data.stanza.from,

View File

@@ -18,7 +18,10 @@ class JID {
} else { } else {
resourcePart = slashParts.sublist(1).join('/'); resourcePart = slashParts.sublist(1).join('/');
assert(resourcePart.isNotEmpty, 'Resource part cannot be there and empty'); assert(
resourcePart.isNotEmpty,
'Resource part cannot be there and empty',
);
} }
final atParts = slashParts.first.split('@'); final atParts = slashParts.first.split('@');
@@ -34,9 +37,9 @@ class JID {
return JID( return JID(
localPart, localPart,
domainPart.endsWith('.') ? domainPart.endsWith('.')
domainPart.substring(0, domainPart.length - 1) : ? domainPart.substring(0, domainPart.length - 1)
domainPart, : domainPart,
resourcePart, resourcePart,
); );
} }
@@ -63,7 +66,7 @@ class JID {
/// Compares the JID with [other]. This function assumes that JID and [other] /// Compares the JID with [other]. This function assumes that JID and [other]
/// are bare, i.e. only the domain- and localparts are compared. If [ensureBare] /// are bare, i.e. only the domain- and localparts are compared. If [ensureBare]
/// is optionally set to true, then [other] MUST be bare. Otherwise, false is returned. /// is optionally set to true, then [other] MUST be bare. Otherwise, false is returned.
bool bareCompare(JID other, { bool ensureBare = false }) { bool bareCompare(JID other, {bool ensureBare = false}) {
if (ensureBare && !other.isBare()) return false; if (ensureBare && !other.isBare()) return false;
return local == other.local && domain == other.domain; return local == other.local && domain == other.domain;
@@ -90,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

@@ -22,8 +22,16 @@ class XmppManagerAttributes {
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, bool forceEncryption}) 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;
@@ -49,5 +57,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

@@ -8,6 +8,7 @@ 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/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_0198/xep_0198.dart';
abstract class XmppManagerBase { abstract class XmppManagerBase {
XmppManagerBase(this.id); XmppManagerBase(this.id);
@@ -98,25 +99,39 @@ abstract class XmppManagerBase {
/// 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 /// Sends a reply of the stanza in [data] with [type]. Replaces the original stanza's
/// children with [children]. /// children with [children].
/// ///
/// Note that this function currently only accepts IQ stanzas. /// Note that this function currently only accepts IQ stanzas.
Future<void> reply(StanzaHandlerData data, String type, List<XMLNode> children) async { Future<void> reply(
assert(data.stanza.tag == 'iq', 'Reply makes little sense for non-IQ stanzas'); 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( final stanza = data.stanza.copyWith(
to: data.stanza.from, to: data.stanza.from,

View File

@@ -27,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,
@@ -71,6 +70,5 @@ class StanzaHandlerData with _$StanzaHandlerData {
MessageReactions? messageReactions, MessageReactions? messageReactions,
// The Id of the sticker pack this sticker belongs to // The Id of the sticker pack this sticker belongs to
String? stickerPackId, String? stickerPackId,
} }) = _StanzaHandlerData;
) = _StanzaHandlerData;
} }

View File

@@ -5,7 +5,7 @@ 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;
final bool matchStanzas; final bool matchStanzas;
@@ -19,11 +19,12 @@ 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) {
matches = [ 'iq', 'presence', 'message' ].contains(node.tag); matches = ['iq', 'presence', 'message'].contains(node.tag);
} }
return matches; return matches;
@@ -75,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,
); );
} }
@@ -87,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

@@ -10,7 +10,8 @@ const pubsubManager = 'org.moxxmpp.pubsubmanager';
const userAvatarManager = 'org.moxxmpp.useravatarmanager'; const userAvatarManager = 'org.moxxmpp.useravatarmanager';
const stableIdManager = 'org.moxxmpp.stableidmanager'; const stableIdManager = 'org.moxxmpp.stableidmanager';
const simsManager = 'org.moxxmpp.simsmanager'; const simsManager = 'org.moxxmpp.simsmanager';
const messageDeliveryReceiptManager = 'org.moxxmpp.messagedeliveryreceiptmanager'; const messageDeliveryReceiptManager =
'org.moxxmpp.messagedeliveryreceiptmanager';
const chatMarkerManager = 'org.moxxmpp.chatmarkermanager'; const chatMarkerManager = 'org.moxxmpp.chatmarkermanager';
const oobManager = 'org.moxxmpp.oobmanager'; const oobManager = 'org.moxxmpp.oobmanager';
const sfsManager = 'org.moxxmpp.sfsmanager'; const sfsManager = 'org.moxxmpp.sfsmanager';
@@ -19,7 +20,8 @@ const blockingManager = 'org.moxxmpp.blockingmanager';
const httpFileUploadManager = 'org.moxxmpp.httpfileuploadmanager'; const httpFileUploadManager = 'org.moxxmpp.httpfileuploadmanager';
const chatStateManager = 'org.moxxmpp.chatstatemanager'; const chatStateManager = 'org.moxxmpp.chatstatemanager';
const pingManager = 'org.moxxmpp.ping'; const pingManager = 'org.moxxmpp.ping';
const fileUploadNotificationManager = 'org.moxxmpp.fileuploadnotificationmanager'; const fileUploadNotificationManager =
'org.moxxmpp.fileuploadnotificationmanager';
const omemoManager = 'org.moxxmpp.omemomanager'; const omemoManager = 'org.moxxmpp.omemomanager';
const emeManager = 'org.moxxmpp.ememanager'; const emeManager = 'org.moxxmpp.ememanager';
const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager'; const cryptographicHashManager = 'org.moxxmpp.cryptographichashmanager';

View File

@@ -90,16 +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');
final hints = List<MessageProcessingHint>.empty(growable: true); final hints = List<MessageProcessingHint>.empty(growable: true);
for (final element in message.findTagsByXmlns(messageProcessingHintsXmlns)) { for (final element
in message.findTagsByXmlns(messageProcessingHintsXmlns)) {
hints.add(messageProcessingHintFromXml(element)); hints.add(messageProcessingHintFromXml(element));
} }
getAttributes().sendEvent(MessageEvent( 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),
@@ -121,13 +126,12 @@ class MessageManager extends XmppManagerBase {
messageRetraction: state.messageRetraction, messageRetraction: state.messageRetraction,
messageCorrectionId: state.lastMessageCorrectionSid, messageCorrectionId: state.lastMessageCorrectionSid,
messageReactions: state.messageReactions, messageReactions: state.messageReactions,
messageProcessingHints: hints.isEmpty ? messageProcessingHints: hints.isEmpty ? null : hints,
null :
hints,
stickerPackId: state.stickerPackId, stickerPackId: state.stickerPackId,
other: state.other, other: state.other,
error: StanzaError.fromStanza(message), error: StanzaError.fromStanza(message),
),); ),
);
return state.copyWith(done: true); return state.copyWith(done: true);
} }
@@ -139,7 +143,10 @@ class MessageManager extends XmppManagerBase {
/// 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( assert(
implies(details.quoteBody != null, details.quoteFrom != null && details.quoteId != null), implies(
details.quoteBody != null,
details.quoteFrom != null && details.quoteId != null,
),
'When quoting a message, then quoteFrom and quoteId must also be non-null', 'When quoting a message, then quoteFrom and quoteId must also be non-null',
); );
@@ -161,19 +168,14 @@ class MessageManager extends XmppManagerBase {
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',
@@ -220,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 && details.setOOBFallbackBody) { 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)));
} }
@@ -229,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,
),
); );
} }

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';
@@ -137,8 +139,10 @@ 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 // XEP-0449

View File

@@ -35,28 +35,41 @@ class NegotiatorAttributes {
this.getSocket, this.getSocket,
this.isAuthenticated, this.isAuthenticated,
); );
/// 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, {String? redact}) 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 /// 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;
} }
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;
@@ -87,7 +100,8 @@ abstract class XmppFeatureNegotiatorBase {
return firstWhereOrNull( return firstWhereOrNull(
features, features,
(XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns, (XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns,
) != null; ) !=
null;
} }
/// Called with the currently received nonza [nonza] when the negotiator is active. /// Called with the currently received nonza [nonza] when the negotiator is active.

View File

@@ -8,25 +8,38 @@ import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ResourceBindingFailedError extends NegotiatorError {} class ResourceBindingFailedError extends NegotiatorError {
@override
bool isRecoverable() => true;
}
/// A negotiator that implements resource binding against a random server-provided
/// resource.
class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase { class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
ResourceBindingNegotiator()
: super(0, false, bindXmlns, resourceBindingNegotiator);
ResourceBindingNegotiator() : _requestSent = false, super(0, false, bindXmlns, resourceBindingNegotiator); /// Flag indicating the state of the negotiator:
bool _requestSent; /// - True: We sent a binding request
/// - False: We have not yet sent the binding request
bool _requestSent = false;
@override @override
bool matchesFeature(List<XMLNode> features) { bool matchesFeature(List<XMLNode> features) {
final sm = attributes.getManagerById<StreamManagementManager>(smManager); final sm = attributes.getManagerById<StreamManagementManager>(smManager);
if (sm != null) { if (sm != null) {
return super.matchesFeature(features) && !sm.streamResumed && attributes.isAuthenticated(); return super.matchesFeature(features) &&
!sm.streamResumed &&
attributes.isAuthenticated();
} }
return super.matchesFeature(features) && attributes.isAuthenticated(); return super.matchesFeature(features) && attributes.isAuthenticated();
} }
@override @override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(XMLNode nonza) async { Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
if (!_requestSent) { if (!_requestSent) {
final stanza = XMLNode.xmlns( final stanza = XMLNode.xmlns(
tag: 'iq', tag: 'iq',
@@ -55,7 +68,8 @@ class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
final jid = bind.firstTag('jid')!; final jid = bind.firstTag('jid')!;
final resource = jid.innerText().split('/')[1]; final resource = jid.innerText().split('/')[1];
await attributes.sendEvent(ResourceBindingSuccessEvent(resource: resource)); await attributes
.sendEvent(ResourceBindingSuccessEvent(resource: resource));
return const Result(NegotiatorState.done); return const Result(NegotiatorState.done);
} }
} }

View File

@@ -1,3 +1,53 @@
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
class SaslFailedError extends NegotiatorError {} 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

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

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

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

View File

@@ -10,8 +10,10 @@ import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/types/result.dart';
class SaslPlainAuthNonza extends SaslAuthNonza { class SaslPlainAuthNonza extends SaslAuthNonza {
SaslPlainAuthNonza(String username, String password) : super( SaslPlainAuthNonza(String username, String password)
'PLAIN', base64.encode(utf8.encode('\u0000$username\u0000$password')), : super(
'PLAIN',
base64.encode(utf8.encode('\u0000$username\u0000$password')),
); );
} }
@@ -30,7 +32,9 @@ class SaslPlainNegotiator extends SaslNegotiator {
if (super.matchesFeature(features)) { if (super.matchesFeature(features)) {
if (!attributes.getSocket().isSecure()) { if (!attributes.getSocket().isSecure()) {
_log.warning('Refusing to match SASL feature due to unsecured connection'); _log.warning(
'Refusing to match SASL feature due to unsecured connection',
);
return false; return false;
} }
@@ -41,7 +45,9 @@ class SaslPlainNegotiator extends SaslNegotiator {
} }
@override @override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(XMLNode nonza) async { Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
if (!_authSent) { if (!_authSent) {
final settings = attributes.getConnectionSettings(); final settings = attributes.getConnectionSettings();
attributes.sendNonza( attributes.sendNonza(
@@ -59,7 +65,9 @@ class SaslPlainNegotiator extends SaslNegotiator {
// We assume it's a <failure/> // We assume it's a <failure/>
final error = nonza.children.first.tag; final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error)); await attributes.sendEvent(AuthenticationFailedEvent(error));
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
} }
} }

View File

@@ -17,28 +17,30 @@ import 'package:saslprep/saslprep.dart';
// NOTE: Inspired by https://github.com/vukoye/xmpp_dart/blob/3b1a0588562b9e591488c99d834088391840911d/lib/src/features/sasl/ScramSaslHandler.dart // NOTE: Inspired by https://github.com/vukoye/xmpp_dart/blob/3b1a0588562b9e591488c99d834088391840911d/lib/src/features/sasl/ScramSaslHandler.dart
enum ScramHashType { enum ScramHashType { sha1, sha256, sha512 }
sha1,
sha256,
sha512
}
HashAlgorithm hashFromType(ScramHashType type) { HashAlgorithm hashFromType(ScramHashType type) {
switch (type) { switch (type) {
case ScramHashType.sha1: return Sha1(); case ScramHashType.sha1:
case ScramHashType.sha256: return Sha256(); return Sha1();
case ScramHashType.sha512: return Sha512(); case ScramHashType.sha256:
return Sha256();
case ScramHashType.sha512:
return Sha512();
} }
} }
int pbkdfBitsFromHash(ScramHashType type) { int pbkdfBitsFromHash(ScramHashType type) {
switch (type) { switch (type) {
// NOTE: SHA1 is 20 octets long => 20 octets * 8 bits/octet // NOTE: SHA1 is 20 octets long => 20 octets * 8 bits/octet
case ScramHashType.sha1: return 160; case ScramHashType.sha1:
return 160;
// NOTE: SHA256 is 32 octets long => 32 octets * 8 bits/octet // NOTE: SHA256 is 32 octets long => 32 octets * 8 bits/octet
case ScramHashType.sha256: return 256; case ScramHashType.sha256:
return 256;
// NOTE: SHA512 is 64 octets long => 64 octets * 8 bits/octet // NOTE: SHA512 is 64 octets long => 64 octets * 8 bits/octet
case ScramHashType.sha512: return 512; case ScramHashType.sha512:
return 512;
} }
} }
@@ -48,30 +50,39 @@ const scramSha512Mechanism = 'SCRAM-SHA-512';
String mechanismNameFromType(ScramHashType type) { String mechanismNameFromType(ScramHashType type) {
switch (type) { switch (type) {
case ScramHashType.sha1: return scramSha1Mechanism; case ScramHashType.sha1:
case ScramHashType.sha256: return scramSha256Mechanism; return scramSha1Mechanism;
case ScramHashType.sha512: return scramSha512Mechanism; case ScramHashType.sha256:
return scramSha256Mechanism;
case ScramHashType.sha512:
return scramSha512Mechanism;
} }
} }
String namespaceFromType(ScramHashType type) { String namespaceFromType(ScramHashType type) {
switch (type) { switch (type) {
case ScramHashType.sha1: return saslScramSha1Negotiator; case ScramHashType.sha1:
case ScramHashType.sha256: return saslScramSha256Negotiator; return saslScramSha1Negotiator;
case ScramHashType.sha512: return saslScramSha512Negotiator; case ScramHashType.sha256:
return saslScramSha256Negotiator;
case ScramHashType.sha512:
return saslScramSha512Negotiator;
} }
} }
class SaslScramAuthNonza extends SaslAuthNonza { class SaslScramAuthNonza extends SaslAuthNonza {
// This subclassing makes less sense here, but this is since the auth nonza here // This subclassing makes less sense here, but this is since the auth nonza here
// requires knowledge of the inner state of the Negotiator. // requires knowledge of the inner state of the Negotiator.
SaslScramAuthNonza({ required ScramHashType type, required String body }) : super( SaslScramAuthNonza({required ScramHashType type, required String body})
mechanismNameFromType(type), body, : super(
mechanismNameFromType(type),
body,
); );
} }
class SaslScramResponseNonza extends XMLNode { class SaslScramResponseNonza extends XMLNode {
SaslScramResponseNonza({ required String body }) : super( SaslScramResponseNonza({required String body})
: super(
tag: 'response', tag: 'response',
attributes: <String, String>{ attributes: <String, String>{
'xmlns': saslXmlns, 'xmlns': saslXmlns,
@@ -80,12 +91,7 @@ class SaslScramResponseNonza extends XMLNode {
); );
} }
enum ScramState { enum ScramState { preSent, initialMessageSent, challengeResponseSent, error }
preSent,
initialMessageSent,
challengeResponseSent,
error
}
const gs2Header = 'n,,'; const gs2Header = 'n,,';
@@ -96,12 +102,16 @@ class SaslScramNegotiator extends SaslNegotiator {
this.initialMessageNoGS2, this.initialMessageNoGS2,
this.clientNonce, this.clientNonce,
this.hashType, this.hashType,
) : ) : _hash = hashFromType(hashType),
_hash = hashFromType(hashType),
_serverSignature = '', _serverSignature = '',
_scramState = ScramState.preSent, _scramState = ScramState.preSent,
_log = Logger('SaslScramNegotiator(${mechanismNameFromType(hashType)})'), _log =
super(priority, namespaceFromType(hashType), mechanismNameFromType(hashType)); Logger('SaslScramNegotiator(${mechanismNameFromType(hashType)})'),
super(
priority,
namespaceFromType(hashType),
mechanismNameFromType(hashType),
);
String? clientNonce; String? clientNonce;
String initialMessageNoGS2; String initialMessageNoGS2;
final ScramHashType hashType; final ScramHashType hashType;
@@ -122,7 +132,9 @@ class SaslScramNegotiator extends SaslNegotiator {
final saltedPasswordRaw = await pbkdf2.deriveKey( final saltedPasswordRaw = await pbkdf2.deriveKey(
secretKey: SecretKey( secretKey: SecretKey(
utf8.encode(Saslprep.saslprep(attributes.getConnectionSettings().password)), utf8.encode(
Saslprep.saslprep(attributes.getConnectionSettings().password),
),
), ),
nonce: base64.decode(salt), nonce: base64.decode(salt),
); );
@@ -131,32 +143,46 @@ class SaslScramNegotiator extends SaslNegotiator {
Future<List<int>> calculateClientKey(List<int> saltedPassword) async { Future<List<int>> calculateClientKey(List<int> saltedPassword) async {
return (await Hmac(_hash).calculateMac( return (await Hmac(_hash).calculateMac(
utf8.encode('Client Key'), secretKey: SecretKey(saltedPassword), utf8.encode('Client Key'),
)).bytes; secretKey: SecretKey(saltedPassword),
))
.bytes;
} }
Future<List<int>> calculateClientSignature(String authMessage, List<int> storedKey) async { Future<List<int>> calculateClientSignature(
String authMessage,
List<int> storedKey,
) async {
return (await Hmac(_hash).calculateMac( return (await Hmac(_hash).calculateMac(
utf8.encode(authMessage), utf8.encode(authMessage),
secretKey: SecretKey(storedKey), secretKey: SecretKey(storedKey),
)).bytes; ))
.bytes;
} }
Future<List<int>> calculateServerKey(List<int> saltedPassword) async { Future<List<int>> calculateServerKey(List<int> saltedPassword) async {
return (await Hmac(_hash).calculateMac( return (await Hmac(_hash).calculateMac(
utf8.encode('Server Key'), utf8.encode('Server Key'),
secretKey: SecretKey(saltedPassword), secretKey: SecretKey(saltedPassword),
)).bytes; ))
.bytes;
} }
Future<List<int>> calculateServerSignature(String authMessage, List<int> serverKey) async { Future<List<int>> calculateServerSignature(
String authMessage,
List<int> serverKey,
) async {
return (await Hmac(_hash).calculateMac( return (await Hmac(_hash).calculateMac(
utf8.encode(authMessage), utf8.encode(authMessage),
secretKey: SecretKey(serverKey), secretKey: SecretKey(serverKey),
)).bytes; ))
.bytes;
} }
List<int> calculateClientProof(List<int> clientKey, List<int> clientSignature) { List<int> calculateClientProof(
List<int> clientKey,
List<int> clientSignature,
) {
final clientProof = List<int>.filled(clientKey.length, 0); final clientProof = List<int>.filled(clientKey.length, 0);
for (var i = 0; i < clientKey.length; i++) { for (var i = 0; i < clientKey.length; i++) {
clientProof[i] = clientKey[i] ^ clientSignature[i]; clientProof[i] = clientKey[i] ^ clientSignature[i];
@@ -170,14 +196,20 @@ class SaslScramNegotiator extends SaslNegotiator {
final challenge = parseKeyValue(challengeString); final challenge = parseKeyValue(challengeString);
final clientFinalMessageBare = 'c=biws,r=${challenge['r']!}'; final clientFinalMessageBare = 'c=biws,r=${challenge['r']!}';
final saltedPassword = await calculateSaltedPassword(challenge['s']!, int.parse(challenge['i']!)); final saltedPassword = await calculateSaltedPassword(
challenge['s']!,
int.parse(challenge['i']!),
);
final clientKey = await calculateClientKey(saltedPassword); final clientKey = await calculateClientKey(saltedPassword);
final storedKey = (await _hash.hash(clientKey)).bytes; final storedKey = (await _hash.hash(clientKey)).bytes;
final authMessage = '$initialMessageNoGS2,$challengeString,$clientFinalMessageBare'; final authMessage =
final clientSignature = await calculateClientSignature(authMessage, storedKey); '$initialMessageNoGS2,$challengeString,$clientFinalMessageBare';
final clientSignature =
await calculateClientSignature(authMessage, storedKey);
final clientProof = calculateClientProof(clientKey, clientSignature); final clientProof = calculateClientProof(clientKey, clientSignature);
final serverKey = await calculateServerKey(saltedPassword); final serverKey = await calculateServerKey(saltedPassword);
_serverSignature = base64.encode(await calculateServerSignature(authMessage, serverKey)); _serverSignature =
base64.encode(await calculateServerSignature(authMessage, serverKey));
return '$clientFinalMessageBare,p=${base64.encode(clientProof)}'; return '$clientFinalMessageBare,p=${base64.encode(clientProof)}';
} }
@@ -186,7 +218,9 @@ class SaslScramNegotiator extends SaslNegotiator {
bool matchesFeature(List<XMLNode> features) { bool matchesFeature(List<XMLNode> features) {
if (super.matchesFeature(features)) { if (super.matchesFeature(features)) {
if (!attributes.getSocket().isSecure()) { if (!attributes.getSocket().isSecure()) {
_log.warning('Refusing to match SASL feature due to unsecured connection'); _log.warning(
'Refusing to match SASL feature due to unsecured connection',
);
return false; return false;
} }
@@ -197,18 +231,27 @@ class SaslScramNegotiator extends SaslNegotiator {
} }
@override @override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(XMLNode nonza) async { Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
switch (_scramState) { switch (_scramState) {
case ScramState.preSent: case ScramState.preSent:
if (clientNonce == null || clientNonce == '') { if (clientNonce == null || clientNonce == '') {
clientNonce = randomAlphaNumeric(40, provider: CoreRandomProvider.from(Random.secure())); clientNonce = randomAlphaNumeric(
40,
provider: CoreRandomProvider.from(Random.secure()),
);
} }
initialMessageNoGS2 = 'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce'; initialMessageNoGS2 =
'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce';
_scramState = ScramState.initialMessageSent; _scramState = ScramState.initialMessageSent;
attributes.sendNonza( attributes.sendNonza(
SaslScramAuthNonza(body: base64.encode(utf8.encode(gs2Header + initialMessageNoGS2)), type: hashType), SaslScramAuthNonza(
body: base64.encode(utf8.encode(gs2Header + initialMessageNoGS2)),
type: hashType,
),
redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(), redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(),
); );
return const Result(NegotiatorState.ready); return const Result(NegotiatorState.ready);
@@ -218,7 +261,9 @@ class SaslScramNegotiator extends SaslNegotiator {
await attributes.sendEvent(AuthenticationFailedEvent(error)); await attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error; _scramState = ScramState.error;
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
final challengeBase64 = nonza.innerText(); final challengeBase64 = nonza.innerText();
@@ -236,23 +281,30 @@ class SaslScramNegotiator extends SaslNegotiator {
final error = nonza.children.first.tag; final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error)); await attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error; _scramState = ScramState.error;
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
// NOTE: This assumes that the string is always "v=..." and contains no other parameters // NOTE: This assumes that the string is always "v=..." and contains no other parameters
final signature = parseKeyValue(utf8.decode(base64.decode(nonza.innerText()))); final signature =
parseKeyValue(utf8.decode(base64.decode(nonza.innerText())));
if (signature['v']! != _serverSignature) { if (signature['v']! != _serverSignature) {
// TODO(Unknown): Notify of a signature mismatch // TODO(Unknown): Notify of a signature mismatch
//final error = nonza.children.first.tag; //final error = nonza.children.first.tag;
//attributes.sendEvent(AuthenticationFailedEvent(error)); //attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error; _scramState = ScramState.error;
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
await attributes.sendEvent(AuthenticationSuccessEvent()); await attributes.sendEvent(AuthenticationSuccessEvent());
return const Result(NegotiatorState.done); return const Result(NegotiatorState.done);
case ScramState.error: case ScramState.error:
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
} }

View File

@@ -5,32 +5,35 @@ 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/types/result.dart';
enum _StartTlsState { enum _StartTlsState { ready, requested }
ready,
requested class StartTLSFailedError extends NegotiatorError {
@override
bool isRecoverable() => true;
} }
class StartTLSFailedError extends NegotiatorError {}
class StartTLSNonza extends XMLNode { class StartTLSNonza extends XMLNode {
StartTLSNonza() : super.xmlns( StartTLSNonza()
: super.xmlns(
tag: 'starttls', tag: 'starttls',
xmlns: startTlsXmlns, xmlns: startTlsXmlns,
); );
} }
/// A negotiator implementing StartTLS.
class StartTlsNegotiator extends XmppFeatureNegotiatorBase { class StartTlsNegotiator extends XmppFeatureNegotiatorBase {
StartTlsNegotiator() : super(10, true, startTlsXmlns, startTlsNegotiator);
StartTlsNegotiator() /// The state of the negotiator.
: _state = _StartTlsState.ready, _StartTlsState _state = _StartTlsState.ready;
_log = Logger('StartTlsNegotiator'),
super(10, true, startTlsXmlns, startTlsNegotiator);
_StartTlsState _state;
final Logger _log; /// Logger.
final Logger _log = Logger('StartTlsNegotiator');
@override @override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(XMLNode nonza) async { Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
switch (_state) { switch (_state) {
case _StartTlsState.ready: case _StartTlsState.ready:
_log.fine('StartTLS is available. Performing StartTLS upgrade...'); _log.fine('StartTLS is available. Performing StartTLS upgrade...');
@@ -38,13 +41,15 @@ class StartTlsNegotiator extends XmppFeatureNegotiatorBase {
attributes.sendNonza(StartTLSNonza()); attributes.sendNonza(StartTLSNonza());
return const Result(NegotiatorState.ready); return const Result(NegotiatorState.ready);
case _StartTlsState.requested: case _StartTlsState.requested:
if (nonza.tag != 'proceed' || nonza.attributes['xmlns'] != startTlsXmlns) { if (nonza.tag != 'proceed' ||
nonza.attributes['xmlns'] != startTlsXmlns) {
_log.severe('Failed to perform StartTLS negotiation'); _log.severe('Failed to perform StartTLS negotiation');
return Result(StartTLSFailedError()); return Result(StartTLSFailedError());
} }
_log.fine('Securing socket'); _log.fine('Securing socket');
final result = await attributes.getSocket() final result = await attributes
.getSocket()
.secure(attributes.getConnectionSettings().jid.domain); .secure(attributes.getConnectionSettings().jid.domain);
if (!result) { if (!result) {
_log.severe('Failed to secure stream'); _log.severe('Failed to secure stream');

View File

@@ -10,7 +10,9 @@ class PingManager extends XmppManagerBase {
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 @override
@@ -25,13 +27,17 @@ class PingManager extends XmppManagerBase {
return; return;
} }
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();

View File

@@ -20,7 +20,8 @@ class PresenceManager extends XmppManagerBase {
PresenceManager() : super(presenceManager); PresenceManager() : super(presenceManager);
/// The list of pre-send callbacks. /// The list of pre-send callbacks.
final List<PresencePreSendCallback> _presenceCallbacks = List.empty(growable: true); final List<PresencePreSendCallback> _presenceCallbacks =
List.empty(growable: true);
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -31,7 +32,7 @@ class PresenceManager extends XmppManagerBase {
]; ];
@override @override
List<String> getDiscoFeatures() => [ capsXmlns ]; List<String> getDiscoFeatures() => [capsXmlns];
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
@@ -41,23 +42,32 @@ class PresenceManager extends XmppManagerBase {
_presenceCallbacks.add(callback); _presenceCallbacks.add(callback);
} }
Future<StanzaHandlerData> _onPresence(Stanza presence, StanzaHandlerData state) async { 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);
} }

View File

@@ -37,7 +37,10 @@ abstract class ReconnectionPolicy {
final Lock shouldReconnectLock = Lock(); final Lock shouldReconnectLock = Lock();
/// Called by XmppConnection to register the policy. /// Called by XmppConnection to register the policy.
void register(PerformReconnectFunction performReconnect, ConnectionLostCallback triggerConnectionLost) { void register(
PerformReconnectFunction performReconnect,
ConnectionLostCallback triggerConnectionLost,
) {
this.performReconnect = performReconnect; this.performReconnect = performReconnect;
this.triggerConnectionLost = triggerConnectionLost; this.triggerConnectionLost = triggerConnectionLost;
@@ -61,7 +64,8 @@ abstract class ReconnectionPolicy {
/// Set whether a reconnection attempt should be made. /// Set whether a reconnection attempt should be made.
Future<void> setShouldReconnect(bool value) async { Future<void> setShouldReconnect(bool value) async {
return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection = value); return shouldReconnectLock
.synchronized(() => _shouldAttemptReconnection = value);
} }
/// Returns true if the manager is currently triggering a reconnection. If not, returns /// Returns true if the manager is currently triggering a reconnection. If not, returns
@@ -77,7 +81,6 @@ abstract class ReconnectionPolicy {
isReconnecting = value; isReconnecting = value;
}); });
} }
} }
/// A simple reconnection strategy: Make the reconnection delays exponentially longer /// A simple reconnection strategy: Make the reconnection delays exponentially longer
@@ -87,7 +90,10 @@ class RandomBackoffReconnectionPolicy extends ReconnectionPolicy {
RandomBackoffReconnectionPolicy( RandomBackoffReconnectionPolicy(
this._minBackoffTime, this._minBackoffTime,
this._maxBackoffTime, this._maxBackoffTime,
) : assert(_minBackoffTime < _maxBackoffTime, '_minBackoffTime must be smaller than _maxBackoffTime'), ) : assert(
_minBackoffTime < _maxBackoffTime,
'_minBackoffTime must be smaller than _maxBackoffTime',
),
super(); super();
/// The maximum time in seconds that a backoff should be. /// The maximum time in seconds that a backoff should be.
@@ -113,12 +119,16 @@ class RandomBackoffReconnectionPolicy extends ReconnectionPolicy {
await lock.synchronized(() async { await lock.synchronized(() async {
_log.fine('Lock aquired'); _log.fine('Lock aquired');
if (!(await getShouldReconnect())) { if (!(await getShouldReconnect())) {
_log.fine('Backoff timer expired but getShouldReconnect() returned false'); _log.fine(
'Backoff timer expired but getShouldReconnect() returned false',
);
return; return;
} }
if (isReconnecting) { if (isReconnecting) {
_log.fine('Backoff timer expired but a reconnection is running, so doing nothing.'); _log.fine(
'Backoff timer expired but a reconnection is running, so doing nothing.',
);
return; return;
} }
@@ -155,11 +165,14 @@ class RandomBackoffReconnectionPolicy extends ReconnectionPolicy {
return _timer == null; return _timer == null;
}); });
if (!shouldContinue) { if (!shouldContinue) {
_log.finest('_onFailure: Not backing off since _timer is already running'); _log.finest(
'_onFailure: Not backing off since _timer is already running',
);
return; return;
} }
final seconds = Random().nextInt(_maxBackoffTime - _minBackoffTime) + _minBackoffTime; final seconds =
Random().nextInt(_maxBackoffTime - _minBackoffTime) + _minBackoffTime;
_log.finest('Failure occured. Starting random backoff with ${seconds}s'); _log.finest('Failure occured. Starting random backoff with ${seconds}s');
_timer?.cancel(); _timer?.cancel();

View File

@@ -18,7 +18,13 @@ import 'package:moxxmpp/src/types/result.dart';
@immutable @immutable
class XmppRosterItem { class XmppRosterItem {
const XmppRosterItem({ required this.jid, required this.subscription, this.ask, this.name, this.groups = const [] }); const XmppRosterItem({
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;
@@ -26,7 +32,7 @@ class XmppRosterItem {
final List<String> groups; final List<String> groups;
@override @override
bool operator==(Object other) { bool operator ==(Object other) {
return other is XmppRosterItem && return other is XmppRosterItem &&
other.jid == jid && other.jid == jid &&
other.name == name && other.name == name &&
@@ -36,7 +42,12 @@ class XmppRosterItem {
} }
@override @override
int get hashCode => jid.hashCode ^ name.hashCode ^ subscription.hashCode ^ ask.hashCode ^ groups.hashCode; int get hashCode =>
jid.hashCode ^
name.hashCode ^
subscription.hashCode ^
ask.hashCode ^
groups.hashCode;
@override @override
String toString() { String toString() {
@@ -49,11 +60,7 @@ class XmppRosterItem {
} }
} }
enum RosterRemovalResult { enum RosterRemovalResult { okay, error, itemNotFound }
okay,
error,
itemNotFound
}
class RosterRequestResult { class RosterRequestResult {
RosterRequestResult(this.items, this.ver); RosterRequestResult(this.items, this.ver);
@@ -69,14 +76,18 @@ class RosterPushResult {
/// 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<Result<NegotiatorState, NegotiatorError>> 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;
@@ -117,7 +128,10 @@ class RosterManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onRosterPush(Stanza stanza, StanzaHandlerData state) async { 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;
@@ -128,7 +142,9 @@ 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);
} }
@@ -166,23 +182,32 @@ class RosterManager extends XmppManagerBase {
/// 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<Result<RosterRequestResult, RosterError>> _handleRosterResponse(XMLNode? query) async { Future<Result<RosterRequestResult, RosterError>> _handleRosterResponse(
XMLNode? query,
) async {
final List<XmppRosterItem> items; final List<XmppRosterItem> items;
String? rosterVersion; String? rosterVersion;
if (query != null) { if (query != null) {
items = query.children.map( items = query.children
.map(
(item) => XmppRosterItem( (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
.findTags('group')
.map((groupNode) => groupNode.innerText())
.toList(),
), ),
).toList(); )
.toList();
rosterVersion = query.attributes['ver'] as String?; rosterVersion = query.attributes['ver'] as String?;
} else { } 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'); 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()); return Result(NoQueryError());
} }
@@ -230,7 +255,8 @@ class RosterManager extends XmppManagerBase {
/// 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<Result<RosterRequestResult?, RosterError>> requestRosterPushes() async { Future<Result<RosterRequestResult?, RosterError>>
requestRosterPushes() async {
final attrs = getAttributes(); final attrs = getAttributes();
final result = await attrs.sendStanza( final result = await attrs.sendStanza(
Stanza.iq( Stanza.iq(
@@ -257,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(
@@ -276,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

@@ -50,7 +50,12 @@ abstract class BaseRosterStateManager {
/// ///
/// [added] is a (possibly empty) list of XmppRosterItems that are added by the /// [added] is a (possibly empty) list of XmppRosterItems that are added by the
/// roster push or roster fetch request. /// roster push or roster fetch request.
Future<void> commitRoster(String? version, List<String> removed, List<XmppRosterItem> modified, List<XmppRosterItem> added); Future<void> commitRoster(
String? version,
List<String> removed,
List<XmppRosterItem> modified,
List<XmppRosterItem> added,
);
/// Internal function. Registers functions from the RosterManger against this /// Internal function. Registers functions from the RosterManger against this
/// instance. /// instance.
@@ -69,7 +74,12 @@ abstract class BaseRosterStateManager {
/// A wrapper around _commitRoster that also sends an event to moxxmpp's event /// A wrapper around _commitRoster that also sends an event to moxxmpp's event
/// bus. /// bus.
Future<void> _commitRoster(String? version, List<String> removed, List<XmppRosterItem> modified, List<XmppRosterItem> added) async { Future<void> _commitRoster(
String? version,
List<String> removed,
List<XmppRosterItem> modified,
List<XmppRosterItem> added,
) async {
_sendEvent( _sendEvent(
RosterUpdatedEvent( RosterUpdatedEvent(
removed, removed,
@@ -216,5 +226,10 @@ class TestingRosterStateManager extends BaseRosterStateManager {
} }
@override @override
Future<void> commitRoster(String? version, List<String> removed, List<XmppRosterItem> modified, List<XmppRosterItem> added) async {} 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,8 +1,12 @@
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
class ConnectionSettings { class ConnectionSettings {
ConnectionSettings({
ConnectionSettings({ required this.jid, required this.password, required this.useDirectTLS, required this.allowPlainAuth }); required this.jid,
required this.password,
required this.useDirectTLS,
required this.allowPlainAuth,
});
final JID jid; final JID jid;
final String password; final String password;
final bool useDirectTLS; final bool useDirectTLS;

View File

@@ -32,13 +32,13 @@ abstract class BaseSocketWrapper {
/// Write [data] into the socket. If [redact] is not null, then [redact] will be /// Write [data] into the socket. If [redact] is not null, then [redact] will be
/// logged instead of [data]. /// logged instead of [data].
void write(String data, { String? redact }); 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
/// provides TLS encryption. Returns true if the connection has been successfully /// provides TLS encryption. Returns true if the connection has been successfully
/// established. Returns false if the connection has failed. /// established. Returns false if the connection has failed.
Future<bool> connect(String domain, { String? host, int? port }); Future<bool> connect(String domain, {String? host, int? port});
/// Returns true if the socket is secured, e.g. using TLS. /// Returns true if the socket is secured, e.g. using TLS.
bool isSecure(); bool isSecure();

View File

@@ -25,59 +25,83 @@ class StanzaError {
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
...id != null ? <String, dynamic>{ 'id': id } : <String, dynamic>{}, ? <String, dynamic>{'type': type}
...to != null ? <String, dynamic>{ 'to': to } : <String, dynamic>{}, : <String, dynamic>{},
...from != null ? <String, dynamic>{ 'from': from } : <String, dynamic>{}, ...id != null ? <String, dynamic>{'id': id} : <String, dynamic>{},
...to != null ? <String, dynamic>{'to': to} : <String, dynamic>{},
...from != null
? <String, dynamic>{'from': from}
: <String, dynamic>{},
'xmlns': stanzaXmlns '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,
); );
} }
@@ -92,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());
}), }),
); );
@@ -104,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,
@@ -119,21 +149,23 @@ class Stanza extends XMLNode {
/// Build an <error /> element with a child <[condition] type="[type]" />. If [text] /// Build an <error /> element with a child <[condition] type="[type]" />. If [text]
/// is not null, then the condition element will contain a <text /> element with [text] /// is not null, then the condition element will contain a <text /> element with [text]
/// as the body. /// as the body.
XMLNode buildErrorElement(String type, String condition, { String? text }) { XMLNode buildErrorElement(String type, String condition, {String? text}) {
return XMLNode( return 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

@@ -16,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) {
@@ -36,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(
@@ -62,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 {
@@ -93,7 +100,7 @@ class XMLNode {
XMLNode? _firstTag(bool Function(XMLNode) test) { XMLNode? _firstTag(bool Function(XMLNode) test) {
try { try {
return children.firstWhere(test); return children.firstWhere(test);
} catch(e) { } catch (e) {
return null; return null;
} }
} }
@@ -102,7 +109,7 @@ class XMLNode {
/// - node's tag is equal to [tag] /// - node's tag is equal to [tag]
/// - (optional) node's xmlns attribute is equal to [xmlns] /// - (optional) node's xmlns attribute is equal to [xmlns]
/// Returns null if none is found. /// Returns null if none is found.
XMLNode? firstTag(String tag, { String? xmlns}) { XMLNode? firstTag(String tag, {String? xmlns}) {
return _firstTag((node) { return _firstTag((node) {
if (xmlns != null) { if (xmlns != null) {
return node.tag == tag && node.attributes['xmlns'] == xmlns; return node.tag == tag && node.attributes['xmlns'] == xmlns;
@@ -121,9 +128,10 @@ 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();
} }

View File

@@ -1,6 +1,9 @@
class Result<T, V> { class Result<T, V> {
const Result(this._data)
const Result(this._data) : assert(_data is T || _data is V, 'Invalid data type: Must be either $T or $V'); : assert(
_data is T || _data is V,
'Invalid data type: Must be either $T or $V',
);
final dynamic _data; final dynamic _data;
bool isType<S>() => _data is S; bool isType<S>() => _data is S;

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);
} }
@@ -48,7 +51,7 @@ XMLNode constructFileThumbnailElement(Thumbnail thumbnail) {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'file-thumbnail', tag: 'file-thumbnail',
xmlns: fileThumbnailsXmlns, xmlns: fileThumbnailsXmlns,
attributes: { 'type': type }, attributes: {'type': type},
children: [ node ], children: [node],
); );
} }

View File

@@ -41,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)!,
@@ -50,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,14 +3,16 @@ 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;
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',
@@ -43,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
...type != null ? <String, dynamic>{ 'type': type } : <String, dynamic>{}, ? <String, dynamic>{'var': varAttr}
...label != null ? <String, dynamic>{ 'label': label } : <String, dynamic>{} : <String, dynamic>{},
...type != null ? <String, dynamic>{'type': type} : <String, dynamic>{},
...label != null
? <String, dynamic>{'label': label}
: <String, dynamic>{}
}, },
children: [ children: [
...description != null ? [XMLNode(tag: 'desc', text: description)] : [], ...description != null ? [XMLNode(tag: 'desc', text: description)] : [],
@@ -81,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(),
),), ),
),
], ],
); );
} }
@@ -128,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

@@ -13,9 +13,7 @@ class DiscoCacheKey {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is DiscoCacheKey && return other is DiscoCacheKey && jid == other.jid && node == other.node;
jid == other.jid &&
node == other.node;
} }
@override @override

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

@@ -5,7 +5,12 @@ 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({ required this.category, required this.type, this.name, this.lang }); const Identity({
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;
@@ -18,7 +23,9 @@ 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}
}, },
); );
} }
@@ -50,7 +57,8 @@ class DiscoInfo {
name: element.attributes['name'] as String?, name: element.attributes['name'] as String?,
), ),
); );
} else if (element.tag == 'x' && element.attributes['xmlns'] == dataFormsXmlns) { } else if (element.tag == 'x' &&
element.attributes['xmlns'] == dataFormsXmlns) {
extendedInfo.add( extendedInfo.add(
parseDataForm(element), parseDataForm(element),
); );
@@ -76,18 +84,22 @@ class DiscoInfo {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'query', tag: 'query',
xmlns: discoInfoXmlns, xmlns: discoInfoXmlns,
attributes: node != null ? attributes: node != null
<String, String>{ 'node': node!, } : ? <String, String>{
<String, String>{}, 'node': node!,
}
: <String, String>{},
children: [ children: [
...identities.map((identity) => identity.toXMLNode()), ...identities.map((identity) => identity.toXMLNode()),
...features.map((feature) => XMLNode( ...features.map(
(feature) => XMLNode(
tag: 'feature', tag: 'feature',
attributes: { 'var': feature, }, attributes: {
),), 'var': feature,
},
if (extendedInfo.isNotEmpty) ),
...extendedInfo.map((ei) => ei.toXml()), ),
if (extendedInfo.isNotEmpty) ...extendedInfo.map((ei) => ei.toXml()),
], ],
); );
} }
@@ -95,7 +107,7 @@ class DiscoInfo {
@immutable @immutable
class DiscoItem { class DiscoItem {
const DiscoItem({ required this.jid, this.node, this.name }); const DiscoItem({required this.jid, this.node, this.name});
final String jid; final String jid;
final String? node; final String? node;
final String? name; final String? name;

View File

@@ -51,10 +51,12 @@ class DiscoManager extends XmppManagerBase {
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {}; final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
/// The tracker for tracking disco#info queries that are in flight. /// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> _discoInfoTracker = WaitForTracker(); final WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>>
_discoInfoTracker = WaitForTracker();
/// The tracker for tracking disco#info queries that are in flight. /// The tracker for tracking disco#info queries that are in flight.
final WaitForTracker<DiscoCacheKey, Result<DiscoError, List<DiscoItem>>> _discoItemsTracker = WaitForTracker(); final WaitForTracker<DiscoCacheKey, Result<DiscoError, List<DiscoItem>>>
_discoItemsTracker = WaitForTracker();
/// Cache lock /// Cache lock
final Lock _cacheLock = Lock(); final Lock _cacheLock = Lock();
@@ -72,7 +74,8 @@ class DiscoManager extends XmppManagerBase {
List<String> get features => _features; List<String> get features => _features;
@visibleForTesting @visibleForTesting
WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> get infoTracker => _discoInfoTracker; WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>>
get infoTracker => _discoInfoTracker;
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -91,7 +94,7 @@ class DiscoManager extends XmppManagerBase {
]; ];
@override @override
List<String> getDiscoFeatures() => [ discoInfoXmlns, discoItemsXmlns ]; List<String> getDiscoFeatures() => [discoInfoXmlns, discoItemsXmlns];
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
@@ -172,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 {
@@ -195,7 +201,10 @@ class DiscoManager extends XmppManagerBase {
); );
} }
Future<StanzaHandlerData> _onDiscoInfoRequest(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onDiscoInfoRequest(
Stanza stanza,
StanzaHandlerData state,
) async {
if (stanza.type != 'get') return state; if (stanza.type != 'get') return state;
final query = stanza.firstTag('query', xmlns: discoInfoXmlns)!; final query = stanza.firstTag('query', xmlns: discoInfoXmlns)!;
@@ -226,7 +235,10 @@ class DiscoManager extends XmppManagerBase {
return state.copyWith(done: true); return state.copyWith(done: true);
} }
Future<StanzaHandlerData> _onDiscoItemsRequest(Stanza stanza, StanzaHandlerData state) async { 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', xmlns: discoItemsXmlns)!; final query = stanza.firstTag('query', xmlns: discoItemsXmlns)!;
@@ -254,7 +266,10 @@ class DiscoManager extends XmppManagerBase {
return state; return state;
} }
Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async { Future<void> _exitDiscoInfoCriticalSection(
DiscoCacheKey key,
Result<DiscoError, DiscoInfo> result,
) async {
await _cacheLock.synchronized(() async { await _cacheLock.synchronized(() async {
// Add to cache if it is a result // Add to cache if it is a result
if (result.isType<DiscoInfo>()) { if (result.isType<DiscoInfo>()) {
@@ -266,10 +281,16 @@ class DiscoManager extends XmppManagerBase {
} }
/// 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, bool shouldEncrypt = true }) 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;
final ffuture = await _cacheLock.synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>(() async { final ffuture = await _cacheLock
.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];
@@ -317,22 +338,26 @@ 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, bool shouldEncrypt = true }) async { Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(
String entity, {
String? node,
bool shouldEncrypt = true,
}) async {
final key = DiscoCacheKey(entity, node); final key = DiscoCacheKey(entity, node);
final future = await _discoItemsTracker.waitFor(key); final future = await _discoItemsTracker.waitFor(key);
if (future != null) { if (future != null) {
return future; return future;
} }
final stanza = await getAttributes() final stanza = await getAttributes().sendStanza(
.sendStanza(
buildDiscoItemsQueryStanza(entity, node: node), buildDiscoItemsQueryStanza(entity, node: node),
encrypted: !shouldEncrypt, encrypted: !shouldEncrypt,
) as Stanza; ) as Stanza;
final query = stanza.firstTag('query'); final query = stanza.firstTag('query');
if (query == null) { if (query == null) {
final result = Result<DiscoError, List<DiscoItem>>(InvalidResponseDiscoError()); final result =
Result<DiscoError, List<DiscoItem>>(InvalidResponseDiscoError());
await _discoItemsTracker.resolve(key, result); await _discoItemsTracker.resolve(key, result);
return result; return result;
} }
@@ -340,16 +365,22 @@ class DiscoManager extends XmppManagerBase {
if (stanza.type == 'error') { if (stanza.type == 'error') {
//final error = stanza.firstTag('error'); //final error = stanza.firstTag('error');
//print("Disco Items error: " + error.toXml()); //print("Disco Items error: " + error.toXml());
final result = Result<DiscoError, List<DiscoItem>>(ErrorResponseDiscoError()); final result =
Result<DiscoError, List<DiscoItem>>(ErrorResponseDiscoError());
await _discoItemsTracker.resolve(key, result); await _discoItemsTracker.resolve(key, result);
return result; return result;
} }
final items = query.findTags('item').map((node) => DiscoItem( 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();
final result = Result<DiscoError, List<DiscoItem>>(items); final result = Result<DiscoError, List<DiscoItem>>(items);
await _discoItemsTracker.resolve(key, result); await _discoItemsTracker.resolve(key, result);
@@ -357,7 +388,11 @@ class DiscoManager extends XmppManagerBase {
} }
/// 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

@@ -16,12 +16,12 @@ class UnknownVCardError extends VCardError {}
class InvalidVCardError 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;
final VCardPhoto? photo; final VCardPhoto? photo;
@@ -49,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();
@@ -114,9 +117,13 @@ class VCardManager extends XmppManagerBase {
encrypted: true, encrypted: true,
); );
if (result.attributes['type'] != 'result') return Result(UnknownVCardError()); 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 Result(UnknownVCardError()); if (vcard == null) {
return Result(UnknownVCardError());
}
return Result(_parseVCard(vcard)); return Result(_parseVCard(vcard));
} }

View File

@@ -33,33 +33,41 @@ class PubSubPublishOptions {
const DataFormField( const DataFormField(
options: [], options: [],
isRequired: false, isRequired: false,
values: [ pubsubPublishOptionsXmlns ], values: [pubsubPublishOptionsXmlns],
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({ required this.id, required this.node, required this.payload }); const PubSubItem({
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;
@@ -84,20 +92,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);
} }
@@ -107,7 +120,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;
} }
@@ -115,19 +130,27 @@ class PubSubManager extends XmppManagerBase {
return count; return count;
} }
Future<PubSubPublishOptions> _preprocessPublishOptions(String jid, String node, PubSubPublishOptions options) async { 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!');
@@ -136,7 +159,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(
@@ -173,13 +198,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');
} }
@@ -208,13 +239,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');
} }
@@ -227,8 +264,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,
@@ -246,8 +282,7 @@ 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);
@@ -264,21 +299,25 @@ class PubSubManager extends XmppManagerBase {
children: [ children: [
XMLNode( XMLNode(
tag: 'publish', tag: 'publish',
attributes: <String, String>{ 'node': node }, attributes: <String, String>{'node': node},
children: [ children: [
XMLNode( XMLNode(
tag: 'item', tag: 'item',
attributes: id != null ? <String, String>{ 'id': id } : <String, String>{}, attributes: id != null
children: [ payload ], ? <String, String>{'id': id}
: <String, String>{},
children: [payload],
) )
], ],
), ),
...options != null ? [ ...options != null
? [
XMLNode( XMLNode(
tag: 'publish-options', tag: 'publish-options',
children: [options.toXml()], children: [options.toXml()],
), ),
] : [], ]
: [],
], ],
) )
], ],
@@ -302,10 +341,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,
@@ -323,20 +368,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',
@@ -346,33 +402,38 @@ class PubSubManager extends XmppManagerBase {
tag: 'pubsub', tag: 'pubsub',
xmlns: pubsubXmlns, xmlns: pubsubXmlns,
children: [ children: [
XMLNode(tag: 'items', attributes: <String, String>{ 'node': node }), XMLNode(tag: 'items', attributes: <String, String>{'node': node}),
], ],
) )
], ],
), ),
); );
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',
@@ -384,11 +445,11 @@ class PubSubManager extends XmppManagerBase {
children: [ children: [
XMLNode( XMLNode(
tag: 'items', tag: 'items',
attributes: <String, String>{ 'node': node }, attributes: <String, String>{'node': node},
children: [ children: [
XMLNode( XMLNode(
tag: 'item', tag: 'item',
attributes: <String, String>{ 'id': id }, attributes: <String, String>{'id': id},
), ),
], ],
), ),
@@ -398,7 +459,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));
@@ -415,7 +478,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
@@ -439,7 +506,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(
@@ -464,7 +533,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);
} }
@@ -499,7 +570,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,7 @@ 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;
} }
@@ -34,7 +34,7 @@ class OOBManager extends XmppManagerBase {
OOBManager() : super(oobManager); OOBManager() : super(oobManager);
@override @override
List<String> getDiscoFeatures() => [ oobDataXmlns ]; List<String> getDiscoFeatures() => [oobDataXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -51,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

@@ -15,7 +15,7 @@ abstract class AvatarError {}
class UnknownAvatarError extends AvatarError {} class UnknownAvatarError extends AvatarError {}
class UserAvatar { 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;
} }
@@ -47,7 +47,8 @@ class UserAvatarMetadata {
class UserAvatarManager extends XmppManagerBase { class UserAvatarManager extends XmppManagerBase {
UserAvatarManager() : super(userAvatarManager); UserAvatarManager() : super(userAvatarManager);
PubSubManager _getPubSubManager() => 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 {
@@ -56,7 +57,9 @@ class UserAvatarManager extends XmppManagerBase {
if (event.item.payload.tag != 'data' || if (event.item.payload.tag != 'data' ||
event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) { event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) {
logger.warning('Received avatar update from ${event.from} but the payload is invalid. Ignoring...'); logger.warning(
'Received avatar update from ${event.from} but the payload is invalid. Ignoring...',
);
return; return;
} }
@@ -96,7 +99,11 @@ class UserAvatarManager extends XmppManagerBase {
/// 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.
Future<Result<AvatarError, bool>> publishUserAvatar(String base64, String hash, bool public) async { Future<Result<AvatarError, bool>> publishUserAvatar(
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(),
@@ -120,7 +127,10 @@ class UserAvatarManager extends XmppManagerBase {
/// 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.
Future<Result<AvatarError, bool>> publishUserAvatarMetadata(UserAvatarMetadata metadata, bool public) async { Future<Result<AvatarError, bool>> publishUserAvatarMetadata(
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(),
@@ -172,7 +182,11 @@ class UserAvatarManager extends XmppManagerBase {
/// the node. /// the node.
Future<Result<AvatarError, String>> getAvatarId(String jid) async { Future<Result<AvatarError, 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, shouldEncrypt: false); final response = await disco.discoItemsQuery(
jid,
node: userAvatarDataXmlns,
shouldEncrypt: false,
);
if (response.isType<DiscoError>()) return Result(UnknownAvatarError()); if (response.isType<DiscoError>()) return Result(UnknownAvatarError());
final items = response.get<List<DiscoItem>>(); final items = response.get<List<DiscoItem>>();

View File

@@ -6,43 +6,44 @@ 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); ChatStateManager() : super(chatStateManager);
@override @override
List<String> getDiscoFeatures() => [ chatStateXmlns ]; List<String> getDiscoFeatures() => [chatStateXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -58,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}'");
} }
} }
@@ -93,14 +103,18 @@ 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(
Stanza.message( Stanza.message(
to: to, to: to,
type: messageType, type: messageType,
children: [ XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns) ], children: [XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns)],
), ),
); );
} }

View File

@@ -21,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);
@@ -36,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,
), ),
@@ -45,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!,
), ),
@@ -63,7 +72,8 @@ 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 /// A manager implementing the advertising of XEP-0115. It responds to the
@@ -71,7 +81,8 @@ Future<String> calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm)
/// the DiscoManager. /// the DiscoManager.
/// NOTE: This manager requires that the DiscoManager is also registered. /// NOTE: This manager requires that the DiscoManager is also registered.
class EntityCapabilitiesManager extends XmppManagerBase { class EntityCapabilitiesManager extends XmppManagerBase {
EntityCapabilitiesManager(this._capabilityHashBase) : super(entityCapabilitiesManager); EntityCapabilitiesManager(this._capabilityHashBase)
: super(entityCapabilitiesManager);
/// The string that is both the node under which we advertise the disco info /// 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. /// and the base for the actual node on which we respond to disco#info requests.
@@ -84,7 +95,7 @@ class EntityCapabilitiesManager extends XmppManagerBase {
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
@override @override
List<String> getDiscoFeatures() => [ capsXmlns ]; List<String> getDiscoFeatures() => [capsXmlns];
/// Computes, if required, the capability hash of the data provided by /// Computes, if required, the capability hash of the data provided by
/// the DiscoManager. /// the DiscoManager.
@@ -128,7 +139,9 @@ class EntityCapabilitiesManager extends XmppManagerBase {
Future<void> postRegisterCallback() async { Future<void> postRegisterCallback() async {
await super.postRegisterCallback(); await super.postRegisterCallback();
getAttributes().getManagerById<DiscoManager>(discoManager)!.registerInfoCallback( getAttributes()
.getManagerById<DiscoManager>(discoManager)!
.registerInfoCallback(
await _getNode(), await _getNode(),
_onInfoQuery, _onInfoQuery,
); );

View File

@@ -19,7 +19,7 @@ XMLNode makeMessageDeliveryResponse(String id) {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'received', tag: 'received',
xmlns: deliveryXmlns, xmlns: deliveryXmlns,
attributes: { 'id': id }, attributes: {'id': id},
); );
} }
@@ -27,7 +27,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
MessageDeliveryReceiptManager() : super(messageDeliveryReceiptManager); MessageDeliveryReceiptManager() : super(messageDeliveryReceiptManager);
@override @override
List<String> getDiscoFeatures() => [ deliveryXmlns ]; List<String> getDiscoFeatures() => [deliveryXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -52,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

@@ -46,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');
@@ -91,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(),
) )
], ],
), ),
@@ -133,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(),
) )
], ],
), ),
@@ -159,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

@@ -54,25 +54,32 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
} else { } else {
// We cannot do a stream resumption // We cannot do a stream resumption
final br = attributes.getNegotiatorById(resourceBindingNegotiator); final br = attributes.getNegotiatorById(resourceBindingNegotiator);
return super.matchesFeature(features) && br?.state == NegotiatorState.done && attributes.isAuthenticated(); return super.matchesFeature(features) &&
br?.state == NegotiatorState.done &&
attributes.isAuthenticated();
} }
} }
@override @override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(XMLNode nonza) async { Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
// negotiate is only called when we matched the stream feature, so we know // negotiate is only called when we matched the stream feature, so we know
// that the server advertises it. // that the server advertises it.
_supported = true; _supported = true;
switch (_state) { switch (_state) {
case _StreamManagementNegotiatorState.ready: case _StreamManagementNegotiatorState.ready:
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!; final sm =
attributes.getManagerById<StreamManagementManager>(smManager)!;
final srid = sm.state.streamResumptionId; final srid = sm.state.streamResumptionId;
final h = sm.state.s2c; final h = sm.state.s2c;
// Attempt stream resumption first // Attempt stream resumption first
if (srid != null) { if (srid != null) {
_log.finest('Found stream resumption Id. Attempting to perform stream resumption'); _log.finest(
'Found stream resumption Id. Attempting to perform stream resumption',
);
_state = _StreamManagementNegotiatorState.resumeRequested; _state = _StreamManagementNegotiatorState.resumeRequested;
attributes.sendNonza(StreamManagementResumeNonza(srid, h)); attributes.sendNonza(StreamManagementResumeNonza(srid, h));
} else { } else {
@@ -86,7 +93,10 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
if (nonza.tag == 'resumed') { if (nonza.tag == 'resumed') {
_log.finest('Stream Management resumption successful'); _log.finest('Stream Management resumption successful');
assert(attributes.getFullJID().resource != '', 'Resume only works when we already have a resource bound and know about it'); 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?; final csi = attributes.getManagerById(csiManager) as CSIManager?;
if (csi != null) { if (csi != null) {
@@ -101,9 +111,12 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
return const Result(NegotiatorState.skipRest); return const Result(NegotiatorState.skipRest);
} else { } else {
// We assume it is <failed /> // We assume it is <failed />
_log.info('Stream resumption failed. Expected <resumed />, got ${nonza.tag}, Proceeding with new stream...'); _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)!;
// We have to do this because we otherwise get a stanza stuck in the queue, // We have to do this because we otherwise get a stanza stuck in the queue,
// thus spamming the server on every <a /> nonza we receive. // thus spamming the server on every <a /> nonza we receive.
@@ -121,7 +134,8 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
_log.finest('Stream Management enabled'); _log.finest('Stream Management enabled');
final id = nonza.attributes['id'] as String?; final id = nonza.attributes['id'] as String?;
if (id != null && ['true', '1'].contains(nonza.attributes['resume'])) { if (id != null &&
['true', '1'].contains(nonza.attributes['resume'])) {
_log.info('Stream Resumption available'); _log.info('Stream Resumption available');
} }

View File

@@ -2,17 +2,16 @@ import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
class StreamManagementEnableNonza extends XMLNode { class StreamManagementEnableNonza extends XMLNode {
StreamManagementEnableNonza() : super( StreamManagementEnableNonza()
: super(
tag: 'enable', tag: 'enable',
attributes: <String, String>{ attributes: <String, String>{'xmlns': smXmlns, 'resume': 'true'},
'xmlns': smXmlns,
'resume': 'true'
},
); );
} }
class StreamManagementResumeNonza extends XMLNode { class StreamManagementResumeNonza extends XMLNode {
StreamManagementResumeNonza(String id, int h) : super( StreamManagementResumeNonza(String id, int h)
: super(
tag: 'resume', tag: 'resume',
attributes: <String, String>{ attributes: <String, String>{
'xmlns': smXmlns, 'xmlns': smXmlns,
@@ -23,17 +22,16 @@ class StreamManagementResumeNonza extends XMLNode {
} }
class StreamManagementAckNonza extends XMLNode { class StreamManagementAckNonza extends XMLNode {
StreamManagementAckNonza(int h) : super( StreamManagementAckNonza(int h)
: super(
tag: 'a', tag: 'a',
attributes: <String, String>{ attributes: <String, String>{'xmlns': smXmlns, 'h': h.toString()},
'xmlns': smXmlns,
'h': h.toString()
},
); );
} }
class StreamManagementRequestNonza extends XMLNode { class StreamManagementRequestNonza extends XMLNode {
StreamManagementRequestNonza() : super( StreamManagementRequestNonza()
: super(
tag: 'r', tag: 'r',
attributes: <String, String>{ attributes: <String, String>{
'xmlns': smXmlns, 'xmlns': smXmlns,

View File

@@ -7,13 +7,12 @@ part 'state.g.dart';
class StreamManagementState with _$StreamManagementState { class StreamManagementState with _$StreamManagementState {
factory StreamManagementState( factory StreamManagementState(
int c2s, int c2s,
int s2c, int s2c, {
{
String? streamResumptionLocation, String? streamResumptionLocation,
String? streamResumptionId, String? streamResumptionId,
} }) = _StreamManagementState;
) = _StreamManagementState;
// JSON // JSON
factory StreamManagementState.fromJson(Map<String, dynamic> json) => _$StreamManagementStateFromJson(json); factory StreamManagementState.fromJson(Map<String, dynamic> json) =>
_$StreamManagementStateFromJson(json);
} }

View File

@@ -83,7 +83,11 @@ class StreamManagementManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async { Future<bool> isSupported() async {
return getAttributes().getNegotiatorById<StreamManagementNegotiator>(streamManagementNegotiator)!.isSupported; return getAttributes()
.getNegotiatorById<StreamManagementNegotiator>(
streamManagementNegotiator,
)!
.isSupported;
} }
/// Returns the amount of stanzas waiting to get acked /// Returns the amount of stanzas waiting to get acked
@@ -223,7 +227,8 @@ class StreamManagementManager extends XmppManagerBase {
_ackLock.synchronized(() async { _ackLock.synchronized(() async {
final now = DateTime.now().millisecondsSinceEpoch; final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastAckTimestamp >= ackTimeout.inMilliseconds && _pendingAcks > 0) { if (now - _lastAckTimestamp >= ackTimeout.inMilliseconds &&
_pendingAcks > 0) {
_stopAckTimer(); _stopAckTimer();
await getAttributes().getConnection().reconnectionPolicy.onFailure(); await getAttributes().getConnection().reconnectionPolicy.onFailure();
} }
@@ -322,7 +327,9 @@ class StreamManagementManager extends XmppManagerBase {
} }
if (h > _state.c2s) { if (h > _state.c2s) {
logger.info('C2S height jumped from ${_state.c2s} (local) to $h (remote).'); logger.info(
'C2S height jumped from ${_state.c2s} (local) to $h (remote).',
);
// ignore: cascade_invocations // ignore: cascade_invocations
logger.info('Proceeding with $h as local C2S counter.'); logger.info('Proceeding with $h as local C2S counter.');
@@ -346,6 +353,7 @@ class StreamManagementManager extends XmppManagerBase {
logger.fine('_incrementC2S: Releasing lock...'); logger.fine('_incrementC2S: Releasing lock...');
}); });
} }
Future<void> _incrementS2C() async { Future<void> _incrementS2C() async {
logger.fine('_incrementS2C: Waiting to aquire lock...'); logger.fine('_incrementS2C: Waiting to aquire lock...');
await _stateLock.synchronized(() async { await _stateLock.synchronized(() async {
@@ -357,13 +365,19 @@ class StreamManagementManager extends XmppManagerBase {
} }
/// Called whenever we receive a stanza from the server. /// Called whenever we receive a stanza from the server.
Future<StanzaHandlerData> _onServerStanzaReceived(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onServerStanzaReceived(
Stanza stanza,
StanzaHandlerData state,
) async {
await _incrementS2C(); await _incrementS2C();
return state; return state;
} }
/// Called whenever we send a stanza. /// Called whenever we send a stanza.
Future<StanzaHandlerData> _onClientStanzaSent(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onClientStanzaSent(
Stanza stanza,
StanzaHandlerData state,
) async {
await _incrementC2S(); await _incrementC2S();
_unackedStanzas[_state.c2s] = stanza; _unackedStanzas[_state.c2s] = stanza;

View File

@@ -28,7 +28,10 @@ class DelayedDeliveryManager extends XmppManagerBase {
), ),
]; ];
Future<StanzaHandlerData> _onIncomingMessage(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onIncomingMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns); final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns);
if (delay == null) return state; if (delay == null) return state;

View File

@@ -59,23 +59,20 @@ class CarbonsManager extends XmppManagerBase {
@override @override
Future<void> onXmppEvent(XmppEvent event) async { Future<void> onXmppEvent(XmppEvent event) async {
if (event is ServerDiscoDoneEvent && !_isEnabled) { if (event is StreamNegotiationsDoneEvent) {
final attrs = getAttributes(); // Reset disco cache info on a new stream
final newStream = await isNewStream();
if (attrs.isFeatureSupported(carbonsXmlns)) { if (newStream) {
logger.finest('Message carbons supported. Enabling...');
await enableCarbons();
logger.finest('Message carbons enabled');
} else {
logger.info('Message carbons not supported.');
}
} else if (event is StreamResumeFailedEvent) {
_gotSupported = false; _gotSupported = false;
_supported = false; _supported = false;
} }
} }
}
Future<StanzaHandlerData> _onMessageReceived(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onMessageReceived(
Stanza message,
StanzaHandlerData state,
) async {
final from = JID.fromString(message.attributes['from']! as String); final from = JID.fromString(message.attributes['from']! as String);
final received = message.firstTag('received', xmlns: carbonsXmlns)!; final received = message.firstTag('received', xmlns: carbonsXmlns)!;
if (!isCarbonValid(from)) return state.copyWith(done: true); if (!isCarbonValid(from)) return state.copyWith(done: true);
@@ -89,7 +86,10 @@ class CarbonsManager extends XmppManagerBase {
); );
} }
Future<StanzaHandlerData> _onMessageSent(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onMessageSent(
Stanza message,
StanzaHandlerData state,
) async {
final from = JID.fromString(message.attributes['from']! as String); final from = JID.fromString(message.attributes['from']! as String);
final sent = message.firstTag('sent', xmlns: carbonsXmlns)!; final sent = message.firstTag('sent', xmlns: carbonsXmlns)!;
if (!isCarbonValid(from)) return state.copyWith(done: true); if (!isCarbonValid(from)) return state.copyWith(done: true);
@@ -178,7 +178,8 @@ class CarbonsManager extends XmppManagerBase {
/// ///
/// Returns true if the carbon is valid. Returns false if not. /// Returns true if the carbon is valid. Returns false if not.
bool isCarbonValid(JID senderJid) { bool isCarbonValid(JID senderJid) {
return _isEnabled && getAttributes().getFullJID().bareCompare( return _isEnabled &&
getAttributes().getFullJID().bareCompare(
senderJid, senderJid,
ensureBare: true, ensureBare: true,
); );

View File

@@ -4,7 +4,10 @@ import 'package:moxxmpp/src/stringxml.dart';
/// Extracts the message stanza from the <forwarded /> node. /// Extracts the message stanza from the <forwarded /> node.
Stanza unpackForwarded(XMLNode forwarded) { Stanza unpackForwarded(XMLNode forwarded) {
assert(forwarded.attributes['xmlns'] == forwardedXmlns, 'Invalid element xmlns'); assert(
forwarded.attributes['xmlns'] == forwardedXmlns,
'Invalid element xmlns',
);
assert(forwarded.tag == 'forwarded', 'Invalid element name'); assert(forwarded.tag == 'forwarded', 'Invalid element name');
// NOTE: We only use this XEP (for now) in the context of Message Carbons // NOTE: We only use this XEP (for now) in the context of Message Carbons

View File

@@ -8,7 +8,7 @@ XMLNode constructHashElement(String algo, String base64Hash) {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'hash', tag: 'hash',
xmlns: hashXmlns, xmlns: hashXmlns,
attributes: { 'algo': algo }, attributes: {'algo': algo},
text: base64Hash, text: base64Hash,
); );
} }
@@ -76,7 +76,10 @@ class CryptographicHashManager extends XmppManagerBase {
'$hashFunctionNameBaseXmlns:$hashBlake2b512', '$hashFunctionNameBaseXmlns:$hashBlake2b512',
]; ];
static Future<List<int>> hashFromData(List<int> data, HashFunction function) async { static Future<List<int>> hashFromData(
List<int> data,
HashFunction function,
) async {
// TODO(PapaTutuWawa): Implement the others as well // TODO(PapaTutuWawa): Implement the others as well
HashAlgorithm algo; HashAlgorithm algo;
switch (function) { switch (function) {

View File

@@ -20,7 +20,7 @@ class LastMessageCorrectionManager extends XmppManagerBase {
LastMessageCorrectionManager() : super(lastMessageCorrectionManager); LastMessageCorrectionManager() : super(lastMessageCorrectionManager);
@override @override
List<String> getDiscoFeatures() => [ lmcXmlns ]; List<String> getDiscoFeatures() => [lmcXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -37,7 +37,10 @@ class LastMessageCorrectionManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!; final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!;
return state.copyWith( return state.copyWith(
lastMessageCorrectionSid: edit.attributes['id']! as String, lastMessageCorrectionSid: edit.attributes['id']! as String,

View File

@@ -16,11 +16,14 @@ XMLNode makeChatMarkerMarkable() {
} }
XMLNode makeChatMarker(String tag, String id) { XMLNode makeChatMarker(String tag, String id) {
assert(['received', 'displayed', 'acknowledged'].contains(tag), 'Invalid chat marker'); assert(
['received', 'displayed', 'acknowledged'].contains(tag),
'Invalid chat marker',
);
return XMLNode.xmlns( return XMLNode.xmlns(
tag: tag, tag: tag,
xmlns: chatMarkersXmlns, xmlns: chatMarkersXmlns,
attributes: { 'id': id }, attributes: {'id': id},
); );
} }
@@ -28,7 +31,7 @@ class ChatMarkerManager extends XmppManagerBase {
ChatMarkerManager() : super(chatMarkerManager); ChatMarkerManager() : super(chatMarkerManager);
@override @override
List<String> getDiscoFeatures() => [ chatMarkersXmlns ]; List<String> getDiscoFeatures() => [chatMarkersXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -44,7 +47,10 @@ class ChatMarkerManager 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 marker = message.firstTagByXmlns(chatMarkersXmlns)!; final marker = message.firstTagByXmlns(chatMarkersXmlns)!;
// Handle the <markable /> explicitly // Handle the <markable /> explicitly
@@ -53,11 +59,13 @@ class ChatMarkerManager extends XmppManagerBase {
if (!['received', 'displayed', 'acknowledged'].contains(marker.tag)) { if (!['received', 'displayed', 'acknowledged'].contains(marker.tag)) {
logger.warning("Unknown message marker '${marker.tag}' found."); logger.warning("Unknown message marker '${marker.tag}' found.");
} else { } else {
getAttributes().sendEvent(ChatMarkerEvent( getAttributes().sendEvent(
ChatMarkerEvent(
from: JID.fromString(message.from!), from: JID.fromString(message.from!),
type: marker.tag, type: marker.tag,
id: marker.attributes['id']! as String, id: marker.attributes['id']! as String,
),); ),
);
} }
return state.copyWith(done: true); return state.copyWith(done: true);

View File

@@ -10,10 +10,14 @@ enum MessageProcessingHint {
MessageProcessingHint messageProcessingHintFromXml(XMLNode element) { MessageProcessingHint messageProcessingHintFromXml(XMLNode element) {
switch (element.tag) { switch (element.tag) {
case 'no-permanent-store': return MessageProcessingHint.noPermanentStore; case 'no-permanent-store':
case 'no-store': return MessageProcessingHint.noStore; return MessageProcessingHint.noPermanentStore;
case 'no-copy': return MessageProcessingHint.noCopies; case 'no-store':
case 'store': return MessageProcessingHint.store; return MessageProcessingHint.noStore;
case 'no-copy':
return MessageProcessingHint.noCopies;
case 'store':
return MessageProcessingHint.store;
} }
assert(false, 'Invalid Message Processing Hint: ${element.tag}'); assert(false, 'Invalid Message Processing Hint: ${element.tag}');

View File

@@ -7,20 +7,18 @@ import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/types/result.dart';
class CSIActiveNonza extends XMLNode { class CSIActiveNonza extends XMLNode {
CSIActiveNonza() : super( CSIActiveNonza()
: super(
tag: 'active', tag: 'active',
attributes: <String, String>{ attributes: <String, String>{'xmlns': csiXmlns},
'xmlns': csiXmlns
},
); );
} }
class CSIInactiveNonza extends XMLNode { class CSIInactiveNonza extends XMLNode {
CSIInactiveNonza() : super( CSIInactiveNonza()
: super(
tag: 'inactive', tag: 'inactive',
attributes: <String, String>{ attributes: <String, String>{'xmlns': csiXmlns},
'xmlns': csiXmlns
},
); );
} }
@@ -33,7 +31,9 @@ class CSINegotiator extends XmppFeatureNegotiatorBase {
bool get isSupported => _supported; bool get isSupported => _supported;
@override @override
Future<Result<NegotiatorState, NegotiatorError>> 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 CSI. // advertises CSI.
_supported = true; _supported = true;
@@ -56,7 +56,9 @@ class CSIManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async { Future<bool> isSupported() async {
return getAttributes().getNegotiatorById<CSINegotiator>(csiNegotiator)!.isSupported; return getAttributes()
.getNegotiatorById<CSINegotiator>(csiNegotiator)!
.isSupported;
} }
/// To be called after a stream has been resumed as CSI does not /// To be called after a stream has been resumed as CSI does not

View File

@@ -13,7 +13,7 @@ import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
/// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of /// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of
/// the message stanza. /// the message stanza.
class StableStanzaId { class StableStanzaId {
const StableStanzaId({ this.originId, this.stanzaId, this.stanzaIdBy }); const StableStanzaId({this.originId, this.stanzaId, this.stanzaIdBy});
final String? originId; final String? originId;
final String? stanzaId; final String? stanzaId;
final String? stanzaIdBy; final String? stanzaIdBy;
@@ -23,7 +23,7 @@ XMLNode makeOriginIdElement(String id) {
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'origin-id', tag: 'origin-id',
xmlns: stableIdXmlns, xmlns: stableIdXmlns,
attributes: { 'id': id }, attributes: {'id': id},
); );
} }
@@ -31,7 +31,7 @@ class StableIdManager extends XmppManagerBase {
StableIdManager() : super(stableIdManager); StableIdManager() : super(stableIdManager);
@override @override
List<String> getDiscoFeatures() => [ stableIdXmlns ]; List<String> getDiscoFeatures() => [stableIdXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -46,7 +46,10 @@ class StableIdManager 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 from = JID.fromString(message.attributes['from']! as String); final from = JID.fromString(message.attributes['from']! as String);
String? originId; String? originId;
String? stanzaId; String? stanzaId;
@@ -74,10 +77,14 @@ class StableIdManager extends XmppManagerBase {
stanzaId = stanzaIdTag.attributes['id']! as String; stanzaId = stanzaIdTag.attributes['id']! as String;
stanzaIdBy = stanzaIdTag.attributes['by']! as String; stanzaIdBy = stanzaIdTag.attributes['by']! as String;
} else { } else {
logger.finest('${from.toString()} does not support $stableIdXmlns. Ignoring stanza id... '); logger.finest(
'${from.toString()} does not support $stableIdXmlns. Ignoring stanza id... ',
);
} }
} else { } else {
logger.finest('Failed to find out if ${from.toString()} supports $stableIdXmlns. Ignoring... '); logger.finest(
'Failed to find out if ${from.toString()} supports $stableIdXmlns. Ignoring... ',
);
} }
} }

View File

@@ -13,7 +13,7 @@ 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_0363/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0363/errors.dart';
const allowedHTTPHeaders = [ 'authorization', 'cookie', 'expires' ]; const allowedHTTPHeaders = ['authorization', 'cookie', 'expires'];
class HttpFileUploadSlot { class HttpFileUploadSlot {
const HttpFileUploadSlot(this.putUrl, this.getUrl, this.headers); const HttpFileUploadSlot(this.putUrl, this.getUrl, this.headers);
@@ -58,7 +58,10 @@ class HttpFileUploadManager extends XmppManagerBase {
/// Returns whether the entity provided an identity that tells us that we can ask it /// Returns whether the entity provided an identity that tells us that we can ask it
/// for an HTTP upload slot. /// for an HTTP upload slot.
bool _containsFileUploadIdentity(DiscoInfo info) { bool _containsFileUploadIdentity(DiscoInfo info) {
return listContains(info.identities, (Identity id) => id.category == 'store' && id.type == 'file'); return listContains(
info.identities,
(Identity id) => id.category == 'store' && id.type == 'file',
);
} }
/// Extract the maximum filesize in octets from the disco response. Returns null /// Extract the maximum filesize in octets from the disco response. Returns null
@@ -77,19 +80,24 @@ class HttpFileUploadManager 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;
_entityJid = null; _entityJid = null;
_maxUploadSize = null; _maxUploadSize = null;
} }
} }
}
@override @override
Future<bool> isSupported() async { Future<bool> isSupported() async {
if (_gotSupported) return _supported; if (_gotSupported) return _supported;
final result = await getAttributes().getManagerById<DiscoManager>(discoManager)!.performDiscoSweep(); final result = await getAttributes()
.getManagerById<DiscoManager>(discoManager)!
.performDiscoSweep();
if (result.isType<DiscoError>()) { if (result.isType<DiscoError>()) {
_gotSupported = false; _gotSupported = false;
_supported = false; _supported = false;
@@ -99,7 +107,8 @@ class HttpFileUploadManager extends XmppManagerBase {
final infos = result.get<List<DiscoInfo>>(); final infos = result.get<List<DiscoInfo>>();
_gotSupported = true; _gotSupported = true;
for (final info in infos) { for (final info in infos) {
if (_containsFileUploadIdentity(info) && info.features.contains(httpFileUploadXmlns)) { if (_containsFileUploadIdentity(info) &&
info.features.contains(httpFileUploadXmlns)) {
logger.info('Discovered HTTP File Upload for ${info.jid}'); logger.info('Discovered HTTP File Upload for ${info.jid}');
_entityJid = info.jid; _entityJid = info.jid;
@@ -116,16 +125,26 @@ class HttpFileUploadManager extends XmppManagerBase {
/// the file's size in octets. [contentType] is optional and refers to the file's /// the file's size in octets. [contentType] is optional and refers to the file's
/// Mime type. /// Mime type.
/// Returns an [HttpFileUploadSlot] if the request was successful; null otherwise. /// Returns an [HttpFileUploadSlot] if the request was successful; null otherwise.
Future<Result<HttpFileUploadSlot, HttpFileUploadError>> requestUploadSlot(String filename, int filesize, { String? contentType }) async { Future<Result<HttpFileUploadSlot, HttpFileUploadError>> requestUploadSlot(
if (!(await isSupported())) return Result(NoEntityKnownError()); String filename,
int filesize, {
String? contentType,
}) async {
if (!(await isSupported())) {
return Result(NoEntityKnownError());
}
if (_entityJid == null) { if (_entityJid == null) {
logger.warning('Attempted to request HTTP File Upload slot but no entity is known to send this request to.'); logger.warning(
'Attempted to request HTTP File Upload slot but no entity is known to send this request to.',
);
return Result(NoEntityKnownError()); return Result(NoEntityKnownError());
} }
if (_maxUploadSize != null && filesize > _maxUploadSize!) { if (_maxUploadSize != null && filesize > _maxUploadSize!) {
logger.warning('Attempted to request HTTP File Upload slot for a file that exceeds the filesize limit'); logger.warning(
'Attempted to request HTTP File Upload slot for a file that exceeds the filesize limit',
);
return Result(FileTooBigError()); return Result(FileTooBigError());
} }
@@ -141,7 +160,7 @@ class HttpFileUploadManager extends XmppManagerBase {
attributes: { attributes: {
'filename': filename, 'filename': filename,
'size': filesize.toString(), 'size': filesize.toString(),
...contentType != null ? { 'content-type': contentType } : {} ...contentType != null ? {'content-type': contentType} : {}
}, },
) )
], ],

View File

@@ -18,25 +18,39 @@ enum ExplicitEncryptionType {
String _explicitEncryptionTypeToString(ExplicitEncryptionType type) { String _explicitEncryptionTypeToString(ExplicitEncryptionType type) {
switch (type) { switch (type) {
case ExplicitEncryptionType.otr: return emeOtr; case ExplicitEncryptionType.otr:
case ExplicitEncryptionType.legacyOpenPGP: return emeLegacyOpenPGP; return emeOtr;
case ExplicitEncryptionType.openPGP: return emeOpenPGP; case ExplicitEncryptionType.legacyOpenPGP:
case ExplicitEncryptionType.omemo: return emeOmemo; return emeLegacyOpenPGP;
case ExplicitEncryptionType.omemo1: return emeOmemo1; case ExplicitEncryptionType.openPGP:
case ExplicitEncryptionType.omemo2: return emeOmemo2; return emeOpenPGP;
case ExplicitEncryptionType.unknown: return ''; case ExplicitEncryptionType.omemo:
return emeOmemo;
case ExplicitEncryptionType.omemo1:
return emeOmemo1;
case ExplicitEncryptionType.omemo2:
return emeOmemo2;
case ExplicitEncryptionType.unknown:
return '';
} }
} }
ExplicitEncryptionType _explicitEncryptionTypeFromString(String str) { ExplicitEncryptionType _explicitEncryptionTypeFromString(String str) {
switch (str) { switch (str) {
case emeOtr: return ExplicitEncryptionType.otr; case emeOtr:
case emeLegacyOpenPGP: return ExplicitEncryptionType.legacyOpenPGP; return ExplicitEncryptionType.otr;
case emeOpenPGP: return ExplicitEncryptionType.openPGP; case emeLegacyOpenPGP:
case emeOmemo: return ExplicitEncryptionType.omemo; return ExplicitEncryptionType.legacyOpenPGP;
case emeOmemo1: return ExplicitEncryptionType.omemo1; case emeOpenPGP:
case emeOmemo2: return ExplicitEncryptionType.omemo2; return ExplicitEncryptionType.openPGP;
default: return ExplicitEncryptionType.unknown; case emeOmemo:
return ExplicitEncryptionType.omemo;
case emeOmemo1:
return ExplicitEncryptionType.omemo1;
case emeOmemo2:
return ExplicitEncryptionType.omemo2;
default:
return ExplicitEncryptionType.unknown;
} }
} }
@@ -58,7 +72,7 @@ class EmeManager extends XmppManagerBase {
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
@override @override
List<String> getDiscoFeatures() => [ emeXmlns ]; List<String> getDiscoFeatures() => [emeXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -71,7 +85,10 @@ class EmeManager extends XmppManagerBase {
), ),
]; ];
Future<StanzaHandlerData> _onStanzaReceived(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onStanzaReceived(
Stanza message,
StanzaHandlerData state,
) async {
final encryption = message.firstTag('encryption', xmlns: emeXmlns)!; final encryption = message.firstTag('encryption', xmlns: emeXmlns)!;
return state.copyWith( return state.copyWith(

View File

@@ -15,6 +15,7 @@ bool checkAffixElements(XMLNode envelope, String sender, JID ourJid) {
if (to == null) return false; if (to == null) return false;
final encReceiver = JID.fromString(to); final encReceiver = JID.fromString(to);
return encSender.toBare().toString() == JID.fromString(sender).toBare().toString() && return encSender.toBare().toString() ==
JID.fromString(sender).toBare().toString() &&
encReceiver.toBare().toString() == ourJid.toBare().toString(); encReceiver.toBare().toString() == ourJid.toBare().toString();
} }

View File

@@ -46,7 +46,8 @@ XMLNode bundleToXML(OmemoBundle bundle) {
for (final pk in bundle.opksEncoded.entries) { for (final pk in bundle.opksEncoded.entries) {
prekeys.add( prekeys.add(
XMLNode( XMLNode(
tag: 'pk', attributes: <String, String>{ tag: 'pk',
attributes: <String, String>{
'id': '${pk.key}', 'id': '${pk.key}',
}, },
text: pk.value, text: pk.value,

View File

@@ -1,6 +1,5 @@
/// A simple wrapper class for defining elements that should not be encrypted. /// A simple wrapper class for defining elements that should not be encrypted.
class DoNotEncrypt { class DoNotEncrypt {
const DoNotEncrypt(this.tag, this.xmlns); const DoNotEncrypt(this.tag, this.xmlns);
final String tag; final String tag;
final String xmlns; final String xmlns;

View File

@@ -113,8 +113,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
// Tell the OmemoManager // Tell the OmemoManager
(await getOmemoManager()) (await getOmemoManager()).onDeviceListUpdate(jid.toString(), ids);
.onDeviceListUpdate(jid.toString(), ids);
// Generate an event // Generate an event
getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids)); getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids));
@@ -124,7 +123,6 @@ abstract class BaseOmemoManager extends XmppManagerBase {
@visibleForOverriding @visibleForOverriding
Future<OmemoManager> getOmemoManager(); Future<OmemoManager> getOmemoManager();
/// Wrapper around using getSessionManager and then calling getDeviceId on it. /// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId(); Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId();
@@ -169,7 +167,6 @@ abstract class BaseOmemoManager extends XmppManagerBase {
tag: 'content', tag: 'content',
children: children, children: children,
), ),
XMLNode( XMLNode(
tag: 'rpad', tag: 'rpad',
text: generateRpad(), text: generateRpad(),
@@ -201,7 +198,11 @@ abstract class BaseOmemoManager extends XmppManagerBase {
return payload.toXml(); return payload.toXml();
} }
XMLNode _buildEncryptedElement(EncryptionResult result, String recipientJid, int deviceId) { XMLNode _buildEncryptedElement(
EncryptionResult result,
String recipientJid,
int deviceId,
) {
final keyElements = <String, List<XMLNode>>{}; final keyElements = <String, List<XMLNode>>{};
for (final key in result.encryptedKeys) { for (final key in result.encryptedKeys) {
final keyElement = XMLNode( final keyElement = XMLNode(
@@ -257,7 +258,10 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
/// For usage with omemo_dart's OmemoManager. /// For usage with omemo_dart's OmemoManager.
Future<void> sendEmptyMessageImpl(EncryptionResult result, String toJid) async { Future<void> sendEmptyMessageImpl(
EncryptionResult result,
String toJid,
) async {
await getAttributes().sendStanza( await getAttributes().sendStanza(
Stanza.message( Stanza.message(
to: toJid, to: toJid,
@@ -302,7 +306,10 @@ abstract class BaseOmemoManager extends XmppManagerBase {
return result.get<OmemoBundle>(); return result.get<OmemoBundle>();
} }
Future<StanzaHandlerData> _onOutgoingStanza(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onOutgoingStanza(
Stanza stanza,
StanzaHandlerData state,
) async {
if (state.encrypted) { if (state.encrypted) {
logger.finest('Not encrypting since state.encrypted is true'); logger.finest('Not encrypting since state.encrypted is true');
return state; return state;
@@ -317,10 +324,14 @@ abstract class BaseOmemoManager extends XmppManagerBase {
final toJid = JID.fromString(stanza.to!).toBare(); final toJid = JID.fromString(stanza.to!).toBare();
final shouldEncryptResult = await shouldEncryptStanza(toJid, stanza); final shouldEncryptResult = await shouldEncryptStanza(toJid, stanza);
if (!shouldEncryptResult && !state.forceEncryption) { if (!shouldEncryptResult && !state.forceEncryption) {
logger.finest('Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.'); logger.finest(
'Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.',
);
return state; return state;
} else { } else {
logger.finest('Encrypting stanza for $toJid: shouldEncryptResult=$shouldEncryptResult, forceEncryption=${state.forceEncryption}'); logger.finest(
'Encrypting stanza for $toJid: shouldEncryptResult=$shouldEncryptResult, forceEncryption=${state.forceEncryption}',
);
} }
final toEncrypt = List<XMLNode>.empty(growable: true); final toEncrypt = List<XMLNode>.empty(growable: true);
@@ -335,14 +346,15 @@ abstract class BaseOmemoManager extends XmppManagerBase {
logger.finest('Beginning encryption'); logger.finest('Beginning encryption');
final carbonsEnabled = getAttributes() final carbonsEnabled = getAttributes()
.getManagerById<CarbonsManager>(carbonsManager)?.isEnabled ?? false; .getManagerById<CarbonsManager>(carbonsManager)
?.isEnabled ??
false;
final om = await getOmemoManager(); final om = await getOmemoManager();
final result = await om.onOutgoingStanza( final result = await om.onOutgoingStanza(
OmemoOutgoingStanza( OmemoOutgoingStanza(
[ [
toJid.toString(), toJid.toString(),
if (carbonsEnabled) if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
getAttributes().getFullJID().toBare().toString(),
], ],
_buildEnvelope(toEncrypt, toJid.toString()), _buildEnvelope(toEncrypt, toJid.toString()),
), ),
@@ -357,9 +369,10 @@ abstract class BaseOmemoManager extends XmppManagerBase {
other: other, other: other,
// If we have no device list for toJid, then the contact most likely does not // If we have no device list for toJid, then the contact most likely does not
// support OMEMO:2 // support OMEMO:2
cancelReason: result.jidEncryptionErrors[toJid.toString()] is NoKeyMaterialAvailableException ? cancelReason: result.jidEncryptionErrors[toJid.toString()]
OmemoNotSupportedForContactException() : is NoKeyMaterialAvailableException
UnknownOmemoError(), ? OmemoNotSupportedForContactException()
: UnknownOmemoError(),
cancel: true, cancel: true,
); );
} }
@@ -396,7 +409,10 @@ abstract class BaseOmemoManager extends XmppManagerBase {
@visibleForOverriding @visibleForOverriding
Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza); Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza);
Future<StanzaHandlerData> _onIncomingStanza(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onIncomingStanza(
Stanza stanza,
StanzaHandlerData state,
) async {
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns); final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns);
if (encrypted == null) return state; if (encrypted == null) return state;
if (stanza.from == null) return state; if (stanza.from == null) return state;
@@ -427,7 +443,8 @@ abstract class BaseOmemoManager extends XmppManagerBase {
OmemoIncomingStanza( OmemoIncomingStanza(
fromJid.toString(), fromJid.toString(),
sid, sid,
state.delayedDelivery?.timestamp.millisecondsSinceEpoch ?? DateTime.now().millisecondsSinceEpoch, state.delayedDelivery?.timestamp.millisecondsSinceEpoch ??
DateTime.now().millisecondsSinceEpoch,
keys, keys,
payloadElement?.innerText(), payloadElement?.innerText(),
), ),
@@ -438,9 +455,13 @@ abstract class BaseOmemoManager extends XmppManagerBase {
if (result.error != null) { if (result.error != null) {
other['encryption_error'] = result.error; other['encryption_error'] = result.error;
} else { } else {
children = stanza.children.where( children = stanza.children
(child) => child.tag != 'encrypted' || child.attributes['xmlns'] != omemoXmlns, .where(
).toList(); (child) =>
child.tag != 'encrypted' ||
child.attributes['xmlns'] != omemoXmlns,
)
.toList();
} }
if (result.payload != null) { if (result.payload != null) {
@@ -490,9 +511,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// device list PubSub node. /// device list PubSub node.
/// ///
/// On success, returns the XML data. On failure, returns an OmemoError. /// On success, returns the XML data. On failure, returns an OmemoError.
Future<Result<OmemoError, XMLNode>> _retrieveDeviceListPayload(JID jid) async { Future<Result<OmemoError, XMLNode>> _retrieveDeviceListPayload(
JID jid,
) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final result = await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns); final result =
await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns);
if (result.isType<PubSubError>()) return Result(UnknownOmemoError()); if (result.isType<PubSubError>()) return Result(UnknownOmemoError());
return Result(result.get<List<PubSubItem>>().first.payload); return Result(result.get<List<PubSubItem>>().first.payload);
} }
@@ -502,7 +526,9 @@ abstract class BaseOmemoManager extends XmppManagerBase {
final itemsRaw = await _retrieveDeviceListPayload(jid); final itemsRaw = await _retrieveDeviceListPayload(jid);
if (itemsRaw.isType<OmemoError>()) return Result(UnknownOmemoError()); if (itemsRaw.isType<OmemoError>()) return Result(UnknownOmemoError());
final ids = itemsRaw.get<XMLNode>().children final ids = itemsRaw
.get<XMLNode>()
.children
.map((child) => int.parse(child.attributes['id']! as String)) .map((child) => int.parse(child.attributes['id']! as String))
.toList(); .toList();
return Result(ids); return Result(ids);
@@ -511,15 +537,20 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieve all device bundles for the JID [jid]. /// Retrieve all device bundles for the JID [jid].
/// ///
/// On success, returns a list of devices. On failure, returns am OmemoError. /// On success, returns a list of devices. On failure, returns am OmemoError.
Future<Result<OmemoError, List<OmemoBundle>>> retrieveDeviceBundles(JID jid) async { Future<Result<OmemoError, List<OmemoBundle>>> retrieveDeviceBundles(
JID jid,
) async {
// TODO(Unknown): Should we query the device list first? // TODO(Unknown): Should we query the device list first?
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final bundlesRaw = await pm.getItems(jid.toString(), omemoBundlesXmlns); final bundlesRaw = await pm.getItems(jid.toString(), omemoBundlesXmlns);
if (bundlesRaw.isType<PubSubError>()) return Result(UnknownOmemoError()); if (bundlesRaw.isType<PubSubError>()) return Result(UnknownOmemoError());
final bundles = bundlesRaw.get<List<PubSubItem>>().map( final bundles = bundlesRaw
.get<List<PubSubItem>>()
.map(
(bundle) => bundleFromXML(jid, int.parse(bundle.id), bundle.payload), (bundle) => bundleFromXML(jid, int.parse(bundle.id), bundle.payload),
).toList(); )
.toList();
return Result(bundles); return Result(bundles);
} }
@@ -527,7 +558,10 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieves a bundle from entity [jid] with the device id [deviceId]. /// Retrieves a bundle from entity [jid] with the device id [deviceId].
/// ///
/// On success, returns the device bundle. On failure, returns an OmemoError. /// On success, returns the device bundle. On failure, returns an OmemoError.
Future<Result<OmemoError, OmemoBundle>> retrieveDeviceBundle(JID jid, int deviceId) async { Future<Result<OmemoError, OmemoBundle>> retrieveDeviceBundle(
JID jid,
int deviceId,
) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final bareJid = jid.toBare().toString(); final bareJid = jid.toBare().toString();
final item = await pm.getItem(bareJid, omemoBundlesXmlns, '$deviceId'); final item = await pm.getItem(bareJid, omemoBundlesXmlns, '$deviceId');
@@ -618,7 +652,8 @@ abstract class BaseOmemoManager extends XmppManagerBase {
if (items.isType<DiscoError>()) return Result(UnknownOmemoError()); if (items.isType<DiscoError>()) return Result(UnknownOmemoError());
final nodes = items.get<List<DiscoItem>>(); final nodes = items.get<List<DiscoItem>>();
final result = nodes.any((item) => item.node == omemoDevicesXmlns) && nodes.any((item) => item.node == omemoBundlesXmlns); final result = nodes.any((item) => item.node == omemoDevicesXmlns) &&
nodes.any((item) => item.node == omemoBundlesXmlns);
return Result(result); return Result(result);
} }

View File

@@ -8,7 +8,14 @@ import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart'; import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
class StatelessMediaSharingData { class StatelessMediaSharingData {
const StatelessMediaSharingData({ required this.mediaType, required this.size, required this.description, required this.hashes, required this.url, required this.thumbnails }); const StatelessMediaSharingData({
required this.mediaType,
required this.size,
required this.description,
required this.hashes,
required this.url,
required this.thumbnails,
});
final String mediaType; final String mediaType;
final int size; final int size;
final String description; final String description;
@@ -29,7 +36,8 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
} }
var url = ''; var url = '';
final references = file.firstTag('sources')!.findTags('reference', xmlns: referenceXmlns); final references =
file.firstTag('sources')!.findTags('reference', xmlns: referenceXmlns);
for (final i in references) { for (final i in references) {
if (i.attributes['type'] != 'data') continue; if (i.attributes['type'] != 'data') continue;
@@ -43,7 +51,8 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
final thumbnails = List<Thumbnail>.empty(growable: true); final thumbnails = List<Thumbnail>.empty(growable: true);
for (final child in file.children) { for (final child in file.children) {
// TODO(Unknown): Handle other thumbnails // TODO(Unknown): Handle other thumbnails
if (child.tag == 'file-thumbnail' && child.attributes['xmlns'] == fileThumbnailsXmlns) { if (child.tag == 'file-thumbnail' &&
child.attributes['xmlns'] == fileThumbnailsXmlns) {
final thumb = parseFileThumbnailElement(child); final thumb = parseFileThumbnailElement(child);
if (thumb != null) { if (thumb != null) {
thumbnails.add(thumb); thumbnails.add(thumb);
@@ -65,7 +74,7 @@ class SIMSManager extends XmppManagerBase {
SIMSManager() : super(simsManager); SIMSManager() : super(simsManager);
@override @override
List<String> getDiscoFeatures() => [ simsXmlns ]; List<String> getDiscoFeatures() => [simsXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -82,7 +91,10 @@ class SIMSManager 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 references = message.findTags('reference', xmlns: referenceXmlns); final references = message.findTags('reference', xmlns: referenceXmlns);
for (final ref in references) { for (final ref in references) {
final sims = ref.firstTag('media-sharing', xmlns: simsXmlns); final sims = ref.firstTag('media-sharing', xmlns: simsXmlns);

View File

@@ -1,7 +1,6 @@
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
class InvalidHashAlgorithmException implements Exception { class InvalidHashAlgorithmException implements Exception {
InvalidHashAlgorithmException(this.name); InvalidHashAlgorithmException(this.name);
final String name; final String name;
@@ -11,16 +10,20 @@ class InvalidHashAlgorithmException implements Exception {
/// Returns the hash algorithm specified by its name, according to XEP-0414. /// Returns the hash algorithm specified by its name, according to XEP-0414.
HashAlgorithm? getHashByName(String name) { HashAlgorithm? getHashByName(String name) {
switch (name) { switch (name) {
case 'sha-1': return Sha1(); case 'sha-1':
case 'sha-256': return Sha256(); return Sha1();
case 'sha-512': return Sha512(); case 'sha-256':
return Sha256();
case 'sha-512':
return Sha512();
// NOTE: cryptography provides an implementation of blake2b, however, // NOTE: cryptography provides an implementation of blake2b, however,
// I have no idea what it's output length is and you cannot set // I have no idea what it's output length is and you cannot set
// one. => New dependency // one. => New dependency
// TODO(Unknown): Implement // TODO(Unknown): Implement
//case "blake2b-256": ; //case "blake2b-256": ;
// hashLengthInBytes == 64 => 512? // hashLengthInBytes == 64 => 512?
case 'blake2b-512': Blake2b(); case 'blake2b-512':
Blake2b();
// NOTE: cryptography does not provide SHA3 hashes => New dependency // NOTE: cryptography does not provide SHA3 hashes => New dependency
// TODO(Unknown): Implement // TODO(Unknown): Implement
//case "sha3-256": ; //case "sha3-256": ;

View File

@@ -15,7 +15,7 @@ class MessageRetractionManager extends XmppManagerBase {
MessageRetractionManager() : super(messageRetractionManager); MessageRetractionManager() : super(messageRetractionManager);
@override @override
List<String> getDiscoFeatures() => [ messageRetractionXmlns ]; List<String> getDiscoFeatures() => [messageRetractionXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -30,7 +30,10 @@ class MessageRetractionManager 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 applyTo = message.firstTag('apply-to', xmlns: fasteningXmlns); final applyTo = message.firstTag('apply-to', xmlns: fasteningXmlns);
if (applyTo == null) { if (applyTo == null) {
return state; return state;
@@ -41,14 +44,13 @@ class MessageRetractionManager extends XmppManagerBase {
return state; return state;
} }
final isFallbackBody = message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null; final isFallbackBody =
message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null;
return state.copyWith( return state.copyWith(
messageRetraction: MessageRetractionData( messageRetraction: MessageRetractionData(
applyTo.attributes['id']! as String, applyTo.attributes['id']! as String,
isFallbackBody ? isFallbackBody ? message.firstTag('body')?.innerText() : null,
message.firstTag('body')?.innerText() :
null,
), ),
); );
} }

View File

@@ -32,7 +32,7 @@ class MessageReactionsManager extends XmppManagerBase {
MessageReactionsManager() : super(messageReactionsManager); MessageReactionsManager() : super(messageReactionsManager);
@override @override
List<String> getDiscoFeatures() => [ messageReactionsXmlns ]; List<String> getDiscoFeatures() => [messageReactionsXmlns];
@override @override
List<StanzaHandler> getIncomingStanzaHandlers() => [ List<StanzaHandler> getIncomingStanzaHandlers() => [
@@ -49,8 +49,12 @@ class MessageReactionsManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onReactionsReceived(Stanza message, StanzaHandlerData state) async { Future<StanzaHandlerData> _onReactionsReceived(
final reactionsElement = message.firstTag('reactions', xmlns: messageReactionsXmlns)!; Stanza message,
StanzaHandlerData state,
) async {
final reactionsElement =
message.firstTag('reactions', xmlns: messageReactionsXmlns)!;
return state.copyWith( return state.copyWith(
messageReactions: MessageReactions( messageReactions: MessageReactions(
reactionsElement.attributes['id']! as String, reactionsElement.attributes['id']! as String,

View File

@@ -18,13 +18,18 @@ class FileMetadataData {
/// Parse [node] as a FileMetadataData element. /// Parse [node] as a FileMetadataData element.
factory FileMetadataData.fromXML(XMLNode node) { factory FileMetadataData.fromXML(XMLNode node) {
assert(node.attributes['xmlns'] == fileMetadataXmlns, 'Invalid element xmlns'); assert(
node.attributes['xmlns'] == fileMetadataXmlns,
'Invalid element xmlns',
);
assert(node.tag == 'file', 'Invalid element anme'); assert(node.tag == 'file', 'Invalid element anme');
final lengthElement = node.firstTag('length'); final lengthElement = node.firstTag('length');
final length = lengthElement != null ? int.parse(lengthElement.innerText()) : null; final length =
lengthElement != null ? int.parse(lengthElement.innerText()) : null;
final sizeElement = node.firstTag('size'); final sizeElement = node.firstTag('size');
final size = sizeElement != null ? int.parse(sizeElement.innerText()) : null; final size =
sizeElement != null ? int.parse(sizeElement.innerText()) : null;
final hashes = <String, String>{}; final hashes = <String, String>{};
for (final e in node.findTags('hash')) { for (final e in node.findTags('hash')) {
@@ -82,13 +87,27 @@ class FileMetadataData {
children: List.empty(growable: true), children: List.empty(growable: true),
); );
if (mediaType != null) node.addChild(XMLNode(tag: 'media-type', text: mediaType)); if (mediaType != null) {
if (width != null) node.addChild(XMLNode(tag: 'width', text: '$width')); node.addChild(XMLNode(tag: 'media-type', text: mediaType));
if (height != null) node.addChild(XMLNode(tag: 'height', text: '$height')); }
if (desc != null) node.addChild(XMLNode(tag: 'desc', text: desc)); if (width != null) {
if (length != null) node.addChild(XMLNode(tag: 'length', text: length.toString())); node.addChild(XMLNode(tag: 'width', text: '$width'));
if (name != null) node.addChild(XMLNode(tag: 'name', text: name)); }
if (size != null) node.addChild(XMLNode(tag: 'size', text: size.toString())); if (height != null) {
node.addChild(XMLNode(tag: 'height', text: '$height'));
}
if (desc != null) {
node.addChild(XMLNode(tag: 'desc', text: desc));
}
if (length != null) {
node.addChild(XMLNode(tag: 'length', text: length.toString()));
}
if (name != null) {
node.addChild(XMLNode(tag: 'name', text: name));
}
if (size != null) {
node.addChild(XMLNode(tag: 'size', text: size.toString()));
}
for (final hash in hashes.entries) { for (final hash in hashes.entries) {
node.addChild( node.addChild(

View File

@@ -21,9 +21,14 @@ class StatelessFileSharingUrlSource extends StatelessFileSharingSource {
StatelessFileSharingUrlSource(this.url); StatelessFileSharingUrlSource(this.url);
factory StatelessFileSharingUrlSource.fromXml(XMLNode element) { factory StatelessFileSharingUrlSource.fromXml(XMLNode element) {
assert(element.attributes['xmlns'] == urlDataXmlns, 'Element has the wrong xmlns'); assert(
element.attributes['xmlns'] == urlDataXmlns,
'Element has the wrong xmlns',
);
return StatelessFileSharingUrlSource(element.attributes['target']! as String); return StatelessFileSharingUrlSource(
element.attributes['target']! as String,
);
} }
final String url; final String url;
@@ -44,7 +49,10 @@ class StatelessFileSharingUrlSource extends StatelessFileSharingSource {
/// StatelessFileSharingSources contained with it. /// StatelessFileSharingSources contained with it.
/// If [checkXmlns] is true, then the sources element must also have an xmlns attribute /// If [checkXmlns] is true, then the sources element must also have an xmlns attribute
/// of "urn:xmpp:sfs:0". /// of "urn:xmpp:sfs:0".
List<StatelessFileSharingSource> processStatelessFileSharingSources(XMLNode node, { bool checkXmlns = true }) { List<StatelessFileSharingSource> processStatelessFileSharingSources(
XMLNode node, {
bool checkXmlns = true,
}) {
final sources = List<StatelessFileSharingSource>.empty(growable: true); final sources = List<StatelessFileSharingSource>.empty(growable: true);
final sourcesElement = node.firstTag( final sourcesElement = node.firstTag(
@@ -88,9 +96,7 @@ class StatelessFileSharingData {
metadata.toXML(), metadata.toXML(),
XMLNode( XMLNode(
tag: 'sources', tag: 'sources',
children: sources children: sources.map((source) => source.toXml()).toList(),
.map((source) => source.toXml())
.toList(),
), ),
], ],
); );
@@ -99,7 +105,8 @@ class StatelessFileSharingData {
StatelessFileSharingUrlSource? getFirstUrlSource() { StatelessFileSharingUrlSource? getFirstUrlSource() {
return firstWhereOrNull( return firstWhereOrNull(
sources, sources,
(StatelessFileSharingSource source) => source is StatelessFileSharingUrlSource, (StatelessFileSharingSource source) =>
source is StatelessFileSharingUrlSource,
) as StatelessFileSharingUrlSource?; ) as StatelessFileSharingUrlSource?;
} }
} }
@@ -122,11 +129,16 @@ class SFSManager 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 sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!; final sfs = message.firstTag('file-sharing', xmlns: sfsXmlns)!;
return state.copyWith( return state.copyWith(
sfs: StatelessFileSharingData.fromXML(sfs, ), sfs: StatelessFileSharingData.fromXML(
sfs,
),
); );
} }
} }

View File

@@ -38,10 +38,18 @@ SFSEncryptionType encryptionTypeFromNamespace(String xmlns) {
} }
class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource { class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource {
StatelessFileSharingEncryptedSource(
StatelessFileSharingEncryptedSource(this.encryption, this.key, this.iv, this.hashes, this.source); this.encryption,
this.key,
this.iv,
this.hashes,
this.source,
);
factory StatelessFileSharingEncryptedSource.fromXml(XMLNode element) { factory StatelessFileSharingEncryptedSource.fromXml(XMLNode element) {
assert(element.attributes['xmlns'] == sfsEncryptionXmlns, 'Element has invalid xmlns'); assert(
element.attributes['xmlns'] == sfsEncryptionXmlns,
'Element has invalid xmlns',
);
final key = base64Decode(element.firstTag('key')!.text!); final key = base64Decode(element.firstTag('key')!.text!);
final iv = base64Decode(element.firstTag('iv')!.text!); final iv = base64Decode(element.firstTag('iv')!.text!);
@@ -50,7 +58,8 @@ class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource {
// Find the first URL source // Find the first URL source
final source = firstWhereOrNull( final source = firstWhereOrNull(
sources, sources,
(XMLNode child) => child.tag == 'url-data' && child.attributes['xmlns'] == urlDataXmlns, (XMLNode child) =>
child.tag == 'url-data' && child.attributes['xmlns'] == urlDataXmlns,
)!; )!;
// Find hashes // Find hashes
@@ -91,7 +100,8 @@ class StatelessFileSharingEncryptedSource extends StatelessFileSharingSource {
tag: 'iv', tag: 'iv',
text: base64Encode(iv), text: base64Encode(iv),
), ),
...hashes.entries.map((hash) => constructHashElement(hash.key, hash.value)), ...hashes.entries
.map((hash) => constructHashElement(hash.key, hash.value)),
XMLNode.xmlns( XMLNode.xmlns(
tag: 'sources', tag: 'sources',
xmlns: sfsXmlns, xmlns: sfsXmlns,

View File

@@ -22,7 +22,9 @@ class Sticker {
assert(node.tag == 'item', 'sticker has wrong tag'); assert(node.tag == 'item', 'sticker has wrong tag');
return Sticker( return Sticker(
FileMetadataData.fromXML(node.firstTag('file', xmlns: fileMetadataXmlns)!), FileMetadataData.fromXML(
node.firstTag('file', xmlns: fileMetadataXmlns)!,
),
processStatelessFileSharingSources(node, checkXmlns: false), processStatelessFileSharingSources(node, checkXmlns: false),
{}, {},
); );
@@ -73,7 +75,11 @@ class StickerPack {
this.restricted, this.restricted,
); );
factory StickerPack.fromXML(String id, XMLNode node, { bool hashAvailable = true }) { factory StickerPack.fromXML(
String id,
XMLNode node, {
bool hashAvailable = true,
}) {
assert(node.tag == 'pack', 'node has wrong tag'); assert(node.tag == 'pack', 'node has wrong tag');
assert(node.attributes['xmlns'] == stickersXmlns, 'node has wrong XMLNS'); assert(node.attributes['xmlns'] == stickersXmlns, 'node has wrong XMLNS');
@@ -142,13 +148,10 @@ class StickerPack {
hashValue, hashValue,
), ),
...restricted ? ...restricted ? [XMLNode(tag: 'restricted')] : [],
[XMLNode(tag: 'restricted')] :
[],
// Stickers // Stickers
...stickers ...stickers.map((sticker) => sticker.toPubSubXML()),
.map((sticker) => sticker.toPubSubXML()),
], ],
); );
} }
@@ -241,7 +244,10 @@ class StickersManager extends XmppManagerBase {
), ),
]; ];
Future<StanzaHandlerData> _onIncomingMessage(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onIncomingMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!; final sticker = stanza.firstTag('sticker', xmlns: stickersXmlns)!;
return state.copyWith( return state.copyWith(
stickerPackId: sticker.attributes['pack']! as String, stickerPackId: sticker.attributes['pack']! as String,
@@ -252,7 +258,11 @@ class StickersManager extends XmppManagerBase {
/// [accessModel] will be used as the PubSub node's access model. /// [accessModel] will be used as the PubSub node's access model.
/// ///
/// On success, returns true. On failure, returns a PubSubError. /// On success, returns true. On failure, returns a PubSubError.
Future<Result<PubSubError, bool>> publishStickerPack(JID jid, StickerPack pack, { String? accessModel }) async { Future<Result<PubSubError, bool>> publishStickerPack(
JID jid,
StickerPack pack, {
String? accessModel,
}) async {
assert(pack.id != '', 'The sticker pack must have an id'); assert(pack.id != '', 'The sticker pack must have an id');
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
@@ -271,7 +281,10 @@ class StickersManager extends XmppManagerBase {
/// Removes the sticker pack with id [id] from the PubSub node of [jid]. /// Removes the sticker pack with id [id] from the PubSub node of [jid].
/// ///
/// On success, returns the true. On failure, returns a PubSubError. /// On success, returns the true. On failure, returns a PubSubError.
Future<Result<PubSubError, bool>> retractStickerPack(JID jid, String id) async { Future<Result<PubSubError, bool>> retractStickerPack(
JID jid,
String id,
) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
return pm.retract( return pm.retract(
@@ -284,7 +297,10 @@ class StickersManager extends XmppManagerBase {
/// Fetches the sticker pack with id [id] from [jid]. /// Fetches the sticker pack with id [id] from [jid].
/// ///
/// On success, returns the StickerPack. On failure, returns a PubSubError. /// On success, returns the StickerPack. On failure, returns a PubSubError.
Future<Result<PubSubError, StickerPack>> fetchStickerPack(JID jid, String id) async { Future<Result<PubSubError, StickerPack>> fetchStickerPack(
JID jid,
String id,
) async {
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!; final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
final stickerPackDataRaw = await pm.getItem( final stickerPackDataRaw = await pm.getItem(
jid.toBare().toString(), jid.toBare().toString(),

View File

@@ -45,10 +45,7 @@ class QuoteData {
/// Takes the body of the message we want to quote [quoteBody] and the content of /// Takes the body of the message we want to quote [quoteBody] and the content of
/// the reply [body] and computes the fallback body and its length. /// the reply [body] and computes the fallback body and its length.
factory QuoteData.fromBodies(String quoteBody, String body) { factory QuoteData.fromBodies(String quoteBody, String body) {
final fallback = quoteBody final fallback = quoteBody.split('\n').map((line) => '> $line\n').join();
.split('\n')
.map((line) => '> $line\n')
.join();
return QuoteData( return QuoteData(
'$fallback$body', '$fallback$body',
@@ -88,7 +85,10 @@ class MessageRepliesManager extends XmppManagerBase {
@override @override
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage(Stanza stanza, StanzaHandlerData state) async { Future<StanzaHandlerData> _onMessage(
Stanza stanza,
StanzaHandlerData state,
) async {
final reply = stanza.firstTag('reply', xmlns: replyXmlns)!; final reply = stanza.firstTag('reply', xmlns: replyXmlns)!;
final id = reply.attributes['id']! as String; final id = reply.attributes['id']! as String;
final to = reply.attributes['to'] as String?; final to = reply.attributes['to'] as String?;

View File

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

View File

@@ -30,8 +30,5 @@ dependencies:
dev_dependencies: dev_dependencies:
build_runner: ^2.1.11 build_runner: ^2.1.11
moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.2+9
test: ^1.16.0 test: ^1.16.0
very_good_analysis: ^3.0.1 very_good_analysis: ^3.0.1

View File

@@ -0,0 +1,172 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/xmpp.dart';
class StubbedDiscoManager extends DiscoManager {
StubbedDiscoManager() : super([]);
@override
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
final result = DiscoInfo.fromQuery(
XMLNode.fromString(
'''<query xmlns='http://jabber.org/protocol/disco#info'>
<identity category='account' type='registered'/>
<identity type='service' category='pubsub' name='PubSub acs-clustered'/>
<feature var='http://jabber.org/protocol/pubsub#retrieve-default'/>
<feature var='http://jabber.org/protocol/pubsub#purge-nodes'/>
<feature var='http://jabber.org/protocol/pubsub#subscribe'/>
<feature var='http://jabber.org/protocol/pubsub#member-affiliation'/>
<feature var='http://jabber.org/protocol/pubsub#subscription-notifications'/>
<feature var='http://jabber.org/protocol/pubsub#create-nodes'/>
<feature var='http://jabber.org/protocol/pubsub#outcast-affiliation'/>
<feature var='http://jabber.org/protocol/pubsub#get-pending'/>
<feature var='http://jabber.org/protocol/pubsub#presence-notifications'/>
<feature var='urn:xmpp:ping'/>
<feature var='http://jabber.org/protocol/pubsub#delete-nodes'/>
<feature var='http://jabber.org/protocol/pubsub#config-node'/>
<feature var='http://jabber.org/protocol/pubsub#retrieve-items'/>
<feature var='http://jabber.org/protocol/pubsub#access-whitelist'/>
<feature var='http://jabber.org/protocol/pubsub#access-presence'/>
<feature var='http://jabber.org/protocol/disco#items'/>
<feature var='http://jabber.org/protocol/pubsub#meta-data'/>
<feature var='http://jabber.org/protocol/pubsub#multi-items'/>
<feature var='http://jabber.org/protocol/pubsub#item-ids'/>
<feature var='urn:xmpp:mam:1'/>
<feature var='http://jabber.org/protocol/pubsub#instant-nodes'/>
<feature var='urn:xmpp:mam:2'/>
<feature var='urn:xmpp:mam:2#extended'/>
<feature var='http://jabber.org/protocol/pubsub#modify-affiliations'/>
<feature var='http://jabber.org/protocol/pubsub#multi-collection'/>
<feature var='http://jabber.org/protocol/pubsub#persistent-items'/>
<feature var='http://jabber.org/protocol/pubsub#create-and-configure'/>
<feature var='http://jabber.org/protocol/pubsub#publisher-affiliation'/>
<feature var='http://jabber.org/protocol/pubsub#access-open'/>
<feature var='http://jabber.org/protocol/pubsub#retrieve-affiliations'/>
<feature var='http://jabber.org/protocol/pubsub#access-authorize'/>
<feature var='jabber:iq:version'/>
<feature var='http://jabber.org/protocol/pubsub#retract-items'/>
<feature var='http://jabber.org/protocol/pubsub#manage-subscriptions'/>
<feature var='http://jabber.org/protocol/commands'/>
<feature var='http://jabber.org/protocol/pubsub#auto-subscribe'/>
<feature var='http://jabber.org/protocol/pubsub#publish-options'/>
<feature var='http://jabber.org/protocol/pubsub#access-roster'/>
<feature var='http://jabber.org/protocol/pubsub#publish'/>
<feature var='http://jabber.org/protocol/pubsub#collections'/>
<feature var='http://jabber.org/protocol/pubsub#retrieve-subscriptions'/>
<feature var='http://jabber.org/protocol/disco#info'/>
<x type='result' xmlns='jabber:x:data'>
<field type='hidden' var='FORM_TYPE'>
<value>http://jabber.org/network/serverinfo</value>
</field>
<field type='list-multi' var='abuse-addresses'>
<value>mailto:support@tigase.net</value>
<value>xmpp:tigase@mix.tigase.im</value>
<value>xmpp:tigase@muc.tigase.org</value>
<value>https://tigase.net/technical-support</value>
</field>
</x>
<feature var='http://jabber.org/protocol/pubsub#auto-create'/>
<feature var='http://jabber.org/protocol/pubsub#auto-subscribe'/>
<feature var='urn:xmpp:mix:pam:2'/>
<feature var='urn:xmpp:carbons:2'/>
<feature var='urn:xmpp:carbons:rules:0'/>
<feature var='jabber:iq:auth'/>
<feature var='vcard-temp'/>
<feature var='http://jabber.org/protocol/amp'/>
<feature var='msgoffline'/>
<feature var='http://jabber.org/protocol/disco#info'/>
<feature var='http://jabber.org/protocol/disco#items'/>
<feature var='urn:xmpp:blocking'/>
<feature var='urn:xmpp:reporting:0'/>
<feature var='urn:xmpp:reporting:abuse:0'/>
<feature var='urn:xmpp:reporting:spam:0'/>
<feature var='urn:xmpp:reporting:1'/>
<feature var='urn:xmpp:ping'/>
<feature var='urn:ietf:params:xml:ns:xmpp-sasl'/>
<feature var='http://jabber.org/protocol/pubsub'/>
<feature var='http://jabber.org/protocol/pubsub#owner'/>
<feature var='http://jabber.org/protocol/pubsub#publish'/>
<identity type='pep' category='pubsub'/>
<feature var='urn:xmpp:pep-vcard-conversion:0'/>
<feature var='urn:xmpp:bookmarks-conversion:0'/>
<feature var='urn:xmpp:archive:auto'/>
<feature var='urn:xmpp:archive:manage'/>
<feature var='urn:xmpp:push:0'/>
<feature var='tigase:push:away:0'/>
<feature var='tigase:push:encrypt:0'/>
<feature var='tigase:push:encrypt:aes-128-gcm'/>
<feature var='tigase:push:filter:ignore-unknown:0'/>
<feature var='tigase:push:filter:groupchat:0'/>
<feature var='tigase:push:filter:muted:0'/>
<feature var='tigase:push:priority:0'/>
<feature var='tigase:push:jingle:0'/>
<feature var='jabber:iq:roster'/>
<feature var='jabber:iq:roster-dynamic'/>
<feature var='urn:xmpp:mam:1'/>
<feature var='urn:xmpp:mam:2'/>
<feature var='urn:xmpp:mam:2#extended'/>
<feature var='urn:xmpp:mix:pam:2#archive'/>
<feature var='jabber:iq:version'/>
<feature var='urn:xmpp:time'/>
<feature var='jabber:iq:privacy'/>
<feature var='urn:ietf:params:xml:ns:xmpp-bind'/>
<feature var='urn:xmpp:extdisco:2'/>
<feature var='http://jabber.org/protocol/commands'/>
<feature var='urn:ietf:params:xml:ns:vcard-4.0'/>
<feature var='jabber:iq:private'/>
<feature var='urn:ietf:params:xml:ns:xmpp-session'/>
</query>'''
),
JID.fromString('pubsub.server.example.org'),
);
return Result(result);
}
}
T? getDiscoManagerStub<T extends XmppManagerBase>(String id) {
return StubbedDiscoManager() as T;
}
void main() {
initLogger();
test('Test publishing with pubsub#max_items when the server does not support it', () async {
XMLNode? sent;
final manager = PubSubManager();
manager.register(
XmppManagerAttributes(
sendStanza: (stanza, { StanzaFromType addFrom = StanzaFromType.full, bool addId = true, bool awaitable = true, bool encrypted = false, bool forceEncryption = false, }) async {
sent = stanza;
return XMLNode.fromString('<iq />');
},
sendNonza: (_) {},
sendEvent: (_) {},
getManagerById: getDiscoManagerStub,
getConnectionSettings: () => ConnectionSettings(
jid: JID.fromString('hallo@example.server'),
password: 'password',
useDirectTLS: true,
allowPlainAuth: false,
),
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('hallo@example.server/uwu'),
getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
getNegotiatorById: getNegotiatorNullStub,
),
);
final result = await manager.preprocessPublishOptions(
'pubsub.server.example.org',
'example:node',
PubSubPublishOptions(
maxItems: 'max',
),
);
});
}

View File

@@ -22,7 +22,8 @@ class TCPSocketWrapper extends BaseSocketWrapper {
final StreamController<String> _dataStream = StreamController.broadcast(); final StreamController<String> _dataStream = StreamController.broadcast();
/// The stream of outgoing (TCPSocketWrapper -> XmppConnection) events. /// The stream of outgoing (TCPSocketWrapper -> XmppConnection) events.
final StreamController<XmppSocketEvent> _eventStream = StreamController.broadcast(); final StreamController<XmppSocketEvent> _eventStream =
StreamController.broadcast();
/// A subscription on the socket's data stream. /// A subscription on the socket's data stream.
StreamSubscription<dynamic>? _socketSubscription; StreamSubscription<dynamic>? _socketSubscription;
@@ -80,7 +81,9 @@ class TCPSocketWrapper extends BaseSocketWrapper {
results.sort(srvRecordSortComparator); results.sort(srvRecordSortComparator);
for (final srv in results) { for (final srv in results) {
try { try {
_log.finest('Attempting secure connection to ${srv.target}:${srv.port}...'); _log.finest(
'Attempting secure connection to ${srv.target}:${srv.port}...',
);
// Workaround: We cannot set the SNI directly when using SecureSocket.connect. // Workaround: We cannot set the SNI directly when using SecureSocket.connect.
// instead, we connect using a regular socket and then secure it. This allows // instead, we connect using a regular socket and then secure it. This allows
@@ -93,14 +96,14 @@ class TCPSocketWrapper extends BaseSocketWrapper {
_socket = await SecureSocket.secure( _socket = await SecureSocket.secure(
sock, sock,
host: domain, host: domain,
supportedProtocols: const [ xmppClientALPNId ], supportedProtocols: const [xmppClientALPNId],
onBadCertificate: (cert) => onBadCertificate(cert, domain), onBadCertificate: (cert) => onBadCertificate(cert, domain),
); );
_secure = true; _secure = true;
_log.finest('Success!'); _log.finest('Success!');
return true; return true;
} on Exception catch(e) { } on Exception catch (e) {
_log.finest('Failure! $e'); _log.finest('Failure! $e');
if (e is HandshakeException) { if (e is HandshakeException) {
@@ -132,7 +135,7 @@ class TCPSocketWrapper extends BaseSocketWrapper {
_log.finest('Success!'); _log.finest('Success!');
return true; return true;
} on Exception catch(e) { } on Exception catch (e) {
_log.finest('Failure! $e'); _log.finest('Failure! $e');
continue; continue;
} }
@@ -154,7 +157,7 @@ class TCPSocketWrapper extends BaseSocketWrapper {
); );
_log.finest('Success!'); _log.finest('Success!');
return true; return true;
} on Exception catch(e) { } on Exception catch (e) {
_log.finest('Failure! $e'); _log.finest('Failure! $e');
return false; return false;
} }
@@ -187,7 +190,7 @@ class TCPSocketWrapper extends BaseSocketWrapper {
_socket = await SecureSocket.secure( _socket = await SecureSocket.secure(
_socket!, _socket!,
supportedProtocols: const [ xmppClientALPNId ], supportedProtocols: const [xmppClientALPNId],
onBadCertificate: (cert) => onBadCertificate(cert, domain), onBadCertificate: (cert) => onBadCertificate(cert, domain),
); );
@@ -232,7 +235,7 @@ class TCPSocketWrapper extends BaseSocketWrapper {
} }
@override @override
Future<bool> connect(String domain, { String? host, int? port }) async { Future<bool> connect(String domain, {String? host, int? port}) async {
_expectSocketClosure = false; _expectSocketClosure = false;
_secure = false; _secure = false;
@@ -280,7 +283,7 @@ class TCPSocketWrapper extends BaseSocketWrapper {
try { try {
_socket!.close(); _socket!.close();
} catch(e) { } catch (e) {
_log.warning('Closing socket threw exception: $e'); _log.warning('Closing socket threw exception: $e');
} }
} }
@@ -289,10 +292,11 @@ class TCPSocketWrapper extends BaseSocketWrapper {
Stream<String> getDataStream() => _dataStream.stream.asBroadcastStream(); Stream<String> getDataStream() => _dataStream.stream.asBroadcastStream();
@override @override
Stream<XmppSocketEvent> getEventStream() => _eventStream.stream.asBroadcastStream(); Stream<XmppSocketEvent> getEventStream() =>
_eventStream.stream.asBroadcastStream();
@override @override
void write(Object? data, { String? redact }) { void write(Object? data, {String? redact}) {
if (_socket == null) { if (_socket == null) {
_log.severe('Failed to write to socket as _socket is null'); _log.severe('Failed to write to socket as _socket is null');
return; return;

View File

@@ -1,6 +1,6 @@
name: moxxmpp_socket_tcp name: moxxmpp_socket_tcp
description: A socket for moxxmpp using TCP that implements the RFC6120 connection algorithm and XEP-0368 description: A socket for moxxmpp using TCP that implements the RFC6120 connection algorithm and XEP-0368
version: 0.1.2+9 version: 0.2.1
homepage: https://codeberg.org/moxxy/moxxmpp homepage: https://codeberg.org/moxxy/moxxmpp
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
@@ -12,7 +12,7 @@ dependencies:
meta: ^1.6.0 meta: ^1.6.0
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.6+1 version: ^0.2.0
dev_dependencies: dev_dependencies:
lints: ^2.0.0 lints: ^2.0.0

74
scripts/lock2nix.py Normal file
View File

@@ -0,0 +1,74 @@
'''
This script generates a .nix file containing all dependencies specified in the
special lock file. The .nix file also contains the derivation "pubCache", which can
be used as the path in the PUB_CACHE environment variable.
'''
import sys
import yaml
import urllib.parse
def generate_derivation(package):
name = package['description']['name']
sha = package['sha256']
version = package['version']
url = ''
if 'archive_url' in package:
url = package['archive_url']
else:
base_url = package['description']['url']
url = f'{base_url}/packages/{name}/versions/{version}.tar.gz'
return f'''
{name} = fetchzip {{
sha256 = "{sha}";
url = "{url}";
stripRoot = false;
extension = "tar.gz";
}};
'''
def main():
if len(sys.argv) != 4:
print('Usage: lock2nix.py <input lock> <output nix> <package name>')
exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2]
package_name = sys.argv[3]
with open(input_file, 'r') as f:
data = yaml.safe_load(f.read())
with open(output_file, 'w') as f:
f.write('# GENERATED BY LOCK2NIX.py\n')
f.write('# DO NOT EDIT BY HAND\n')
f.write('{fetchzip, runCommand} : rec {')
steps = ''
for package in data['packages']:
try:
f.write(generate_derivation(data['packages'][package]))
except ex:
print(f'Failed with {ex} for package {package}')
print(package)
source = data['packages'][package]['source']
prefix = urllib.parse.quote(
data['packages'][package]['description']['url'][8:],
safe='',
).replace('%2F', '%47')
name = data['packages'][package]['description']['name']
version = data['packages'][package]['version']
steps += f'''
mkdir -p $out/{source}/{prefix}
ln -s ${{{package}}} $out/{source}/{prefix}/{name}-{version}
'''
f.write(f'''
pubCache = runCommand "{package_name}-pub-cache" {{}} ''{steps}'';
''')
f.write('\n}')
if __name__ == '__main__':
main()

70
scripts/pubspec2lock.py Normal file
View File

@@ -0,0 +1,70 @@
'''
Takes a Dart pubspec.lock file as input and outputs a specialized lock file
that contains archive URLs and sha256 hashes for nix. Only useful in combination
with lock2nix.py.
'''
import sys
import subprocess
import urllib
import yaml
import requests
def main():
if len(sys.argv) != 3:
print('Usage: pubspec2lock.py <input> <output>')
exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2]
with open(input_file, 'r') as f:
data = yaml.safe_load(f.read())
result = {
'packages': {}
}
for package_name in data['packages']:
print(package_name)
package = data['packages'][package_name]
cleaned_url = package["description"]["url"]
if cleaned_url[-1] == '/':
cleaned_url = cleaned_url[:-1]
latest = requests.get(
f'{cleaned_url}/api/packages/{package_name}',
headers={
'Accept': 'application/vnd.pub.v2+json',
},
)
latest_data = None
try:
latest_data = latest.json()
except:
print(latest.text)
exit(1)
for version_data in latest_data['versions']:
if version_data['version'] == package['version']:
package['archive_url'] = version_data['archive_url']
p = subprocess.run([
'nix-prefetch-url',
'--unpack',
urllib.parse.unquote(version_data['archive_url']),
], capture_output=True)
sha256 = p.stdout.decode('utf8')[:-1]
package['sha256'] = sha256
break
result['packages'][package_name] = package
with open(output_file, 'w') as f:
f.write('# CREATED BY pubspec2lock.py\n')
f.write('# DO NOT EDIT BY HAND\n')
f.write(yaml.dump(result))
if __name__ == '__main__':
main()