Compare commits
112 Commits
moxxmpp-v0
...
e1e492832e
| Author | SHA1 | Date | |
|---|---|---|---|
| e1e492832e | |||
| 1950394f7d | |||
| 308f7d93f5 | |||
| de85bf848d | |||
| 7a6bf468bc | |||
| 9cb6346c4d | |||
| f49eb66bb7 | |||
| 324ef9ca29 | |||
| 5b4dcc67b2 | |||
| 9010218b10 | |||
| 61144a10b3 | |||
| 7a1f737c65 | |||
| 546c032d43 | |||
| b1869be3d9 | |||
| 574fdfecaa | |||
| 25c778965c | |||
| 976c0040b5 | |||
| b53c62b40c | |||
|
|
2cdc56c882 | ||
|
|
f5059d8008 | ||
|
|
792ec4d731 | ||
| 93d08188ea | |||
| e9ad5a6c66 | |||
| 8b0f118e2d | |||
|
|
60c89e28d3 | ||
| 38155051f5 | |||
|
|
7b215d5c6e | ||
| 1000e0756b | |||
| 902b497526 | |||
| 039f954e70 | |||
| 5dc2b127fa | |||
| 252cc44841 | |||
| 96d9ce4761 | |||
| 7f294d6632 | |||
| e17de9065b | |||
| 098687de45 | |||
| 6da3342f22 | |||
| 47337540f5 | |||
| 7e588f01b0 | |||
| c7c6c9dae4 | |||
| c77cfc4dcd | |||
| 1bd61076ea | |||
| bff4a6f707 | |||
| 1cc266c675 | |||
| 72099dfde5 | |||
| c9c45baabc | |||
| a01022c217 | |||
| c3459e6820 | |||
| e031e6d760 | |||
| 6c63b53cf4 | |||
| 1aa50699ad | |||
| b2c54ae8c0 | |||
| b16c9f4b30 | |||
| a8d80eaddf | |||
| 9baf1ed73c | |||
| ce3ea656ad | |||
| ed49212f5a | |||
| ad1242c47d | |||
| 890fcfb506 | |||
| d7723615fe | |||
| 6517065a1a | |||
| 9223a7d403 | |||
| 7ce6703c5b | |||
| 37261cddbb | |||
| d8c2ef6f3b | |||
| 98e5324409 | |||
| a69c2a23f2 | |||
| d8de093e4d | |||
| 678564dbb3 | |||
| 09d2601e85 | |||
| 41560682a1 | |||
| 473f8e4bb6 | |||
| 67446285c1 | |||
| e12f4688d3 | |||
| 2581bbe203 | |||
| 995f2e0248 | |||
| e2c8f79429 | |||
| 763c93857d | |||
| 55d2ef9c25 | |||
| f37cbd1616 | |||
| 2a3449d0f2 | |||
| 596693c206 | |||
| 22aa07c4ba | |||
| 62001c1e29 | |||
| ca85c94fe5 | |||
| 637e1e25a6 | |||
| 09696c1c4d | |||
| 298a8342b8 | |||
| d64220426b | |||
| 88efdc361c | |||
| cc1b371198 | |||
| d9e4a3c1d4 | |||
| 0ae13acca0 | |||
| d383fa31ae | |||
| d1de394cd9 | |||
| 14c48bcc64 | |||
| 138edffb0a | |||
| eb8f6ba17a | |||
| beff05765b | |||
| 3b7ded3b96 | |||
| edc86a10b3 | |||
| 39e9c55fae | |||
| 1b2c567787 | |||
| d3955479f7 | |||
| 300a52f9fe | |||
| 2e3472d88f | |||
| 6b106fe365 | |||
| bfd28c281e | |||
| c307567025 | |||
| 5dd96f518b | |||
| 6d9010b11c | |||
| 9cc735d854 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ko_fi: papatutuwawa
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -10,3 +10,9 @@ build/
|
|||||||
# Omit committing pubspec.lock for library packages; see
|
# Omit committing pubspec.lock for library packages; see
|
||||||
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
# https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
|
|
||||||
|
# Omit pubspec override files generated by melos
|
||||||
|
**/pubspec_overrides.yaml
|
||||||
|
|
||||||
|
# Flake results
|
||||||
|
result
|
||||||
|
|||||||
14
.gitlint
Normal file
14
.gitlint
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[general]
|
||||||
|
ignore=B5,B6,B7,B8
|
||||||
|
|
||||||
|
[title-max-length]
|
||||||
|
line-length=72
|
||||||
|
|
||||||
|
[title-trailing-punctuation]
|
||||||
|
[title-hard-tab]
|
||||||
|
[title-match-regex]
|
||||||
|
regex=^((feat|fix|chore|refactor|docs|release|test)\((meta|tests|style|docs|xep|core|example)+(,(meta|tests|style|docs|xep|core|example))*\)|release): [A-Z0-9].*$
|
||||||
|
|
||||||
|
|
||||||
|
[body-trailing-whitespace]
|
||||||
|
[body-first-line-empty]
|
||||||
28
.woodpecker.yml
Normal file
28
.woodpecker.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
pipeline:
|
||||||
|
# Check moxxmpp
|
||||||
|
moxxmpp-lint:
|
||||||
|
image: dart:2.18.1
|
||||||
|
commands:
|
||||||
|
- cd packages/moxxmpp
|
||||||
|
- dart pub get
|
||||||
|
- dart analyze --fatal-infos --fatal-warnings
|
||||||
|
moxxmpp-test:
|
||||||
|
image: dart:2.18.1
|
||||||
|
commands:
|
||||||
|
- cd packages/moxxmpp
|
||||||
|
- dart pub get
|
||||||
|
- dart test
|
||||||
|
|
||||||
|
# Check moxxmpp_socket_tcp
|
||||||
|
moxxmpp_socket_tcp-lint:
|
||||||
|
image: dart:2.18.1
|
||||||
|
commands:
|
||||||
|
- cd packages/moxxmpp_socket_tcp
|
||||||
|
- dart pub get
|
||||||
|
- dart analyze --fatal-infos --fatal-warnings
|
||||||
|
# moxxmpp-test:
|
||||||
|
# image: dart:2.18.1
|
||||||
|
# commands:
|
||||||
|
# - cd packages/moxxmpp
|
||||||
|
# - dart pub get
|
||||||
|
# - dart test
|
||||||
19
CONTRIBUTING.md
Normal file
19
CONTRIBUTING.md
Normal 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.
|
||||||
14
README.md
14
README.md
@@ -3,13 +3,15 @@
|
|||||||
moxxmpp is a XMPP library written purely in Dart for usage in Moxxy.
|
moxxmpp is a XMPP library written purely in Dart for usage in Moxxy.
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
### moxxmpp
|
### [moxxmpp](./packages/moxxmpp)
|
||||||
|
|
||||||
This package contains the actual XMPP code that is platform-independent.
|
This package contains the actual XMPP code that is platform-independent.
|
||||||
|
|
||||||
### moxxmpp_socket
|
Documentation is available [here](https://moxxy.org/developers/docs/moxxmpp/).
|
||||||
|
|
||||||
`moxxmpp_socket` contains the implementation of the `BaseSocketWrapper` class that
|
### [moxxmpp_socket_tcp](./packages/moxxmpp_socket_tcp)
|
||||||
|
|
||||||
|
`moxxmpp_socket_tcp` contains the implementation of the `BaseSocketWrapper` class that
|
||||||
implements the RFC6120 connection algorithm and XEP-0368 direct TLS connections,
|
implements the RFC6120 connection algorithm and XEP-0368 direct TLS connections,
|
||||||
if a DNS implementation is given, and supports StartTLS.
|
if a DNS implementation is given, and supports StartTLS.
|
||||||
|
|
||||||
@@ -25,3 +27,9 @@ the development shell provided by the NixOS Flake, ensure that `ANDROID_HOME` an
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
See `./LICENSE`.
|
See `./LICENSE`.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
|
||||||
|
|
||||||
|
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/assets/repo/kofi.png" height="36" style="height: 36px; border: 0px;"></img>](https://ko-fi.com/papatutuwawa)
|
||||||
|
|||||||
@@ -11,5 +11,3 @@ analyzer:
|
|||||||
exclude:
|
exclude:
|
||||||
- "**/*.g.dart"
|
- "**/*.g.dart"
|
||||||
- "**/*.freezed.dart"
|
- "**/*.freezed.dart"
|
||||||
- "test/"
|
|
||||||
- "integration_test/"
|
|
||||||
|
|||||||
@@ -11,19 +11,22 @@ 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.priority,
|
(record) => MoxSrvRecord(
|
||||||
record.weight,
|
record.priority,
|
||||||
record.target,
|
record.weight,
|
||||||
record.port,
|
record.target,
|
||||||
),)
|
record.port,
|
||||||
.toList();
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
||||||
@@ -78,21 +88,47 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
CSINegotiator(),
|
CSINegotiator(),
|
||||||
RosterFeatureNegotiator(),
|
RosterFeatureNegotiator(),
|
||||||
SaslPlainNegotiator(),
|
SaslPlainNegotiator(),
|
||||||
|
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
|
||||||
SaslScramNegotiator(9, '', '', ScramHashType.sha256),
|
SaslScramNegotiator(9, '', '', ScramHashType.sha256),
|
||||||
SaslScramNegotiator(8, '', '', ScramHashType.sha1),
|
SaslScramNegotiator(8, '', '', ScramHashType.sha1),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _buttonPressed() async {
|
Future<void> _buttonPressed() async {
|
||||||
|
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,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await connection.connect();
|
final result = await connection.connect(waitUntilLogin: true);
|
||||||
|
setState(() {
|
||||||
|
connected = result.isType<bool>() && result.get<bool>();
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
if (result.isType<XmppError>()) {
|
||||||
|
logger.severe(result.get<XmppError>());
|
||||||
|
if (context.mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('Error'),
|
||||||
|
content: Text(result.get<XmppError>().toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -100,20 +136,24 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
|
backgroundColor: connected ? Colors.green : Colors.deepPurple[800],
|
||||||
|
foregroundColor: connected ? Colors.black : Colors.white,
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
TextField(
|
TextField(
|
||||||
|
enabled: !loading,
|
||||||
controller: jidController,
|
controller: jidController,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'JID',
|
labelText: 'JID',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
|
enabled: !loading,
|
||||||
controller: passwordController,
|
controller: passwordController,
|
||||||
decoration: InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Password',
|
labelText: 'Password',
|
||||||
),
|
),
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
@@ -121,10 +161,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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ dependencies:
|
|||||||
version: 0.1.4+1
|
version: 0.1.4+1
|
||||||
moxxmpp:
|
moxxmpp:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.1.2+1
|
version: 0.1.6+1
|
||||||
moxxmpp_socket_tcp:
|
moxxmpp_socket_tcp:
|
||||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
version: 0.1.2+1
|
version: 0.1.2+9
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
12
flake.lock
generated
12
flake.lock
generated
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
28
flake.nix
28
flake.nix
@@ -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";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,15 +29,30 @@
|
|||||||
useGoogleAPIs = false;
|
useGoogleAPIs = false;
|
||||||
useGoogleTVAddOns = false;
|
useGoogleTVAddOns = false;
|
||||||
};
|
};
|
||||||
pinnedJDK = pkgs.jdk;
|
pinnedJDK = pkgs.jdk17;
|
||||||
|
|
||||||
|
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
||||||
|
pyyaml
|
||||||
|
requests
|
||||||
|
]);
|
||||||
|
|
||||||
|
moxxmppPubCache = import ./nix/pubcache.moxxmpp.nix {
|
||||||
|
inherit (pkgs) fetchzip runCommand;
|
||||||
|
};
|
||||||
in {
|
in {
|
||||||
|
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
35
nix/moxxmpp-docs.nix
Normal 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
730
nix/moxxmpp.lock
Normal 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
814
nix/pubcache.moxxmpp.nix
Normal 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
|
||||||
|
'';
|
||||||
|
|
||||||
|
}
|
||||||
1
packages/moxxmpp/.pubignore
Normal file
1
packages/moxxmpp/.pubignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pubspec_overrides.yaml
|
||||||
@@ -1,3 +1,43 @@
|
|||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
- **BREAKING**: Removed `connectAwaitable` and merged it with `connect`.
|
||||||
|
- **BREAKING**: Removed `allowPlainAuth` from `ConnectionSettings`. If you don't want to use SASL PLAIN, don't register the negotiator. If you want to only conditionally use SASL PLAIN, extend the `SaslPlainNegotiator` and override its `matchesFeature` method to only call the super method when SASL PLAIN should be used.
|
||||||
|
|
||||||
|
## 0.1.6+1
|
||||||
|
|
||||||
|
- **FIX**: Fix LMC not working.
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
- **FEAT**: Implement XEP-0308.
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
- **FEAT**: Message events now contain the stanza error, if available.
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
- **FIX**: Only stanza-id required 'sid:0' support.
|
||||||
|
- **FEAT**: Implement parsing and sending of retractions.
|
||||||
|
|
||||||
|
## 0.1.3+1
|
||||||
|
|
||||||
|
- **FIX**: Expose the error classes.
|
||||||
|
|
||||||
|
## 0.1.3
|
||||||
|
|
||||||
|
- **REFACTOR**: Replace MayFail by Result.
|
||||||
|
- **FIX**: Remove the old Results API.
|
||||||
|
- **FEAT**: Rework how the negotiator system works.
|
||||||
|
|
||||||
|
## 0.1.2+3
|
||||||
|
|
||||||
|
- **FIX**: SASL SCRAM-SHA-{256,512} should now work.
|
||||||
|
|
||||||
|
## 0.1.2+2
|
||||||
|
|
||||||
|
- **FIX**: Fix reconnections when the connection is awaited.
|
||||||
|
|
||||||
## 0.1.2+1
|
## 0.1.2+1
|
||||||
|
|
||||||
- **FIX**: A certificate rejection does not crash the connection.
|
- **FIX**: A certificate rejection does not crash the connection.
|
||||||
|
|||||||
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
A pure-Dart XMPP library written for Moxxy.
|
A pure-Dart XMPP library written for Moxxy.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Include the following as a dependency in your pubspec file:
|
||||||
|
|
||||||
|
```
|
||||||
|
moxxmpp:
|
||||||
|
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||||
|
version: 0.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
You can find the documentation [here](https://moxxy.org/developers/docs/moxxmpp/).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
See `./LICENSE`.
|
See `./LICENSE`.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
|
||||||
|
|
||||||
|
[<img src="https://codeberg.org/moxxy/moxxyv2/raw/branch/master/assets/repo/kofi.png" height="36" style="height: 36px; border: 0px;"></img>](https://ko-fi.com/papatutuwawa)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
library moxxmpp;
|
library moxxmpp;
|
||||||
|
|
||||||
export 'package:moxxmpp/src/connection.dart';
|
export 'package:moxxmpp/src/connection.dart';
|
||||||
|
export 'package:moxxmpp/src/connection_errors.dart';
|
||||||
|
export 'package:moxxmpp/src/connectivity.dart';
|
||||||
|
export 'package:moxxmpp/src/errors.dart';
|
||||||
export 'package:moxxmpp/src/events.dart';
|
export 'package:moxxmpp/src/events.dart';
|
||||||
export 'package:moxxmpp/src/iq.dart';
|
export 'package:moxxmpp/src/iq.dart';
|
||||||
export 'package:moxxmpp/src/jid.dart';
|
export 'package:moxxmpp/src/jid.dart';
|
||||||
@@ -16,6 +19,7 @@ export 'package:moxxmpp/src/negotiators/manager.dart';
|
|||||||
export 'package:moxxmpp/src/negotiators/namespaces.dart';
|
export 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||||
export 'package:moxxmpp/src/negotiators/negotiator.dart';
|
export 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
export 'package:moxxmpp/src/negotiators/resource_binding.dart';
|
export 'package:moxxmpp/src/negotiators/resource_binding.dart';
|
||||||
|
export 'package:moxxmpp/src/negotiators/sasl/errors.dart';
|
||||||
export 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
export 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
||||||
export 'package:moxxmpp/src/negotiators/sasl/plain.dart';
|
export 'package:moxxmpp/src/negotiators/sasl/plain.dart';
|
||||||
export 'package:moxxmpp/src/negotiators/sasl/scram.dart';
|
export 'package:moxxmpp/src/negotiators/sasl/scram.dart';
|
||||||
@@ -25,13 +29,14 @@ export 'package:moxxmpp/src/presence.dart';
|
|||||||
export 'package:moxxmpp/src/reconnect.dart';
|
export 'package:moxxmpp/src/reconnect.dart';
|
||||||
export 'package:moxxmpp/src/rfcs/rfc_2782.dart';
|
export 'package:moxxmpp/src/rfcs/rfc_2782.dart';
|
||||||
export 'package:moxxmpp/src/rfcs/rfc_4790.dart';
|
export 'package:moxxmpp/src/rfcs/rfc_4790.dart';
|
||||||
export 'package:moxxmpp/src/roster.dart';
|
export 'package:moxxmpp/src/roster/errors.dart';
|
||||||
|
export 'package:moxxmpp/src/roster/roster.dart';
|
||||||
|
export 'package:moxxmpp/src/roster/state.dart';
|
||||||
export 'package:moxxmpp/src/settings.dart';
|
export 'package:moxxmpp/src/settings.dart';
|
||||||
export 'package:moxxmpp/src/socket.dart';
|
export 'package:moxxmpp/src/socket.dart';
|
||||||
export 'package:moxxmpp/src/stanza.dart';
|
export 'package:moxxmpp/src/stanza.dart';
|
||||||
export 'package:moxxmpp/src/stringxml.dart';
|
export 'package:moxxmpp/src/stringxml.dart';
|
||||||
export 'package:moxxmpp/src/types/error.dart';
|
export 'package:moxxmpp/src/types/result.dart';
|
||||||
export 'package:moxxmpp/src/types/resultv2.dart';
|
|
||||||
export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
|
export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
|
||||||
export 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
|
export 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0004.dart';
|
export 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||||
@@ -57,11 +62,13 @@ export 'package:moxxmpp/src/xeps/xep_0203.dart';
|
|||||||
export 'package:moxxmpp/src/xeps/xep_0280.dart';
|
export 'package:moxxmpp/src/xeps/xep_0280.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0297.dart';
|
export 'package:moxxmpp/src/xeps/xep_0297.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0300.dart';
|
export 'package:moxxmpp/src/xeps/xep_0300.dart';
|
||||||
|
export 'package:moxxmpp/src/xeps/xep_0308.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0333.dart';
|
export 'package:moxxmpp/src/xeps/xep_0333.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0334.dart';
|
export 'package:moxxmpp/src/xeps/xep_0334.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0352.dart';
|
export 'package:moxxmpp/src/xeps/xep_0352.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0359.dart';
|
export 'package:moxxmpp/src/xeps/xep_0359.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0363.dart';
|
export 'package:moxxmpp/src/xeps/xep_0363/errors.dart';
|
||||||
|
export 'package:moxxmpp/src/xeps/xep_0363/xep_0363.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0380.dart';
|
export 'package:moxxmpp/src/xeps/xep_0380.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0384/crypto.dart';
|
export 'package:moxxmpp/src/xeps/xep_0384/crypto.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0384/errors.dart';
|
export 'package:moxxmpp/src/xeps/xep_0384/errors.dart';
|
||||||
@@ -70,7 +77,10 @@ export 'package:moxxmpp/src/xeps/xep_0384/types.dart';
|
|||||||
export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart';
|
export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0385.dart';
|
export 'package:moxxmpp/src/xeps/xep_0385.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0414.dart';
|
export 'package:moxxmpp/src/xeps/xep_0414.dart';
|
||||||
|
export 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||||
|
export 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0446.dart';
|
export 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0447.dart';
|
export 'package:moxxmpp/src/xeps/xep_0447.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0448.dart';
|
export 'package:moxxmpp/src/xeps/xep_0448.dart';
|
||||||
|
export 'package:moxxmpp/src/xeps/xep_0449.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0461.dart';
|
export 'package:moxxmpp/src/xeps/xep_0461.dart';
|
||||||
|
|||||||
94
packages/moxxmpp/lib/src/awaiter.dart
Normal file
94
packages/moxxmpp/lib/src/awaiter.dart
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
|
/// A surrogate key for awaiting stanzas.
|
||||||
|
@immutable
|
||||||
|
class _StanzaSurrogateKey {
|
||||||
|
const _StanzaSurrogateKey(this.sentTo, this.id, this.tag);
|
||||||
|
|
||||||
|
/// The JID the original stanza was sent to. We expect the result to come from the
|
||||||
|
/// same JID.
|
||||||
|
final String sentTo;
|
||||||
|
|
||||||
|
/// The ID of the original stanza. We expect the result to have the same ID.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// The tag name of the stanza.
|
||||||
|
final String tag;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => sentTo.hashCode ^ id.hashCode ^ tag.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is _StanzaSurrogateKey &&
|
||||||
|
other.sentTo == sentTo &&
|
||||||
|
other.id == id &&
|
||||||
|
other.tag == tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This class handles the await semantics for stanzas. Stanzas are given a "unique"
|
||||||
|
/// key equal to the tuple (to, id, tag) with which their response is identified.
|
||||||
|
///
|
||||||
|
/// That means that when sending ```<iq to="example@some.server.example" id="abc123" />```,
|
||||||
|
/// the response stanza must be from "example@some.server.example", have id "abc123" and
|
||||||
|
/// be an iq stanza.
|
||||||
|
///
|
||||||
|
/// This class also handles some "edge cases" of RFC 6120, like an empty "from" attribute.
|
||||||
|
class StanzaAwaiter {
|
||||||
|
/// The pending stanzas, identified by their surrogate key.
|
||||||
|
final Map<_StanzaSurrogateKey, Completer<XMLNode>> _pending = {};
|
||||||
|
|
||||||
|
/// The critical section for accessing [StanzaAwaiter._pending].
|
||||||
|
final Lock _lock = Lock();
|
||||||
|
|
||||||
|
/// Register a stanza as pending.
|
||||||
|
/// [to] is the value of the stanza's "to" attribute.
|
||||||
|
/// [id] is the value of the stanza's "id" attribute.
|
||||||
|
/// [tag] is the stanza's tag name.
|
||||||
|
///
|
||||||
|
/// Returns a future that might resolve to the response to the stanza.
|
||||||
|
Future<Future<XMLNode>> addPending(String to, String id, String tag) async {
|
||||||
|
final completer = await _lock.synchronized(() {
|
||||||
|
final completer = Completer<XMLNode>();
|
||||||
|
_pending[_StanzaSurrogateKey(to, id, tag)] = completer;
|
||||||
|
return completer;
|
||||||
|
});
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the stanza [stanza] is being awaited. [bareJid] is the bare JID of
|
||||||
|
/// the connection.
|
||||||
|
/// If [stanza] is awaited, resolves the future and returns true. If not, returns
|
||||||
|
/// false.
|
||||||
|
Future<bool> onData(XMLNode stanza, JID bareJid) async {
|
||||||
|
assert(bareJid.isBare(), 'bareJid must be bare');
|
||||||
|
|
||||||
|
final id = stanza.attributes['id'] as String?;
|
||||||
|
if (id == null) return false;
|
||||||
|
|
||||||
|
final key = _StanzaSurrogateKey(
|
||||||
|
// Section 8.1.2.1 § 3 of RFC 6120 says that an empty "from" indicates that the
|
||||||
|
// attribute is implicitly from our own bare JID.
|
||||||
|
stanza.attributes['from'] as String? ?? bareJid.toString(),
|
||||||
|
id,
|
||||||
|
stanza.tag,
|
||||||
|
);
|
||||||
|
|
||||||
|
return _lock.synchronized(() {
|
||||||
|
final completer = _pending[key];
|
||||||
|
if (completer != null) {
|
||||||
|
_pending.remove(key);
|
||||||
|
completer.complete(stanza);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,22 +6,27 @@ 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
|
||||||
return event.qualifiedName != 'stream:stream';
|
.toXmlEvents()
|
||||||
}).transform(_decoder).listen((nodes) {
|
.selectSubtreeEvents((event) {
|
||||||
for (final node in nodes) {
|
return event.qualifiedName != 'stream:stream';
|
||||||
if (node.nodeType == XmlNodeType.ELEMENT) {
|
})
|
||||||
_streamController.add(XMLNode.fromXmlElement(node as XmlElement));
|
.transform(_decoder)
|
||||||
}
|
.listen((nodes) {
|
||||||
}
|
for (final node in nodes) {
|
||||||
});
|
if (node.nodeType == XmlNodeType.ELEMENT) {
|
||||||
|
_streamController.add(XMLNode.fromXmlElement(node as XmlElement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
return _streamController.stream;
|
return _streamController.stream;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
54
packages/moxxmpp/lib/src/connection_errors.dart
Normal file
54
packages/moxxmpp/lib/src/connection_errors.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:moxxmpp/src/errors.dart';
|
||||||
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
|
|
||||||
|
/// The reason a call to `XmppConnection.connect` failed.
|
||||||
|
abstract class XmppConnectionError extends XmppError {}
|
||||||
|
|
||||||
|
/// Returned by `XmppConnection.connect` when a connection is already active.
|
||||||
|
class ConnectionAlreadyRunningError extends XmppConnectionError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned by `XmppConnection.connect` when a negotiator returned an unrecoverable
|
||||||
|
/// error. Only returned when waitUntilLogin is true.
|
||||||
|
class NegotiatorReturnedError extends XmppConnectionError {
|
||||||
|
NegotiatorReturnedError(this.error);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => error.isRecoverable();
|
||||||
|
|
||||||
|
/// The error returned by the negotiator.
|
||||||
|
final NegotiatorError error;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StreamFailureError extends XmppConnectionError {
|
||||||
|
StreamFailureError(this.error);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => error.isRecoverable();
|
||||||
|
|
||||||
|
/// The error that causes a connection failure.
|
||||||
|
final XmppError error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned by `XmppConnection.connect` when no connection could
|
||||||
|
/// be established.
|
||||||
|
class NoConnectionPossibleError extends XmppConnectionError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned if no matching authentication mechanism has been presented
|
||||||
|
class NoMatchingAuthenticationMechanismAvailableError
|
||||||
|
extends XmppConnectionError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned if no negotiator was picked, even though negotiations are not done
|
||||||
|
/// yet.
|
||||||
|
class NoAuthenticatorAvailableError extends XmppConnectionError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => false;
|
||||||
|
}
|
||||||
18
packages/moxxmpp/lib/src/connectivity.dart
Normal file
18
packages/moxxmpp/lib/src/connectivity.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/// This manager class is responsible to tell the moxxmpp XmppConnection
|
||||||
|
/// when a connection can be established or not, regarding the network availability.
|
||||||
|
abstract class ConnectivityManager {
|
||||||
|
/// Returns true if a network connection is available. If not, returns false.
|
||||||
|
Future<bool> hasConnection();
|
||||||
|
|
||||||
|
/// Returns a future that resolves once we have a network connection.
|
||||||
|
Future<void> waitForConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An implementation of [ConnectivityManager] that is always connected.
|
||||||
|
class AlwaysConnectedConnectivityManager extends ConnectivityManager {
|
||||||
|
@override
|
||||||
|
Future<bool> hasConnection() async => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> waitForConnection() async {}
|
||||||
|
}
|
||||||
37
packages/moxxmpp/lib/src/errors.dart
Normal file
37
packages/moxxmpp/lib/src/errors.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:moxxmpp/src/socket.dart';
|
||||||
|
|
||||||
|
/// An internal error class
|
||||||
|
// ignore: one_member_abstracts
|
||||||
|
abstract class XmppError {
|
||||||
|
/// Return true if we can recover from the error by attempting a reconnection.
|
||||||
|
bool isRecoverable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned if we could not establish a TCP connection
|
||||||
|
/// to the server.
|
||||||
|
class NoConnectionError extends XmppError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned if a socket error occured
|
||||||
|
class SocketError extends XmppError {
|
||||||
|
SocketError(this.event);
|
||||||
|
final XmppSocketErrorEvent event;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned if we time out
|
||||||
|
class TimeoutError extends XmppError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned if we received a stream error
|
||||||
|
class StreamError extends XmppError {
|
||||||
|
// TODO(PapaTutuWawa): Be more precise
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => true;
|
||||||
|
}
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
import 'package:moxxmpp/src/connection.dart';
|
import 'package:moxxmpp/src/connection.dart';
|
||||||
|
import 'package:moxxmpp/src/errors.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
import 'package:moxxmpp/src/managers/data.dart';
|
import 'package:moxxmpp/src/managers/data.dart';
|
||||||
|
import 'package:moxxmpp/src/roster/roster.dart';
|
||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
|
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0066.dart';
|
import 'package:moxxmpp/src/xeps/xep_0066.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0085.dart';
|
import 'package:moxxmpp/src/xeps/xep_0085.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0334.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0385.dart';
|
import 'package:moxxmpp/src/xeps/xep_0385.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0447.dart';
|
import 'package:moxxmpp/src/xeps/xep_0447.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0461.dart';
|
import 'package:moxxmpp/src/xeps/xep_0461.dart';
|
||||||
@@ -25,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +48,29 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Triggered when stream resumption failed
|
/// Triggered when stream resumption failed
|
||||||
class StreamResumeFailedEvent extends XmppEvent {}
|
class StreamResumeFailedEvent extends XmppEvent {}
|
||||||
|
|
||||||
|
/// Triggered when the roster has been modified
|
||||||
|
class RosterUpdatedEvent extends XmppEvent {
|
||||||
|
RosterUpdatedEvent(this.removed, this.modified, this.added);
|
||||||
|
|
||||||
|
/// A list of bare JIDs that are removed from the roster
|
||||||
|
final List<String> removed;
|
||||||
|
|
||||||
|
/// A list of XmppRosterItems that are modified. Can be correlated with one's cache
|
||||||
|
/// using the jid attribute.
|
||||||
|
final List<XmppRosterItem> modified;
|
||||||
|
|
||||||
|
/// A list of XmppRosterItems that are added to the roster.
|
||||||
|
final List<XmppRosterItem> added;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when a message is received
|
||||||
class MessageEvent extends XmppEvent {
|
class MessageEvent extends XmppEvent {
|
||||||
MessageEvent({
|
MessageEvent({
|
||||||
required this.body,
|
required this.body,
|
||||||
@@ -62,6 +83,7 @@ class MessageEvent extends XmppEvent {
|
|||||||
required this.isMarkable,
|
required this.isMarkable,
|
||||||
required this.encrypted,
|
required this.encrypted,
|
||||||
required this.other,
|
required this.other,
|
||||||
|
this.error,
|
||||||
this.type,
|
this.type,
|
||||||
this.oob,
|
this.oob,
|
||||||
this.sfs,
|
this.sfs,
|
||||||
@@ -71,7 +93,13 @@ class MessageEvent extends XmppEvent {
|
|||||||
this.fun,
|
this.fun,
|
||||||
this.funReplacement,
|
this.funReplacement,
|
||||||
this.funCancellation,
|
this.funCancellation,
|
||||||
|
this.messageRetraction,
|
||||||
|
this.messageCorrectionId,
|
||||||
|
this.messageReactions,
|
||||||
|
this.messageProcessingHints,
|
||||||
|
this.stickerPackId,
|
||||||
});
|
});
|
||||||
|
final StanzaError? error;
|
||||||
final String body;
|
final String body;
|
||||||
final JID fromJid;
|
final JID fromJid;
|
||||||
final JID toJid;
|
final JID toJid;
|
||||||
@@ -90,12 +118,17 @@ class MessageEvent extends XmppEvent {
|
|||||||
final String? funReplacement;
|
final String? funReplacement;
|
||||||
final String? funCancellation;
|
final String? funCancellation;
|
||||||
final bool encrypted;
|
final bool encrypted;
|
||||||
|
final MessageRetractionData? messageRetraction;
|
||||||
|
final String? messageCorrectionId;
|
||||||
|
final MessageReactions? messageReactions;
|
||||||
|
final List<MessageProcessingHint>? messageProcessingHints;
|
||||||
|
final String? stickerPackId;
|
||||||
final Map<String, dynamic> other;
|
final Map<String, dynamic> other;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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;
|
||||||
}
|
}
|
||||||
@@ -114,9 +147,9 @@ class ChatMarkerEvent extends XmppEvent {
|
|||||||
// Triggered when we received a Stream resumption ID
|
// Triggered when we received a Stream resumption ID
|
||||||
class StreamManagementEnabledEvent extends XmppEvent {
|
class StreamManagementEnabledEvent extends XmppEvent {
|
||||||
StreamManagementEnabledEvent({
|
StreamManagementEnabledEvent({
|
||||||
required this.resource,
|
required this.resource,
|
||||||
this.id,
|
this.id,
|
||||||
this.location,
|
this.location,
|
||||||
});
|
});
|
||||||
final String resource;
|
final String resource;
|
||||||
final String? id;
|
final String? id;
|
||||||
@@ -125,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,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;
|
||||||
@@ -163,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;
|
||||||
}
|
}
|
||||||
@@ -176,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,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 {}
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
import 'package:moxxmpp/src/connection.dart';
|
import 'package:moxxmpp/src/connection.dart';
|
||||||
|
import 'package:moxxmpp/src/managers/data.dart';
|
||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
|
|
||||||
bool handleUnhandledStanza(XmppConnection conn, Stanza stanza) {
|
/// Bounce a stanza if it was not handled by any manager. [conn] is the connection object
|
||||||
if (stanza.type != 'error' && stanza.type != 'result') {
|
/// to use for sending the stanza. [data] is the StanzaHandlerData of the unhandled
|
||||||
conn.sendStanza(stanza.errorReply('cancel', 'feature-not-implemented'));
|
/// stanza.
|
||||||
}
|
Future<void> handleUnhandledStanza(
|
||||||
|
XmppConnection conn,
|
||||||
|
StanzaHandlerData data,
|
||||||
|
) async {
|
||||||
|
if (data.stanza.type != 'error' && data.stanza.type != 'result') {
|
||||||
|
final stanza = data.stanza.copyWith(
|
||||||
|
to: data.stanza.from,
|
||||||
|
from: data.stanza.to,
|
||||||
|
type: 'error',
|
||||||
|
children: [
|
||||||
|
buildErrorElement(
|
||||||
|
'cancel',
|
||||||
|
'feature-not-implemented',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
await conn.sendStanza(
|
||||||
|
stanza,
|
||||||
|
awaitable: false,
|
||||||
|
forceEncryption: data.encrypted,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,79 @@
|
|||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
/// Represents a Jabber ID in parsed form.
|
||||||
@immutable
|
@immutable
|
||||||
class JID {
|
class JID {
|
||||||
|
|
||||||
const JID(this.local, this.domain, this.resource);
|
const JID(this.local, this.domain, this.resource);
|
||||||
|
|
||||||
|
/// Parses the string [jid] into a JID instance.
|
||||||
factory JID.fromString(String jid) {
|
factory JID.fromString(String jid) {
|
||||||
// 0: Parsing either the local or domain part
|
// Algorithm taken from here: https://blog.samwhited.com/2021/02/xmpp-addresses/
|
||||||
// 1: Parsing the domain part
|
var localPart = '';
|
||||||
// 2: Parsing the resource
|
var domainPart = '';
|
||||||
var state = 0;
|
var resourcePart = '';
|
||||||
var buffer = '';
|
|
||||||
var local_ = '';
|
|
||||||
var domain_ = '';
|
|
||||||
var resource_ = '';
|
|
||||||
|
|
||||||
for (var i = 0; i < jid.length; i++) {
|
|
||||||
final c = jid[i];
|
|
||||||
final eol = i == jid.length - 1;
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case 0: {
|
|
||||||
if (c == '@') {
|
|
||||||
local_ = buffer;
|
|
||||||
buffer = '';
|
|
||||||
state = 1;
|
|
||||||
} else if (c == '/') {
|
|
||||||
domain_ = buffer;
|
|
||||||
buffer = '';
|
|
||||||
state = 2;
|
|
||||||
} else if (eol) {
|
|
||||||
domain_ = buffer + c;
|
|
||||||
} else {
|
|
||||||
buffer += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 1: {
|
|
||||||
if (c == '/') {
|
|
||||||
domain_ = buffer;
|
|
||||||
buffer = '';
|
|
||||||
state = 2;
|
|
||||||
} else if (eol) {
|
|
||||||
domain_ = buffer;
|
|
||||||
|
|
||||||
if (c != ' ') {
|
final slashParts = jid.split('/');
|
||||||
domain_ = domain_ + c;
|
if (slashParts.length == 1) {
|
||||||
}
|
resourcePart = '';
|
||||||
} else if (c != ' ') {
|
} else {
|
||||||
buffer += c;
|
resourcePart = slashParts.sublist(1).join('/');
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 2: {
|
|
||||||
if (eol) {
|
|
||||||
resource_ = buffer;
|
|
||||||
|
|
||||||
if (c != ' ') {
|
assert(
|
||||||
resource_ = resource_ + c;
|
resourcePart.isNotEmpty,
|
||||||
}
|
'Resource part cannot be there and empty',
|
||||||
} else if (c != ''){
|
);
|
||||||
buffer += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return JID(local_, domain_, resource_);
|
final atParts = slashParts.first.split('@');
|
||||||
|
if (atParts.length == 1) {
|
||||||
|
localPart = '';
|
||||||
|
domainPart = atParts.first;
|
||||||
|
} else {
|
||||||
|
localPart = atParts.first;
|
||||||
|
domainPart = atParts.sublist(1).join('@');
|
||||||
|
|
||||||
|
assert(localPart.isNotEmpty, 'Local part cannot be there and empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
return JID(
|
||||||
|
localPart,
|
||||||
|
domainPart.endsWith('.')
|
||||||
|
? domainPart.substring(0, domainPart.length - 1)
|
||||||
|
: domainPart,
|
||||||
|
resourcePart,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final String local;
|
final String local;
|
||||||
final String domain;
|
final String domain;
|
||||||
final String resource;
|
final String resource;
|
||||||
|
|
||||||
|
/// Returns true if the JID is bare.
|
||||||
bool isBare() => resource.isEmpty;
|
bool isBare() => resource.isEmpty;
|
||||||
|
|
||||||
|
/// Returns true if the JID is full.
|
||||||
bool isFull() => resource.isNotEmpty;
|
bool isFull() => resource.isNotEmpty;
|
||||||
|
|
||||||
JID toBare() => JID(local, domain, '');
|
/// Converts the JID into a bare JID.
|
||||||
|
JID toBare() {
|
||||||
|
if (isBare()) return this;
|
||||||
|
|
||||||
|
return JID(local, domain, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the JID into one with a resource part of [resource].
|
||||||
JID withResource(String resource) => JID(local, domain, resource);
|
JID withResource(String resource) => JID(local, domain, resource);
|
||||||
|
|
||||||
|
/// Compares the JID with [other]. This function assumes that JID and [other]
|
||||||
|
/// are bare, i.e. only the domain- and localparts are compared. If [ensureBare]
|
||||||
|
/// is optionally set to true, then [other] MUST be bare. Otherwise, false is returned.
|
||||||
|
bool bareCompare(JID other, {bool ensureBare = false}) {
|
||||||
|
if (ensureBare && !other.isBare()) return false;
|
||||||
|
|
||||||
|
return local == other.local && domain == other.domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts to JID instance into its string representation of
|
||||||
|
/// localpart@domainpart/resource.
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
var result = '';
|
var result = '';
|
||||||
@@ -97,7 +93,9 @@ class JID {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is JID) {
|
if (other is JID) {
|
||||||
return other.local == local && other.domain == domain && other.resource == resource;
|
return other.local == local &&
|
||||||
|
other.domain == domain &&
|
||||||
|
other.resource == resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:moxxmpp/src/connection.dart';
|
import 'package:moxxmpp/src/connection.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
@@ -11,7 +10,6 @@ import 'package:moxxmpp/src/stanza.dart';
|
|||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
|
||||||
class XmppManagerAttributes {
|
class XmppManagerAttributes {
|
||||||
|
|
||||||
XmppManagerAttributes({
|
XmppManagerAttributes({
|
||||||
required this.sendStanza,
|
required this.sendStanza,
|
||||||
required this.sendNonza,
|
required this.sendNonza,
|
||||||
@@ -24,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}) 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;
|
||||||
@@ -41,7 +47,7 @@ class XmppManagerAttributes {
|
|||||||
|
|
||||||
/// Returns true if a server feature is supported
|
/// Returns true if a server feature is supported
|
||||||
final bool Function(String) isFeatureSupported;
|
final bool Function(String) isFeatureSupported;
|
||||||
|
|
||||||
/// Returns the full JID of the current account
|
/// Returns the full JID of the current account
|
||||||
final JID Function() getFullJID;
|
final JID Function() getFullJID;
|
||||||
|
|
||||||
@@ -51,5 +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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/managers/attributes.dart';
|
import 'package:moxxmpp/src/managers/attributes.dart';
|
||||||
|
import 'package:moxxmpp/src/managers/data.dart';
|
||||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||||
|
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||||
|
|
||||||
abstract class XmppManagerBase {
|
abstract class XmppManagerBase {
|
||||||
|
XmppManagerBase(this.id);
|
||||||
|
|
||||||
late final XmppManagerAttributes _managerAttributes;
|
late final XmppManagerAttributes _managerAttributes;
|
||||||
late final Logger _log;
|
late final Logger _log;
|
||||||
|
|
||||||
|
/// Flag indicating that the post registration callback has been called once.
|
||||||
|
bool initialized = false;
|
||||||
|
|
||||||
/// Registers the callbacks from XmppConnection with the manager
|
/// Registers the callbacks from XmppConnection with the manager
|
||||||
void register(XmppManagerAttributes attributes) {
|
void register(XmppManagerAttributes attributes) {
|
||||||
_managerAttributes = attributes;
|
_managerAttributes = attributes;
|
||||||
_log = Logger(getName());
|
_log = Logger(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the attributes that are registered with the manager.
|
/// Returns the attributes that are registered with the manager.
|
||||||
/// Must only be called after register has been called on it.
|
/// Must only be called after register has been called on it.
|
||||||
XmppManagerAttributes getAttributes() {
|
XmppManagerAttributes getAttributes() {
|
||||||
@@ -21,52 +32,118 @@ abstract class XmppManagerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
|
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
|
||||||
/// send. These are run before the stanza is sent.
|
/// send. These are run before the stanza is sent. The higher the value of the
|
||||||
|
/// handler's priority, the earlier it is run.
|
||||||
List<StanzaHandler> getOutgoingPreStanzaHandlers() => [];
|
List<StanzaHandler> getOutgoingPreStanzaHandlers() => [];
|
||||||
|
|
||||||
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
|
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
|
||||||
/// send. These are run after the stanza is sent.
|
/// send. These are run after the stanza is sent. The higher the value of the
|
||||||
|
/// handler's priority, the earlier it is run.
|
||||||
List<StanzaHandler> getOutgoingPostStanzaHandlers() => [];
|
List<StanzaHandler> getOutgoingPostStanzaHandlers() => [];
|
||||||
|
|
||||||
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
|
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
|
||||||
/// receive.
|
/// receive. The higher the value of the
|
||||||
|
/// handler's priority, the earlier it is run.
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [];
|
List<StanzaHandler> getIncomingStanzaHandlers() => [];
|
||||||
|
|
||||||
/// Return the NonzaHandlers associated with this manager.
|
/// Return the StanzaHandlers associated with this manager that deal with stanza handlers
|
||||||
|
/// that have to run before the main ones run. This is useful, for example, for OMEMO
|
||||||
|
/// as we have to decrypt the stanza before we do anything else. The higher the value
|
||||||
|
/// of the handler's priority, the earlier it is run.
|
||||||
|
List<StanzaHandler> getIncomingPreStanzaHandlers() => [];
|
||||||
|
|
||||||
|
/// Return the NonzaHandlers associated with this manager. The higher the value of the
|
||||||
|
/// handler's priority, the earlier it is run.
|
||||||
List<NonzaHandler> getNonzaHandlers() => [];
|
List<NonzaHandler> getNonzaHandlers() => [];
|
||||||
|
|
||||||
/// Return a list of features that should be included in a disco response.
|
/// Return a list of features that should be included in a disco response.
|
||||||
List<String> getDiscoFeatures() => [];
|
List<String> getDiscoFeatures() => [];
|
||||||
|
|
||||||
/// Return the Id (akin to xmlns) of this manager.
|
|
||||||
String getId();
|
|
||||||
|
|
||||||
/// Return a name that will be used for logging.
|
/// Return a list of identities that should be included in a disco response.
|
||||||
String getName();
|
List<Identity> getDiscoIdentities() => [];
|
||||||
|
|
||||||
|
/// Return the Id (akin to xmlns) of this manager.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// The name of the manager.
|
||||||
|
String get name => toString();
|
||||||
|
|
||||||
/// Return the logger for this manager.
|
/// Return the logger for this manager.
|
||||||
Logger get logger => _log;
|
Logger get logger => _log;
|
||||||
|
|
||||||
/// Called when XmppConnection triggers an event
|
/// Called when XmppConnection triggers an event
|
||||||
Future<void> onXmppEvent(XmppEvent event) async {}
|
Future<void> onXmppEvent(XmppEvent event) async {}
|
||||||
|
|
||||||
/// Returns true if the XEP is supported on the server. If not, returns false
|
/// Returns true if the XEP is supported on the server. If not, returns false
|
||||||
Future<bool> isSupported();
|
Future<bool> isSupported();
|
||||||
|
|
||||||
|
/// Called after the registration of all managers against the XmppConnection is done.
|
||||||
|
/// This method is only called once during the entire lifetime of it.
|
||||||
|
@mustCallSuper
|
||||||
|
Future<void> postRegisterCallback() async {
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
final disco = getAttributes().getManagerById<DiscoManager>(discoManager);
|
||||||
|
if (disco != null) {
|
||||||
|
if (getDiscoFeatures().isNotEmpty) {
|
||||||
|
disco.addFeatures(getDiscoFeatures());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getDiscoIdentities().isNotEmpty) {
|
||||||
|
disco.addIdentities(getDiscoIdentities());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs all NonzaHandlers of this Manager which match the nonza. Resolves to true if
|
/// Runs all NonzaHandlers of this Manager which match the nonza. Resolves to true if
|
||||||
/// the nonza has been handled by one of the handlers. Resolves to false otherwise.
|
/// the nonza has been handled by one of the handlers. Resolves to false otherwise.
|
||||||
Future<bool> runNonzaHandlers(XMLNode nonza) async {
|
Future<bool> runNonzaHandlers(XMLNode nonza) async {
|
||||||
var handled = false;
|
var handled = false;
|
||||||
await Future.forEach(
|
await Future.forEach(getNonzaHandlers(), (NonzaHandler handler) async {
|
||||||
getNonzaHandlers(),
|
if (handler.matches(nonza)) {
|
||||||
(NonzaHandler handler) async {
|
handled = true;
|
||||||
if (handler.matches(nonza)) {
|
await handler.callback(nonza);
|
||||||
handled = true;
|
|
||||||
await handler.callback(nonza);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true, if the current stream negotiations resulted in a new stream. Useful
|
||||||
|
/// for plugins to reset their cache in case of a new stream.
|
||||||
|
/// The value only makes sense after receiving a StreamNegotiationsDoneEvent.
|
||||||
|
Future<bool> isNewStream() async {
|
||||||
|
final sm =
|
||||||
|
getAttributes().getManagerById<StreamManagementManager>(smManager);
|
||||||
|
|
||||||
|
return sm?.streamResumed == false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a reply of the stanza in [data] with [type]. Replaces the original stanza's
|
||||||
|
/// children with [children].
|
||||||
|
///
|
||||||
|
/// Note that this function currently only accepts IQ stanzas.
|
||||||
|
Future<void> reply(
|
||||||
|
StanzaHandlerData data,
|
||||||
|
String type,
|
||||||
|
List<XMLNode> children,
|
||||||
|
) async {
|
||||||
|
assert(
|
||||||
|
data.stanza.tag == 'iq',
|
||||||
|
'Reply makes little sense for non-IQ stanzas',
|
||||||
|
);
|
||||||
|
|
||||||
|
final stanza = data.stanza.copyWith(
|
||||||
|
to: data.stanza.from,
|
||||||
|
from: data.stanza.to,
|
||||||
|
type: type,
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
await getAttributes().sendStanza(
|
||||||
|
stanza,
|
||||||
|
awaitable: false,
|
||||||
|
forceEncryption: data.encrypted,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:moxxmpp/src/xeps/xep_0203.dart';
|
|||||||
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0380.dart';
|
import 'package:moxxmpp/src/xeps/xep_0380.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0385.dart';
|
import 'package:moxxmpp/src/xeps/xep_0385.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0447.dart';
|
import 'package:moxxmpp/src/xeps/xep_0447.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0461.dart';
|
import 'package:moxxmpp/src/xeps/xep_0461.dart';
|
||||||
@@ -25,36 +27,48 @@ 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,
|
StatelessMediaSharingData? sims,
|
||||||
StatelessMediaSharingData? sims,
|
StatelessFileSharingData? sfs,
|
||||||
StatelessFileSharingData? sfs,
|
OOBData? oob,
|
||||||
OOBData? oob,
|
StableStanzaId? stableId,
|
||||||
StableStanzaId? stableId,
|
ReplyData? reply,
|
||||||
ReplyData? reply,
|
ChatState? chatState,
|
||||||
ChatState? chatState,
|
@Default(false) bool isCarbon,
|
||||||
@Default(false) bool isCarbon,
|
@Default(false) bool deliveryReceiptRequested,
|
||||||
@Default(false) bool deliveryReceiptRequested,
|
@Default(false) bool isMarkable,
|
||||||
@Default(false) bool isMarkable,
|
// File Upload Notifications
|
||||||
// File Upload Notifications
|
// A notification
|
||||||
// A notification
|
FileMetadataData? fun,
|
||||||
FileMetadataData? fun,
|
// The stanza id this replaces
|
||||||
// The stanza id this replaces
|
String? funReplacement,
|
||||||
String? funReplacement,
|
// The stanza id this cancels
|
||||||
// The stanza id this cancels
|
String? funCancellation,
|
||||||
String? funCancellation,
|
// Whether the stanza was received encrypted
|
||||||
// Whether the stanza was received encrypted
|
@Default(false) bool encrypted,
|
||||||
@Default(false) bool encrypted,
|
// If true, forces the encryption manager to encrypt to the JID, even if it
|
||||||
// The stated type of encryption used, if any was used
|
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
|
||||||
ExplicitEncryptionType? encryptionType,
|
// but forceEncryption is true, then the OMEMO manager will try to encrypt
|
||||||
// Delayed Delivery
|
// to the JID anyway.
|
||||||
DelayedDelivery? delayedDelivery,
|
@Default(false) bool forceEncryption,
|
||||||
// This is for stanza handlers that are not part of the XMPP library but still need
|
// The stated type of encryption used, if any was used
|
||||||
// pass data around.
|
ExplicitEncryptionType? encryptionType,
|
||||||
@Default(<String, dynamic>{}) Map<String, dynamic> other,
|
// Delayed Delivery
|
||||||
}
|
DelayedDelivery? delayedDelivery,
|
||||||
) = _StanzaHandlerData;
|
// This is for stanza handlers that are not part of the XMPP library but still need
|
||||||
|
// pass data around.
|
||||||
|
@Default(<String, dynamic>{}) Map<String, dynamic> other,
|
||||||
|
// If non-null, then it indicates the origin Id of the message that should be
|
||||||
|
// retracted
|
||||||
|
MessageRetractionData? messageRetraction,
|
||||||
|
// If non-null, then the message is a correction for the specified stanza Id
|
||||||
|
String? lastMessageCorrectionSid,
|
||||||
|
// Reactions data
|
||||||
|
MessageReactions? messageReactions,
|
||||||
|
// The Id of the sticker pack this sticker belongs to
|
||||||
|
String? stickerPackId,
|
||||||
|
}) = _StanzaHandlerData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,13 +48,27 @@ mixin _$StanzaHandlerData {
|
|||||||
String? get funCancellation =>
|
String? get funCancellation =>
|
||||||
throw _privateConstructorUsedError; // Whether the stanza was received encrypted
|
throw _privateConstructorUsedError; // Whether the stanza was received encrypted
|
||||||
bool get encrypted =>
|
bool get encrypted =>
|
||||||
|
throw _privateConstructorUsedError; // If true, forces the encryption manager to encrypt to the JID, even if it
|
||||||
|
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
|
||||||
|
// but forceEncryption is true, then the OMEMO manager will try to encrypt
|
||||||
|
// to the JID anyway.
|
||||||
|
bool get forceEncryption =>
|
||||||
throw _privateConstructorUsedError; // The stated type of encryption used, if any was used
|
throw _privateConstructorUsedError; // The stated type of encryption used, if any was used
|
||||||
ExplicitEncryptionType? get encryptionType =>
|
ExplicitEncryptionType? get encryptionType =>
|
||||||
throw _privateConstructorUsedError; // Delayed Delivery
|
throw _privateConstructorUsedError; // Delayed Delivery
|
||||||
DelayedDelivery? get delayedDelivery =>
|
DelayedDelivery? get delayedDelivery =>
|
||||||
throw _privateConstructorUsedError; // This is for stanza handlers that are not part of the XMPP library but still need
|
throw _privateConstructorUsedError; // This is for stanza handlers that are not part of the XMPP library but still need
|
||||||
// pass data around.
|
// pass data around.
|
||||||
Map<String, dynamic> get other => throw _privateConstructorUsedError;
|
Map<String, dynamic> get other =>
|
||||||
|
throw _privateConstructorUsedError; // If non-null, then it indicates the origin Id of the message that should be
|
||||||
|
// retracted
|
||||||
|
MessageRetractionData? get messageRetraction =>
|
||||||
|
throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id
|
||||||
|
String? get lastMessageCorrectionSid =>
|
||||||
|
throw _privateConstructorUsedError; // Reactions data
|
||||||
|
MessageReactions? get messageReactions =>
|
||||||
|
throw _privateConstructorUsedError; // The Id of the sticker pack this sticker belongs to
|
||||||
|
String? get stickerPackId => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
|
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
|
||||||
@@ -85,9 +99,14 @@ abstract class $StanzaHandlerDataCopyWith<$Res> {
|
|||||||
String? funReplacement,
|
String? funReplacement,
|
||||||
String? funCancellation,
|
String? funCancellation,
|
||||||
bool encrypted,
|
bool encrypted,
|
||||||
|
bool forceEncryption,
|
||||||
ExplicitEncryptionType? encryptionType,
|
ExplicitEncryptionType? encryptionType,
|
||||||
DelayedDelivery? delayedDelivery,
|
DelayedDelivery? delayedDelivery,
|
||||||
Map<String, dynamic> other});
|
Map<String, dynamic> other,
|
||||||
|
MessageRetractionData? messageRetraction,
|
||||||
|
String? lastMessageCorrectionSid,
|
||||||
|
MessageReactions? messageReactions,
|
||||||
|
String? stickerPackId});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -119,9 +138,14 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
|
|||||||
Object? funReplacement = freezed,
|
Object? funReplacement = freezed,
|
||||||
Object? funCancellation = freezed,
|
Object? funCancellation = freezed,
|
||||||
Object? encrypted = freezed,
|
Object? encrypted = freezed,
|
||||||
|
Object? forceEncryption = freezed,
|
||||||
Object? encryptionType = freezed,
|
Object? encryptionType = freezed,
|
||||||
Object? delayedDelivery = freezed,
|
Object? delayedDelivery = freezed,
|
||||||
Object? other = freezed,
|
Object? other = freezed,
|
||||||
|
Object? messageRetraction = freezed,
|
||||||
|
Object? lastMessageCorrectionSid = freezed,
|
||||||
|
Object? messageReactions = freezed,
|
||||||
|
Object? stickerPackId = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
done: done == freezed
|
done: done == freezed
|
||||||
@@ -196,6 +220,10 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
|
|||||||
? _value.encrypted
|
? _value.encrypted
|
||||||
: encrypted // ignore: cast_nullable_to_non_nullable
|
: encrypted // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
|
forceEncryption: forceEncryption == freezed
|
||||||
|
? _value.forceEncryption
|
||||||
|
: forceEncryption // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
encryptionType: encryptionType == freezed
|
encryptionType: encryptionType == freezed
|
||||||
? _value.encryptionType
|
? _value.encryptionType
|
||||||
: encryptionType // ignore: cast_nullable_to_non_nullable
|
: encryptionType // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -208,6 +236,22 @@ class _$StanzaHandlerDataCopyWithImpl<$Res>
|
|||||||
? _value.other
|
? _value.other
|
||||||
: other // ignore: cast_nullable_to_non_nullable
|
: other // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,
|
as Map<String, dynamic>,
|
||||||
|
messageRetraction: messageRetraction == freezed
|
||||||
|
? _value.messageRetraction
|
||||||
|
: messageRetraction // ignore: cast_nullable_to_non_nullable
|
||||||
|
as MessageRetractionData?,
|
||||||
|
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
|
||||||
|
? _value.lastMessageCorrectionSid
|
||||||
|
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
messageReactions: messageReactions == freezed
|
||||||
|
? _value.messageReactions
|
||||||
|
: messageReactions // ignore: cast_nullable_to_non_nullable
|
||||||
|
as MessageReactions?,
|
||||||
|
stickerPackId: stickerPackId == freezed
|
||||||
|
? _value.stickerPackId
|
||||||
|
: stickerPackId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,9 +282,14 @@ abstract class _$$_StanzaHandlerDataCopyWith<$Res>
|
|||||||
String? funReplacement,
|
String? funReplacement,
|
||||||
String? funCancellation,
|
String? funCancellation,
|
||||||
bool encrypted,
|
bool encrypted,
|
||||||
|
bool forceEncryption,
|
||||||
ExplicitEncryptionType? encryptionType,
|
ExplicitEncryptionType? encryptionType,
|
||||||
DelayedDelivery? delayedDelivery,
|
DelayedDelivery? delayedDelivery,
|
||||||
Map<String, dynamic> other});
|
Map<String, dynamic> other,
|
||||||
|
MessageRetractionData? messageRetraction,
|
||||||
|
String? lastMessageCorrectionSid,
|
||||||
|
MessageReactions? messageReactions,
|
||||||
|
String? stickerPackId});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -274,9 +323,14 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
|
|||||||
Object? funReplacement = freezed,
|
Object? funReplacement = freezed,
|
||||||
Object? funCancellation = freezed,
|
Object? funCancellation = freezed,
|
||||||
Object? encrypted = freezed,
|
Object? encrypted = freezed,
|
||||||
|
Object? forceEncryption = freezed,
|
||||||
Object? encryptionType = freezed,
|
Object? encryptionType = freezed,
|
||||||
Object? delayedDelivery = freezed,
|
Object? delayedDelivery = freezed,
|
||||||
Object? other = freezed,
|
Object? other = freezed,
|
||||||
|
Object? messageRetraction = freezed,
|
||||||
|
Object? lastMessageCorrectionSid = freezed,
|
||||||
|
Object? messageReactions = freezed,
|
||||||
|
Object? stickerPackId = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$_StanzaHandlerData(
|
return _then(_$_StanzaHandlerData(
|
||||||
done == freezed
|
done == freezed
|
||||||
@@ -351,6 +405,10 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
|
|||||||
? _value.encrypted
|
? _value.encrypted
|
||||||
: encrypted // ignore: cast_nullable_to_non_nullable
|
: encrypted // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
|
forceEncryption: forceEncryption == freezed
|
||||||
|
? _value.forceEncryption
|
||||||
|
: forceEncryption // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
encryptionType: encryptionType == freezed
|
encryptionType: encryptionType == freezed
|
||||||
? _value.encryptionType
|
? _value.encryptionType
|
||||||
: encryptionType // ignore: cast_nullable_to_non_nullable
|
: encryptionType // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -363,6 +421,22 @@ class __$$_StanzaHandlerDataCopyWithImpl<$Res>
|
|||||||
? _value._other
|
? _value._other
|
||||||
: other // ignore: cast_nullable_to_non_nullable
|
: other // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,
|
as Map<String, dynamic>,
|
||||||
|
messageRetraction: messageRetraction == freezed
|
||||||
|
? _value.messageRetraction
|
||||||
|
: messageRetraction // ignore: cast_nullable_to_non_nullable
|
||||||
|
as MessageRetractionData?,
|
||||||
|
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
|
||||||
|
? _value.lastMessageCorrectionSid
|
||||||
|
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
messageReactions: messageReactions == freezed
|
||||||
|
? _value.messageReactions
|
||||||
|
: messageReactions // ignore: cast_nullable_to_non_nullable
|
||||||
|
as MessageReactions?,
|
||||||
|
stickerPackId: stickerPackId == freezed
|
||||||
|
? _value.stickerPackId
|
||||||
|
: stickerPackId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,9 +459,14 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
|
|||||||
this.funReplacement,
|
this.funReplacement,
|
||||||
this.funCancellation,
|
this.funCancellation,
|
||||||
this.encrypted = false,
|
this.encrypted = false,
|
||||||
|
this.forceEncryption = false,
|
||||||
this.encryptionType,
|
this.encryptionType,
|
||||||
this.delayedDelivery,
|
this.delayedDelivery,
|
||||||
final Map<String, dynamic> other = const <String, dynamic>{}})
|
final Map<String, dynamic> other = const <String, dynamic>{},
|
||||||
|
this.messageRetraction,
|
||||||
|
this.lastMessageCorrectionSid,
|
||||||
|
this.messageReactions,
|
||||||
|
this.stickerPackId})
|
||||||
: _other = other;
|
: _other = other;
|
||||||
|
|
||||||
// Indicates to the runner that processing is now done. This means that all
|
// Indicates to the runner that processing is now done. This means that all
|
||||||
@@ -445,6 +524,13 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
|
|||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final bool encrypted;
|
final bool encrypted;
|
||||||
|
// If true, forces the encryption manager to encrypt to the JID, even if it
|
||||||
|
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
|
||||||
|
// but forceEncryption is true, then the OMEMO manager will try to encrypt
|
||||||
|
// to the JID anyway.
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool forceEncryption;
|
||||||
// The stated type of encryption used, if any was used
|
// The stated type of encryption used, if any was used
|
||||||
@override
|
@override
|
||||||
final ExplicitEncryptionType? encryptionType;
|
final ExplicitEncryptionType? encryptionType;
|
||||||
@@ -463,9 +549,23 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
|
|||||||
return EqualUnmodifiableMapView(_other);
|
return EqualUnmodifiableMapView(_other);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If non-null, then it indicates the origin Id of the message that should be
|
||||||
|
// retracted
|
||||||
|
@override
|
||||||
|
final MessageRetractionData? messageRetraction;
|
||||||
|
// If non-null, then the message is a correction for the specified stanza Id
|
||||||
|
@override
|
||||||
|
final String? lastMessageCorrectionSid;
|
||||||
|
// Reactions data
|
||||||
|
@override
|
||||||
|
final MessageReactions? messageReactions;
|
||||||
|
// The Id of the sticker pack this sticker belongs to
|
||||||
|
@override
|
||||||
|
final String? stickerPackId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other)';
|
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, forceEncryption: $forceEncryption, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -497,11 +597,21 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
|
|||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.funCancellation, funCancellation) &&
|
.equals(other.funCancellation, funCancellation) &&
|
||||||
const DeepCollectionEquality().equals(other.encrypted, encrypted) &&
|
const DeepCollectionEquality().equals(other.encrypted, encrypted) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.forceEncryption, forceEncryption) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.encryptionType, encryptionType) &&
|
.equals(other.encryptionType, encryptionType) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.delayedDelivery, delayedDelivery) &&
|
.equals(other.delayedDelivery, delayedDelivery) &&
|
||||||
const DeepCollectionEquality().equals(other._other, this._other));
|
const DeepCollectionEquality().equals(other._other, this._other) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.messageRetraction, messageRetraction) &&
|
||||||
|
const DeepCollectionEquality().equals(
|
||||||
|
other.lastMessageCorrectionSid, lastMessageCorrectionSid) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.messageReactions, messageReactions) &&
|
||||||
|
const DeepCollectionEquality()
|
||||||
|
.equals(other.stickerPackId, stickerPackId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -525,9 +635,14 @@ class _$_StanzaHandlerData implements _StanzaHandlerData {
|
|||||||
const DeepCollectionEquality().hash(funReplacement),
|
const DeepCollectionEquality().hash(funReplacement),
|
||||||
const DeepCollectionEquality().hash(funCancellation),
|
const DeepCollectionEquality().hash(funCancellation),
|
||||||
const DeepCollectionEquality().hash(encrypted),
|
const DeepCollectionEquality().hash(encrypted),
|
||||||
|
const DeepCollectionEquality().hash(forceEncryption),
|
||||||
const DeepCollectionEquality().hash(encryptionType),
|
const DeepCollectionEquality().hash(encryptionType),
|
||||||
const DeepCollectionEquality().hash(delayedDelivery),
|
const DeepCollectionEquality().hash(delayedDelivery),
|
||||||
const DeepCollectionEquality().hash(_other)
|
const DeepCollectionEquality().hash(_other),
|
||||||
|
const DeepCollectionEquality().hash(messageRetraction),
|
||||||
|
const DeepCollectionEquality().hash(lastMessageCorrectionSid),
|
||||||
|
const DeepCollectionEquality().hash(messageReactions),
|
||||||
|
const DeepCollectionEquality().hash(stickerPackId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@@ -554,9 +669,14 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
|
|||||||
final String? funReplacement,
|
final String? funReplacement,
|
||||||
final String? funCancellation,
|
final String? funCancellation,
|
||||||
final bool encrypted,
|
final bool encrypted,
|
||||||
|
final bool forceEncryption,
|
||||||
final ExplicitEncryptionType? encryptionType,
|
final ExplicitEncryptionType? encryptionType,
|
||||||
final DelayedDelivery? delayedDelivery,
|
final DelayedDelivery? delayedDelivery,
|
||||||
final Map<String, dynamic> other}) = _$_StanzaHandlerData;
|
final Map<String, dynamic> other,
|
||||||
|
final MessageRetractionData? messageRetraction,
|
||||||
|
final String? lastMessageCorrectionSid,
|
||||||
|
final MessageReactions? messageReactions,
|
||||||
|
final String? stickerPackId}) = _$_StanzaHandlerData;
|
||||||
|
|
||||||
@override // Indicates to the runner that processing is now done. This means that all
|
@override // Indicates to the runner that processing is now done. This means that all
|
||||||
// pre-processing is done and no other handlers should be consulted.
|
// pre-processing is done and no other handlers should be consulted.
|
||||||
@@ -599,6 +719,11 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
|
|||||||
String? get funCancellation;
|
String? get funCancellation;
|
||||||
@override // Whether the stanza was received encrypted
|
@override // Whether the stanza was received encrypted
|
||||||
bool get encrypted;
|
bool get encrypted;
|
||||||
|
@override // If true, forces the encryption manager to encrypt to the JID, even if it
|
||||||
|
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
|
||||||
|
// but forceEncryption is true, then the OMEMO manager will try to encrypt
|
||||||
|
// to the JID anyway.
|
||||||
|
bool get forceEncryption;
|
||||||
@override // The stated type of encryption used, if any was used
|
@override // The stated type of encryption used, if any was used
|
||||||
ExplicitEncryptionType? get encryptionType;
|
ExplicitEncryptionType? get encryptionType;
|
||||||
@override // Delayed Delivery
|
@override // Delayed Delivery
|
||||||
@@ -606,6 +731,15 @@ abstract class _StanzaHandlerData implements StanzaHandlerData {
|
|||||||
@override // This is for stanza handlers that are not part of the XMPP library but still need
|
@override // This is for stanza handlers that are not part of the XMPP library but still need
|
||||||
// pass data around.
|
// pass data around.
|
||||||
Map<String, dynamic> get other;
|
Map<String, dynamic> get other;
|
||||||
|
@override // If non-null, then it indicates the origin Id of the message that should be
|
||||||
|
// retracted
|
||||||
|
MessageRetractionData? get messageRetraction;
|
||||||
|
@override // If non-null, then the message is a correction for the specified stanza Id
|
||||||
|
String? get lastMessageCorrectionSid;
|
||||||
|
@override // Reactions data
|
||||||
|
MessageReactions? get messageReactions;
|
||||||
|
@override // The Id of the sticker pack this sticker belongs to
|
||||||
|
String? get stickerPackId;
|
||||||
@override
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>
|
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>
|
||||||
|
|||||||
@@ -5,8 +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;
|
||||||
@@ -20,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;
|
||||||
@@ -32,45 +32,43 @@ abstract class Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NonzaHandler extends Handler {
|
class NonzaHandler extends Handler {
|
||||||
|
|
||||||
NonzaHandler({
|
NonzaHandler({
|
||||||
required this.callback,
|
required this.callback,
|
||||||
String? nonzaTag,
|
String? nonzaTag,
|
||||||
String? nonzaXmlns,
|
String? nonzaXmlns,
|
||||||
}) : super(
|
}) : super(
|
||||||
false,
|
false,
|
||||||
nonzaTag: nonzaTag,
|
nonzaTag: nonzaTag,
|
||||||
nonzaXmlns: nonzaXmlns,
|
nonzaXmlns: nonzaXmlns,
|
||||||
);
|
);
|
||||||
final Future<bool> Function(XMLNode) callback;
|
final Future<bool> Function(XMLNode) callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
class StanzaHandler extends Handler {
|
class StanzaHandler extends Handler {
|
||||||
|
|
||||||
StanzaHandler({
|
StanzaHandler({
|
||||||
required this.callback,
|
required this.callback,
|
||||||
this.tagXmlns,
|
this.tagXmlns,
|
||||||
this.tagName,
|
this.tagName,
|
||||||
this.priority = 0,
|
this.priority = 0,
|
||||||
String? stanzaTag,
|
String? stanzaTag,
|
||||||
}) : super(
|
}) : super(
|
||||||
true,
|
true,
|
||||||
nonzaTag: stanzaTag,
|
nonzaTag: stanzaTag,
|
||||||
nonzaXmlns: stanzaXmlns,
|
nonzaXmlns: stanzaXmlns,
|
||||||
);
|
);
|
||||||
final String? tagName;
|
final String? tagName;
|
||||||
final String? tagXmlns;
|
final String? tagXmlns;
|
||||||
final int priority;
|
final int priority;
|
||||||
final Future<StanzaHandlerData> Function(Stanza, StanzaHandlerData) callback;
|
final Future<StanzaHandlerData> Function(Stanza, StanzaHandlerData) callback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool matches(XMLNode node) {
|
bool matches(XMLNode node) {
|
||||||
var matches = super.matches(node);
|
var matches = super.matches(node);
|
||||||
|
|
||||||
if (matches == false) {
|
if (matches == false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName != null) {
|
if (tagName != null) {
|
||||||
final firstTag = node.firstTag(tagName!, xmlns: tagXmlns);
|
final firstTag = node.firstTag(tagName!, xmlns: tagXmlns);
|
||||||
|
|
||||||
@@ -78,16 +76,19 @@ 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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName == null && tagXmlns == null) {
|
if (tagName == null && tagXmlns == null) {
|
||||||
matches = true;
|
matches = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int stanzaHandlerSortComparator(StanzaHandler a, StanzaHandler b) => b.priority.compareTo(a.priority);
|
int stanzaHandlerSortComparator(StanzaHandler a, StanzaHandler b) =>
|
||||||
|
b.priority.compareTo(a.priority);
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
const smManager = 'im.moxxmpp.streammangementmanager';
|
const smManager = 'org.moxxmpp.streammangementmanager';
|
||||||
const discoManager = 'im.moxxmpp.discomanager';
|
const discoManager = 'org.moxxmpp.discomanager';
|
||||||
const messageManager = 'im.moxxmpp.messagemanager';
|
const messageManager = 'org.moxxmpp.messagemanager';
|
||||||
const rosterManager = 'im.moxxmpp.rostermanager';
|
const rosterManager = 'org.moxxmpp.rostermanager';
|
||||||
const presenceManager = 'im.moxxmpp.presencemanager';
|
const presenceManager = 'org.moxxmpp.presencemanager';
|
||||||
const csiManager = 'im.moxxmpp.csimanager';
|
const csiManager = 'org.moxxmpp.csimanager';
|
||||||
const carbonsManager = 'im.moxxmpp.carbonsmanager';
|
const carbonsManager = 'org.moxxmpp.carbonsmanager';
|
||||||
const vcardManager = 'im.moxxmpp.vcardmanager';
|
const vcardManager = 'org.moxxmpp.vcardmanager';
|
||||||
const pubsubManager = 'im.moxxmpp.pubsubmanager';
|
const pubsubManager = 'org.moxxmpp.pubsubmanager';
|
||||||
const userAvatarManager = 'im.moxxmpp.useravatarmanager';
|
const userAvatarManager = 'org.moxxmpp.useravatarmanager';
|
||||||
const stableIdManager = 'im.moxxmpp.stableidmanager';
|
const stableIdManager = 'org.moxxmpp.stableidmanager';
|
||||||
const simsManager = 'im.moxxmpp.simsmanager';
|
const simsManager = 'org.moxxmpp.simsmanager';
|
||||||
const messageDeliveryReceiptManager = 'im.moxxmpp.messagedeliveryreceiptmanager';
|
const messageDeliveryReceiptManager =
|
||||||
const chatMarkerManager = 'im.moxxmpp.chatmarkermanager';
|
'org.moxxmpp.messagedeliveryreceiptmanager';
|
||||||
const oobManager = 'im.moxxmpp.oobmanager';
|
const chatMarkerManager = 'org.moxxmpp.chatmarkermanager';
|
||||||
const sfsManager = 'im.moxxmpp.sfsmanager';
|
const oobManager = 'org.moxxmpp.oobmanager';
|
||||||
const messageRepliesManager = 'im.moxxmpp.messagerepliesmanager';
|
const sfsManager = 'org.moxxmpp.sfsmanager';
|
||||||
const blockingManager = 'im.moxxmpp.blockingmanager';
|
const messageRepliesManager = 'org.moxxmpp.messagerepliesmanager';
|
||||||
const httpFileUploadManager = 'im.moxxmpp.httpfileuploadmanager';
|
const blockingManager = 'org.moxxmpp.blockingmanager';
|
||||||
const chatStateManager = 'im.moxxmpp.chatstatemanager';
|
const httpFileUploadManager = 'org.moxxmpp.httpfileuploadmanager';
|
||||||
const pingManager = 'im.moxxmpp.ping';
|
const chatStateManager = 'org.moxxmpp.chatstatemanager';
|
||||||
const fileUploadNotificationManager = 'im.moxxmpp.fileuploadnotificationmanager';
|
const pingManager = 'org.moxxmpp.ping';
|
||||||
|
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';
|
||||||
const delayedDeliveryManager = 'org.moxxmpp.delayeddeliverymanager';
|
const delayedDeliveryManager = 'org.moxxmpp.delayeddeliverymanager';
|
||||||
|
const messageRetractionManager = 'org.moxxmpp.messageretractionmanager';
|
||||||
|
const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager';
|
||||||
|
const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager';
|
||||||
|
const stickersManager = 'org.moxxmpp.stickersmanager';
|
||||||
|
const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:moxlib/moxlib.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
import 'package:moxxmpp/src/managers/base.dart';
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
@@ -11,14 +12,23 @@ import 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
|
|||||||
import 'package:moxxmpp/src/xeps/xep_0066.dart';
|
import 'package:moxxmpp/src/xeps/xep_0066.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0085.dart';
|
import 'package:moxxmpp/src/xeps/xep_0085.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0184.dart';
|
import 'package:moxxmpp/src/xeps/xep_0184.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0308.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0333.dart';
|
import 'package:moxxmpp/src/xeps/xep_0333.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0334.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0447.dart';
|
import 'package:moxxmpp/src/xeps/xep_0447.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0448.dart';
|
import 'package:moxxmpp/src/xeps/xep_0448.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0461.dart';
|
||||||
|
|
||||||
|
/// Data used to build a message stanza.
|
||||||
|
///
|
||||||
|
/// [setOOBFallbackBody] indicates, when using SFS, whether a OOB fallback should be
|
||||||
|
/// added. This is recommended when sharing files but may cause issues when the message
|
||||||
|
/// stanza should include a SFS element without any fallbacks.
|
||||||
class MessageDetails {
|
class MessageDetails {
|
||||||
|
|
||||||
const MessageDetails({
|
const MessageDetails({
|
||||||
required this.to,
|
required this.to,
|
||||||
this.body,
|
this.body,
|
||||||
@@ -35,6 +45,12 @@ class MessageDetails {
|
|||||||
this.funReplacement,
|
this.funReplacement,
|
||||||
this.funCancellation,
|
this.funCancellation,
|
||||||
this.shouldEncrypt = false,
|
this.shouldEncrypt = false,
|
||||||
|
this.messageRetraction,
|
||||||
|
this.lastMessageCorrectionId,
|
||||||
|
this.messageReactions,
|
||||||
|
this.messageProcessingHints,
|
||||||
|
this.stickerPackId,
|
||||||
|
this.setOOBFallbackBody = true,
|
||||||
});
|
});
|
||||||
final String to;
|
final String to;
|
||||||
final String? body;
|
final String? body;
|
||||||
@@ -51,52 +67,71 @@ class MessageDetails {
|
|||||||
final String? funReplacement;
|
final String? funReplacement;
|
||||||
final String? funCancellation;
|
final String? funCancellation;
|
||||||
final bool shouldEncrypt;
|
final bool shouldEncrypt;
|
||||||
|
final MessageRetractionData? messageRetraction;
|
||||||
|
final String? lastMessageCorrectionId;
|
||||||
|
final MessageReactions? messageReactions;
|
||||||
|
final String? stickerPackId;
|
||||||
|
final List<MessageProcessingHint>? messageProcessingHints;
|
||||||
|
final bool setOOBFallbackBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageManager extends XmppManagerBase {
|
class MessageManager extends XmppManagerBase {
|
||||||
@override
|
MessageManager() : super(messageManager);
|
||||||
String getId() => messageManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'MessageManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
callback: _onMessage,
|
callback: _onMessage,
|
||||||
priority: -100,
|
priority: -100,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
Future<StanzaHandlerData> _onMessage(Stanza _, StanzaHandlerData state) async {
|
Future<StanzaHandlerData> _onMessage(
|
||||||
|
Stanza _,
|
||||||
|
StanzaHandlerData state,
|
||||||
|
) async {
|
||||||
final message = state.stanza;
|
final message = state.stanza;
|
||||||
final body = message.firstTag('body');
|
final body = message.firstTag('body');
|
||||||
|
|
||||||
getAttributes().sendEvent(MessageEvent(
|
final hints = List<MessageProcessingHint>.empty(growable: true);
|
||||||
body: body != null ? body.innerText() : '',
|
for (final element
|
||||||
fromJid: JID.fromString(message.attributes['from']! as String),
|
in message.findTagsByXmlns(messageProcessingHintsXmlns)) {
|
||||||
toJid: JID.fromString(message.attributes['to']! as String),
|
hints.add(messageProcessingHintFromXml(element));
|
||||||
sid: message.attributes['id']! as String,
|
}
|
||||||
stanzaId: state.stableId ?? const StableStanzaId(),
|
|
||||||
isCarbon: state.isCarbon,
|
getAttributes().sendEvent(
|
||||||
deliveryReceiptRequested: state.deliveryReceiptRequested,
|
MessageEvent(
|
||||||
isMarkable: state.isMarkable,
|
body: body != null ? body.innerText() : '',
|
||||||
type: message.attributes['type'] as String?,
|
fromJid: JID.fromString(message.attributes['from']! as String),
|
||||||
oob: state.oob,
|
toJid: JID.fromString(message.attributes['to']! as String),
|
||||||
sfs: state.sfs,
|
sid: message.attributes['id']! as String,
|
||||||
sims: state.sims,
|
stanzaId: state.stableId ?? const StableStanzaId(),
|
||||||
reply: state.reply,
|
isCarbon: state.isCarbon,
|
||||||
chatState: state.chatState,
|
deliveryReceiptRequested: state.deliveryReceiptRequested,
|
||||||
fun: state.fun,
|
isMarkable: state.isMarkable,
|
||||||
funReplacement: state.funReplacement,
|
type: message.attributes['type'] as String?,
|
||||||
funCancellation: state.funCancellation,
|
oob: state.oob,
|
||||||
encrypted: state.encrypted,
|
sfs: state.sfs,
|
||||||
other: state.other,
|
sims: state.sims,
|
||||||
),);
|
reply: state.reply,
|
||||||
|
chatState: state.chatState,
|
||||||
|
fun: state.fun,
|
||||||
|
funReplacement: state.funReplacement,
|
||||||
|
funCancellation: state.funCancellation,
|
||||||
|
encrypted: state.encrypted,
|
||||||
|
messageRetraction: state.messageRetraction,
|
||||||
|
messageCorrectionId: state.lastMessageCorrectionSid,
|
||||||
|
messageReactions: state.messageReactions,
|
||||||
|
messageProcessingHints: hints.isEmpty ? null : hints,
|
||||||
|
stickerPackId: state.stickerPackId,
|
||||||
|
other: state.other,
|
||||||
|
error: StanzaError.fromStanza(message),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
@@ -107,6 +142,14 @@ class MessageManager extends XmppManagerBase {
|
|||||||
/// element to this id. If originId is non-null, then it will create an "origin-id"
|
/// element to this id. If originId is non-null, then it will create an "origin-id"
|
||||||
/// child in the message stanza and set its id to originId.
|
/// child in the message stanza and set its id to originId.
|
||||||
void sendMessage(MessageDetails details) {
|
void sendMessage(MessageDetails details) {
|
||||||
|
assert(
|
||||||
|
implies(
|
||||||
|
details.quoteBody != null,
|
||||||
|
details.quoteFrom != null && details.quoteId != null,
|
||||||
|
),
|
||||||
|
'When quoting a message, then quoteFrom and quoteId must also be non-null',
|
||||||
|
);
|
||||||
|
|
||||||
final stanza = Stanza.message(
|
final stanza = Stanza.message(
|
||||||
to: details.to,
|
to: details.to,
|
||||||
type: 'chat',
|
type: 'chat',
|
||||||
@@ -115,35 +158,30 @@ class MessageManager extends XmppManagerBase {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (details.quoteBody != null) {
|
if (details.quoteBody != null) {
|
||||||
final fallback = '> ${details.quoteBody!}';
|
final quote = QuoteData.fromBodies(details.quoteBody!, details.body!);
|
||||||
|
|
||||||
stanza
|
stanza
|
||||||
..addChild(
|
..addChild(
|
||||||
XMLNode(tag: 'body', text: '$fallback\n${details.body}'),
|
XMLNode(tag: 'body', text: quote.body),
|
||||||
)
|
)
|
||||||
..addChild(
|
..addChild(
|
||||||
XMLNode.xmlns(
|
XMLNode.xmlns(
|
||||||
tag: 'reply',
|
tag: 'reply',
|
||||||
xmlns: replyXmlns,
|
xmlns: replyXmlns,
|
||||||
attributes: {
|
attributes: {'to': details.quoteFrom!, 'id': details.quoteId!},
|
||||||
'to': details.quoteFrom!,
|
|
||||||
'id': details.quoteId!
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
..addChild(
|
..addChild(
|
||||||
XMLNode.xmlns(
|
XMLNode.xmlns(
|
||||||
tag: 'fallback',
|
tag: 'fallback',
|
||||||
xmlns: fallbackXmlns,
|
xmlns: fallbackXmlns,
|
||||||
attributes: {
|
attributes: {'for': replyXmlns},
|
||||||
'for': replyXmlns
|
|
||||||
},
|
|
||||||
children: [
|
children: [
|
||||||
XMLNode(
|
XMLNode(
|
||||||
tag: 'body',
|
tag: 'body',
|
||||||
attributes: <String, String>{
|
attributes: <String, String>{
|
||||||
'start': '0',
|
'start': '0',
|
||||||
'end': '${fallback.length}'
|
'end': '${quote.fallbackLength}',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -151,7 +189,7 @@ class MessageManager extends XmppManagerBase {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var body = details.body;
|
var body = details.body;
|
||||||
if (details.sfs != null) {
|
if (details.sfs != null && details.setOOBFallbackBody) {
|
||||||
// TODO(Unknown): Maybe find a better solution
|
// TODO(Unknown): Maybe find a better solution
|
||||||
final firstSource = details.sfs!.sources.first;
|
final firstSource = details.sfs!.sources.first;
|
||||||
if (firstSource is StatelessFileSharingUrlSource) {
|
if (firstSource is StatelessFileSharingUrlSource) {
|
||||||
@@ -159,11 +197,15 @@ class MessageManager extends XmppManagerBase {
|
|||||||
} else if (firstSource is StatelessFileSharingEncryptedSource) {
|
} else if (firstSource is StatelessFileSharingEncryptedSource) {
|
||||||
body = firstSource.source.url;
|
body = firstSource.source.url;
|
||||||
}
|
}
|
||||||
|
} else if (details.messageRetraction?.fallback != null) {
|
||||||
|
body = details.messageRetraction!.fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
stanza.addChild(
|
if (body != null) {
|
||||||
XMLNode(tag: 'body', text: body),
|
stanza.addChild(
|
||||||
);
|
XMLNode(tag: 'body', text: body),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (details.requestDeliveryReceipt) {
|
if (details.requestDeliveryReceipt) {
|
||||||
@@ -180,16 +222,20 @@ class MessageManager extends XmppManagerBase {
|
|||||||
stanza.addChild(details.sfs!.toXML());
|
stanza.addChild(details.sfs!.toXML());
|
||||||
|
|
||||||
final source = details.sfs!.sources.first;
|
final source = details.sfs!.sources.first;
|
||||||
if (source is StatelessFileSharingUrlSource) {
|
if (source is StatelessFileSharingUrlSource &&
|
||||||
|
details.setOOBFallbackBody) {
|
||||||
// SFS recommends OOB as a fallback
|
// SFS recommends OOB as a fallback
|
||||||
stanza.addChild(constructOOBNode(OOBData(url: source.url)));
|
stanza.addChild(constructOOBNode(OOBData(url: source.url)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +262,64 @@ class MessageManager extends XmppManagerBase {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (details.messageRetraction != null) {
|
||||||
|
stanza.addChild(
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'apply-to',
|
||||||
|
xmlns: fasteningXmlns,
|
||||||
|
attributes: <String, String>{
|
||||||
|
'id': details.messageRetraction!.id,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'retract',
|
||||||
|
xmlns: messageRetractionXmlns,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (details.messageRetraction!.fallback != null) {
|
||||||
|
stanza.addChild(
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'fallback',
|
||||||
|
xmlns: fallbackIndicationXmlns,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.lastMessageCorrectionId != null) {
|
||||||
|
stanza.addChild(
|
||||||
|
makeLastMessageCorrectionEdit(
|
||||||
|
details.lastMessageCorrectionId!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.messageReactions != null) {
|
||||||
|
stanza.addChild(details.messageReactions!.toXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.messageProcessingHints != null) {
|
||||||
|
for (final hint in details.messageProcessingHints!) {
|
||||||
|
stanza.addChild(hint.toXml());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.stickerPackId != null) {
|
||||||
|
stanza.addChild(
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'sticker',
|
||||||
|
xmlns: stickersXmlns,
|
||||||
|
attributes: {
|
||||||
|
'pack': details.stickerPackId!,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getAttributes().sendStanza(stanza, awaitable: false);
|
getAttributes().sendStanza(stanza, awaitable: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -76,6 +78,9 @@ const hashSha3512 = 'sha3-512';
|
|||||||
const hashBlake2b256 = 'blake2b-256';
|
const hashBlake2b256 = 'blake2b-256';
|
||||||
const hashBlake2b512 = 'blake2b-512';
|
const hashBlake2b512 = 'blake2b-512';
|
||||||
|
|
||||||
|
// XEP-0308
|
||||||
|
const lmcXmlns = 'urn:xmpp:message-correct:0';
|
||||||
|
|
||||||
// XEP-0333
|
// XEP-0333
|
||||||
const chatMarkersXmlns = 'urn:xmpp:chat-markers:0';
|
const chatMarkersXmlns = 'urn:xmpp:chat-markers:0';
|
||||||
|
|
||||||
@@ -114,6 +119,18 @@ const simsXmlns = 'urn:xmpp:sims:1';
|
|||||||
// XEP-0420
|
// XEP-0420
|
||||||
const sceXmlns = 'urn:xmpp:sce:1';
|
const sceXmlns = 'urn:xmpp:sce:1';
|
||||||
|
|
||||||
|
// XEP-0422
|
||||||
|
const fasteningXmlns = 'urn:xmpp:fasten:0';
|
||||||
|
|
||||||
|
// XEP-0424
|
||||||
|
const messageRetractionXmlns = 'urn:xmpp:message-retract:0';
|
||||||
|
|
||||||
|
// XEP-0428
|
||||||
|
const fallbackIndicationXmlns = 'urn:xmpp:fallback:0';
|
||||||
|
|
||||||
|
// XEP-0444
|
||||||
|
const messageReactionsXmlns = 'urn:xmpp:reactions:0';
|
||||||
|
|
||||||
// XEP-0446
|
// XEP-0446
|
||||||
const fileMetadataXmlns = 'urn:xmpp:file:metadata:0';
|
const fileMetadataXmlns = 'urn:xmpp:file:metadata:0';
|
||||||
|
|
||||||
@@ -122,10 +139,15 @@ const sfsXmlns = 'urn:xmpp:sfs:0';
|
|||||||
|
|
||||||
// XEP-0448
|
// XEP-0448
|
||||||
const sfsEncryptionXmlns = 'urn:xmpp:esfs:0';
|
const sfsEncryptionXmlns = 'urn:xmpp:esfs:0';
|
||||||
const sfsEncryptionAes128GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-128-gcm-nopadding:0';
|
const sfsEncryptionAes128GcmNoPaddingXmlns =
|
||||||
const sfsEncryptionAes256GcmNoPaddingXmlns = 'urn:xmpp:ciphers:aes-256-gcm-nopadding:0';
|
'urn:xmpp:ciphers:aes-128-gcm-nopadding:0';
|
||||||
|
const sfsEncryptionAes256GcmNoPaddingXmlns =
|
||||||
|
'urn:xmpp:ciphers:aes-256-gcm-nopadding:0';
|
||||||
const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0';
|
const sfsEncryptionAes256CbcPkcs7Xmlns = 'urn:xmpp:ciphers:aes-256-cbc-pkcs7:0';
|
||||||
|
|
||||||
|
// XEP-0449
|
||||||
|
const stickersXmlns = 'urn:xmpp:stickers:0';
|
||||||
|
|
||||||
// XEP-0461
|
// XEP-0461
|
||||||
const replyXmlns = 'urn:xmpp:reply:0';
|
const replyXmlns = 'urn:xmpp:reply:0';
|
||||||
const fallbackXmlns = 'urn:xmpp:feature-fallback:0';
|
const fallbackXmlns = 'urn:xmpp:feature-fallback:0';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:moxlib/moxlib.dart';
|
import 'package:moxlib/moxlib.dart';
|
||||||
|
import 'package:moxxmpp/src/errors.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
import 'package:moxxmpp/src/managers/base.dart';
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
import 'package:moxxmpp/src/settings.dart';
|
import 'package:moxxmpp/src/settings.dart';
|
||||||
import 'package:moxxmpp/src/socket.dart';
|
import 'package:moxxmpp/src/socket.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
|
|
||||||
/// The state a negotiator is currently in
|
/// The state a negotiator is currently in
|
||||||
enum NegotiatorState {
|
enum NegotiatorState {
|
||||||
@@ -14,15 +16,15 @@ enum NegotiatorState {
|
|||||||
done,
|
done,
|
||||||
// Cancel the current attempt but we are not done
|
// Cancel the current attempt but we are not done
|
||||||
retryLater,
|
retryLater,
|
||||||
// The negotiator is in an error state
|
|
||||||
error,
|
|
||||||
// Skip the rest of the negotiation and assume the stream ready. Only use this when
|
// Skip the rest of the negotiation and assume the stream ready. Only use this when
|
||||||
// using stream restoration XEPs, like Stream Management.
|
// using stream restoration XEPs, like Stream Management.
|
||||||
skipRest,
|
skipRest,
|
||||||
}
|
}
|
||||||
|
|
||||||
class NegotiatorAttributes {
|
/// A base class for all errors that may occur during feature negotiation
|
||||||
|
abstract class NegotiatorError extends XmppError {}
|
||||||
|
|
||||||
|
class NegotiatorAttributes {
|
||||||
const NegotiatorAttributes(
|
const NegotiatorAttributes(
|
||||||
this.sendNonza,
|
this.sendNonza,
|
||||||
this.getConnectionSettings,
|
this.getConnectionSettings,
|
||||||
@@ -33,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;
|
||||||
@@ -68,24 +83,25 @@ abstract class XmppFeatureNegotiatorBase {
|
|||||||
|
|
||||||
/// The Id of the negotiator
|
/// The Id of the negotiator
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
/// The state the negotiator is currently in
|
/// The state the negotiator is currently in
|
||||||
NegotiatorState state;
|
NegotiatorState state;
|
||||||
|
|
||||||
late NegotiatorAttributes _attributes;
|
late NegotiatorAttributes _attributes;
|
||||||
|
|
||||||
/// Register the negotiator against a connection class by means of [attributes].
|
/// Register the negotiator against a connection class by means of [attributes].
|
||||||
void register(NegotiatorAttributes attributes) {
|
void register(NegotiatorAttributes attributes) {
|
||||||
_attributes = attributes;
|
_attributes = attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if a feature in [features], which are the children of the
|
/// Returns true if a feature in [features], which are the children of the
|
||||||
/// <stream:features /> nonza, can be negotiated. Otherwise, returns false.
|
/// <stream:features /> nonza, can be negotiated. Otherwise, returns false.
|
||||||
bool matchesFeature(List<XMLNode> features) {
|
bool matchesFeature(List<XMLNode> features) {
|
||||||
return firstWhereOrNull(
|
return 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.
|
||||||
@@ -97,12 +113,12 @@ abstract class XmppFeatureNegotiatorBase {
|
|||||||
/// must switch some internal state to prevent getting matched immediately again.
|
/// must switch some internal state to prevent getting matched immediately again.
|
||||||
/// If ready is returned, then the negotiator indicates that it is not done with
|
/// If ready is returned, then the negotiator indicates that it is not done with
|
||||||
/// negotiation.
|
/// negotiation.
|
||||||
Future<void> negotiate(XMLNode nonza);
|
Future<Result<NegotiatorState, NegotiatorError>> negotiate(XMLNode nonza);
|
||||||
|
|
||||||
/// Reset the negotiator to a state that negotation can happen again.
|
/// Reset the negotiator to a state that negotation can happen again.
|
||||||
void reset() {
|
void reset() {
|
||||||
state = NegotiatorState.ready;
|
state = NegotiatorState.ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
NegotiatorAttributes get attributes => _attributes;
|
NegotiatorAttributes get attributes => _attributes;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,42 @@ import 'package:moxxmpp/src/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
|
class ResourceBindingFailedError extends NegotiatorError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => true;
|
||||||
|
}
|
||||||
|
|
||||||
ResourceBindingNegotiator() : _requestSent = false, super(0, false, bindXmlns, resourceBindingNegotiator);
|
/// A negotiator that implements resource binding against a random server-provided
|
||||||
bool _requestSent;
|
/// resource.
|
||||||
|
class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
|
||||||
|
ResourceBindingNegotiator()
|
||||||
|
: super(0, false, bindXmlns, resourceBindingNegotiator);
|
||||||
|
|
||||||
|
/// Flag indicating the state of the negotiator:
|
||||||
|
/// - True: We sent a binding request
|
||||||
|
/// - False: We have not yet sent the binding request
|
||||||
|
bool _requestSent = false;
|
||||||
|
|
||||||
@override
|
@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<void> 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',
|
||||||
@@ -42,21 +58,22 @@ class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
|
|||||||
|
|
||||||
_requestSent = true;
|
_requestSent = true;
|
||||||
attributes.sendNonza(stanza);
|
attributes.sendNonza(stanza);
|
||||||
|
return const Result(NegotiatorState.ready);
|
||||||
} else {
|
} else {
|
||||||
if (nonza.tag != 'iq' || nonza.attributes['type'] != 'result') {
|
if (nonza.tag != 'iq' || nonza.attributes['type'] != 'result') {
|
||||||
state = NegotiatorState.error;
|
return Result(ResourceBindingFailedError());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final bind = nonza.firstTag('bind')!;
|
final bind = nonza.firstTag('bind')!;
|
||||||
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
|
||||||
state = NegotiatorState.done;
|
.sendEvent(ResourceBindingSuccessEvent(resource: resource));
|
||||||
|
return const Result(NegotiatorState.done);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void reset() {
|
void reset() {
|
||||||
_requestSent = false;
|
_requestSent = false;
|
||||||
|
|||||||
53
packages/moxxmpp/lib/src/negotiators/sasl/errors.dart
Normal file
53
packages/moxxmpp/lib/src/negotiators/sasl/errors.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
|
||||||
|
abstract class SaslError extends NegotiatorError {
|
||||||
|
static SaslError fromFailure(XMLNode failure) {
|
||||||
|
XMLNode? error;
|
||||||
|
for (final child in failure.children) {
|
||||||
|
if (child.tag == 'text') continue;
|
||||||
|
|
||||||
|
error = child;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (error?.tag) {
|
||||||
|
case 'credentials-expired':
|
||||||
|
return SaslCredentialsExpiredError();
|
||||||
|
case 'not-authorized':
|
||||||
|
return SaslNotAuthorizedError();
|
||||||
|
case 'account-disabled':
|
||||||
|
return SaslAccountDisabledError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SaslUnspecifiedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when the server returned us a <not-authorized /> failure during SASL
|
||||||
|
/// (https://xmpp.org/rfcs/rfc6120.html#sasl-errors-not-authorized).
|
||||||
|
class SaslNotAuthorizedError extends SaslError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when the server returned us a <credentials-expired /> failure during SASL
|
||||||
|
/// (https://xmpp.org/rfcs/rfc6120.html#sasl-errors-credentials-expired).
|
||||||
|
class SaslCredentialsExpiredError extends SaslError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggered when the server returned us a <account-disabled /> failure during SASL
|
||||||
|
/// (https://xmpp.org/rfcs/rfc6120.html#sasl-errors-account-disabled).
|
||||||
|
class SaslAccountDisabledError extends SaslError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An unspecified SASL error, i.e. everything not matched by any more precise erorr
|
||||||
|
/// class.
|
||||||
|
class SaslUnspecifiedError extends SaslError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => true;
|
||||||
|
}
|
||||||
@@ -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,31 +11,33 @@ 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 == '=') {
|
{
|
||||||
state = ParserState.variableValue;
|
if (char == '=') {
|
||||||
} else if (char == ',') {
|
state = ParserState.variableValue;
|
||||||
name = '';
|
} else if (char == ',') {
|
||||||
} else {
|
name = '';
|
||||||
name += char;
|
} else {
|
||||||
|
name += char;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
values[name] = value;
|
||||||
|
value = '';
|
||||||
|
name = '';
|
||||||
|
state = ParserState.variableName;
|
||||||
|
} else {
|
||||||
value += char;
|
value += char;
|
||||||
}
|
}
|
||||||
|
|
||||||
values[name] = value;
|
|
||||||
value = '';
|
|
||||||
name = '';
|
|
||||||
state = ParserState.variableName;
|
|
||||||
} else {
|
|
||||||
value += char;
|
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ 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;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool matchesFeature(List<XMLNode> features) {
|
bool matchesFeature(List<XMLNode> features) {
|
||||||
// Is SASL advertised?
|
// Is SASL advertised?
|
||||||
@@ -20,8 +21,9 @@ abstract class SaslNegotiator extends XmppFeatureNegotiatorBase {
|
|||||||
|
|
||||||
// Is SASL PLAIN advertised?
|
// Is SASL PLAIN advertised?
|
||||||
return firstWhereOrNull(
|
return firstWhereOrNull(
|
||||||
mechanisms.children,
|
mechanisms.children,
|
||||||
(XMLNode mechanism) => mechanism.text == mechanismName,
|
(XMLNode mechanism) => mechanism.text == mechanismName,
|
||||||
) != null;
|
) !=
|
||||||
|
null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ 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)
|
||||||
tag: 'auth',
|
: super(
|
||||||
attributes: <String, String>{
|
tag: 'auth',
|
||||||
'xmlns': saslXmlns,
|
attributes: <String, String>{
|
||||||
'mechanism': mechanism ,
|
'xmlns': saslXmlns,
|
||||||
},
|
'mechanism': mechanism,
|
||||||
text: body,
|
},
|
||||||
);
|
text: body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,38 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
|
import 'package:moxxmpp/src/negotiators/sasl/errors.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
|
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.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')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SaslPlainNegotiator extends SaslNegotiator {
|
class SaslPlainNegotiator extends SaslNegotiator {
|
||||||
|
|
||||||
SaslPlainNegotiator()
|
SaslPlainNegotiator()
|
||||||
: _authSent = false,
|
: _authSent = false,
|
||||||
_log = Logger('SaslPlainNegotiator'),
|
_log = Logger('SaslPlainNegotiator'),
|
||||||
super(0, saslPlainNegotiator, 'PLAIN');
|
super(0, saslPlainNegotiator, 'PLAIN');
|
||||||
bool _authSent;
|
bool _authSent;
|
||||||
|
|
||||||
final Logger _log;
|
final Logger _log;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool matchesFeature(List<XMLNode> features) {
|
bool matchesFeature(List<XMLNode> features) {
|
||||||
if (!attributes.getConnectionSettings().allowPlainAuth) return false;
|
|
||||||
|
|
||||||
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 +43,9 @@ class SaslPlainNegotiator extends SaslNegotiator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> 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(
|
||||||
@@ -49,17 +53,19 @@ class SaslPlainNegotiator extends SaslNegotiator {
|
|||||||
redact: SaslPlainAuthNonza('******', '******').toXml(),
|
redact: SaslPlainAuthNonza('******', '******').toXml(),
|
||||||
);
|
);
|
||||||
_authSent = true;
|
_authSent = true;
|
||||||
|
return const Result(NegotiatorState.ready);
|
||||||
} else {
|
} else {
|
||||||
final tag = nonza.tag;
|
final tag = nonza.tag;
|
||||||
if (tag == 'success') {
|
if (tag == 'success') {
|
||||||
await attributes.sendEvent(AuthenticationSuccessEvent());
|
await attributes.sendEvent(AuthenticationSuccessEvent());
|
||||||
state = NegotiatorState.done;
|
return const Result(NegotiatorState.done);
|
||||||
} else {
|
} else {
|
||||||
// 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(
|
||||||
state = NegotiatorState.error;
|
SaslError.fromFailure(nonza),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,46 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math' show Random;
|
import 'dart:math' show Random;
|
||||||
|
|
||||||
import 'package:cryptography/cryptography.dart';
|
import 'package:cryptography/cryptography.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
|
import 'package:moxxmpp/src/negotiators/sasl/errors.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/sasl/kv.dart';
|
import 'package:moxxmpp/src/negotiators/sasl/kv.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
|
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
import 'package:random_string/random_string.dart';
|
import 'package:random_string/random_string.dart';
|
||||||
import 'package:saslprep/saslprep.dart';
|
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) {
|
||||||
|
switch (type) {
|
||||||
|
// NOTE: SHA1 is 20 octets long => 20 octets * 8 bits/octet
|
||||||
|
case ScramHashType.sha1:
|
||||||
|
return 160;
|
||||||
|
// NOTE: SHA256 is 32 octets long => 32 octets * 8 bits/octet
|
||||||
|
case ScramHashType.sha256:
|
||||||
|
return 256;
|
||||||
|
// NOTE: SHA512 is 64 octets long => 64 octets * 8 bits/octet
|
||||||
|
case ScramHashType.sha512:
|
||||||
|
return 512;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,61 +50,68 @@ 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})
|
||||||
tag: 'response',
|
: super(
|
||||||
attributes: <String, String>{
|
tag: 'response',
|
||||||
'xmlns': saslXmlns,
|
attributes: <String, String>{
|
||||||
},
|
'xmlns': saslXmlns,
|
||||||
text: body,
|
},
|
||||||
);
|
text: body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ScramState {
|
enum ScramState { preSent, initialMessageSent, challengeResponseSent, error }
|
||||||
preSent,
|
|
||||||
initialMessageSent,
|
|
||||||
challengeResponseSent,
|
|
||||||
error
|
|
||||||
}
|
|
||||||
|
|
||||||
const gs2Header = 'n,,';
|
const gs2Header = 'n,,';
|
||||||
|
|
||||||
class SaslScramNegotiator extends SaslNegotiator {
|
class SaslScramNegotiator extends SaslNegotiator {
|
||||||
|
|
||||||
// NOTE: NEVER, and I mean, NEVER set clientNonce or initalMessageNoGS2. They are just there for testing
|
// NOTE: NEVER, and I mean, NEVER set clientNonce or initalMessageNoGS2. They are just there for testing
|
||||||
SaslScramNegotiator(
|
SaslScramNegotiator(
|
||||||
int priority,
|
int priority,
|
||||||
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 =
|
||||||
_log = Logger('SaslScramNegotiator(${mechanismNameFromType(hashType)})'),
|
Logger('SaslScramNegotiator(${mechanismNameFromType(hashType)})'),
|
||||||
super(priority, namespaceFromType(hashType), mechanismNameFromType(hashType));
|
super(
|
||||||
|
priority,
|
||||||
|
namespaceFromType(hashType),
|
||||||
|
mechanismNameFromType(hashType),
|
||||||
|
);
|
||||||
String? clientNonce;
|
String? clientNonce;
|
||||||
String initialMessageNoGS2;
|
String initialMessageNoGS2;
|
||||||
final ScramHashType hashType;
|
final ScramHashType hashType;
|
||||||
@@ -106,12 +127,14 @@ class SaslScramNegotiator extends SaslNegotiator {
|
|||||||
final pbkdf2 = Pbkdf2(
|
final pbkdf2 = Pbkdf2(
|
||||||
macAlgorithm: Hmac(_hash),
|
macAlgorithm: Hmac(_hash),
|
||||||
iterations: iterations,
|
iterations: iterations,
|
||||||
bits: 160, // NOTE: RFC says 20 octets => 20 octets * 8 bits/octet
|
bits: pbkdfBitsFromHash(hashType),
|
||||||
);
|
);
|
||||||
|
|
||||||
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),
|
||||||
);
|
);
|
||||||
@@ -120,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];
|
||||||
@@ -153,20 +190,26 @@ class SaslScramNegotiator extends SaslNegotiator {
|
|||||||
|
|
||||||
return clientProof;
|
return clientProof;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> calculateChallengeResponse(String base64Challenge) async {
|
Future<String> calculateChallengeResponse(String base64Challenge) async {
|
||||||
final challengeString = utf8.decode(base64.decode(base64Challenge));
|
final challengeString = utf8.decode(base64.decode(base64Challenge));
|
||||||
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)}';
|
||||||
}
|
}
|
||||||
@@ -175,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,31 +229,41 @@ class SaslScramNegotiator extends SaslNegotiator {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> 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(),
|
||||||
);
|
);
|
||||||
break;
|
return const Result(NegotiatorState.ready);
|
||||||
case ScramState.initialMessageSent:
|
case ScramState.initialMessageSent:
|
||||||
if (nonza.tag != 'challenge') {
|
if (nonza.tag != 'challenge') {
|
||||||
final error = nonza.children.first.tag;
|
final error = nonza.children.first.tag;
|
||||||
await attributes.sendEvent(AuthenticationFailedEvent(error));
|
await attributes.sendEvent(AuthenticationFailedEvent(error));
|
||||||
|
|
||||||
state = NegotiatorState.error;
|
|
||||||
_scramState = ScramState.error;
|
_scramState = ScramState.error;
|
||||||
return;
|
return Result(
|
||||||
|
SaslError.fromFailure(nonza),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final challengeBase64 = nonza.innerText();
|
final challengeBase64 = nonza.innerText();
|
||||||
@@ -219,34 +274,37 @@ class SaslScramNegotiator extends SaslNegotiator {
|
|||||||
SaslScramResponseNonza(body: responseBase64),
|
SaslScramResponseNonza(body: responseBase64),
|
||||||
redact: SaslScramResponseNonza(body: '******').toXml(),
|
redact: SaslScramResponseNonza(body: '******').toXml(),
|
||||||
);
|
);
|
||||||
return;
|
return const Result(NegotiatorState.ready);
|
||||||
case ScramState.challengeResponseSent:
|
case ScramState.challengeResponseSent:
|
||||||
if (nonza.tag != 'success') {
|
if (nonza.tag != 'success') {
|
||||||
// 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));
|
||||||
_scramState = ScramState.error;
|
_scramState = ScramState.error;
|
||||||
state = NegotiatorState.error;
|
return Result(
|
||||||
return;
|
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;
|
||||||
state = NegotiatorState.error;
|
return Result(
|
||||||
return;
|
SaslError.fromFailure(nonza),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await attributes.sendEvent(AuthenticationSuccessEvent());
|
await attributes.sendEvent(AuthenticationSuccessEvent());
|
||||||
state = NegotiatorState.done;
|
return const Result(NegotiatorState.done);
|
||||||
return;
|
|
||||||
case ScramState.error:
|
case ScramState.error:
|
||||||
state = NegotiatorState.error;
|
return Result(
|
||||||
return;
|
SaslError.fromFailure(nonza),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,56 +3,61 @@ import 'package:moxxmpp/src/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
|
|
||||||
enum _StartTlsState {
|
enum _StartTlsState { ready, requested }
|
||||||
ready,
|
|
||||||
requested
|
class StartTLSFailedError extends NegotiatorError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
class StartTLSNonza extends XMLNode {
|
class StartTLSNonza extends XMLNode {
|
||||||
StartTLSNonza() : super.xmlns(
|
StartTLSNonza()
|
||||||
tag: 'starttls',
|
: super.xmlns(
|
||||||
xmlns: startTlsXmlns,
|
tag: 'starttls',
|
||||||
);
|
xmlns: startTlsXmlns,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A negotiator implementing StartTLS.
|
||||||
class StartTlsNegotiator extends XmppFeatureNegotiatorBase {
|
class StartTlsNegotiator extends XmppFeatureNegotiatorBase {
|
||||||
|
StartTlsNegotiator() : super(10, true, startTlsXmlns, startTlsNegotiator);
|
||||||
StartTlsNegotiator()
|
|
||||||
: _state = _StartTlsState.ready,
|
|
||||||
_log = Logger('StartTlsNegotiator'),
|
|
||||||
super(10, true, startTlsXmlns, startTlsNegotiator);
|
|
||||||
_StartTlsState _state;
|
|
||||||
|
|
||||||
final Logger _log;
|
/// The state of the negotiator.
|
||||||
|
_StartTlsState _state = _StartTlsState.ready;
|
||||||
|
|
||||||
|
/// Logger.
|
||||||
|
final Logger _log = Logger('StartTlsNegotiator');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> 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...');
|
||||||
_state = _StartTlsState.requested;
|
_state = _StartTlsState.requested;
|
||||||
attributes.sendNonza(StartTLSNonza());
|
attributes.sendNonza(StartTLSNonza());
|
||||||
break;
|
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');
|
||||||
state = NegotiatorState.error;
|
return Result(StartTLSFailedError());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.fine('Securing socket');
|
_log.fine('Securing socket');
|
||||||
final result = await attributes.getSocket()
|
final result = await attributes
|
||||||
.secure(attributes.getConnectionSettings().jid.domain);
|
.getSocket()
|
||||||
|
.secure(attributes.getConnectionSettings().jid.domain);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
_log.severe('Failed to secure stream');
|
_log.severe('Failed to secure stream');
|
||||||
state = NegotiatorState.error;
|
return Result(StartTLSFailedError());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.fine('Stream is now TLS secured');
|
_log.fine('Stream is now TLS secured');
|
||||||
state = NegotiatorState.done;
|
return const Result(NegotiatorState.done);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||||
|
|
||||||
class PingManager extends XmppManagerBase {
|
class PingManager extends XmppManagerBase {
|
||||||
@override
|
PingManager() : super(pingManager);
|
||||||
String getId() => pingManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'PingManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
void _logWarning() {
|
void _logWarning() {
|
||||||
logger.warning('Cannot send keepalives as SM is not available, the socket disallows whitespace pings and does not manage its own keepalives. Cannot guarantee that the connection survives.');
|
logger.warning(
|
||||||
|
'Cannot send keepalives as SM is not available, the socket disallows whitespace pings and does not manage its own keepalives. Cannot guarantee that the connection survives.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onXmppEvent(XmppEvent event) async {
|
Future<void> onXmppEvent(XmppEvent event) async {
|
||||||
if (event is SendPingEvent) {
|
if (event is SendPingEvent) {
|
||||||
@@ -28,14 +26,18 @@ class PingManager extends XmppManagerBase {
|
|||||||
logger.finest('Not sending ping as the socket manages it.');
|
logger.finest('Not sending ping as the socket manages it.');
|
||||||
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();
|
||||||
|
|||||||
@@ -8,98 +8,92 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
|
||||||
import 'package:moxxmpp/src/xeps/xep_0115.dart';
|
|
||||||
import 'package:moxxmpp/src/xeps/xep_0414.dart';
|
|
||||||
|
|
||||||
|
/// A function that will be called when presence, outside of subscription request
|
||||||
|
/// management, will be sent. Useful for managers that want to add [XMLNode]s to said
|
||||||
|
/// presence.
|
||||||
|
typedef PresencePreSendCallback = Future<List<XMLNode>> Function();
|
||||||
|
|
||||||
|
/// A mandatory manager that handles initial presence sending, sending of subscription
|
||||||
|
/// request management requests and triggers events for incoming presence stanzas.
|
||||||
class PresenceManager extends XmppManagerBase {
|
class PresenceManager extends XmppManagerBase {
|
||||||
PresenceManager(this._capHashNode) : _capabilityHash = null, super();
|
PresenceManager() : super(presenceManager);
|
||||||
String? _capabilityHash;
|
|
||||||
final String _capHashNode;
|
|
||||||
|
|
||||||
String get capabilityHashNode => _capHashNode;
|
/// The list of pre-send callbacks.
|
||||||
|
final List<PresencePreSendCallback> _presenceCallbacks =
|
||||||
@override
|
List.empty(growable: true);
|
||||||
String getId() => presenceManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'PresenceManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'presence',
|
stanzaTag: 'presence',
|
||||||
callback: _onPresence,
|
callback: _onPresence,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> getDiscoFeatures() => [ capsXmlns ];
|
List<String> getDiscoFeatures() => [capsXmlns];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
Future<StanzaHandlerData> _onPresence(Stanza presence, StanzaHandlerData state) async {
|
/// Register the pre-send callback [callback].
|
||||||
|
void registerPreSendCallback(PresencePreSendCallback callback) {
|
||||||
|
_presenceCallbacks.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
{
|
||||||
SubscriptionRequestReceivedEvent(from: JID.fromString(presence.from!)),
|
attrs.sendEvent(
|
||||||
);
|
SubscriptionRequestReceivedEvent(
|
||||||
return state.copyWith(done: true);
|
from: JID.fromString(presence.from!),
|
||||||
}
|
),
|
||||||
default: break;
|
);
|
||||||
|
return state.copyWith(done: true);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (presence.from != null) {
|
if (presence.from != null) {
|
||||||
logger.finest("Received presence from '${presence.from}'");
|
logger.finest("Received presence from '${presence.from}'");
|
||||||
|
|
||||||
getAttributes().sendEvent(PresenceReceivedEvent(JID.fromString(presence.from!), presence));
|
getAttributes().sendEvent(
|
||||||
|
PresenceReceivedEvent(JID.fromString(presence.from!), presence),
|
||||||
|
);
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the capability hash.
|
|
||||||
Future<String> getCapabilityHash() async {
|
|
||||||
final manager = getAttributes().getManagerById(discoManager)! as DiscoManager;
|
|
||||||
_capabilityHash ??= await calculateCapabilityHash(
|
|
||||||
DiscoInfo(
|
|
||||||
manager.getRegisteredDiscoFeatures(),
|
|
||||||
manager.getIdentities(),
|
|
||||||
[],
|
|
||||||
getAttributes().getFullJID(),
|
|
||||||
),
|
|
||||||
getHashByName('sha-1')!,
|
|
||||||
);
|
|
||||||
|
|
||||||
return _capabilityHash!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends the initial presence to enable receiving messages.
|
/// Sends the initial presence to enable receiving messages.
|
||||||
Future<void> sendInitialPresence() async {
|
Future<void> sendInitialPresence() async {
|
||||||
|
final children = List<XMLNode>.from([
|
||||||
|
XMLNode(
|
||||||
|
tag: 'show',
|
||||||
|
text: 'chat',
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (final callback in _presenceCallbacks) {
|
||||||
|
children.addAll(
|
||||||
|
await callback(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final attrs = getAttributes();
|
final attrs = getAttributes();
|
||||||
attrs.sendNonza(
|
attrs.sendNonza(
|
||||||
Stanza.presence(
|
Stanza.presence(
|
||||||
from: attrs.getFullJID().toString(),
|
from: attrs.getFullJID().toString(),
|
||||||
children: [
|
children: children,
|
||||||
XMLNode(
|
|
||||||
tag: 'show',
|
|
||||||
text: 'chat',
|
|
||||||
),
|
|
||||||
XMLNode.xmlns(
|
|
||||||
tag: 'c',
|
|
||||||
xmlns: capsXmlns,
|
|
||||||
attributes: {
|
|
||||||
'hash': 'sha-1',
|
|
||||||
'node': _capHashNode,
|
|
||||||
'ver': await getCapabilityHash()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,7 +107,7 @@ class PresenceManager extends XmppManagerBase {
|
|||||||
addFrom: StanzaFromType.full,
|
addFrom: StanzaFromType.full,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a subscription request to [to].
|
/// Sends a subscription request to [to].
|
||||||
void sendSubscriptionRequest(String to) {
|
void sendSubscriptionRequest(String to) {
|
||||||
getAttributes().sendStanza(
|
getAttributes().sendStanza(
|
||||||
|
|||||||
@@ -2,35 +2,51 @@ import 'dart:async';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:moxxmpp/src/util/queue.dart';
|
||||||
import 'package:synchronized/synchronized.dart';
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
abstract class ReconnectionPolicy {
|
/// A callback function to be called when the connection to the server has been lost.
|
||||||
|
typedef ConnectionLostCallback = Future<void> Function();
|
||||||
|
|
||||||
ReconnectionPolicy()
|
/// A function that, when called, causes the XmppConnection to connect to the server, if
|
||||||
: _shouldAttemptReconnection = false,
|
/// another reconnection is not already running.
|
||||||
_isReconnecting = false,
|
typedef PerformReconnectFunction = Future<void> Function();
|
||||||
_isReconnectingLock = Lock();
|
|
||||||
|
abstract class ReconnectionPolicy {
|
||||||
/// Function provided by XmppConnection that allows the policy
|
/// Function provided by XmppConnection that allows the policy
|
||||||
/// to perform a reconnection.
|
/// to perform a reconnection.
|
||||||
Future<void> Function()? performReconnect;
|
PerformReconnectFunction? performReconnect;
|
||||||
|
|
||||||
/// Function provided by XmppConnection that allows the policy
|
/// Function provided by XmppConnection that allows the policy
|
||||||
/// to say that we lost the connection.
|
/// to say that we lost the connection.
|
||||||
void Function()? triggerConnectionLost;
|
ConnectionLostCallback? triggerConnectionLost;
|
||||||
|
|
||||||
/// Indicate if should try to reconnect.
|
/// Indicate if should try to reconnect.
|
||||||
bool _shouldAttemptReconnection;
|
bool _shouldAttemptReconnection = false;
|
||||||
|
|
||||||
/// Indicate if a reconnection attempt is currently running.
|
/// Indicate if a reconnection attempt is currently running.
|
||||||
bool _isReconnecting;
|
@protected
|
||||||
|
bool isReconnecting = false;
|
||||||
|
|
||||||
/// And the corresponding lock
|
/// And the corresponding lock
|
||||||
final Lock _isReconnectingLock;
|
@protected
|
||||||
|
final Lock lock = Lock();
|
||||||
|
|
||||||
|
/// The lock for accessing [_shouldAttemptReconnection]
|
||||||
|
@protected
|
||||||
|
final Lock shouldReconnectLock = Lock();
|
||||||
|
|
||||||
/// Called by XmppConnection to register the policy.
|
/// Called by XmppConnection to register the policy.
|
||||||
void register(Future<void> Function() performReconnect, void Function() triggerConnectionLost) {
|
void register(
|
||||||
|
PerformReconnectFunction performReconnect,
|
||||||
|
ConnectionLostCallback triggerConnectionLost,
|
||||||
|
) {
|
||||||
this.performReconnect = performReconnect;
|
this.performReconnect = performReconnect;
|
||||||
this.triggerConnectionLost = triggerConnectionLost;
|
this.triggerConnectionLost = triggerConnectionLost;
|
||||||
|
|
||||||
unawaited(reset());
|
unawaited(reset());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// In case the policy depends on some internal state, this state must be reset
|
/// In case the policy depends on some internal state, this state must be reset
|
||||||
/// to an initial state when reset is called. In case timers run, they must be
|
/// to an initial state when reset is called. In case timers run, they must be
|
||||||
/// terminated.
|
/// terminated.
|
||||||
@@ -42,90 +58,131 @@ abstract class ReconnectionPolicy {
|
|||||||
/// Caled by the XmppConnection when the reconnection was successful.
|
/// Caled by the XmppConnection when the reconnection was successful.
|
||||||
Future<void> onSuccess();
|
Future<void> onSuccess();
|
||||||
|
|
||||||
bool get shouldReconnect => _shouldAttemptReconnection;
|
Future<bool> getShouldReconnect() async {
|
||||||
|
return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection);
|
||||||
|
}
|
||||||
|
|
||||||
/// Set whether a reconnection attempt should be made.
|
/// Set whether a reconnection attempt should be made.
|
||||||
void setShouldReconnect(bool value) {
|
Future<void> setShouldReconnect(bool value) async {
|
||||||
_shouldAttemptReconnection = value;
|
return 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
|
||||||
/// false.
|
/// false.
|
||||||
Future<bool> isReconnectionRunning() async {
|
Future<bool> isReconnectionRunning() async {
|
||||||
return _isReconnectingLock.synchronized(() => _isReconnecting);
|
return lock.synchronized(() => isReconnecting);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the _isReconnecting state to [value].
|
/// Set the isReconnecting state to [value].
|
||||||
@protected
|
@protected
|
||||||
Future<void> setIsReconnecting(bool value) async {
|
Future<void> setIsReconnecting(bool value) async {
|
||||||
await _isReconnectingLock.synchronized(() async {
|
await lock.synchronized(() async {
|
||||||
_isReconnecting = value;
|
isReconnecting = value;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
Future<bool> testAndSetIsReconnecting() async {
|
|
||||||
return _isReconnectingLock.synchronized(() {
|
|
||||||
if (_isReconnecting) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
_isReconnecting = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A simple reconnection strategy: Make the reconnection delays exponentially longer
|
/// A simple reconnection strategy: Make the reconnection delays exponentially longer
|
||||||
/// for every failed attempt.
|
/// for every failed attempt.
|
||||||
class ExponentialBackoffReconnectionPolicy extends ReconnectionPolicy {
|
/// NOTE: This ReconnectionPolicy may be broken
|
||||||
|
class RandomBackoffReconnectionPolicy extends ReconnectionPolicy {
|
||||||
|
RandomBackoffReconnectionPolicy(
|
||||||
|
this._minBackoffTime,
|
||||||
|
this._maxBackoffTime,
|
||||||
|
) : assert(
|
||||||
|
_minBackoffTime < _maxBackoffTime,
|
||||||
|
'_minBackoffTime must be smaller than _maxBackoffTime',
|
||||||
|
),
|
||||||
|
super();
|
||||||
|
|
||||||
ExponentialBackoffReconnectionPolicy()
|
/// The maximum time in seconds that a backoff should be.
|
||||||
: _counter = 0,
|
final int _maxBackoffTime;
|
||||||
_log = Logger('ExponentialBackoffReconnectionPolicy'),
|
|
||||||
super();
|
/// The minimum time in seconds that a backoff should be.
|
||||||
int _counter;
|
final int _minBackoffTime;
|
||||||
|
|
||||||
|
/// Backoff timer.
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
final Logger _log;
|
|
||||||
|
final Lock _timerLock = Lock();
|
||||||
|
|
||||||
|
/// Logger.
|
||||||
|
final Logger _log = Logger('RandomBackoffReconnectionPolicy');
|
||||||
|
|
||||||
|
/// Event queue
|
||||||
|
final AsyncQueue _eventQueue = AsyncQueue();
|
||||||
|
|
||||||
/// Called when the backoff expired
|
/// Called when the backoff expired
|
||||||
Future<void> _onTimerElapsed() async {
|
Future<void> _onTimerElapsed() async {
|
||||||
final isReconnecting = await isReconnectionRunning();
|
_log.fine('Timer elapsed. Waiting for lock');
|
||||||
if (shouldReconnect) {
|
await lock.synchronized(() async {
|
||||||
if (!isReconnecting) {
|
_log.fine('Lock aquired');
|
||||||
await performReconnect!();
|
if (!(await getShouldReconnect())) {
|
||||||
} else {
|
_log.fine(
|
||||||
// Should never happen.
|
'Backoff timer expired but getShouldReconnect() returned false',
|
||||||
_log.fine('Backoff timer expired but reconnection is running, so doing nothing.');
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (isReconnecting) {
|
||||||
|
_log.fine(
|
||||||
|
'Backoff timer expired but a reconnection is running, so doing nothing.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.fine('Triggering reconnect');
|
||||||
|
isReconnecting = true;
|
||||||
|
await performReconnect!();
|
||||||
|
});
|
||||||
|
|
||||||
|
await _timerLock.synchronized(() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _reset() async {
|
||||||
|
_log.finest('Resetting internal state');
|
||||||
|
|
||||||
|
await _timerLock.synchronized(() {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await setIsReconnecting(false);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> reset() async {
|
Future<void> reset() async {
|
||||||
_log.finest('Resetting internal state');
|
// ignore: unnecessary_lambdas
|
||||||
_counter = 0;
|
await _eventQueue.addJob(() => _reset());
|
||||||
await setIsReconnecting(false);
|
}
|
||||||
|
|
||||||
if (_timer != null) {
|
Future<void> _onFailure() async {
|
||||||
_timer!.cancel();
|
final shouldContinue = await _timerLock.synchronized(() {
|
||||||
_timer = null;
|
return _timer == null;
|
||||||
|
});
|
||||||
|
if (!shouldContinue) {
|
||||||
|
_log.finest(
|
||||||
|
'_onFailure: Not backing off since _timer is already running',
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final seconds =
|
||||||
|
Random().nextInt(_maxBackoffTime - _minBackoffTime) + _minBackoffTime;
|
||||||
|
_log.finest('Failure occured. Starting random backoff with ${seconds}s');
|
||||||
|
_timer?.cancel();
|
||||||
|
|
||||||
|
_timer = Timer(Duration(seconds: seconds), _onTimerElapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onFailure() async {
|
Future<void> onFailure() async {
|
||||||
_log.finest('Failure occured. Starting exponential backoff');
|
// ignore: unnecessary_lambdas
|
||||||
_counter++;
|
await _eventQueue.addJob(() => _onFailure());
|
||||||
await setIsReconnecting(true);
|
|
||||||
|
|
||||||
if (_timer != null) {
|
|
||||||
_timer!.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait at max 80 seconds.
|
|
||||||
final seconds = min(pow(2, _counter).toInt(), 80);
|
|
||||||
_timer = Timer(Duration(seconds: seconds), _onTimerElapsed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -134,7 +191,7 @@ class ExponentialBackoffReconnectionPolicy extends ReconnectionPolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A stub reconnection policy for tests
|
/// A stub reconnection policy for tests.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
class TestingReconnectionPolicy extends ReconnectionPolicy {
|
class TestingReconnectionPolicy extends ReconnectionPolicy {
|
||||||
TestingReconnectionPolicy() : super();
|
TestingReconnectionPolicy() : super();
|
||||||
@@ -148,3 +205,23 @@ class TestingReconnectionPolicy extends ReconnectionPolicy {
|
|||||||
@override
|
@override
|
||||||
Future<void> reset() async {}
|
Future<void> reset() async {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A reconnection policy for tests that waits a constant number of seconds before
|
||||||
|
/// attempting a reconnection.
|
||||||
|
@visibleForTesting
|
||||||
|
class TestingSleepReconnectionPolicy extends ReconnectionPolicy {
|
||||||
|
TestingSleepReconnectionPolicy(this._sleepAmount) : super();
|
||||||
|
final int _sleepAmount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onSuccess() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onFailure() async {
|
||||||
|
await Future<void>.delayed(Duration(seconds: _sleepAmount));
|
||||||
|
await performReconnect!();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> reset() async {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,31 @@ int ioctetSortComparator(String a, String b) {
|
|||||||
if (a.codeUnitAt(0) < b.codeUnitAt(0)) {
|
if (a.codeUnitAt(0) < b.codeUnitAt(0)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ioctetSortComparatorRaw(List<int> a, List<int> b) {
|
||||||
|
if (a.isEmpty && b.isEmpty) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.isEmpty && b.isNotEmpty) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.isNotEmpty && b.isEmpty) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a[0] == b[0]) {
|
||||||
|
return ioctetSortComparatorRaw(a.sublist(1), b.sublist(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(Unknown): Is this correct?
|
||||||
|
if (a[0] < b[0]) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
7
packages/moxxmpp/lib/src/roster/errors.dart
Normal file
7
packages/moxxmpp/lib/src/roster/errors.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
abstract class RosterError {}
|
||||||
|
|
||||||
|
/// Returned when the server's response did not contain a <query /> element
|
||||||
|
class NoQueryError extends RosterError {}
|
||||||
|
|
||||||
|
/// Unspecified error
|
||||||
|
class UnknownError extends RosterError {}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import 'package:moxxmpp/src/events.dart';
|
import 'dart:async';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
|
import 'package:moxxmpp/src/managers/attributes.dart';
|
||||||
import 'package:moxxmpp/src/managers/base.dart';
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
import 'package:moxxmpp/src/managers/data.dart';
|
import 'package:moxxmpp/src/managers/data.dart';
|
||||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||||
@@ -7,57 +10,88 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
|
import 'package:moxxmpp/src/roster/errors.dart';
|
||||||
|
import 'package:moxxmpp/src/roster/state.dart';
|
||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
import 'package:moxxmpp/src/types/error.dart';
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
|
|
||||||
const rosterErrorNoQuery = 1;
|
|
||||||
const rosterErrorNonResult = 2;
|
|
||||||
|
|
||||||
|
@immutable
|
||||||
class XmppRosterItem {
|
class XmppRosterItem {
|
||||||
|
const XmppRosterItem({
|
||||||
XmppRosterItem({ required this.jid, required this.subscription, this.ask, this.name, this.groups = const [] });
|
required this.jid,
|
||||||
|
required this.subscription,
|
||||||
|
this.ask,
|
||||||
|
this.name,
|
||||||
|
this.groups = const [],
|
||||||
|
});
|
||||||
final String jid;
|
final String jid;
|
||||||
final String? name;
|
final String? name;
|
||||||
final String subscription;
|
final String subscription;
|
||||||
final String? ask;
|
final String? ask;
|
||||||
final List<String> groups;
|
final List<String> groups;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is XmppRosterItem &&
|
||||||
|
other.jid == jid &&
|
||||||
|
other.name == name &&
|
||||||
|
other.subscription == subscription &&
|
||||||
|
other.ask == ask &&
|
||||||
|
const ListEquality<String>().equals(other.groups, groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
jid.hashCode ^
|
||||||
|
name.hashCode ^
|
||||||
|
subscription.hashCode ^
|
||||||
|
ask.hashCode ^
|
||||||
|
groups.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'XmppRosterItem('
|
||||||
|
'jid: $jid, '
|
||||||
|
'name: $name, '
|
||||||
|
'subscription: $subscription, '
|
||||||
|
'ask: $ask, '
|
||||||
|
'groups: $groups)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RosterRemovalResult {
|
enum RosterRemovalResult { okay, error, itemNotFound }
|
||||||
okay,
|
|
||||||
error,
|
|
||||||
itemNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
class RosterRequestResult {
|
class RosterRequestResult {
|
||||||
|
RosterRequestResult(this.items, this.ver);
|
||||||
RosterRequestResult({ required this.items, this.ver });
|
|
||||||
List<XmppRosterItem> items;
|
List<XmppRosterItem> items;
|
||||||
String? ver;
|
String? ver;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RosterPushEvent extends XmppEvent {
|
class RosterPushResult {
|
||||||
|
RosterPushResult(this.item, this.ver);
|
||||||
RosterPushEvent({ required this.item, this.ver });
|
|
||||||
final XmppRosterItem item;
|
final XmppRosterItem item;
|
||||||
final String? ver;
|
final String? ver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Stub feature negotiator for finding out whether roster versioning is supported.
|
/// A Stub feature negotiator for finding out whether roster versioning is supported.
|
||||||
class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase {
|
class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase {
|
||||||
RosterFeatureNegotiator() : _supported = false, super(11, false, rosterVersioningXmlns, rosterNegotiator);
|
RosterFeatureNegotiator()
|
||||||
|
: _supported = false,
|
||||||
|
super(11, false, rosterVersioningXmlns, rosterNegotiator);
|
||||||
|
|
||||||
/// True if rosterVersioning is supported. False otherwise.
|
/// True if rosterVersioning is supported. False otherwise.
|
||||||
bool _supported;
|
bool _supported;
|
||||||
bool get isSupported => _supported;
|
bool get isSupported => _supported;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> negotiate(XMLNode nonza) async {
|
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||||
|
XMLNode nonza,
|
||||||
|
) async {
|
||||||
// negotiate is only called when the negotiator matched, meaning the server
|
// negotiate is only called when the negotiator matched, meaning the server
|
||||||
// advertises roster versioning.
|
// advertises roster versioning.
|
||||||
_supported = true;
|
_supported = true;
|
||||||
state = NegotiatorState.done;
|
return const Result(NegotiatorState.done);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -70,40 +104,34 @@ class RosterFeatureNegotiator extends XmppFeatureNegotiatorBase {
|
|||||||
|
|
||||||
/// This manager requires a RosterFeatureNegotiator to be registered.
|
/// This manager requires a RosterFeatureNegotiator to be registered.
|
||||||
class RosterManager extends XmppManagerBase {
|
class RosterManager extends XmppManagerBase {
|
||||||
|
RosterManager(this._stateManager) : super(rosterManager);
|
||||||
|
|
||||||
RosterManager() : _rosterVersion = null, super();
|
/// The class managing the entire roster state.
|
||||||
String? _rosterVersion;
|
final BaseRosterStateManager _stateManager;
|
||||||
|
|
||||||
@override
|
|
||||||
String getId() => rosterManager;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getName() => 'RosterManager';
|
void register(XmppManagerAttributes attributes) {
|
||||||
|
super.register(attributes);
|
||||||
|
_stateManager.register(attributes.sendEvent);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'iq',
|
stanzaTag: 'iq',
|
||||||
tagName: 'query',
|
tagName: 'query',
|
||||||
tagXmlns: rosterXmlns,
|
tagXmlns: rosterXmlns,
|
||||||
callback: _onRosterPush,
|
callback: _onRosterPush,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
/// Override-able functions
|
|
||||||
Future<void> commitLastRosterVersion(String version) async {}
|
|
||||||
Future<void> loadLastRosterVersion() async {}
|
|
||||||
|
|
||||||
void setRosterVersion(String ver) {
|
Future<StanzaHandlerData> _onRosterPush(
|
||||||
assert(_rosterVersion == null, 'A roster version must not be empty');
|
Stanza stanza,
|
||||||
|
StanzaHandlerData state,
|
||||||
_rosterVersion = ver;
|
) 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;
|
||||||
@@ -114,11 +142,14 @@ class RosterManager extends XmppManagerBase {
|
|||||||
// - empty, i.e. not set
|
// - empty, i.e. not set
|
||||||
// - a full JID of our own
|
// - a full JID of our own
|
||||||
if (from != null && JID.fromString(from).toBare() != selfJid) {
|
if (from != null && JID.fromString(from).toBare() != selfJid) {
|
||||||
logger.warning('Roster push invalid! Unexpected from attribute: ${stanza.toXml()}');
|
logger.warning(
|
||||||
|
'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}',
|
||||||
|
);
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
final query = stanza.firstTag('query', xmlns: rosterXmlns)!;
|
final query = stanza.firstTag('query', xmlns: rosterXmlns)!;
|
||||||
|
logger.fine('Roster push: ${query.toXml()}');
|
||||||
final item = query.firstTag('item');
|
final item = query.firstTag('item');
|
||||||
|
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
@@ -126,95 +157,106 @@ class RosterManager extends XmppManagerBase {
|
|||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.attributes['ver'] != null) {
|
unawaited(
|
||||||
final ver = query.attributes['ver']! as String;
|
_stateManager.handleRosterPush(
|
||||||
await commitLastRosterVersion(ver);
|
RosterPushResult(
|
||||||
_rosterVersion = ver;
|
XmppRosterItem(
|
||||||
}
|
jid: item.attributes['jid']! as String,
|
||||||
|
subscription: item.attributes['subscription']! as String,
|
||||||
attrs.sendEvent(RosterPushEvent(
|
ask: item.attributes['ask'] as String?,
|
||||||
item: XmppRosterItem(
|
name: item.attributes['name'] as String?,
|
||||||
jid: item.attributes['jid']! as String,
|
),
|
||||||
subscription: item.attributes['subscription']! as String,
|
query.attributes['ver'] as String?,
|
||||||
ask: item.attributes['ask'] as String?,
|
),
|
||||||
name: item.attributes['name'] as String?,
|
|
||||||
),
|
),
|
||||||
ver: query.attributes['ver'] as String?,
|
);
|
||||||
),);
|
|
||||||
await attrs.sendStanza(stanza.reply());
|
await reply(
|
||||||
|
state,
|
||||||
|
'result',
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared code between requesting rosters without and with roster versioning, if
|
/// Shared code between requesting rosters without and with roster versioning, if
|
||||||
/// the server deems a regular roster response more efficient than n roster pushes.
|
/// the server deems a regular roster response more efficient than n roster pushes.
|
||||||
Future<MayFail<RosterRequestResult>> _handleRosterResponse(XMLNode? query) async {
|
Future<Result<RosterRequestResult, RosterError>> _handleRosterResponse(
|
||||||
|
XMLNode? query,
|
||||||
|
) async {
|
||||||
final List<XmppRosterItem> items;
|
final List<XmppRosterItem> items;
|
||||||
|
String? rosterVersion;
|
||||||
if (query != null) {
|
if (query != null) {
|
||||||
items = query.children.map((item) => XmppRosterItem(
|
items = query.children
|
||||||
name: item.attributes['name'] as String?,
|
.map(
|
||||||
jid: item.attributes['jid']! as String,
|
(item) => XmppRosterItem(
|
||||||
subscription: item.attributes['subscription']! as String,
|
name: item.attributes['name'] as String?,
|
||||||
ask: item.attributes['ask'] as String?,
|
jid: item.attributes['jid']! as String,
|
||||||
groups: item.findTags('group').map((groupNode) => groupNode.innerText()).toList(),
|
subscription: item.attributes['subscription']! as String,
|
||||||
),).toList();
|
ask: item.attributes['ask'] as String?,
|
||||||
|
groups: item
|
||||||
|
.findTags('group')
|
||||||
|
.map((groupNode) => groupNode.innerText())
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (query.attributes['ver'] != null) {
|
rosterVersion = query.attributes['ver'] as String?;
|
||||||
final ver_ = query.attributes['ver']! as String;
|
|
||||||
await commitLastRosterVersion(ver_);
|
|
||||||
_rosterVersion = ver_;
|
|
||||||
}
|
|
||||||
} 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(
|
||||||
return MayFail.failure(rosterErrorNoQuery);
|
'Server response to roster request without roster versioning does not contain a <query /> element, while the type is not error. This violates RFC6121',
|
||||||
|
);
|
||||||
|
return Result(NoQueryError());
|
||||||
}
|
}
|
||||||
|
|
||||||
final ver = query.attributes['ver'] as String?;
|
final result = RosterRequestResult(
|
||||||
if (ver != null) {
|
items,
|
||||||
_rosterVersion = ver;
|
rosterVersion,
|
||||||
await commitLastRosterVersion(ver);
|
|
||||||
}
|
|
||||||
|
|
||||||
return MayFail.success(
|
|
||||||
RosterRequestResult(
|
|
||||||
items: items,
|
|
||||||
ver: ver,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
_stateManager.handleRosterFetch(result),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Result(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Requests the roster following RFC 6121 without using roster versioning.
|
/// Requests the roster following RFC 6121.
|
||||||
Future<MayFail<RosterRequestResult>> requestRoster() async {
|
Future<Result<RosterRequestResult, RosterError>> requestRoster() async {
|
||||||
final attrs = getAttributes();
|
final attrs = getAttributes();
|
||||||
|
final query = XMLNode.xmlns(
|
||||||
|
tag: 'query',
|
||||||
|
xmlns: rosterXmlns,
|
||||||
|
);
|
||||||
|
final rosterVersion = await _stateManager.getRosterVersion();
|
||||||
|
if (rosterVersion != null && rosterVersioningAvailable()) {
|
||||||
|
query.attributes['ver'] = rosterVersion;
|
||||||
|
}
|
||||||
|
|
||||||
final response = await attrs.sendStanza(
|
final response = await attrs.sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
type: 'get',
|
type: 'get',
|
||||||
children: [
|
children: [
|
||||||
XMLNode.xmlns(
|
query,
|
||||||
tag: 'query',
|
|
||||||
xmlns: rosterXmlns,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.attributes['type'] != 'result') {
|
if (response.attributes['type'] != 'result') {
|
||||||
logger.warning('Error requesting roster without roster versioning: ${response.toXml()}');
|
logger.warning('Error requesting roster: ${response.toXml()}');
|
||||||
return MayFail.failure(rosterErrorNonResult);
|
return Result(UnknownError());
|
||||||
}
|
}
|
||||||
|
|
||||||
final query = response.firstTag('query', xmlns: rosterXmlns);
|
final responseQuery = response.firstTag('query', xmlns: rosterXmlns);
|
||||||
return _handleRosterResponse(query);
|
return _handleRosterResponse(responseQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Requests a series of roster pushes according to RFC6121. Requires that the server
|
/// Requests a series of roster pushes according to RFC6121. Requires that the server
|
||||||
/// advertises urn:xmpp:features:rosterver in the stream features.
|
/// advertises urn:xmpp:features:rosterver in the stream features.
|
||||||
Future<MayFail<RosterRequestResult?>> requestRosterPushes() async {
|
Future<Result<RosterRequestResult?, RosterError>>
|
||||||
if (_rosterVersion == null) {
|
requestRosterPushes() async {
|
||||||
await loadLastRosterVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
final attrs = getAttributes();
|
final attrs = getAttributes();
|
||||||
final result = await attrs.sendStanza(
|
final result = await attrs.sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
@@ -224,7 +266,7 @@ class RosterManager extends XmppManagerBase {
|
|||||||
tag: 'query',
|
tag: 'query',
|
||||||
xmlns: rosterXmlns,
|
xmlns: rosterXmlns,
|
||||||
attributes: {
|
attributes: {
|
||||||
'ver': _rosterVersion ?? ''
|
'ver': await _stateManager.getRosterVersion() ?? '',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -233,7 +275,7 @@ class RosterManager extends XmppManagerBase {
|
|||||||
|
|
||||||
if (result.attributes['type'] != 'result') {
|
if (result.attributes['type'] != 'result') {
|
||||||
logger.warning('Requesting roster pushes failed: ${result.toXml()}');
|
logger.warning('Requesting roster pushes failed: ${result.toXml()}');
|
||||||
return MayFail.failure(rosterErrorNonResult);
|
return Result(UnknownError());
|
||||||
}
|
}
|
||||||
|
|
||||||
final query = result.firstTag('query', xmlns: rosterXmlns);
|
final query = result.firstTag('query', xmlns: rosterXmlns);
|
||||||
@@ -241,12 +283,18 @@ class RosterManager extends XmppManagerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool rosterVersioningAvailable() {
|
bool rosterVersioningAvailable() {
|
||||||
return getAttributes().getNegotiatorById<RosterFeatureNegotiator>(rosterNegotiator)!.isSupported;
|
return getAttributes()
|
||||||
|
.getNegotiatorById<RosterFeatureNegotiator>(rosterNegotiator)!
|
||||||
|
.isSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to add [jid] with a title of [title] and groups [groups] to the roster.
|
/// Attempts to add [jid] with a title of [title] and groups [groups] to the roster.
|
||||||
/// Returns true if the process was successful, false otherwise.
|
/// Returns true if the process was successful, false otherwise.
|
||||||
Future<bool> addToRoster(String jid, String title, { List<String>? groups }) async {
|
Future<bool> addToRoster(
|
||||||
|
String jid,
|
||||||
|
String title, {
|
||||||
|
List<String>? groups,
|
||||||
|
}) async {
|
||||||
final attrs = getAttributes();
|
final attrs = getAttributes();
|
||||||
final response = await attrs.sendStanza(
|
final response = await attrs.sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
@@ -260,9 +308,13 @@ class RosterManager extends XmppManagerBase {
|
|||||||
tag: 'item',
|
tag: 'item',
|
||||||
attributes: <String, String>{
|
attributes: <String, String>{
|
||||||
'jid': jid,
|
'jid': jid,
|
||||||
...title == jid.split('@')[0] ? <String, String>{} : <String, String>{ 'name': title }
|
...title == jid.split('@')[0]
|
||||||
|
? <String, String>{}
|
||||||
|
: <String, String>{'name': title}
|
||||||
},
|
},
|
||||||
children: (groups ?? []).map((group) => XMLNode(tag: 'group', text: group)).toList(),
|
children: (groups ?? [])
|
||||||
|
.map((group) => XMLNode(tag: 'group', text: group))
|
||||||
|
.toList(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
235
packages/moxxmpp/lib/src/roster/state.dart
Normal file
235
packages/moxxmpp/lib/src/roster/state.dart
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:moxxmpp/src/events.dart';
|
||||||
|
import 'package:moxxmpp/src/roster/roster.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
|
class _RosterProcessTriple {
|
||||||
|
const _RosterProcessTriple(this.removed, this.modified, this.added);
|
||||||
|
final String? removed;
|
||||||
|
final XmppRosterItem? modified;
|
||||||
|
final XmppRosterItem? added;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RosterCacheLoadResult {
|
||||||
|
const RosterCacheLoadResult(this.version, this.roster);
|
||||||
|
final String? version;
|
||||||
|
final List<XmppRosterItem> roster;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This class manages the roster state in order to correctly process and persist
|
||||||
|
/// roster pushes and facilitate roster versioning requests.
|
||||||
|
abstract class BaseRosterStateManager {
|
||||||
|
/// The cached version of the roster. If null, then it has not been loaded yet.
|
||||||
|
List<XmppRosterItem>? _currentRoster;
|
||||||
|
|
||||||
|
/// The cached version of the roster version.
|
||||||
|
String? _currentVersion;
|
||||||
|
|
||||||
|
/// A critical section locking both _currentRoster and _currentVersion.
|
||||||
|
final Lock _lock = Lock();
|
||||||
|
|
||||||
|
/// A function to send an XmppEvent to moxxmpp's main event bus
|
||||||
|
late void Function(XmppEvent) _sendEvent;
|
||||||
|
|
||||||
|
/// Overrideable function
|
||||||
|
/// Loads the old cached version of the roster and optionally that roster version
|
||||||
|
/// from persistent storage into a RosterCacheLoadResult object.
|
||||||
|
Future<RosterCacheLoadResult> loadRosterCache();
|
||||||
|
|
||||||
|
/// Overrideable function
|
||||||
|
/// Commits the roster data to persistent storage.
|
||||||
|
///
|
||||||
|
/// [version] is the roster version string. If none was provided, then this value
|
||||||
|
/// is null.
|
||||||
|
///
|
||||||
|
/// [removed] is a (possibly empty) list of bare JIDs that are removed from the
|
||||||
|
/// roster.
|
||||||
|
///
|
||||||
|
/// [modified] is a (possibly empty) list of XmppRosterItems that are modified. Correlation with
|
||||||
|
/// the cache is done using its jid attribute.
|
||||||
|
///
|
||||||
|
/// [added] is a (possibly empty) list of XmppRosterItems that are added by the
|
||||||
|
/// roster push or roster fetch request.
|
||||||
|
Future<void> commitRoster(
|
||||||
|
String? version,
|
||||||
|
List<String> removed,
|
||||||
|
List<XmppRosterItem> modified,
|
||||||
|
List<XmppRosterItem> added,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Internal function. Registers functions from the RosterManger against this
|
||||||
|
/// instance.
|
||||||
|
void register(void Function(XmppEvent) sendEvent) {
|
||||||
|
_sendEvent = sendEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load and cache or return the cached roster version.
|
||||||
|
Future<String?> getRosterVersion() async {
|
||||||
|
return _lock.synchronized(() async {
|
||||||
|
await _loadRosterCache();
|
||||||
|
|
||||||
|
return _currentVersion;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around _commitRoster that also sends an event to moxxmpp's event
|
||||||
|
/// bus.
|
||||||
|
Future<void> _commitRoster(
|
||||||
|
String? version,
|
||||||
|
List<String> removed,
|
||||||
|
List<XmppRosterItem> modified,
|
||||||
|
List<XmppRosterItem> added,
|
||||||
|
) async {
|
||||||
|
_sendEvent(
|
||||||
|
RosterUpdatedEvent(
|
||||||
|
removed,
|
||||||
|
modified,
|
||||||
|
added,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await commitRoster(version, removed, modified, added);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the cached roster data into memory, if that has not already happened.
|
||||||
|
/// NOTE: Must be called from within the _lock critical section.
|
||||||
|
Future<void> _loadRosterCache() async {
|
||||||
|
if (_currentRoster == null) {
|
||||||
|
final result = await loadRosterCache();
|
||||||
|
|
||||||
|
_currentRoster = result.roster;
|
||||||
|
_currentVersion = result.version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processes only single XmppRosterItem [item].
|
||||||
|
/// NOTE: Requires to be called from within the _lock critical section.
|
||||||
|
_RosterProcessTriple _handleRosterItem(XmppRosterItem item) {
|
||||||
|
if (item.subscription == 'remove') {
|
||||||
|
// The item has been removed
|
||||||
|
_currentRoster!.removeWhere((i) => i.jid == item.jid);
|
||||||
|
return _RosterProcessTriple(
|
||||||
|
item.jid,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final index = _currentRoster!.indexWhere((i) => i.jid == item.jid);
|
||||||
|
if (index == -1) {
|
||||||
|
// The item does not exist
|
||||||
|
_currentRoster!.add(item);
|
||||||
|
return _RosterProcessTriple(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
} else if (_currentRoster![index] != item) {
|
||||||
|
// The item is updated
|
||||||
|
_currentRoster![index] = item;
|
||||||
|
return _RosterProcessTriple(
|
||||||
|
null,
|
||||||
|
item,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item has not been modified or added
|
||||||
|
return const _RosterProcessTriple(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles a roster push from the RosterManager.
|
||||||
|
Future<void> handleRosterPush(RosterPushResult event) async {
|
||||||
|
await _lock.synchronized(() async {
|
||||||
|
await _loadRosterCache();
|
||||||
|
|
||||||
|
_currentVersion = event.ver;
|
||||||
|
final result = _handleRosterItem(event.item);
|
||||||
|
|
||||||
|
if (result.removed != null) {
|
||||||
|
return _commitRoster(
|
||||||
|
_currentVersion,
|
||||||
|
[result.removed!],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
} else if (result.modified != null) {
|
||||||
|
return _commitRoster(
|
||||||
|
_currentVersion,
|
||||||
|
[],
|
||||||
|
[result.modified!],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
} else if (result.added != null) {
|
||||||
|
return _commitRoster(
|
||||||
|
_currentVersion,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[result.added!],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles the result from a roster fetch.
|
||||||
|
Future<void> handleRosterFetch(RosterRequestResult result) async {
|
||||||
|
await _lock.synchronized(() async {
|
||||||
|
final removed = List<String>.empty(growable: true);
|
||||||
|
final modified = List<XmppRosterItem>.empty(growable: true);
|
||||||
|
final added = List<XmppRosterItem>.empty(growable: true);
|
||||||
|
|
||||||
|
await _loadRosterCache();
|
||||||
|
|
||||||
|
_currentVersion = result.ver;
|
||||||
|
for (final item in result.items) {
|
||||||
|
final result = _handleRosterItem(item);
|
||||||
|
|
||||||
|
if (result.removed != null) removed.add(result.removed!);
|
||||||
|
if (result.modified != null) modified.add(result.modified!);
|
||||||
|
if (result.added != null) added.add(result.added!);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _commitRoster(
|
||||||
|
_currentVersion,
|
||||||
|
removed,
|
||||||
|
modified,
|
||||||
|
added,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
List<XmppRosterItem> getRosterItems() => _currentRoster!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
class TestingRosterStateManager extends BaseRosterStateManager {
|
||||||
|
TestingRosterStateManager(
|
||||||
|
this.initialRosterVersion,
|
||||||
|
this.initialRoster,
|
||||||
|
);
|
||||||
|
final String? initialRosterVersion;
|
||||||
|
final List<XmppRosterItem> initialRoster;
|
||||||
|
int loadCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RosterCacheLoadResult> loadRosterCache() async {
|
||||||
|
loadCount++;
|
||||||
|
return RosterCacheLoadResult(
|
||||||
|
initialRosterVersion,
|
||||||
|
initialRoster,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> commitRoster(
|
||||||
|
String? version,
|
||||||
|
List<String> removed,
|
||||||
|
List<XmppRosterItem> modified,
|
||||||
|
List<XmppRosterItem> added,
|
||||||
|
) async {}
|
||||||
|
}
|
||||||
@@ -1,6 +1 @@
|
|||||||
enum RoutingState {
|
enum RoutingState { error, preConnection, negotiating, handleStanzas }
|
||||||
error,
|
|
||||||
preConnection,
|
|
||||||
negotiating,
|
|
||||||
handleStanzas
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +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,
|
||||||
|
});
|
||||||
final JID jid;
|
final JID jid;
|
||||||
final String password;
|
final String password;
|
||||||
final bool useDirectTLS;
|
final bool useDirectTLS;
|
||||||
final bool allowPlainAuth;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ abstract class XmppSocketEvent {}
|
|||||||
|
|
||||||
/// Triggered by the socket when an error occurs.
|
/// Triggered by the socket when an error occurs.
|
||||||
class XmppSocketErrorEvent extends XmppSocketEvent {
|
class XmppSocketErrorEvent extends XmppSocketEvent {
|
||||||
|
|
||||||
XmppSocketErrorEvent(this.error);
|
XmppSocketErrorEvent(this.error);
|
||||||
final Object error;
|
final Object error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Triggered when the socket is closed
|
/// Triggered when the socket is closed
|
||||||
class XmppSocketClosureEvent extends XmppSocketEvent {}
|
class XmppSocketClosureEvent extends XmppSocketEvent {
|
||||||
|
XmppSocketClosureEvent(this.expected);
|
||||||
|
|
||||||
|
/// Indicate that the socket did not close unexpectedly.
|
||||||
|
final bool expected;
|
||||||
|
}
|
||||||
|
|
||||||
/// This class is the base for a socket that XmppConnection can use.
|
/// This class is the base for a socket that XmppConnection can use.
|
||||||
abstract class BaseSocketWrapper {
|
abstract class BaseSocketWrapper {
|
||||||
@@ -21,20 +25,20 @@ abstract class BaseSocketWrapper {
|
|||||||
/// This must return events generated by the socket.
|
/// This must return events generated by the socket.
|
||||||
/// See sub-classes of [XmppSocketEvent] for possible events.
|
/// See sub-classes of [XmppSocketEvent] for possible events.
|
||||||
Stream<XmppSocketEvent> getEventStream();
|
Stream<XmppSocketEvent> getEventStream();
|
||||||
|
|
||||||
/// This must close the socket but not the streams so that the same class can be
|
/// This must close the socket but not the streams so that the same class can be
|
||||||
/// reused by calling [this.connect] again.
|
/// reused by calling [this.connect] again.
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
/// Write [data] into the socket. If [redact] is not null, then [redact] will be
|
/// Write [data] into the socket. 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();
|
||||||
|
|||||||
@@ -1,61 +1,107 @@
|
|||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
|
||||||
|
/// A simple description of the <error /> element that may be inside a stanza
|
||||||
|
class StanzaError {
|
||||||
|
StanzaError(this.type, this.error);
|
||||||
|
String type;
|
||||||
|
String error;
|
||||||
|
|
||||||
|
/// Returns a StanzaError if [stanza] contains a <error /> element. If not, returns
|
||||||
|
/// null.
|
||||||
|
static StanzaError? fromStanza(Stanza stanza) {
|
||||||
|
final error = stanza.firstTag('error');
|
||||||
|
if (error == null) return null;
|
||||||
|
|
||||||
|
final stanzaError = error.firstTagByXmlns(fullStanzaXmlns);
|
||||||
|
if (stanzaError == null) return null;
|
||||||
|
|
||||||
|
return StanzaError(
|
||||||
|
error.attributes['type']! as String,
|
||||||
|
stanzaError.tag,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Stanza extends XMLNode {
|
class Stanza extends XMLNode {
|
||||||
// ignore: use_super_parameters
|
// ignore: use_super_parameters
|
||||||
Stanza({ this.to, this.from, this.type, this.id, List<XMLNode> children = const [], required String tag, Map<String, String> attributes = const {} }) : super(
|
Stanza({
|
||||||
tag: tag,
|
this.to,
|
||||||
attributes: <String, dynamic>{
|
this.from,
|
||||||
...attributes,
|
this.type,
|
||||||
...type != null ? <String, dynamic>{ 'type': type } : <String, dynamic>{},
|
this.id,
|
||||||
...id != null ? <String, dynamic>{ 'id': id } : <String, dynamic>{},
|
List<XMLNode> children = const [],
|
||||||
...to != null ? <String, dynamic>{ 'to': to } : <String, dynamic>{},
|
required String tag,
|
||||||
...from != null ? <String, dynamic>{ 'from': from } : <String, dynamic>{},
|
Map<String, String> attributes = const {},
|
||||||
'xmlns': stanzaXmlns
|
}) : super(
|
||||||
},
|
tag: tag,
|
||||||
children: children,
|
attributes: <String, dynamic>{
|
||||||
);
|
...attributes,
|
||||||
|
...type != null
|
||||||
|
? <String, dynamic>{'type': type}
|
||||||
|
: <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
|
||||||
|
},
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
|
||||||
factory Stanza.iq({ String? to, String? from, String? type, String? id, List<XMLNode> children = const [], Map<String, String>? attributes = const {} }) {
|
factory Stanza.iq({
|
||||||
|
String? to,
|
||||||
|
String? from,
|
||||||
|
String? type,
|
||||||
|
String? id,
|
||||||
|
List<XMLNode> children = const [],
|
||||||
|
Map<String, String>? attributes = const {},
|
||||||
|
}) {
|
||||||
return Stanza(
|
return Stanza(
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
from: from,
|
from: from,
|
||||||
to: to,
|
to: to,
|
||||||
id: id,
|
id: id,
|
||||||
type: type,
|
type: type,
|
||||||
attributes: <String, String>{
|
attributes: <String, String>{...attributes!, 'xmlns': stanzaXmlns},
|
||||||
...attributes!,
|
|
||||||
'xmlns': stanzaXmlns
|
|
||||||
},
|
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Stanza.presence({ String? to, String? from, String? type, String? id, List<XMLNode> children = const [], Map<String, String>? attributes = const {} }) {
|
factory Stanza.presence({
|
||||||
|
String? to,
|
||||||
|
String? from,
|
||||||
|
String? type,
|
||||||
|
String? id,
|
||||||
|
List<XMLNode> children = const [],
|
||||||
|
Map<String, String>? attributes = const {},
|
||||||
|
}) {
|
||||||
return Stanza(
|
return Stanza(
|
||||||
tag: 'presence',
|
tag: 'presence',
|
||||||
from: from,
|
from: from,
|
||||||
to: to,
|
to: to,
|
||||||
id: id,
|
id: id,
|
||||||
type: type,
|
type: type,
|
||||||
attributes: <String, String>{
|
attributes: <String, String>{...attributes!, 'xmlns': stanzaXmlns},
|
||||||
...attributes!,
|
|
||||||
'xmlns': stanzaXmlns
|
|
||||||
},
|
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
factory Stanza.message({ String? to, String? from, String? type, String? id, List<XMLNode> children = const [], Map<String, String>? attributes = const {} }) {
|
factory Stanza.message({
|
||||||
|
String? to,
|
||||||
|
String? from,
|
||||||
|
String? type,
|
||||||
|
String? id,
|
||||||
|
List<XMLNode> children = const [],
|
||||||
|
Map<String, String>? attributes = const {},
|
||||||
|
}) {
|
||||||
return Stanza(
|
return Stanza(
|
||||||
tag: 'message',
|
tag: 'message',
|
||||||
from: from,
|
from: from,
|
||||||
to: to,
|
to: to,
|
||||||
id: id,
|
id: id,
|
||||||
type: type,
|
type: type,
|
||||||
attributes: <String, String>{
|
attributes: <String, String>{...attributes!, 'xmlns': stanzaXmlns},
|
||||||
...attributes!,
|
|
||||||
'xmlns': stanzaXmlns
|
|
||||||
},
|
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,10 +116,10 @@ class Stanza extends XMLNode {
|
|||||||
children: node.children,
|
children: node.children,
|
||||||
// TODO(Unknown): Remove to, from, id, and type
|
// TODO(Unknown): Remove to, from, id, and type
|
||||||
// TODO(Unknown): Not sure if this is the correct way to approach this
|
// TODO(Unknown): Not sure if this is the correct way to approach this
|
||||||
attributes: node.attributes
|
attributes:
|
||||||
.map<String, String>((String key, dynamic value) {
|
node.attributes.map<String, String>((String key, dynamic value) {
|
||||||
return MapEntry(key, value.toString());
|
return MapEntry(key, value.toString());
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +128,13 @@ class Stanza extends XMLNode {
|
|||||||
String? type;
|
String? type;
|
||||||
String? id;
|
String? id;
|
||||||
|
|
||||||
Stanza copyWith({ String? id, String? from, String? to, String? type, List<XMLNode>? children }) {
|
Stanza copyWith({
|
||||||
|
String? id,
|
||||||
|
String? from,
|
||||||
|
String? to,
|
||||||
|
String? type,
|
||||||
|
List<XMLNode>? children,
|
||||||
|
}) {
|
||||||
return Stanza(
|
return Stanza(
|
||||||
tag: tag,
|
tag: tag,
|
||||||
to: to ?? this.to,
|
to: to ?? this.to,
|
||||||
@@ -92,40 +144,29 @@ class Stanza extends XMLNode {
|
|||||||
children: children ?? this.children,
|
children: children ?? this.children,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Stanza reply({ List<XMLNode> children = const [] }) {
|
|
||||||
return copyWith(
|
|
||||||
from: attributes['to'] as String?,
|
|
||||||
to: attributes['from'] as String?,
|
|
||||||
type: tag == 'iq' ? 'result' : attributes['type'] as String?,
|
|
||||||
children: children,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Stanza errorReply(String type, String condition, { String? text }) {
|
/// Build an <error /> element with a child <[condition] type="[type]" />. If [text]
|
||||||
return copyWith(
|
/// is not null, then the condition element will contain a <text /> element with [text]
|
||||||
from: attributes['to'] as String?,
|
/// as the body.
|
||||||
to: attributes['from'] as String?,
|
XMLNode buildErrorElement(String type, String condition, {String? text}) {
|
||||||
type: 'error',
|
return XMLNode(
|
||||||
children: [
|
tag: 'error',
|
||||||
XMLNode(
|
attributes: <String, dynamic>{'type': type},
|
||||||
tag: 'error',
|
children: [
|
||||||
attributes: <String, dynamic>{ 'type': type },
|
XMLNode.xmlns(
|
||||||
children: [
|
tag: condition,
|
||||||
XMLNode.xmlns(
|
xmlns: fullStanzaXmlns,
|
||||||
tag: condition,
|
children: text != null
|
||||||
xmlns: fullStanzaXmlns,
|
? [
|
||||||
children: text != null ?[
|
|
||||||
XMLNode.xmlns(
|
XMLNode.xmlns(
|
||||||
tag: 'text',
|
tag: 'text',
|
||||||
xmlns: fullStanzaXmlns,
|
xmlns: fullStanzaXmlns,
|
||||||
text: text,
|
text: text,
|
||||||
)
|
)
|
||||||
] : [],
|
]
|
||||||
)
|
: [],
|
||||||
],
|
),
|
||||||
)
|
],
|
||||||
],
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:xml/xml.dart';
|
import 'package:xml/xml.dart';
|
||||||
|
|
||||||
class XMLNode {
|
class XMLNode {
|
||||||
|
|
||||||
XMLNode({
|
XMLNode({
|
||||||
required this.tag,
|
required this.tag,
|
||||||
this.attributes = const <String, dynamic>{},
|
this.attributes = const <String, dynamic>{},
|
||||||
@@ -11,13 +10,15 @@ class XMLNode {
|
|||||||
this.isDeclaration = false,
|
this.isDeclaration = false,
|
||||||
});
|
});
|
||||||
XMLNode.xmlns({
|
XMLNode.xmlns({
|
||||||
required this.tag,
|
required this.tag,
|
||||||
required String xmlns,
|
required String xmlns,
|
||||||
Map<String, String> attributes = const <String, String>{},
|
Map<String, String> attributes = const <String, String>{},
|
||||||
this.children = const [],
|
this.children = const [],
|
||||||
this.closeTag = true,
|
this.closeTag = true,
|
||||||
this.text,
|
this.text,
|
||||||
}) : attributes = <String, String>{ 'xmlns': xmlns, ...attributes }, isDeclaration = false;
|
}) : attributes = <String, String>{'xmlns': xmlns, ...attributes},
|
||||||
|
isDeclaration = false;
|
||||||
|
|
||||||
/// Because this API is better ;)
|
/// Because this API is better ;)
|
||||||
/// Don't use in production. Just for testing
|
/// Don't use in production. Just for testing
|
||||||
factory XMLNode.fromXmlElement(XmlElement element) {
|
factory XMLNode.fromXmlElement(XmlElement element) {
|
||||||
@@ -37,10 +38,12 @@ class XMLNode {
|
|||||||
return XMLNode(
|
return XMLNode(
|
||||||
tag: element.name.qualified,
|
tag: element.name.qualified,
|
||||||
attributes: attributes,
|
attributes: attributes,
|
||||||
children: element.childElements.toList().map(XMLNode.fromXmlElement).toList(),
|
children:
|
||||||
|
element.childElements.toList().map(XMLNode.fromXmlElement).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Just for testing purposes
|
/// Just for testing purposes
|
||||||
factory XMLNode.fromString(String str) {
|
factory XMLNode.fromString(String str) {
|
||||||
return XMLNode.fromXmlElement(
|
return XMLNode.fromXmlElement(
|
||||||
@@ -62,13 +65,16 @@ class XMLNode {
|
|||||||
/// Renders the attributes of the node into "attr1=\"value\" attr2=...".
|
/// Renders the attributes of the node into "attr1=\"value\" attr2=...".
|
||||||
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(
|
||||||
if (value is String) {
|
value is String || value is int,
|
||||||
return "$key='$value'";
|
'XML values must either be string or int',
|
||||||
} else {
|
);
|
||||||
return '$key=$value';
|
if (value is String) {
|
||||||
}
|
return "$key='$value'";
|
||||||
|
} else {
|
||||||
|
return '$key=$value';
|
||||||
|
}
|
||||||
}).join(' ');
|
}).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +87,8 @@ class XMLNode {
|
|||||||
return '<$tag$attrString>$text</$tag>';
|
return '<$tag$attrString>$text</$tag>';
|
||||||
} else {
|
} else {
|
||||||
return '<$decl$tag ${renderAttributes()}${closeTag ? " />" : "$decl>"}';
|
return '<$decl$tag ${renderAttributes()}${closeTag ? " />" : "$decl>"}';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final childXml = children.map((child) => child.toXml()).join();
|
final childXml = children.map((child) => child.toXml()).join();
|
||||||
final xml = '<$decl$tag ${renderAttributes()}$decl>$childXml';
|
final xml = '<$decl$tag ${renderAttributes()}$decl>$childXml';
|
||||||
return xml + (closeTag ? '</$tag>' : '');
|
return xml + (closeTag ? '</$tag>' : '');
|
||||||
@@ -94,16 +100,16 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the first xml node that matches the description:
|
/// Returns the first xml node that matches the description:
|
||||||
/// - 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;
|
||||||
@@ -120,15 +126,22 @@ class XMLNode {
|
|||||||
return node.attributes['xmlns'] == xmlns;
|
return node.attributes['xmlns'] == xmlns;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all children whose tag is equal to [tag].
|
/// Returns all children whose tag is equal to [tag].
|
||||||
List<XMLNode> findTags(String tag, { String? xmlns }) {
|
List<XMLNode> findTags(String tag, {String? xmlns}) {
|
||||||
return children.where((element) {
|
return children.where((element) {
|
||||||
final xmlnsMatches = xmlns != null ? element.attributes['xmlns'] == xmlns : true;
|
final xmlnsMatches =
|
||||||
|
xmlns != null ? element.attributes['xmlns'] == xmlns : true;
|
||||||
return element.tag == tag && xmlnsMatches;
|
return element.tag == tag && xmlnsMatches;
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<XMLNode> findTagsByXmlns(String xmlns) {
|
||||||
|
return children
|
||||||
|
.where((element) => element.attributes['xmlns'] == xmlns)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the inner text of the node. If none is set, returns the "".
|
/// Returns the inner text of the node. If none is set, returns the "".
|
||||||
String innerText() {
|
String innerText() {
|
||||||
return text ?? '';
|
return text ?? '';
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
/// A wrapper class that can be used to indicate that a function may return a valid
|
|
||||||
/// instance of [T] but may also fail.
|
|
||||||
/// The way [MayFail] is intended to be used to to have function specific - or application
|
|
||||||
/// specific - error codes that can be either handled by code or be translated into a
|
|
||||||
/// localised error message for the user.
|
|
||||||
class MayFail<T> {
|
|
||||||
|
|
||||||
MayFail({ this.result, this.errorCode });
|
|
||||||
MayFail.success(this.result);
|
|
||||||
MayFail.failure(this.errorCode);
|
|
||||||
T? result;
|
|
||||||
int? errorCode;
|
|
||||||
|
|
||||||
bool isError() => result == null && errorCode != null;
|
|
||||||
|
|
||||||
T getValue() => result!;
|
|
||||||
|
|
||||||
int getErrorCode() => errorCode!;
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
/// Class that is supposed to by used with a state type S and a value type V.
|
class Result<T, V> {
|
||||||
/// The state indicates if an action was successful or not, while the value
|
const Result(this._data)
|
||||||
/// type indicates the return value, i.e. a result in a computation or the
|
: assert(
|
||||||
/// actual error description.
|
_data is T || _data is V,
|
||||||
class Result<S, V> {
|
'Invalid data type: Must be either $T or $V',
|
||||||
|
);
|
||||||
|
final dynamic _data;
|
||||||
|
|
||||||
Result(S state, V value) : _state = state, _value = value;
|
bool isType<S>() => _data is S;
|
||||||
final S _state;
|
|
||||||
final V _value;
|
|
||||||
|
|
||||||
S getState() => _state;
|
S get<S>() {
|
||||||
V getValue() => _value;
|
assert(_data is S, 'Data is not $S');
|
||||||
|
|
||||||
|
return _data as S;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
class Result<T, V> {
|
|
||||||
|
|
||||||
const Result(this._data) : assert(_data is T || _data is V, 'Invalid data type: Must be either $T or $V');
|
|
||||||
final dynamic _data;
|
|
||||||
|
|
||||||
bool isType<S>() => _data is S;
|
|
||||||
|
|
||||||
S get<S>() {
|
|
||||||
assert(_data is S, 'Data is not $S');
|
|
||||||
|
|
||||||
return _data as S;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
56
packages/moxxmpp/lib/src/util/queue.dart
Normal file
56
packages/moxxmpp/lib/src/util/queue.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
|
/// A job to be submitted to an [AsyncQueue].
|
||||||
|
typedef AsyncQueueJob = Future<void> Function();
|
||||||
|
|
||||||
|
/// A (hopefully) async-safe queue that attempts to force
|
||||||
|
/// in-order execution of its jobs.
|
||||||
|
class AsyncQueue {
|
||||||
|
/// The lock for accessing [AsyncQueue._lock] and [AsyncQueue._running].
|
||||||
|
final Lock _lock = Lock();
|
||||||
|
|
||||||
|
/// The actual job queue.
|
||||||
|
final Queue<AsyncQueueJob> _queue = Queue<AsyncQueueJob>();
|
||||||
|
|
||||||
|
/// Indicates whether we are currently executing a job.
|
||||||
|
bool _running = false;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
Queue<AsyncQueueJob> get queue => _queue;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool get isRunning => _running;
|
||||||
|
|
||||||
|
/// Adds a job [job] to the queue.
|
||||||
|
Future<void> addJob(AsyncQueueJob job) async {
|
||||||
|
await _lock.synchronized(() {
|
||||||
|
_queue.add(job);
|
||||||
|
|
||||||
|
if (!_running && _queue.isNotEmpty) {
|
||||||
|
_running = true;
|
||||||
|
unawaited(_popJob());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clear() async {
|
||||||
|
await _lock.synchronized(_queue.clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _popJob() async {
|
||||||
|
final job = _queue.removeFirst();
|
||||||
|
final future = job();
|
||||||
|
await future;
|
||||||
|
|
||||||
|
await _lock.synchronized(() {
|
||||||
|
if (_queue.isNotEmpty) {
|
||||||
|
unawaited(_popJob());
|
||||||
|
} else {
|
||||||
|
_running = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
67
packages/moxxmpp/lib/src/util/wait.dart
Normal file
67
packages/moxxmpp/lib/src/util/wait.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
|
/// This class allows for multiple asynchronous code places to wait on the
|
||||||
|
/// same computation of type [V], indentified by a key of type [K].
|
||||||
|
class WaitForTracker<K, V> {
|
||||||
|
/// The mapping of key -> Completer for the pending tasks.
|
||||||
|
final Map<K, List<Completer<V>>> _tracker = {};
|
||||||
|
|
||||||
|
/// The lock for accessing _tracker.
|
||||||
|
final Lock _lock = Lock();
|
||||||
|
|
||||||
|
/// Wait for a task with key [key]. If there was no such task already
|
||||||
|
/// present, returns null. If one or more tasks were already present, returns
|
||||||
|
/// a future that will resolve to the result of the first task.
|
||||||
|
Future<Future<V>?> waitFor(K key) async {
|
||||||
|
final result = await _lock.synchronized(() {
|
||||||
|
if (_tracker.containsKey(key)) {
|
||||||
|
// The task already exists. Just append outselves
|
||||||
|
final completer = Completer<V>();
|
||||||
|
_tracker[key]!.add(completer);
|
||||||
|
return completer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The task does not exist yet
|
||||||
|
_tracker[key] = List<Completer<V>>.empty(growable: true);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result?.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a task with key [key] to [value].
|
||||||
|
Future<void> resolve(K key, V value) async {
|
||||||
|
await _lock.synchronized(() {
|
||||||
|
if (!_tracker.containsKey(key)) return;
|
||||||
|
|
||||||
|
for (final completer in _tracker[key]!) {
|
||||||
|
completer.complete(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tracker.remove(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resolveAll(V value) async {
|
||||||
|
await _lock.synchronized(() {
|
||||||
|
for (final key in _tracker.keys) {
|
||||||
|
for (final completer in _tracker[key]!) {
|
||||||
|
completer.complete(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove all tasks from the tracker.
|
||||||
|
Future<void> clear() async {
|
||||||
|
await _lock.synchronized(_tracker.clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool hasTasksRunning() => _tracker.isNotEmpty;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
List<Completer<V>> getRunningTasks(K key) => _tracker[key]!;
|
||||||
|
}
|
||||||
@@ -8,20 +8,23 @@ 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();
|
{
|
||||||
return BlurhashThumbnail(hash);
|
final hash = node.firstTag('blurhash')!.innerText();
|
||||||
}
|
return BlurhashThumbnail(hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -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],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,44 +11,42 @@ import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
|||||||
const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0';
|
const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0';
|
||||||
|
|
||||||
class FileUploadNotificationManager extends XmppManagerBase {
|
class FileUploadNotificationManager extends XmppManagerBase {
|
||||||
FileUploadNotificationManager() : super();
|
FileUploadNotificationManager() : super(fileUploadNotificationManager);
|
||||||
|
|
||||||
@override
|
|
||||||
String getId() => fileUploadNotificationManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'FileUploadNotificationManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
tagName: 'file-upload',
|
tagName: 'file-upload',
|
||||||
tagXmlns: fileUploadNotificationXmlns,
|
tagXmlns: fileUploadNotificationXmlns,
|
||||||
callback: _onFileUploadNotificationReceived,
|
callback: _onFileUploadNotificationReceived,
|
||||||
priority: -99,
|
priority: -99,
|
||||||
),
|
),
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
tagName: 'replaces',
|
tagName: 'replaces',
|
||||||
tagXmlns: fileUploadNotificationXmlns,
|
tagXmlns: fileUploadNotificationXmlns,
|
||||||
callback: _onFileUploadNotificationReplacementReceived,
|
callback: _onFileUploadNotificationReplacementReceived,
|
||||||
priority: -99,
|
priority: -99,
|
||||||
),
|
),
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
tagName: 'cancelled',
|
tagName: 'cancelled',
|
||||||
tagXmlns: fileUploadNotificationXmlns,
|
tagXmlns: fileUploadNotificationXmlns,
|
||||||
callback: _onFileUploadNotificationCancellationReceived,
|
callback: _onFileUploadNotificationCancellationReceived,
|
||||||
priority: -99,
|
priority: -99,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
Future<StanzaHandlerData> _onFileUploadNotificationReceived(Stanza message, StanzaHandlerData state) async {
|
Future<StanzaHandlerData> _onFileUploadNotificationReceived(
|
||||||
final funElement = message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!;
|
Stanza message,
|
||||||
|
StanzaHandlerData state,
|
||||||
|
) async {
|
||||||
|
final funElement =
|
||||||
|
message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!;
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
fun: FileMetadataData.fromXML(
|
fun: FileMetadataData.fromXML(
|
||||||
funElement.firstTag('file', xmlns: fileMetadataXmlns)!,
|
funElement.firstTag('file', xmlns: fileMetadataXmlns)!,
|
||||||
@@ -56,15 +54,23 @@ class FileUploadNotificationManager extends XmppManagerBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived(Stanza message, StanzaHandlerData state) async {
|
Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived(
|
||||||
final element = message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!;
|
Stanza message,
|
||||||
|
StanzaHandlerData state,
|
||||||
|
) async {
|
||||||
|
final element =
|
||||||
|
message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!;
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
funReplacement: element.attributes['id']! as String,
|
funReplacement: element.attributes['id']! as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived(Stanza message, StanzaHandlerData state) async {
|
Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived(
|
||||||
final element = message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!;
|
Stanza message,
|
||||||
|
StanzaHandlerData state,
|
||||||
|
) async {
|
||||||
|
final element =
|
||||||
|
message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!;
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
funCancellation: element.attributes['id']! as String,
|
funCancellation: element.attributes['id']! as String,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,15 +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',
|
||||||
@@ -23,15 +24,14 @@ class DataFormOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DataFormField {
|
class DataFormField {
|
||||||
|
|
||||||
const DataFormField({
|
const DataFormField({
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.values,
|
required this.values,
|
||||||
required this.isRequired,
|
required this.isRequired,
|
||||||
this.varAttr,
|
this.varAttr,
|
||||||
this.type,
|
this.type,
|
||||||
this.description,
|
this.description,
|
||||||
this.label,
|
this.label,
|
||||||
});
|
});
|
||||||
final String? description;
|
final String? description;
|
||||||
final bool isRequired;
|
final bool isRequired;
|
||||||
@@ -45,9 +45,13 @@ class DataFormField {
|
|||||||
return XMLNode(
|
return XMLNode(
|
||||||
tag: 'field',
|
tag: 'field',
|
||||||
attributes: <String, dynamic>{
|
attributes: <String, dynamic>{
|
||||||
...varAttr != null ? <String, dynamic>{ 'var': varAttr } : <String, dynamic>{},
|
...varAttr != null
|
||||||
...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)] : [],
|
||||||
@@ -60,14 +64,13 @@ class DataFormField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DataForm {
|
class DataForm {
|
||||||
|
|
||||||
const DataForm({
|
const DataForm({
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.instructions,
|
required this.instructions,
|
||||||
required this.fields,
|
required this.fields,
|
||||||
required this.reported,
|
required this.reported,
|
||||||
required this.items,
|
required this.items,
|
||||||
this.title,
|
this.title,
|
||||||
});
|
});
|
||||||
final String type;
|
final String type;
|
||||||
final String? title;
|
final String? title;
|
||||||
@@ -79,23 +82,23 @@ class DataForm {
|
|||||||
DataFormField? getFieldByVar(String varAttr) {
|
DataFormField? getFieldByVar(String varAttr) {
|
||||||
return firstWhereOrNull(fields, (field) => field.varAttr == varAttr);
|
return firstWhereOrNull(fields, (field) => field.varAttr == varAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
XMLNode toXml() {
|
XMLNode toXml() {
|
||||||
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(
|
||||||
tag: 'item',
|
(item) => XMLNode(
|
||||||
children: item.map((i) => i.toXml()).toList(),
|
tag: 'item',
|
||||||
),),
|
children: item.map((i) => i.toXml()).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -131,10 +134,19 @@ DataForm parseDataForm(XMLNode x) {
|
|||||||
|
|
||||||
final type = x.attributes['type']! as String;
|
final type = x.attributes['type']! as String;
|
||||||
final title = x.firstTag('title')?.innerText();
|
final title = x.firstTag('title')?.innerText();
|
||||||
final instructions = x.findTags('instructions').map((i) => i.innerText()).toList();
|
final instructions =
|
||||||
|
x.findTags('instructions').map((i) => i.innerText()).toList();
|
||||||
final fields = x.findTags('field').map(_parseDataFormField).toList();
|
final fields = x.findTags('field').map(_parseDataFormField).toList();
|
||||||
final reported = x.firstTag('reported')?.findTags('field').map((i) => _parseDataFormField(i.firstTag('field')!)).toList() ?? [];
|
final reported = x
|
||||||
final items = x.findTags('item').map((i) => i.findTags('field').map(_parseDataFormField).toList()).toList();
|
.firstTag('reported')
|
||||||
|
?.findTags('field')
|
||||||
|
.map((i) => _parseDataFormField(i.firstTag('field')!))
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
final items = x
|
||||||
|
.findTags('item')
|
||||||
|
.map((i) => i.findTags('field').map(_parseDataFormField).toList())
|
||||||
|
.toList();
|
||||||
|
|
||||||
return DataForm(
|
return DataForm(
|
||||||
type: type,
|
type: type,
|
||||||
|
|||||||
21
packages/moxxmpp/lib/src/xeps/xep_0030/cache.dart
Normal file
21
packages/moxxmpp/lib/src/xeps/xep_0030/cache.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
@internal
|
||||||
|
@immutable
|
||||||
|
class DiscoCacheKey {
|
||||||
|
const DiscoCacheKey(this.jid, this.node);
|
||||||
|
|
||||||
|
/// The JID we're requesting disco data from.
|
||||||
|
final String jid;
|
||||||
|
|
||||||
|
/// Optionally the node we are requesting from.
|
||||||
|
final String? node;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is DiscoCacheKey && jid == other.jid && node == other.node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => jid.hashCode ^ node.hashCode;
|
||||||
|
}
|
||||||
@@ -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} : {},
|
||||||
)
|
)
|
||||||
],);
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||||
|
|
||||||
class Identity {
|
class Identity {
|
||||||
|
const Identity({
|
||||||
const Identity({ required this.category, required this.type, this.name, this.lang });
|
required this.category,
|
||||||
|
required this.type,
|
||||||
|
this.name,
|
||||||
|
this.lang,
|
||||||
|
});
|
||||||
final String category;
|
final String category;
|
||||||
final String type;
|
final String type;
|
||||||
final String? name;
|
final String? name;
|
||||||
@@ -17,30 +23,109 @@ class Identity {
|
|||||||
'category': category,
|
'category': category,
|
||||||
'type': type,
|
'type': type,
|
||||||
'name': name,
|
'name': name,
|
||||||
...lang == null ? <String, dynamic>{} : <String, dynamic>{ 'xml:lang': lang }
|
...lang == null
|
||||||
|
? <String, dynamic>{}
|
||||||
|
: <String, dynamic>{'xml:lang': lang}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
class DiscoInfo {
|
class DiscoInfo {
|
||||||
|
|
||||||
const DiscoInfo(
|
const DiscoInfo(
|
||||||
this.features,
|
this.features,
|
||||||
this.identities,
|
this.identities,
|
||||||
this.extendedInfo,
|
this.extendedInfo,
|
||||||
|
this.node,
|
||||||
this.jid,
|
this.jid,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory DiscoInfo.fromQuery(XMLNode query, JID jid) {
|
||||||
|
final features = List<String>.empty(growable: true);
|
||||||
|
final identities = List<Identity>.empty(growable: true);
|
||||||
|
final extendedInfo = List<DataForm>.empty(growable: true);
|
||||||
|
|
||||||
|
for (final element in query.children) {
|
||||||
|
if (element.tag == 'feature') {
|
||||||
|
features.add(element.attributes['var']! as String);
|
||||||
|
} else if (element.tag == 'identity') {
|
||||||
|
identities.add(
|
||||||
|
Identity(
|
||||||
|
category: element.attributes['category']! as String,
|
||||||
|
type: element.attributes['type']! as String,
|
||||||
|
name: element.attributes['name'] as String?,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (element.tag == 'x' &&
|
||||||
|
element.attributes['xmlns'] == dataFormsXmlns) {
|
||||||
|
extendedInfo.add(
|
||||||
|
parseDataForm(element),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiscoInfo(
|
||||||
|
features,
|
||||||
|
identities,
|
||||||
|
extendedInfo,
|
||||||
|
query.attributes['node'] as String?,
|
||||||
|
jid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final List<String> features;
|
final List<String> features;
|
||||||
final List<Identity> identities;
|
final List<Identity> identities;
|
||||||
final List<DataForm> extendedInfo;
|
final List<DataForm> extendedInfo;
|
||||||
final JID jid;
|
final String? node;
|
||||||
|
final JID? jid;
|
||||||
|
|
||||||
|
XMLNode toXml() {
|
||||||
|
return XMLNode.xmlns(
|
||||||
|
tag: 'query',
|
||||||
|
xmlns: discoInfoXmlns,
|
||||||
|
attributes: node != null
|
||||||
|
? <String, String>{
|
||||||
|
'node': node!,
|
||||||
|
}
|
||||||
|
: <String, String>{},
|
||||||
|
children: [
|
||||||
|
...identities.map((identity) => identity.toXMLNode()),
|
||||||
|
...features.map(
|
||||||
|
(feature) => XMLNode(
|
||||||
|
tag: 'feature',
|
||||||
|
attributes: {
|
||||||
|
'var': feature,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (extendedInfo.isNotEmpty) ...extendedInfo.map((ei) => ei.toXml()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@immutable
|
||||||
class DiscoItem {
|
class DiscoItem {
|
||||||
|
const DiscoItem({required this.jid, this.node, this.name});
|
||||||
const DiscoItem({ required this.jid, this.node, this.name });
|
|
||||||
final String jid;
|
final String jid;
|
||||||
final String? node;
|
final String? node;
|
||||||
final String? name;
|
final String? name;
|
||||||
|
|
||||||
|
XMLNode toXml() {
|
||||||
|
final attributes = {
|
||||||
|
'jid': jid,
|
||||||
|
};
|
||||||
|
if (node != null) {
|
||||||
|
attributes['node'] = node!;
|
||||||
|
}
|
||||||
|
if (name != null) {
|
||||||
|
attributes['name'] = name!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return XMLNode(
|
||||||
|
tag: 'node',
|
||||||
|
attributes: attributes,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:moxxmpp/src/connection.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
import 'package:moxxmpp/src/managers/base.dart';
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
@@ -7,87 +8,93 @@ import 'package:moxxmpp/src/managers/data.dart';
|
|||||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/presence.dart';
|
|
||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
import 'package:moxxmpp/src/types/resultv2.dart';
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
import 'package:moxxmpp/src/util/wait.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/helpers.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/helpers.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0115.dart';
|
import 'package:moxxmpp/src/xeps/xep_0115.dart';
|
||||||
import 'package:synchronized/synchronized.dart';
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
@immutable
|
/// Callback that is called when a disco#info requests is received on a given node.
|
||||||
class DiscoCacheKey {
|
typedef DiscoInfoRequestCallback = Future<DiscoInfo> Function();
|
||||||
|
|
||||||
const DiscoCacheKey(this.jid, this.node);
|
/// Callback that is called when a disco#items requests is received on a given node.
|
||||||
final String jid;
|
typedef DiscoItemsRequestCallback = Future<List<DiscoItem>> Function();
|
||||||
final String? node;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is DiscoCacheKey && jid == other.jid && node == other.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => jid.hashCode ^ node.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// This manager implements XEP-0030 by providing a way of performing disco#info and
|
||||||
|
/// disco#items requests and answering those requests.
|
||||||
|
/// A caching mechanism is also provided.
|
||||||
class DiscoManager extends XmppManagerBase {
|
class DiscoManager extends XmppManagerBase {
|
||||||
|
/// [identities] is a list of disco identities that should be added by default
|
||||||
DiscoManager()
|
/// to a disco#info response.
|
||||||
: _features = List.empty(growable: true),
|
DiscoManager(List<Identity> identities)
|
||||||
_capHashCache = {},
|
: _identities = List<Identity>.from(identities),
|
||||||
_capHashInfoCache = {},
|
super(discoManager);
|
||||||
_discoInfoCache = {},
|
|
||||||
_runningInfoQueries = {},
|
|
||||||
_cacheLock = Lock(),
|
|
||||||
super();
|
|
||||||
/// Our features
|
/// Our features
|
||||||
final List<String> _features;
|
final List<String> _features = List.empty(growable: true);
|
||||||
|
|
||||||
// Map full JID to Capability hashes
|
/// Disco identities that we advertise
|
||||||
final Map<String, CapabilityHashInfo> _capHashCache;
|
final List<Identity> _identities;
|
||||||
// Map capability hash to the disco info
|
|
||||||
final Map<String, DiscoInfo> _capHashInfoCache;
|
/// Map full JID to Capability hashes
|
||||||
// Map full JID to Disco Info
|
final Map<String, CapabilityHashInfo> _capHashCache = {};
|
||||||
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache;
|
|
||||||
// Mapping the full JID to a list of running requests
|
/// Map capability hash to the disco info
|
||||||
final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries;
|
final Map<String, DiscoInfo> _capHashInfoCache = {};
|
||||||
// Cache lock
|
|
||||||
final Lock _cacheLock;
|
/// Map full JID to Disco Info
|
||||||
|
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
|
||||||
|
|
||||||
|
/// The tracker for tracking disco#info queries that are in flight.
|
||||||
|
final WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>>
|
||||||
|
_discoInfoTracker = WaitForTracker();
|
||||||
|
|
||||||
|
/// The tracker for tracking disco#info queries that are in flight.
|
||||||
|
final WaitForTracker<DiscoCacheKey, Result<DiscoError, List<DiscoItem>>>
|
||||||
|
_discoItemsTracker = WaitForTracker();
|
||||||
|
|
||||||
|
/// Cache lock
|
||||||
|
final Lock _cacheLock = Lock();
|
||||||
|
|
||||||
|
/// disco#info callbacks: node -> Callback
|
||||||
|
final Map<String, DiscoInfoRequestCallback> _discoInfoCallbacks = {};
|
||||||
|
|
||||||
|
/// disco#items callbacks: node -> Callback
|
||||||
|
final Map<String, DiscoItemsRequestCallback> _discoItemsCallbacks = {};
|
||||||
|
|
||||||
|
/// The list of identities that are registered.
|
||||||
|
List<Identity> get identities => _identities;
|
||||||
|
|
||||||
|
/// The list of disco features that are registered.
|
||||||
|
List<String> get features => _features;
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
bool hasInfoQueriesRunning() => _runningInfoQueries.isNotEmpty;
|
WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>>
|
||||||
|
get infoTracker => _discoInfoTracker;
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
List<Completer<Result<DiscoError, DiscoInfo>>> getRunningInfoQueries(DiscoCacheKey key) => _runningInfoQueries[key]!;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
tagName: 'query',
|
tagName: 'query',
|
||||||
tagXmlns: discoInfoXmlns,
|
tagXmlns: discoInfoXmlns,
|
||||||
stanzaTag: 'iq',
|
stanzaTag: 'iq',
|
||||||
callback: _onDiscoInfoRequest,
|
callback: _onDiscoInfoRequest,
|
||||||
),
|
),
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
tagName: 'query',
|
tagName: 'query',
|
||||||
tagXmlns: discoItemsXmlns,
|
tagXmlns: discoItemsXmlns,
|
||||||
stanzaTag: 'iq',
|
stanzaTag: 'iq',
|
||||||
callback: _onDiscoItemsRequest,
|
callback: _onDiscoItemsRequest,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getId() => discoManager;
|
List<String> getDiscoFeatures() => [discoInfoXmlns, discoItemsXmlns];
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'DiscoManager';
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> getDiscoFeatures() => [ discoInfoXmlns, discoItemsXmlns ];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
@@ -96,17 +103,41 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
Future<void> onXmppEvent(XmppEvent event) async {
|
Future<void> onXmppEvent(XmppEvent event) async {
|
||||||
if (event is PresenceReceivedEvent) {
|
if (event is PresenceReceivedEvent) {
|
||||||
await _onPresence(event.jid, event.presence);
|
await _onPresence(event.jid, event.presence);
|
||||||
} else if (event is StreamResumeFailedEvent) {
|
} else if (event is ConnectionStateChangedEvent) {
|
||||||
|
// TODO(Unknown): This handling is stupid. We should have an event that is
|
||||||
|
// triggered when we cannot guarantee that everything is as
|
||||||
|
// it was before.
|
||||||
|
if (event.state != XmppConnectionState.connected) return;
|
||||||
|
if (event.resumed) return;
|
||||||
|
|
||||||
|
// Cancel all waiting requests
|
||||||
|
await _discoInfoTracker.resolveAll(
|
||||||
|
Result<DiscoError, DiscoInfo>(UnknownDiscoError()),
|
||||||
|
);
|
||||||
|
await _discoItemsTracker.resolveAll(
|
||||||
|
Result<DiscoError, List<DiscoItem>>(UnknownDiscoError()),
|
||||||
|
);
|
||||||
|
|
||||||
await _cacheLock.synchronized(() async {
|
await _cacheLock.synchronized(() async {
|
||||||
// Clear the cache
|
// Clear the cache
|
||||||
_discoInfoCache.clear();
|
_discoInfoCache.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a callback [callback] for a disco#info query on [node].
|
||||||
|
void registerInfoCallback(String node, DiscoInfoRequestCallback callback) {
|
||||||
|
_discoInfoCallbacks[node] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a callback [callback] for a disco#items query on [node].
|
||||||
|
void registerItemsCallback(String node, DiscoItemsRequestCallback callback) {
|
||||||
|
_discoItemsCallbacks[node] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
/// Adds a list of features to the possible disco info response.
|
/// Adds a list of features to the possible disco info response.
|
||||||
/// This function only adds features that are not already present in the disco features.
|
/// This function only adds features that are not already present in the disco features.
|
||||||
void addDiscoFeatures(List<String> features) {
|
void addFeatures(List<String> features) {
|
||||||
for (final feat in features) {
|
for (final feat in features) {
|
||||||
if (!_features.contains(feat)) {
|
if (!_features.contains(feat)) {
|
||||||
_features.add(feat);
|
_features.add(feat);
|
||||||
@@ -114,6 +145,16 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a list of identities to the possible disco info response.
|
||||||
|
/// This function only adds features that are not already present in the disco features.
|
||||||
|
void addIdentities(List<Identity> identities) {
|
||||||
|
for (final identity in identities) {
|
||||||
|
if (!_identities.contains(identity)) {
|
||||||
|
_identities.add(identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onPresence(JID from, Stanza presence) async {
|
Future<void> _onPresence(JID from, Stanza presence) async {
|
||||||
final c = presence.firstTag('c', xmlns: capsXmlns);
|
final c = presence.firstTag('c', xmlns: capsXmlns);
|
||||||
if (c == null) return;
|
if (c == null) return;
|
||||||
@@ -123,7 +164,7 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
c.attributes['node']! as String,
|
c.attributes['node']! as String,
|
||||||
c.attributes['hash']! as String,
|
c.attributes['hash']! as String,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if we already know of that cache
|
// Check if we already know of that cache
|
||||||
var cached = false;
|
var cached = false;
|
||||||
await _cacheLock.synchronized(() async {
|
await _cacheLock.synchronized(() async {
|
||||||
@@ -134,8 +175,11 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
if (cached) return;
|
if (cached) return;
|
||||||
|
|
||||||
// Request the cap hash
|
// Request the cap hash
|
||||||
logger.finest("Received capability hash we don't know about. Requesting it...");
|
logger.finest(
|
||||||
final result = await discoInfoQuery(from.toString(), node: '${info.node}#${info.ver}');
|
"Received capability hash we don't know about. Requesting it...",
|
||||||
|
);
|
||||||
|
final result =
|
||||||
|
await discoInfoQuery(from.toString(), node: '${info.node}#${info.ver}');
|
||||||
if (result.isType<DiscoError>()) return;
|
if (result.isType<DiscoError>()) return;
|
||||||
|
|
||||||
await _cacheLock.synchronized(() async {
|
await _cacheLock.synchronized(() async {
|
||||||
@@ -143,179 +187,131 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
_capHashInfoCache[info.ver] = result.get<DiscoInfo>();
|
_capHashInfoCache[info.ver] = result.get<DiscoInfo>();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the list of disco features registered.
|
/// Returns the [DiscoInfo] object that would be used as the response to a disco#info
|
||||||
List<String> getRegisteredDiscoFeatures() => _features;
|
/// query against our bare JID with no node. The results node attribute is set
|
||||||
|
/// to [node].
|
||||||
/// May be overriden. Specifies the identities which will be returned in a disco info response.
|
DiscoInfo getDiscoInfo(String? node) {
|
||||||
List<Identity> getIdentities() => const [ Identity(category: 'client', type: 'pc', name: 'moxxmpp', lang: 'en') ];
|
return DiscoInfo(
|
||||||
|
_features,
|
||||||
Future<StanzaHandlerData> _onDiscoInfoRequest(Stanza stanza, StanzaHandlerData state) async {
|
_identities,
|
||||||
|
const [],
|
||||||
|
node,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<StanzaHandlerData> _onDiscoInfoRequest(
|
||||||
|
Stanza stanza,
|
||||||
|
StanzaHandlerData state,
|
||||||
|
) async {
|
||||||
if (stanza.type != 'get') return state;
|
if (stanza.type != 'get') return state;
|
||||||
|
|
||||||
final presence = getAttributes().getManagerById(presenceManager)! as PresenceManager;
|
final query = stanza.firstTag('query', xmlns: discoInfoXmlns)!;
|
||||||
final query = stanza.firstTag('query')!;
|
|
||||||
final node = query.attributes['node'] as String?;
|
final node = query.attributes['node'] as String?;
|
||||||
final capHash = await presence.getCapabilityHash();
|
|
||||||
final isCapabilityNode = node == '${presence.capabilityHashNode}#$capHash';
|
|
||||||
|
|
||||||
if (!isCapabilityNode && node != null) {
|
if (_discoInfoCallbacks.containsKey(node)) {
|
||||||
await getAttributes().sendStanza(Stanza.iq(
|
// We can now assume that node != null
|
||||||
to: stanza.from,
|
final result = await _discoInfoCallbacks[node]!();
|
||||||
from: stanza.to,
|
await reply(
|
||||||
id: stanza.id,
|
state,
|
||||||
type: 'error',
|
'result',
|
||||||
children: [
|
[
|
||||||
XMLNode.xmlns(
|
result.toXml(),
|
||||||
tag: 'query',
|
],
|
||||||
// TODO(PapaTutuWawa): Why are we copying the xmlns?
|
);
|
||||||
xmlns: query.attributes['xmlns']! as String,
|
|
||||||
attributes: <String, String>{
|
|
||||||
'node': node
|
|
||||||
},
|
|
||||||
),
|
|
||||||
XMLNode(
|
|
||||||
tag: 'error',
|
|
||||||
attributes: <String, String>{
|
|
||||||
'type': 'cancel'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
XMLNode.xmlns(
|
|
||||||
tag: 'not-allowed',
|
|
||||||
xmlns: fullStanzaXmlns,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
,);
|
|
||||||
|
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
await getAttributes().sendStanza(stanza.reply(
|
await reply(
|
||||||
children: [
|
state,
|
||||||
XMLNode.xmlns(
|
'result',
|
||||||
tag: 'query',
|
[
|
||||||
xmlns: discoInfoXmlns,
|
getDiscoInfo(node).toXml(),
|
||||||
attributes: {
|
],
|
||||||
...!isCapabilityNode ? {} : {
|
);
|
||||||
'node': '${presence.capabilityHashNode}#$capHash'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
...getIdentities().map((identity) => identity.toXMLNode()),
|
|
||||||
..._features.map((feat) {
|
|
||||||
return XMLNode(
|
|
||||||
tag: 'feature',
|
|
||||||
attributes: <String, dynamic>{ 'var': feat },
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),);
|
|
||||||
|
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<StanzaHandlerData> _onDiscoItemsRequest(Stanza stanza, StanzaHandlerData state) async {
|
Future<StanzaHandlerData> _onDiscoItemsRequest(
|
||||||
|
Stanza stanza,
|
||||||
|
StanzaHandlerData state,
|
||||||
|
) async {
|
||||||
if (stanza.type != 'get') return state;
|
if (stanza.type != 'get') return state;
|
||||||
|
|
||||||
final query = stanza.firstTag('query')!;
|
final query = stanza.firstTag('query', xmlns: discoItemsXmlns)!;
|
||||||
if (query.attributes['node'] != null) {
|
final node = query.attributes['node'] as String?;
|
||||||
// TODO(Unknown): Handle the node we specified for XEP-0115
|
if (_discoItemsCallbacks.containsKey(node)) {
|
||||||
await getAttributes().sendStanza(
|
final result = await _discoItemsCallbacks[node]!();
|
||||||
Stanza.iq(
|
await reply(
|
||||||
to: stanza.from,
|
state,
|
||||||
from: stanza.to,
|
'result',
|
||||||
id: stanza.id,
|
[
|
||||||
type: 'error',
|
|
||||||
children: [
|
|
||||||
XMLNode.xmlns(
|
|
||||||
tag: 'query',
|
|
||||||
// TODO(PapaTutuWawa): Why copy the xmlns?
|
|
||||||
xmlns: query.attributes['xmlns']! as String,
|
|
||||||
attributes: <String, String>{
|
|
||||||
'node': query.attributes['node']! as String,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
XMLNode(
|
|
||||||
tag: 'error',
|
|
||||||
attributes: <String, dynamic>{
|
|
||||||
'type': 'cancel'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
XMLNode.xmlns(
|
|
||||||
tag: 'not-allowed',
|
|
||||||
xmlns: fullStanzaXmlns,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return state.copyWith(done: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
await getAttributes().sendStanza(
|
|
||||||
stanza.reply(
|
|
||||||
children: [
|
|
||||||
XMLNode.xmlns(
|
XMLNode.xmlns(
|
||||||
tag: 'query',
|
tag: 'query',
|
||||||
xmlns: discoItemsXmlns,
|
xmlns: discoItemsXmlns,
|
||||||
|
attributes: <String, String>{
|
||||||
|
'node': node!,
|
||||||
|
},
|
||||||
|
children: result.map((item) => item.toXml()).toList(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
);
|
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async {
|
Future<void> _exitDiscoInfoCriticalSection(
|
||||||
return _cacheLock.synchronized(() async {
|
DiscoCacheKey key,
|
||||||
// Complete all futures
|
Result<DiscoError, DiscoInfo> result,
|
||||||
for (final completer in _runningInfoQueries[key]!) {
|
) async {
|
||||||
completer.complete(result);
|
await _cacheLock.synchronized(() async {
|
||||||
}
|
|
||||||
|
|
||||||
// Add to cache if it is a result
|
// Add to cache if it is a result
|
||||||
if (result.isType<DiscoInfo>()) {
|
if (result.isType<DiscoInfo>()) {
|
||||||
_discoInfoCache[key] = result.get<DiscoInfo>();
|
_discoInfoCache[key] = result.get<DiscoInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from the request cache
|
|
||||||
_runningInfoQueries.remove(key);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await _discoInfoTracker.resolve(key, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node].
|
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node].
|
||||||
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node}) async {
|
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(
|
||||||
|
String entity, {
|
||||||
|
String? node,
|
||||||
|
bool shouldEncrypt = true,
|
||||||
|
}) async {
|
||||||
final cacheKey = DiscoCacheKey(entity, node);
|
final cacheKey = DiscoCacheKey(entity, node);
|
||||||
DiscoInfo? info;
|
DiscoInfo? info;
|
||||||
Completer<Result<DiscoError, DiscoInfo>>? completer;
|
final ffuture = await _cacheLock
|
||||||
await _cacheLock.synchronized(() async {
|
.synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>(
|
||||||
|
() async {
|
||||||
// Check if we already know what the JID supports
|
// Check if we already know what the JID supports
|
||||||
if (_discoInfoCache.containsKey(cacheKey)) {
|
if (_discoInfoCache.containsKey(cacheKey)) {
|
||||||
info = _discoInfoCache[cacheKey];
|
info = _discoInfoCache[cacheKey];
|
||||||
|
return null;
|
||||||
} else {
|
} else {
|
||||||
// Is a request running?
|
return _discoInfoTracker.waitFor(cacheKey);
|
||||||
if (_runningInfoQueries.containsKey(cacheKey)) {
|
|
||||||
completer = Completer();
|
|
||||||
_runningInfoQueries[cacheKey]!.add(completer!);
|
|
||||||
} else {
|
|
||||||
_runningInfoQueries[cacheKey] = List.from(<Completer<DiscoInfo?>>[]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
return Result<DiscoError, DiscoInfo>(info);
|
return Result<DiscoError, DiscoInfo>(info);
|
||||||
} else if (completer != null) {
|
} else {
|
||||||
return completer!.future;
|
final future = await ffuture;
|
||||||
|
if (future != null) {
|
||||||
|
return future;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final stanza = await getAttributes().sendStanza(
|
final stanza = await getAttributes().sendStanza(
|
||||||
buildDiscoInfoQueryStanza(entity, node),
|
buildDiscoInfoQueryStanza(entity, node),
|
||||||
|
encrypted: !shouldEncrypt,
|
||||||
);
|
);
|
||||||
final query = stanza.firstTag('query');
|
final query = stanza.firstTag('query');
|
||||||
if (query == null) {
|
if (query == null) {
|
||||||
@@ -324,34 +320,17 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
final error = stanza.firstTag('error');
|
if (stanza.attributes['type'] == 'error') {
|
||||||
if (error != null && stanza.attributes['type'] == 'error') {
|
//final error = stanza.firstTag('error');
|
||||||
final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError());
|
final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError());
|
||||||
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
final features = List<String>.empty(growable: true);
|
|
||||||
final identities = List<Identity>.empty(growable: true);
|
|
||||||
|
|
||||||
for (final element in query.children) {
|
|
||||||
if (element.tag == 'feature') {
|
|
||||||
features.add(element.attributes['var']! as String);
|
|
||||||
} else if (element.tag == 'identity') {
|
|
||||||
identities.add(Identity(
|
|
||||||
category: element.attributes['category']! as String,
|
|
||||||
type: element.attributes['type']! as String,
|
|
||||||
name: element.attributes['name'] as String?,
|
|
||||||
),);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final result = Result<DiscoError, DiscoInfo>(
|
final result = Result<DiscoError, DiscoInfo>(
|
||||||
DiscoInfo(
|
DiscoInfo.fromQuery(
|
||||||
features,
|
query,
|
||||||
identities,
|
JID.fromString(entity),
|
||||||
query.findTags('x', xmlns: dataFormsXmlns).map(parseDataForm).toList(),
|
|
||||||
JID.fromString(stanza.attributes['from']! as String),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
||||||
@@ -359,30 +338,61 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
|
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
|
||||||
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, { String? node }) async {
|
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(
|
||||||
final stanza = await getAttributes()
|
String entity, {
|
||||||
.sendStanza(buildDiscoItemsQueryStanza(entity, node: node)) as Stanza;
|
String? node,
|
||||||
|
bool shouldEncrypt = true,
|
||||||
final query = stanza.firstTag('query');
|
}) async {
|
||||||
if (query == null) return Result(InvalidResponseDiscoError());
|
final key = DiscoCacheKey(entity, node);
|
||||||
|
final future = await _discoItemsTracker.waitFor(key);
|
||||||
final error = stanza.firstTag('error');
|
if (future != null) {
|
||||||
if (error != null && stanza.type == 'error') {
|
return future;
|
||||||
//print("Disco Items error: " + error.toXml());
|
|
||||||
return Result(ErrorResponseDiscoError());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final items = query.findTags('item').map((node) => DiscoItem(
|
final stanza = await getAttributes().sendStanza(
|
||||||
jid: node.attributes['jid']! as String,
|
buildDiscoItemsQueryStanza(entity, node: node),
|
||||||
node: node.attributes['node'] as String?,
|
encrypted: !shouldEncrypt,
|
||||||
name: node.attributes['name'] as String?,
|
) as Stanza;
|
||||||
),).toList();
|
|
||||||
|
|
||||||
return Result(items);
|
final query = stanza.firstTag('query');
|
||||||
|
if (query == null) {
|
||||||
|
final result =
|
||||||
|
Result<DiscoError, List<DiscoItem>>(InvalidResponseDiscoError());
|
||||||
|
await _discoItemsTracker.resolve(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stanza.type == 'error') {
|
||||||
|
//final error = stanza.firstTag('error');
|
||||||
|
//print("Disco Items error: " + error.toXml());
|
||||||
|
final result =
|
||||||
|
Result<DiscoError, List<DiscoItem>>(ErrorResponseDiscoError());
|
||||||
|
await _discoItemsTracker.resolve(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final items = query
|
||||||
|
.findTags('item')
|
||||||
|
.map(
|
||||||
|
(node) => DiscoItem(
|
||||||
|
jid: node.attributes['jid']! as String,
|
||||||
|
node: node.attributes['node'] as String?,
|
||||||
|
name: node.attributes['name'] as String?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final result = Result<DiscoError, List<DiscoItem>>(items);
|
||||||
|
await _discoItemsTracker.resolve(key, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queries information about a jid based on its node and capability hash.
|
/// Queries information about a jid based on its node and capability hash.
|
||||||
Future<Result<DiscoError, DiscoInfo>> discoInfoCapHashQuery(String jid, String node, String ver) async {
|
Future<Result<DiscoError, DiscoInfo>> discoInfoCapHashQuery(
|
||||||
|
String jid,
|
||||||
|
String node,
|
||||||
|
String ver,
|
||||||
|
) async {
|
||||||
return discoInfoQuery(jid, node: '$node#$ver');
|
return discoInfoQuery(jid, node: '$node#$ver');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,51 +7,52 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
|
|
||||||
|
abstract class VCardError {}
|
||||||
|
|
||||||
|
class UnknownVCardError extends VCardError {}
|
||||||
|
|
||||||
|
class InvalidVCardError extends VCardError {}
|
||||||
|
|
||||||
class VCardPhoto {
|
class VCardPhoto {
|
||||||
|
const VCardPhoto({this.binval});
|
||||||
const VCardPhoto({ this.binval });
|
|
||||||
final String? binval;
|
final String? binval;
|
||||||
}
|
}
|
||||||
|
|
||||||
class VCard {
|
class VCard {
|
||||||
|
const VCard({this.nickname, this.url, this.photo});
|
||||||
const VCard({ this.nickname, this.url, this.photo });
|
|
||||||
final String? nickname;
|
final String? nickname;
|
||||||
final String? url;
|
final String? url;
|
||||||
final VCardPhoto? photo;
|
final VCardPhoto? photo;
|
||||||
}
|
}
|
||||||
|
|
||||||
class VCardManager extends XmppManagerBase {
|
class VCardManager extends XmppManagerBase {
|
||||||
|
VCardManager() : super(vcardManager);
|
||||||
VCardManager() : _lastHash = {}, super();
|
final Map<String, String> _lastHash = {};
|
||||||
final Map<String, String> _lastHash;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getId() => vcardManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'vCardManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'presence',
|
stanzaTag: 'presence',
|
||||||
tagName: 'x',
|
tagName: 'x',
|
||||||
tagXmlns: vCardTempUpdate,
|
tagXmlns: vCardTempUpdate,
|
||||||
callback: _onPresence,
|
callback: _onPresence,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
/// In case we get the avatar hash some other way.
|
/// In case we get the avatar hash some other way.
|
||||||
void setLastHash(String jid, String hash) {
|
void setLastHash(String jid, String hash) {
|
||||||
_lastHash[jid] = hash;
|
_lastHash[jid] = hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<StanzaHandlerData> _onPresence(Stanza presence, StanzaHandlerData state) async {
|
Future<StanzaHandlerData> _onPresence(
|
||||||
|
Stanza presence,
|
||||||
|
StanzaHandlerData state,
|
||||||
|
) async {
|
||||||
final x = presence.firstTag('x', xmlns: vCardTempUpdate)!;
|
final x = presence.firstTag('x', xmlns: vCardTempUpdate)!;
|
||||||
final hash = x.firstTag('photo')!.innerText();
|
final hash = x.firstTag('photo')!.innerText();
|
||||||
|
|
||||||
@@ -59,12 +60,18 @@ class VCardManager extends XmppManagerBase {
|
|||||||
final lastHash = _lastHash[from];
|
final lastHash = _lastHash[from];
|
||||||
if (lastHash != hash) {
|
if (lastHash != hash) {
|
||||||
_lastHash[from] = hash;
|
_lastHash[from] = hash;
|
||||||
final vcard = await requestVCard(from);
|
final vcardResult = await requestVCard(from);
|
||||||
|
|
||||||
if (vcard != null) {
|
if (vcardResult.isType<VCard>()) {
|
||||||
final binval = vcard.photo?.binval;
|
final binval = vcardResult.get<VCard>().photo?.binval;
|
||||||
if (binval != null) {
|
if (binval != null) {
|
||||||
getAttributes().sendEvent(AvatarUpdatedEvent(jid: from, base64: binval, hash: hash));
|
getAttributes().sendEvent(
|
||||||
|
AvatarUpdatedEvent(
|
||||||
|
jid: from,
|
||||||
|
base64: binval,
|
||||||
|
hash: hash,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.warning('No avatar data found');
|
logger.warning('No avatar data found');
|
||||||
}
|
}
|
||||||
@@ -72,10 +79,10 @@ class VCardManager extends XmppManagerBase {
|
|||||||
logger.warning('Failed to retrieve vCard for $from');
|
logger.warning('Failed to retrieve vCard for $from');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
VCardPhoto? _parseVCardPhoto(XMLNode? node) {
|
VCardPhoto? _parseVCardPhoto(XMLNode? node) {
|
||||||
if (node == null) return null;
|
if (node == null) return null;
|
||||||
|
|
||||||
@@ -83,19 +90,19 @@ class VCardManager extends XmppManagerBase {
|
|||||||
binval: node.firstTag('BINVAL')?.innerText(),
|
binval: node.firstTag('BINVAL')?.innerText(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
VCard _parseVCard(XMLNode vcard) {
|
VCard _parseVCard(XMLNode vcard) {
|
||||||
final nickname = vcard.firstTag('NICKNAME')?.innerText();
|
final nickname = vcard.firstTag('NICKNAME')?.innerText();
|
||||||
final url = vcard.firstTag('URL')?.innerText();
|
final url = vcard.firstTag('URL')?.innerText();
|
||||||
|
|
||||||
return VCard(
|
return VCard(
|
||||||
url: url,
|
url: url,
|
||||||
nickname: nickname,
|
nickname: nickname,
|
||||||
photo: _parseVCardPhoto(vcard.firstTag('PHOTO')),
|
photo: _parseVCardPhoto(vcard.firstTag('PHOTO')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<VCard?> requestVCard(String jid) async {
|
Future<Result<VCardError, VCard>> requestVCard(String jid) async {
|
||||||
final result = await getAttributes().sendStanza(
|
final result = await getAttributes().sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
to: jid,
|
to: jid,
|
||||||
@@ -107,12 +114,17 @@ class VCardManager extends XmppManagerBase {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
encrypted: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.attributes['type'] != 'result') return null;
|
if (result.attributes['type'] != 'result') {
|
||||||
|
return Result(UnknownVCardError());
|
||||||
|
}
|
||||||
final vcard = result.firstTag('vCard', xmlns: vCardTempXmlns);
|
final vcard = result.firstTag('vCard', xmlns: vCardTempXmlns);
|
||||||
if (vcard == null) return null;
|
if (vcard == null) {
|
||||||
|
return Result(UnknownVCardError());
|
||||||
return _parseVCard(vcard);
|
}
|
||||||
}
|
|
||||||
|
return Result(_parseVCard(vcard));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ PubSubError getPubSubError(XMLNode stanza) {
|
|||||||
return EjabberdMaxItemsError();
|
return EjabberdMaxItemsError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnknownPubSubError();
|
return UnknownPubSubError();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
import 'package:moxxmpp/src/managers/base.dart';
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
@@ -7,7 +9,7 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
import 'package:moxxmpp/src/types/resultv2.dart';
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
@@ -16,14 +18,13 @@ import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
|
|||||||
import 'package:moxxmpp/src/xeps/xep_0060/helpers.dart';
|
import 'package:moxxmpp/src/xeps/xep_0060/helpers.dart';
|
||||||
|
|
||||||
class PubSubPublishOptions {
|
class PubSubPublishOptions {
|
||||||
|
|
||||||
const PubSubPublishOptions({
|
const PubSubPublishOptions({
|
||||||
this.accessModel,
|
this.accessModel,
|
||||||
this.maxItems,
|
this.maxItems,
|
||||||
});
|
});
|
||||||
final String? accessModel;
|
final String? accessModel;
|
||||||
final String? maxItems;
|
final String? maxItems;
|
||||||
|
|
||||||
XMLNode toXml() {
|
XMLNode toXml() {
|
||||||
return DataForm(
|
return DataForm(
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
@@ -34,34 +35,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(
|
? [
|
||||||
options: [],
|
DataFormField(
|
||||||
isRequired: false,
|
options: [],
|
||||||
values: [ accessModel! ],
|
isRequired: false,
|
||||||
varAttr: 'pubsub#access_model',
|
values: [accessModel!],
|
||||||
)
|
varAttr: 'pubsub#access_model',
|
||||||
] : [],
|
)
|
||||||
...maxItems != null ? [
|
]
|
||||||
DataFormField(
|
: [],
|
||||||
options: [],
|
...maxItems != null
|
||||||
isRequired: false,
|
? [
|
||||||
values: [maxItems! ],
|
DataFormField(
|
||||||
varAttr: 'pubsub#max_items',
|
options: [],
|
||||||
),
|
isRequired: false,
|
||||||
] : [],
|
values: [maxItems!],
|
||||||
|
varAttr: 'pubsub#max_items',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
],
|
],
|
||||||
).toXml();
|
).toXml();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PubSubItem {
|
class PubSubItem {
|
||||||
|
const PubSubItem({
|
||||||
const PubSubItem({ required this.id, required this.node, required this.payload });
|
required this.id,
|
||||||
|
required this.node,
|
||||||
|
required this.payload,
|
||||||
|
});
|
||||||
final String id;
|
final String id;
|
||||||
final String node;
|
final String node;
|
||||||
final XMLNode payload;
|
final XMLNode payload;
|
||||||
@@ -71,40 +79,41 @@ class PubSubItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PubSubManager extends XmppManagerBase {
|
class PubSubManager extends XmppManagerBase {
|
||||||
@override
|
PubSubManager() : super(pubsubManager);
|
||||||
String getId() => pubsubManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'PubsubManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
tagName: 'event',
|
tagName: 'event',
|
||||||
tagXmlns: pubsubEventXmlns,
|
tagXmlns: pubsubEventXmlns,
|
||||||
callback: _onPubsubMessage,
|
callback: _onPubsubMessage,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@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(
|
||||||
item: PubSubItem(
|
PubSubNotificationEvent(
|
||||||
id: item.attributes['id']! as String,
|
item: PubSubItem(
|
||||||
node: items.attributes['node']! as String,
|
id: item.attributes['id']! as String,
|
||||||
payload: item.children[0],
|
node: items.attributes['node']! as String,
|
||||||
|
payload: item.children[0],
|
||||||
|
),
|
||||||
|
from: message.attributes['from']! as String,
|
||||||
),
|
),
|
||||||
from: message.attributes['from']! as String,
|
);
|
||||||
),);
|
|
||||||
|
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,27 +122,40 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<PubSubPublishOptions> _preprocessPublishOptions(String jid, String node, PubSubPublishOptions options) async {
|
// TODO(PapaTutuWawa): This should return a Result<T> in case we cannot proceed
|
||||||
|
// with the requested configuration.
|
||||||
|
@visibleForTesting
|
||||||
|
Future<PubSubPublishOptions> preprocessPublishOptions(
|
||||||
|
String jid,
|
||||||
|
String node,
|
||||||
|
PubSubPublishOptions options,
|
||||||
|
) async {
|
||||||
if (options.maxItems != null) {
|
if (options.maxItems != null) {
|
||||||
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
|
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
|
||||||
final result = await dm.discoInfoQuery(jid);
|
final result = await dm.discoInfoQuery(jid);
|
||||||
if (result.isType<DiscoError>()) {
|
if (result.isType<DiscoError>()) {
|
||||||
if (options.maxItems == 'max') {
|
if (options.maxItems == 'max') {
|
||||||
logger.severe('disco#info query failed and options.maxItems is set to "max".');
|
logger.severe(
|
||||||
|
'disco#info query failed and options.maxItems is set to "max".',
|
||||||
|
);
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final nodeMultiItemsSupported = result.isType<DiscoInfo>() && result.get<DiscoInfo>().features.contains(pubsubNodeConfigMultiItems);
|
final nodeMultiItemsSupported = result.isType<DiscoInfo>() &&
|
||||||
final nodeMaxSupported = result.isType<DiscoInfo>() && result.get<DiscoInfo>().features.contains(pubsubNodeConfigMax);
|
result.get<DiscoInfo>().features.contains(pubsubNodeConfigMultiItems);
|
||||||
|
final nodeMaxSupported = result.isType<DiscoInfo>() &&
|
||||||
|
result.get<DiscoInfo>().features.contains(pubsubNodeConfigMax);
|
||||||
if (options.maxItems != null && !nodeMultiItemsSupported) {
|
if (options.maxItems != null && !nodeMultiItemsSupported) {
|
||||||
// TODO(PapaTutuWawa): Here, we need to admit defeat
|
// TODO(PapaTutuWawa): Here, we need to admit defeat
|
||||||
logger.finest('PubSub host does not support multi-items!');
|
logger.finest('PubSub host does not support multi-items!');
|
||||||
@@ -142,7 +164,9 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
accessModel: options.accessModel,
|
accessModel: options.accessModel,
|
||||||
);
|
);
|
||||||
} else if (options.maxItems == 'max' && !nodeMaxSupported) {
|
} else if (options.maxItems == 'max' && !nodeMaxSupported) {
|
||||||
logger.finest('PubSub host does not support node-config-max. Working around it');
|
logger.finest(
|
||||||
|
'PubSub host does not support node-config-max. Working around it',
|
||||||
|
);
|
||||||
final count = await _getNodeItemCount(jid, node) + 1;
|
final count = await _getNodeItemCount(jid, node) + 1;
|
||||||
|
|
||||||
return PubSubPublishOptions(
|
return PubSubPublishOptions(
|
||||||
@@ -154,7 +178,7 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<PubSubError, bool>> subscribe(String jid, String node) async {
|
Future<Result<PubSubError, bool>> subscribe(String jid, String node) async {
|
||||||
final attrs = getAttributes();
|
final attrs = getAttributes();
|
||||||
final result = await attrs.sendStanza(
|
final result = await attrs.sendStanza(
|
||||||
@@ -179,13 +203,19 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.attributes['type'] != 'result') return Result(UnknownPubSubError());
|
if (result.attributes['type'] != 'result') {
|
||||||
|
return Result(UnknownPubSubError());
|
||||||
|
}
|
||||||
|
|
||||||
final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns);
|
final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns);
|
||||||
if (pubsub == null) return Result(UnknownPubSubError());
|
if (pubsub == null) {
|
||||||
|
return Result(UnknownPubSubError());
|
||||||
|
}
|
||||||
|
|
||||||
final subscription = pubsub.firstTag('subscription');
|
final subscription = pubsub.firstTag('subscription');
|
||||||
if (subscription == null) return Result(UnknownPubSubError());
|
if (subscription == null) {
|
||||||
|
return Result(UnknownPubSubError());
|
||||||
|
}
|
||||||
|
|
||||||
return Result(subscription.attributes['subscription'] == 'subscribed');
|
return Result(subscription.attributes['subscription'] == 'subscribed');
|
||||||
}
|
}
|
||||||
@@ -214,27 +244,32 @@ 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish [payload] to the PubSub node [node] on JID [jid]. Returns true if it
|
/// Publish [payload] to the PubSub node [node] on JID [jid]. Returns true if it
|
||||||
/// was successful. False otherwise.
|
/// was successful. False otherwise.
|
||||||
Future<Result<PubSubError, bool>> publish(
|
Future<Result<PubSubError, bool>> publish(
|
||||||
String jid,
|
String jid,
|
||||||
String node,
|
String node,
|
||||||
XMLNode payload, {
|
XMLNode payload, {
|
||||||
String? id,
|
String? id,
|
||||||
PubSubPublishOptions? options,
|
PubSubPublishOptions? options,
|
||||||
}
|
}) async {
|
||||||
) async {
|
|
||||||
return _publish(
|
return _publish(
|
||||||
jid,
|
jid,
|
||||||
node,
|
node,
|
||||||
@@ -248,15 +283,14 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
String jid,
|
String jid,
|
||||||
String node,
|
String node,
|
||||||
XMLNode payload, {
|
XMLNode payload, {
|
||||||
String? id,
|
String? id,
|
||||||
PubSubPublishOptions? options,
|
PubSubPublishOptions? options,
|
||||||
// Should, if publishing fails, try to reconfigure and publish again?
|
// Should, if publishing fails, try to reconfigure and publish again?
|
||||||
bool tryConfigureAndPublish = true,
|
bool tryConfigureAndPublish = true,
|
||||||
}
|
}) async {
|
||||||
) async {
|
|
||||||
PubSubPublishOptions? pubOptions;
|
PubSubPublishOptions? pubOptions;
|
||||||
if (options != null) {
|
if (options != null) {
|
||||||
pubOptions = await _preprocessPublishOptions(jid, node, options);
|
pubOptions = await preprocessPublishOptions(jid, node, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await getAttributes().sendStanza(
|
final result = await getAttributes().sendStanza(
|
||||||
@@ -270,21 +304,22 @@ 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 ? [
|
if (pubOptions != null)
|
||||||
XMLNode(
|
XMLNode(
|
||||||
tag: 'publish-options',
|
tag: 'publish-options',
|
||||||
children: [options.toXml()],
|
children: [pubOptions.toXml()],
|
||||||
),
|
),
|
||||||
] : [],
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -308,10 +343,16 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
options: options,
|
options: options,
|
||||||
tryConfigureAndPublish: false,
|
tryConfigureAndPublish: false,
|
||||||
);
|
);
|
||||||
if (publishResult.isType<PubSubError>()) return publishResult;
|
if (publishResult.isType<PubSubError>()) {
|
||||||
} else if (error is EjabberdMaxItemsError && tryConfigureAndPublish && options != null) {
|
return publishResult;
|
||||||
|
}
|
||||||
|
} else if (error is EjabberdMaxItemsError &&
|
||||||
|
tryConfigureAndPublish &&
|
||||||
|
options != null) {
|
||||||
// TODO(Unknown): Remove once ejabberd fixes the bug. See errors.dart for more info.
|
// TODO(Unknown): Remove once ejabberd fixes the bug. See errors.dart for more info.
|
||||||
logger.warning('Publish failed due to the server rejecting the usage of "max" for "max_items" in publish options. Configuring...');
|
logger.warning(
|
||||||
|
'Publish failed due to the server rejecting the usage of "max" for "max_items" in publish options. Configuring...',
|
||||||
|
);
|
||||||
final count = await _getNodeItemCount(jid, node) + 1;
|
final count = await _getNodeItemCount(jid, node) + 1;
|
||||||
return publish(
|
return publish(
|
||||||
jid,
|
jid,
|
||||||
@@ -329,20 +370,31 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final pubsubElement = result.firstTag('pubsub', xmlns: pubsubXmlns);
|
final pubsubElement = result.firstTag('pubsub', xmlns: pubsubXmlns);
|
||||||
if (pubsubElement == null) return Result(MalformedResponseError());
|
if (pubsubElement == null) {
|
||||||
|
return Result(MalformedResponseError());
|
||||||
|
}
|
||||||
|
|
||||||
final publishElement = pubsubElement.firstTag('publish');
|
final publishElement = pubsubElement.firstTag('publish');
|
||||||
if (publishElement == null) return Result(MalformedResponseError());
|
if (publishElement == null) {
|
||||||
|
return Result(MalformedResponseError());
|
||||||
|
}
|
||||||
|
|
||||||
final item = publishElement.firstTag('item');
|
final item = publishElement.firstTag('item');
|
||||||
if (item == null) return Result(MalformedResponseError());
|
if (item == null) {
|
||||||
|
return Result(MalformedResponseError());
|
||||||
|
}
|
||||||
|
|
||||||
if (id != null) return Result(item.attributes['id'] == id);
|
if (id != null) {
|
||||||
|
return Result(item.attributes['id'] == id);
|
||||||
|
}
|
||||||
|
|
||||||
return const Result(true);
|
return const Result(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<PubSubError, List<PubSubItem>>> getItems(String jid, String node) async {
|
Future<Result<PubSubError, List<PubSubItem>>> getItems(
|
||||||
|
String jid,
|
||||||
|
String node,
|
||||||
|
) async {
|
||||||
final result = await getAttributes().sendStanza(
|
final result = await getAttributes().sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
type: 'get',
|
type: 'get',
|
||||||
@@ -352,33 +404,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')!
|
return PubSubItem(
|
||||||
.children.map((item) {
|
id: item.attributes['id']! as String,
|
||||||
return PubSubItem(
|
payload: item.children[0],
|
||||||
id: item.attributes['id']! as String,
|
node: node,
|
||||||
payload: item.children[0],
|
);
|
||||||
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',
|
||||||
@@ -390,11 +447,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},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -404,7 +461,9 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.attributes['type'] != 'result') return Result(getPubSubError(result));
|
if (result.attributes['type'] != 'result') {
|
||||||
|
return Result(getPubSubError(result));
|
||||||
|
}
|
||||||
|
|
||||||
final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns);
|
final pubsub = result.firstTag('pubsub', xmlns: pubsubXmlns);
|
||||||
if (pubsub == null) return Result(getPubSubError(result));
|
if (pubsub == null) return Result(getPubSubError(result));
|
||||||
@@ -421,7 +480,11 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
return Result(item);
|
return Result(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<PubSubError, bool>> configure(String jid, String node, PubSubPublishOptions options) async {
|
Future<Result<PubSubError, bool>> configure(
|
||||||
|
String jid,
|
||||||
|
String node,
|
||||||
|
PubSubPublishOptions options,
|
||||||
|
) async {
|
||||||
final attrs = getAttributes();
|
final attrs = getAttributes();
|
||||||
|
|
||||||
// Request the form
|
// Request the form
|
||||||
@@ -445,7 +508,9 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (form.attributes['type'] != 'result') return Result(getPubSubError(form));
|
if (form.attributes['type'] != 'result') {
|
||||||
|
return Result(getPubSubError(form));
|
||||||
|
}
|
||||||
|
|
||||||
final submit = await attrs.sendStanza(
|
final submit = await attrs.sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
@@ -470,7 +535,9 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (submit.attributes['type'] != 'result') return Result(getPubSubError(form));
|
if (submit.attributes['type'] != 'result') {
|
||||||
|
return Result(getPubSubError(form));
|
||||||
|
}
|
||||||
|
|
||||||
return const Result(true);
|
return const Result(true);
|
||||||
}
|
}
|
||||||
@@ -505,7 +572,11 @@ class PubSubManager extends XmppManagerBase {
|
|||||||
return const Result(true);
|
return const Result(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result<PubSubError, bool>> retract(JID host, String node, String itemId) async {
|
Future<Result<PubSubError, bool>> retract(
|
||||||
|
JID host,
|
||||||
|
String node,
|
||||||
|
String itemId,
|
||||||
|
) async {
|
||||||
final request = await getAttributes().sendStanza(
|
final request = await getAttributes().sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
type: 'set',
|
type: 'set',
|
||||||
|
|||||||
@@ -8,8 +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;
|
||||||
}
|
}
|
||||||
@@ -23,7 +22,7 @@ XMLNode constructOOBNode(OOBData data) {
|
|||||||
if (data.desc != null) {
|
if (data.desc != null) {
|
||||||
children.add(XMLNode(tag: 'desc', text: data.desc));
|
children.add(XMLNode(tag: 'desc', text: data.desc));
|
||||||
}
|
}
|
||||||
|
|
||||||
return XMLNode.xmlns(
|
return XMLNode.xmlns(
|
||||||
tag: 'x',
|
tag: 'x',
|
||||||
xmlns: oobDataXmlns,
|
xmlns: oobDataXmlns,
|
||||||
@@ -32,31 +31,30 @@ XMLNode constructOOBNode(OOBData data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class OOBManager extends XmppManagerBase {
|
class OOBManager extends XmppManagerBase {
|
||||||
@override
|
OOBManager() : super(oobManager);
|
||||||
String getName() => 'OOBName';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getId() => oobManager;
|
List<String> getDiscoFeatures() => [oobDataXmlns];
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> getDiscoFeatures() => [ oobDataXmlns ];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
tagName: 'x',
|
tagName: 'x',
|
||||||
tagXmlns: oobDataXmlns,
|
tagXmlns: oobDataXmlns,
|
||||||
callback: _onMessage,
|
callback: _onMessage,
|
||||||
// Before the message manager
|
// Before the message manager
|
||||||
priority: -99,
|
priority: -99,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@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');
|
||||||
|
|||||||
@@ -3,21 +3,24 @@ import 'package:moxxmpp/src/managers/base.dart';
|
|||||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
|
import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
|
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
|
||||||
|
|
||||||
class UserAvatar {
|
abstract class AvatarError {}
|
||||||
|
|
||||||
const UserAvatar({ required this.base64, required this.hash });
|
class UnknownAvatarError extends AvatarError {}
|
||||||
|
|
||||||
|
class UserAvatar {
|
||||||
|
const UserAvatar({required this.base64, required this.hash});
|
||||||
final String base64;
|
final String base64;
|
||||||
final String hash;
|
final String hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserAvatarMetadata {
|
class UserAvatarMetadata {
|
||||||
|
|
||||||
const UserAvatarMetadata(
|
const UserAvatarMetadata(
|
||||||
this.id,
|
this.id,
|
||||||
this.length,
|
this.length,
|
||||||
@@ -25,30 +28,41 @@ class UserAvatarMetadata {
|
|||||||
this.height,
|
this.height,
|
||||||
this.mime,
|
this.mime,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// The amount of bytes in the file
|
/// The amount of bytes in the file
|
||||||
final int length;
|
final int length;
|
||||||
|
|
||||||
/// The identifier of the avatar
|
/// The identifier of the avatar
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
/// Image proportions
|
/// Image proportions
|
||||||
final int width;
|
final int width;
|
||||||
final int height;
|
final int height;
|
||||||
|
|
||||||
/// The MIME type of the avatar
|
/// The MIME type of the avatar
|
||||||
final String mime;
|
final String mime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: This class requires a PubSubManager
|
/// NOTE: This class requires a PubSubManager
|
||||||
class UserAvatarManager extends XmppManagerBase {
|
class UserAvatarManager extends XmppManagerBase {
|
||||||
@override
|
UserAvatarManager() : super(userAvatarManager);
|
||||||
String getId() => userAvatarManager;
|
|
||||||
|
|
||||||
@override
|
PubSubManager _getPubSubManager() =>
|
||||||
String getName() => 'UserAvatarManager';
|
getAttributes().getManagerById(pubsubManager)! as PubSubManager;
|
||||||
|
|
||||||
PubSubManager _getPubSubManager() => getAttributes().getManagerById(pubsubManager)! as PubSubManager;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onXmppEvent(XmppEvent event) async {
|
Future<void> onXmppEvent(XmppEvent event) async {
|
||||||
if (event is PubSubNotificationEvent) {
|
if (event is PubSubNotificationEvent) {
|
||||||
|
if (event.item.node != userAvatarDataXmlns) return;
|
||||||
|
|
||||||
|
if (event.item.payload.tag != 'data' ||
|
||||||
|
event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) {
|
||||||
|
logger.warning(
|
||||||
|
'Received avatar update from ${event.from} but the payload is invalid. Ignoring...',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
getAttributes().sendEvent(
|
getAttributes().sendEvent(
|
||||||
AvatarUpdatedEvent(
|
AvatarUpdatedEvent(
|
||||||
jid: event.from,
|
jid: event.from,
|
||||||
@@ -62,30 +76,34 @@ class UserAvatarManager extends XmppManagerBase {
|
|||||||
// TODO(PapaTutuWawa): Check for PEP support
|
// TODO(PapaTutuWawa): Check for PEP support
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
/// Requests the avatar from [jid]. Returns the avatar data if the request was
|
/// Requests the avatar from [jid]. Returns the avatar data if the request was
|
||||||
/// successful. Null otherwise
|
/// successful. Null otherwise
|
||||||
// TODO(Unknown): Migrate to Resultsv2
|
Future<Result<AvatarError, UserAvatar>> getUserAvatar(String jid) async {
|
||||||
Future<UserAvatar?> getUserAvatar(String jid) async {
|
|
||||||
final pubsub = _getPubSubManager();
|
final pubsub = _getPubSubManager();
|
||||||
final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns);
|
final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns);
|
||||||
if (resultsRaw.isType<PubSubError>()) return null;
|
if (resultsRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
|
||||||
|
|
||||||
final results = resultsRaw.get<List<PubSubItem>>();
|
final results = resultsRaw.get<List<PubSubItem>>();
|
||||||
if (results.isEmpty) return null;
|
if (results.isEmpty) return Result(UnknownAvatarError());
|
||||||
|
|
||||||
final item = results[0];
|
final item = results[0];
|
||||||
return UserAvatar(
|
return Result(
|
||||||
base64: item.payload.innerText(),
|
UserAvatar(
|
||||||
hash: item.id,
|
base64: item.payload.innerText(),
|
||||||
|
hash: item.id,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish the avatar data, [base64], on the pubsub node using [hash] as
|
/// Publish the avatar data, [base64], on the pubsub node using [hash] as
|
||||||
/// the item id. [hash] must be the SHA-1 hash of the image data, while
|
/// the item id. [hash] must be the SHA-1 hash of the image data, while
|
||||||
/// [base64] must be the base64-encoded version of the image data.
|
/// [base64] must be the base64-encoded version of the image data.
|
||||||
// TODO(Unknown): Migrate to Resultsv2
|
Future<Result<AvatarError, bool>> publishUserAvatar(
|
||||||
Future<bool> publishUserAvatar(String base64, String hash, bool public) async {
|
String base64,
|
||||||
|
String hash,
|
||||||
|
bool public,
|
||||||
|
) async {
|
||||||
final pubsub = _getPubSubManager();
|
final pubsub = _getPubSubManager();
|
||||||
final result = await pubsub.publish(
|
final result = await pubsub.publish(
|
||||||
getAttributes().getFullJID().toBare().toString(),
|
getAttributes().getFullJID().toBare().toString(),
|
||||||
@@ -101,14 +119,18 @@ class UserAvatarManager extends XmppManagerBase {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return !result.isType<PubSubError>();
|
if (result.isType<PubSubError>()) return Result(UnknownAvatarError());
|
||||||
|
|
||||||
|
return const Result(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish avatar metadata [metadata] to the User Avatar's metadata node. If [public]
|
/// Publish avatar metadata [metadata] to the User Avatar's metadata node. If [public]
|
||||||
/// is true, then the node will be set to an 'open' access model. If [public] is false,
|
/// is true, then the node will be set to an 'open' access model. If [public] is false,
|
||||||
/// then the node will be set to an 'roster' access model.
|
/// then the node will be set to an 'roster' access model.
|
||||||
// TODO(Unknown): Migrate to Resultsv2
|
Future<Result<AvatarError, bool>> publishUserAvatarMetadata(
|
||||||
Future<bool> publishUserAvatarMetadata(UserAvatarMetadata metadata, bool public) async {
|
UserAvatarMetadata metadata,
|
||||||
|
bool public,
|
||||||
|
) async {
|
||||||
final pubsub = _getPubSubManager();
|
final pubsub = _getPubSubManager();
|
||||||
final result = await pubsub.publish(
|
final result = await pubsub.publish(
|
||||||
getAttributes().getFullJID().toBare().toString(),
|
getAttributes().getFullJID().toBare().toString(),
|
||||||
@@ -135,39 +157,41 @@ class UserAvatarManager extends XmppManagerBase {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.isType<PubSubError>();
|
if (result.isType<PubSubError>()) return Result(UnknownAvatarError());
|
||||||
|
return const Result(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe the data and metadata node of [jid].
|
/// Subscribe the data and metadata node of [jid].
|
||||||
// TODO(Unknown): Migrate to Resultsv2
|
Future<Result<AvatarError, bool>> subscribe(String jid) async {
|
||||||
Future<bool> subscribe(String jid) async {
|
|
||||||
await _getPubSubManager().subscribe(jid, userAvatarDataXmlns);
|
await _getPubSubManager().subscribe(jid, userAvatarDataXmlns);
|
||||||
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
|
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
|
||||||
|
|
||||||
return true;
|
return const Result(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unsubscribe the data and metadata node of [jid].
|
/// Unsubscribe the data and metadata node of [jid].
|
||||||
// TODO(Unknown): Migrate to Resultsv2
|
Future<Result<AvatarError, bool>> unsubscribe(String jid) async {
|
||||||
Future<bool> unsubscribe(String jid) async {
|
|
||||||
await _getPubSubManager().unsubscribe(jid, userAvatarDataXmlns);
|
await _getPubSubManager().unsubscribe(jid, userAvatarDataXmlns);
|
||||||
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
|
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
|
||||||
|
|
||||||
return true;
|
return const Result(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the PubSub Id of an avatar after doing a disco#items query.
|
/// Returns the PubSub Id of an avatar after doing a disco#items query.
|
||||||
/// Note that this assumes that there is only one (1) item published on
|
/// Note that this assumes that there is only one (1) item published on
|
||||||
/// the node.
|
/// the node.
|
||||||
// TODO(Unknown): Migrate to Resultsv2
|
Future<Result<AvatarError, String>> getAvatarId(String jid) async {
|
||||||
Future<String?> getAvatarId(String jid) async {
|
|
||||||
final disco = getAttributes().getManagerById(discoManager)! as DiscoManager;
|
final disco = getAttributes().getManagerById(discoManager)! as DiscoManager;
|
||||||
final response = await disco.discoItemsQuery(jid, node: userAvatarDataXmlns);
|
final response = await disco.discoItemsQuery(
|
||||||
if (response.isType<DiscoError>()) return null;
|
jid,
|
||||||
|
node: userAvatarDataXmlns,
|
||||||
|
shouldEncrypt: false,
|
||||||
|
);
|
||||||
|
if (response.isType<DiscoError>()) return Result(UnknownAvatarError());
|
||||||
|
|
||||||
final items = response.get<List<DiscoItem>>();
|
final items = response.get<List<DiscoItem>>();
|
||||||
if (items.isEmpty) return null;
|
if (items.isEmpty) return Result(UnknownAvatarError());
|
||||||
|
|
||||||
return items.first.name;
|
return Result(items.first.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,90 +6,96 @@ 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': {
|
}
|
||||||
return ChatState.composing;
|
case 'composing':
|
||||||
}
|
{
|
||||||
case 'paused': {
|
return ChatState.composing;
|
||||||
return ChatState.paused;
|
}
|
||||||
}
|
case 'paused':
|
||||||
case 'inactive': {
|
{
|
||||||
return ChatState.inactive;
|
return ChatState.paused;
|
||||||
}
|
}
|
||||||
case 'gone': {
|
case 'inactive':
|
||||||
return ChatState.gone;
|
{
|
||||||
}
|
return ChatState.inactive;
|
||||||
default: {
|
}
|
||||||
return ChatState.gone;
|
case 'gone':
|
||||||
}
|
{
|
||||||
|
return ChatState.gone;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
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 {
|
||||||
@override
|
ChatStateManager() : super(chatStateManager);
|
||||||
List<String> getDiscoFeatures() => [ chatStateXmlns ];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getName() => 'ChatStateManager';
|
List<String> getDiscoFeatures() => [chatStateXmlns];
|
||||||
|
|
||||||
@override
|
|
||||||
String getId() => chatStateManager;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
tagXmlns: chatStateXmlns,
|
tagXmlns: chatStateXmlns,
|
||||||
callback: _onChatStateReceived,
|
callback: _onChatStateReceived,
|
||||||
// Before the message handler
|
// Before the message handler
|
||||||
priority: -99,
|
priority: -99,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@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;
|
}
|
||||||
case 'composing': {
|
break;
|
||||||
chatState = ChatState.composing;
|
case 'composing':
|
||||||
}
|
{
|
||||||
break;
|
chatState = ChatState.composing;
|
||||||
case 'paused': {
|
}
|
||||||
chatState = ChatState.paused;
|
break;
|
||||||
}
|
case 'paused':
|
||||||
break;
|
{
|
||||||
case 'inactive': {
|
chatState = ChatState.paused;
|
||||||
chatState = ChatState.inactive;
|
}
|
||||||
}
|
break;
|
||||||
break;
|
case 'inactive':
|
||||||
case 'gone': {
|
{
|
||||||
chatState = ChatState.gone;
|
chatState = ChatState.inactive;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default: {
|
case 'gone':
|
||||||
logger.warning("Received invalid chat state '${element.tag}'");
|
{
|
||||||
}
|
chatState = ChatState.gone;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
logger.warning("Received invalid chat state '${element.tag}'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.copyWith(chatState: chatState);
|
return state.copyWith(chatState: chatState);
|
||||||
@@ -97,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)],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:cryptography/cryptography.dart';
|
import 'package:cryptography/cryptography.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
|
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||||
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
|
import 'package:moxxmpp/src/presence.dart';
|
||||||
import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
|
import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
|
||||||
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0414.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
class CapabilityHashInfo {
|
class CapabilityHashInfo {
|
||||||
|
|
||||||
const CapabilityHashInfo(this.ver, this.node, this.hash);
|
const CapabilityHashInfo(this.ver, this.node, this.hash);
|
||||||
final String ver;
|
final String ver;
|
||||||
final String node;
|
final String node;
|
||||||
@@ -13,11 +21,17 @@ 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(
|
||||||
.toList();
|
(Identity i) =>
|
||||||
|
'${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}',
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
// ignore: cascade_invocations
|
// ignore: cascade_invocations
|
||||||
identitiesSorted.sort(ioctetSortComparator);
|
identitiesSorted.sort(ioctetSortComparator);
|
||||||
buffer.write('${identitiesSorted.join("<")}<');
|
buffer.write('${identitiesSorted.join("<")}<');
|
||||||
@@ -28,20 +42,23 @@ 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.getFieldByVar('FORM_TYPE')!.values.first,
|
(a, b) => ioctetSortComparator(
|
||||||
b.getFieldByVar('FORM_TYPE')!.values.first,
|
a.getFieldByVar('FORM_TYPE')!.values.first,
|
||||||
),
|
b.getFieldByVar('FORM_TYPE')!.values.first,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
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
|
||||||
a.varAttr!,
|
..sort(
|
||||||
b.varAttr!,
|
(a, b) => ioctetSortComparator(
|
||||||
),
|
a.varAttr!,
|
||||||
);
|
b.varAttr!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
for (final field in sortedFields) {
|
for (final field in sortedFields) {
|
||||||
if (field.varAttr == 'FORM_TYPE') continue;
|
if (field.varAttr == 'FORM_TYPE') continue;
|
||||||
@@ -54,6 +71,85 @@ Future<String> calculateCapabilityHash(DiscoInfo info, HashAlgorithm algorithm)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes);
|
return base64
|
||||||
|
.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A manager implementing the advertising of XEP-0115. It responds to the
|
||||||
|
/// disco#info requests on the specified node with the information provided by
|
||||||
|
/// the DiscoManager.
|
||||||
|
/// NOTE: This manager requires that the DiscoManager is also registered.
|
||||||
|
class EntityCapabilitiesManager extends XmppManagerBase {
|
||||||
|
EntityCapabilitiesManager(this._capabilityHashBase)
|
||||||
|
: super(entityCapabilitiesManager);
|
||||||
|
|
||||||
|
/// The string that is both the node under which we advertise the disco info
|
||||||
|
/// and the base for the actual node on which we respond to disco#info requests.
|
||||||
|
final String _capabilityHashBase;
|
||||||
|
|
||||||
|
/// The cached capability hash.
|
||||||
|
String? _capabilityHash;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> getDiscoFeatures() => [capsXmlns];
|
||||||
|
|
||||||
|
/// Computes, if required, the capability hash of the data provided by
|
||||||
|
/// the DiscoManager.
|
||||||
|
Future<String> getCapabilityHash() async {
|
||||||
|
_capabilityHash ??= await calculateCapabilityHash(
|
||||||
|
getAttributes()
|
||||||
|
.getManagerById<DiscoManager>(discoManager)!
|
||||||
|
.getDiscoInfo(null),
|
||||||
|
getHashByName('sha-1')!,
|
||||||
|
);
|
||||||
|
|
||||||
|
return _capabilityHash!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _getNode() async {
|
||||||
|
final hash = await getCapabilityHash();
|
||||||
|
return '$_capabilityHashBase#$hash';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DiscoInfo> _onInfoQuery() async {
|
||||||
|
return getAttributes()
|
||||||
|
.getManagerById<DiscoManager>(discoManager)!
|
||||||
|
.getDiscoInfo(await _getNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<XMLNode>> _prePresenceSent() async {
|
||||||
|
return [
|
||||||
|
XMLNode.xmlns(
|
||||||
|
tag: 'c',
|
||||||
|
xmlns: capsXmlns,
|
||||||
|
attributes: {
|
||||||
|
'hash': 'sha-1',
|
||||||
|
'node': _capabilityHashBase,
|
||||||
|
'ver': await getCapabilityHash(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> postRegisterCallback() async {
|
||||||
|
await super.postRegisterCallback();
|
||||||
|
|
||||||
|
getAttributes()
|
||||||
|
.getManagerById<DiscoManager>(discoManager)!
|
||||||
|
.registerInfoCallback(
|
||||||
|
await _getNode(),
|
||||||
|
_onInfoQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
getAttributes()
|
||||||
|
.getManagerById<PresenceManager>(presenceManager)!
|
||||||
|
.registerPreSendCallback(
|
||||||
|
_prePresenceSent,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,52 +19,57 @@ XMLNode makeMessageDeliveryResponse(String id) {
|
|||||||
return XMLNode.xmlns(
|
return XMLNode.xmlns(
|
||||||
tag: 'received',
|
tag: 'received',
|
||||||
xmlns: deliveryXmlns,
|
xmlns: deliveryXmlns,
|
||||||
attributes: { 'id': id },
|
attributes: {'id': id},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessageDeliveryReceiptManager extends XmppManagerBase {
|
class MessageDeliveryReceiptManager extends XmppManagerBase {
|
||||||
@override
|
MessageDeliveryReceiptManager() : super(messageDeliveryReceiptManager);
|
||||||
List<String> getDiscoFeatures() => [ deliveryXmlns ];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getName() => 'MessageDeliveryReceiptManager';
|
List<String> getDiscoFeatures() => [deliveryXmlns];
|
||||||
|
|
||||||
@override
|
|
||||||
String getId() => messageDeliveryReceiptManager;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
tagName: 'received',
|
tagName: 'received',
|
||||||
tagXmlns: deliveryXmlns,
|
tagXmlns: deliveryXmlns,
|
||||||
callback: _onDeliveryReceiptReceived,
|
callback: _onDeliveryReceiptReceived,
|
||||||
// Before the message handler
|
// Before the message handler
|
||||||
priority: -99,
|
priority: -99,
|
||||||
),
|
),
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
tagName: 'request',
|
tagName: 'request',
|
||||||
tagXmlns: deliveryXmlns,
|
tagXmlns: deliveryXmlns,
|
||||||
callback: _onDeliveryRequestReceived,
|
callback: _onDeliveryRequestReceived,
|
||||||
// Before the message handler
|
// Before the message handler
|
||||||
priority: -99,
|
priority: -99,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,32 +9,26 @@ import 'package:moxxmpp/src/stringxml.dart';
|
|||||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||||
|
|
||||||
class BlockingManager extends XmppManagerBase {
|
class BlockingManager extends XmppManagerBase {
|
||||||
BlockingManager() : _supported = false, _gotSupported = false, super();
|
BlockingManager() : super(blockingManager);
|
||||||
|
|
||||||
bool _supported;
|
bool _supported = false;
|
||||||
bool _gotSupported;
|
bool _gotSupported = false;
|
||||||
|
|
||||||
@override
|
|
||||||
String getId() => blockingManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'BlockingManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'iq',
|
stanzaTag: 'iq',
|
||||||
tagName: 'unblock',
|
tagName: 'unblock',
|
||||||
tagXmlns: blockingXmlns,
|
tagXmlns: blockingXmlns,
|
||||||
callback: _unblockPush,
|
callback: _unblockPush,
|
||||||
),
|
),
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'iq',
|
stanzaTag: 'iq',
|
||||||
tagName: 'block',
|
tagName: 'block',
|
||||||
tagXmlns: blockingXmlns,
|
tagXmlns: blockingXmlns,
|
||||||
callback: _blockPush,
|
callback: _blockPush,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async {
|
Future<bool> isSupported() async {
|
||||||
@@ -52,25 +46,37 @@ class BlockingManager extends XmppManagerBase {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onXmppEvent(XmppEvent event) async {
|
Future<void> onXmppEvent(XmppEvent event) async {
|
||||||
if (event is StreamResumeFailedEvent) {
|
if (event is StreamNegotiationsDoneEvent) {
|
||||||
_gotSupported = false;
|
final newStream = await isNewStream();
|
||||||
_supported = false;
|
if (newStream) {
|
||||||
|
_gotSupported = 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');
|
||||||
|
|
||||||
@@ -88,7 +94,7 @@ class BlockingManager extends XmppManagerBase {
|
|||||||
|
|
||||||
return state.copyWith(done: true);
|
return state.copyWith(done: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> block(List<String> items) async {
|
Future<bool> block(List<String> items) async {
|
||||||
final result = await getAttributes().sendStanza(
|
final result = await getAttributes().sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
@@ -97,14 +103,12 @@ class BlockingManager extends XmppManagerBase {
|
|||||||
XMLNode.xmlns(
|
XMLNode.xmlns(
|
||||||
tag: 'block',
|
tag: 'block',
|
||||||
xmlns: blockingXmlns,
|
xmlns: blockingXmlns,
|
||||||
children: items
|
children: items.map((item) {
|
||||||
.map((item) {
|
return XMLNode(
|
||||||
return XMLNode(
|
tag: 'item',
|
||||||
tag: 'item',
|
attributes: <String, String>{'jid': item},
|
||||||
attributes: <String, String>{ 'jid': item },
|
);
|
||||||
);
|
}).toList(),
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -128,7 +132,7 @@ class BlockingManager extends XmppManagerBase {
|
|||||||
|
|
||||||
return result.attributes['type'] == 'result';
|
return result.attributes['type'] == 'result';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> unblock(List<String> items) async {
|
Future<bool> unblock(List<String> items) async {
|
||||||
assert(items.isNotEmpty, 'The list of items to unblock must be non-empty');
|
assert(items.isNotEmpty, 'The list of items to unblock must be non-empty');
|
||||||
|
|
||||||
@@ -139,10 +143,14 @@ class BlockingManager extends XmppManagerBase {
|
|||||||
XMLNode.xmlns(
|
XMLNode.xmlns(
|
||||||
tag: 'unblock',
|
tag: 'unblock',
|
||||||
xmlns: blockingXmlns,
|
xmlns: blockingXmlns,
|
||||||
children: items.map((item) => XMLNode(
|
children: items
|
||||||
tag: 'item',
|
.map(
|
||||||
attributes: <String, String>{ 'jid': item },
|
(item) => XMLNode(
|
||||||
),).toList(),
|
tag: 'item',
|
||||||
|
attributes: <String, String>{'jid': item},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -165,6 +173,9 @@ class BlockingManager extends XmppManagerBase {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final blocklist = result.firstTag('blocklist', xmlns: blockingXmlns)!;
|
final blocklist = result.firstTag('blocklist', xmlns: blockingXmlns)!;
|
||||||
return blocklist.findTags('item').map((item) => item.attributes['jid']! as String).toList();
|
return blocklist
|
||||||
|
.findTags('item')
|
||||||
|
.map((item) => item.attributes['jid']! as String)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:moxxmpp/src/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
|
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
|
import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||||
@@ -23,18 +24,17 @@ enum _StreamManagementNegotiatorState {
|
|||||||
/// StreamManagementManager at least once before connecting, if stream resumption
|
/// StreamManagementManager at least once before connecting, if stream resumption
|
||||||
/// is wanted.
|
/// is wanted.
|
||||||
class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
|
class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
|
||||||
|
|
||||||
StreamManagementNegotiator()
|
StreamManagementNegotiator()
|
||||||
: _state = _StreamManagementNegotiatorState.ready,
|
: _state = _StreamManagementNegotiatorState.ready,
|
||||||
_supported = false,
|
_supported = false,
|
||||||
_resumeFailed = false,
|
_resumeFailed = false,
|
||||||
_isResumed = false,
|
_isResumed = false,
|
||||||
_log = Logger('StreamManagementNegotiator'),
|
_log = Logger('StreamManagementNegotiator'),
|
||||||
super(10, false, smXmlns, streamManagementNegotiator);
|
super(10, false, smXmlns, streamManagementNegotiator);
|
||||||
_StreamManagementNegotiatorState _state;
|
_StreamManagementNegotiatorState _state;
|
||||||
bool _resumeFailed;
|
bool _resumeFailed;
|
||||||
bool _isResumed;
|
bool _isResumed;
|
||||||
|
|
||||||
final Logger _log;
|
final Logger _log;
|
||||||
|
|
||||||
/// True if Stream Management is supported on this stream.
|
/// True if Stream Management is supported on this stream.
|
||||||
@@ -43,7 +43,7 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
|
|||||||
|
|
||||||
/// True if the current stream is resumed. False if not.
|
/// True if the current stream is resumed. False if not.
|
||||||
bool get isResumed => _isResumed;
|
bool get isResumed => _isResumed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool matchesFeature(List<XMLNode> features) {
|
bool matchesFeature(List<XMLNode> features) {
|
||||||
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
|
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
|
||||||
@@ -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<void> 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 {
|
||||||
@@ -80,48 +87,55 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
|
|||||||
_state = _StreamManagementNegotiatorState.enableRequested;
|
_state = _StreamManagementNegotiatorState.enableRequested;
|
||||||
attributes.sendNonza(StreamManagementEnableNonza());
|
attributes.sendNonza(StreamManagementEnableNonza());
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case _StreamManagementNegotiatorState.resumeRequested:
|
|
||||||
if (nonza.tag == 'resumed') {
|
|
||||||
_log.finest('Stream Management resumption successful');
|
|
||||||
|
|
||||||
assert(attributes.getFullJID().resource != '', 'Resume only works when we already have a resource bound and know about it');
|
return const Result(NegotiatorState.ready);
|
||||||
|
case _StreamManagementNegotiatorState.resumeRequested:
|
||||||
|
if (nonza.tag == 'resumed') {
|
||||||
|
_log.finest('Stream Management resumption successful');
|
||||||
|
|
||||||
final csi = attributes.getManagerById(csiManager) as CSIManager?;
|
assert(
|
||||||
if (csi != null) {
|
attributes.getFullJID().resource != '',
|
||||||
csi.restoreCSIState();
|
'Resume only works when we already have a resource bound and know about it',
|
||||||
}
|
);
|
||||||
|
|
||||||
final h = int.parse(nonza.attributes['h']! as String);
|
final csi = attributes.getManagerById(csiManager) as CSIManager?;
|
||||||
await attributes.sendEvent(StreamResumedEvent(h: h));
|
if (csi != null) {
|
||||||
|
csi.restoreCSIState();
|
||||||
_resumeFailed = false;
|
|
||||||
_isResumed = true;
|
|
||||||
state = NegotiatorState.skipRest;
|
|
||||||
} else {
|
|
||||||
// We assume it is <failed />
|
|
||||||
_log.info('Stream resumption failed. Expected <resumed />, got ${nonza.tag}, Proceeding with new stream...');
|
|
||||||
await attributes.sendEvent(StreamResumeFailedEvent());
|
|
||||||
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
|
|
||||||
|
|
||||||
// We have to do this because we otherwise get a stanza stuck in the queue,
|
|
||||||
// thus spamming the server on every <a /> nonza we receive.
|
|
||||||
// ignore: cascade_invocations
|
|
||||||
await sm.setState(StreamManagementState(0, 0));
|
|
||||||
await sm.commitState();
|
|
||||||
|
|
||||||
_resumeFailed = true;
|
|
||||||
_isResumed = false;
|
|
||||||
_state = _StreamManagementNegotiatorState.ready;
|
|
||||||
state = NegotiatorState.retryLater;
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
final h = int.parse(nonza.attributes['h']! as String);
|
||||||
|
await attributes.sendEvent(StreamResumedEvent(h: h));
|
||||||
|
|
||||||
|
_resumeFailed = false;
|
||||||
|
_isResumed = true;
|
||||||
|
return const Result(NegotiatorState.skipRest);
|
||||||
|
} else {
|
||||||
|
// We assume it is <failed />
|
||||||
|
_log.info(
|
||||||
|
'Stream resumption failed. Expected <resumed />, got ${nonza.tag}, Proceeding with new stream...',
|
||||||
|
);
|
||||||
|
await attributes.sendEvent(StreamResumeFailedEvent());
|
||||||
|
final sm =
|
||||||
|
attributes.getManagerById<StreamManagementManager>(smManager)!;
|
||||||
|
|
||||||
|
// We have to do this because we otherwise get a stanza stuck in the queue,
|
||||||
|
// thus spamming the server on every <a /> nonza we receive.
|
||||||
|
// ignore: cascade_invocations
|
||||||
|
await sm.setState(StreamManagementState(0, 0));
|
||||||
|
await sm.commitState();
|
||||||
|
|
||||||
|
_resumeFailed = true;
|
||||||
|
_isResumed = false;
|
||||||
|
_state = _StreamManagementNegotiatorState.ready;
|
||||||
|
return const Result(NegotiatorState.retryLater);
|
||||||
|
}
|
||||||
case _StreamManagementNegotiatorState.enableRequested:
|
case _StreamManagementNegotiatorState.enableRequested:
|
||||||
if (nonza.tag == 'enabled') {
|
if (nonza.tag == 'enabled') {
|
||||||
_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');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,14 +147,12 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
state = NegotiatorState.done;
|
return const Result(NegotiatorState.done);
|
||||||
} else {
|
} else {
|
||||||
// We assume a <failed />
|
// We assume a <failed />
|
||||||
_log.warning('Stream Management enablement failed');
|
_log.warning('Stream Management enablement failed');
|
||||||
state = NegotiatorState.done;
|
return const Result(NegotiatorState.done);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,41 +2,39 @@ 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()
|
||||||
tag: 'enable',
|
: super(
|
||||||
attributes: <String, String>{
|
tag: 'enable',
|
||||||
'xmlns': smXmlns,
|
attributes: <String, String>{'xmlns': smXmlns, 'resume': 'true'},
|
||||||
'resume': 'true'
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamManagementResumeNonza extends XMLNode {
|
class StreamManagementResumeNonza extends XMLNode {
|
||||||
StreamManagementResumeNonza(String id, int h) : super(
|
StreamManagementResumeNonza(String id, int h)
|
||||||
tag: 'resume',
|
: super(
|
||||||
attributes: <String, String>{
|
tag: 'resume',
|
||||||
'xmlns': smXmlns,
|
attributes: <String, String>{
|
||||||
'previd': id,
|
'xmlns': smXmlns,
|
||||||
'h': h.toString()
|
'previd': id,
|
||||||
},
|
'h': h.toString()
|
||||||
);
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamManagementAckNonza extends XMLNode {
|
class StreamManagementAckNonza extends XMLNode {
|
||||||
StreamManagementAckNonza(int h) : super(
|
StreamManagementAckNonza(int h)
|
||||||
tag: 'a',
|
: super(
|
||||||
attributes: <String, String>{
|
tag: 'a',
|
||||||
'xmlns': smXmlns,
|
attributes: <String, String>{'xmlns': smXmlns, 'h': h.toString()},
|
||||||
'h': h.toString()
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamManagementRequestNonza extends XMLNode {
|
class StreamManagementRequestNonza extends XMLNode {
|
||||||
StreamManagementRequestNonza() : super(
|
StreamManagementRequestNonza()
|
||||||
tag: 'r',
|
: super(
|
||||||
attributes: <String, String>{
|
tag: 'r',
|
||||||
'xmlns': smXmlns,
|
attributes: <String, String>{
|
||||||
},
|
'xmlns': smXmlns,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,40 +21,41 @@ const xmlUintMax = 4294967296; // 2**32
|
|||||||
typedef StanzaAckedCallback = bool Function(Stanza stanza);
|
typedef StanzaAckedCallback = bool Function(Stanza stanza);
|
||||||
|
|
||||||
class StreamManagementManager extends XmppManagerBase {
|
class StreamManagementManager extends XmppManagerBase {
|
||||||
|
|
||||||
StreamManagementManager({
|
StreamManagementManager({
|
||||||
this.ackTimeout = const Duration(seconds: 30),
|
this.ackTimeout = const Duration(seconds: 30),
|
||||||
})
|
}) : super(smManager);
|
||||||
: _state = StreamManagementState(0, 0),
|
|
||||||
_unackedStanzas = {},
|
|
||||||
_stateLock = Lock(),
|
|
||||||
_streamManagementEnabled = false,
|
|
||||||
_lastAckTimestamp = -1,
|
|
||||||
_pendingAcks = 0,
|
|
||||||
_streamResumed = false,
|
|
||||||
_ackLock = Lock();
|
|
||||||
/// The queue of stanzas that are not (yet) acked
|
/// The queue of stanzas that are not (yet) acked
|
||||||
final Map<int, Stanza> _unackedStanzas;
|
final Map<int, Stanza> _unackedStanzas = {};
|
||||||
|
|
||||||
/// Commitable state of the StreamManagementManager
|
/// Commitable state of the StreamManagementManager
|
||||||
StreamManagementState _state;
|
StreamManagementState _state = StreamManagementState(0, 0);
|
||||||
|
|
||||||
/// Mutex lock for _state
|
/// Mutex lock for _state
|
||||||
final Lock _stateLock;
|
final Lock _stateLock = Lock();
|
||||||
|
|
||||||
/// If the have enabled SM on the stream yet
|
/// If the have enabled SM on the stream yet
|
||||||
bool _streamManagementEnabled;
|
bool _streamManagementEnabled = false;
|
||||||
|
|
||||||
/// If the current stream has been resumed;
|
/// If the current stream has been resumed;
|
||||||
bool _streamResumed;
|
bool _streamResumed = false;
|
||||||
|
|
||||||
/// The time in which the response to an ack is still valid. Counts as a timeout
|
/// The time in which the response to an ack is still valid. Counts as a timeout
|
||||||
/// otherwise
|
/// otherwise
|
||||||
@internal
|
@internal
|
||||||
final Duration ackTimeout;
|
final Duration ackTimeout;
|
||||||
|
|
||||||
/// The time at which the last ack has been sent
|
/// The time at which the last ack has been sent
|
||||||
int _lastAckTimestamp;
|
int _lastAckTimestamp = -1;
|
||||||
|
|
||||||
/// The timer to see if we timed the connection out
|
/// The timer to see if we timed the connection out
|
||||||
Timer? _ackTimer;
|
Timer? _ackTimer;
|
||||||
|
|
||||||
/// Counts how many acks we're waiting for
|
/// Counts how many acks we're waiting for
|
||||||
int _pendingAcks;
|
int _pendingAcks = 0;
|
||||||
|
|
||||||
/// Lock for both [_lastAckTimestamp] and [_pendingAcks].
|
/// Lock for both [_lastAckTimestamp] and [_pendingAcks].
|
||||||
final Lock _ackLock;
|
final Lock _ackLock = Lock();
|
||||||
|
|
||||||
/// Functions for testing
|
/// Functions for testing
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
@@ -79,12 +80,16 @@ class StreamManagementManager extends XmppManagerBase {
|
|||||||
bool shouldTriggerAckedEvent(Stanza stanza) {
|
bool shouldTriggerAckedEvent(Stanza stanza) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
int getUnackedStanzaCount() => _unackedStanzas.length;
|
int getUnackedStanzaCount() => _unackedStanzas.length;
|
||||||
|
|
||||||
@@ -116,46 +121,40 @@ class StreamManagementManager extends XmppManagerBase {
|
|||||||
_pendingAcks = 0;
|
_pendingAcks = 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamManagementState get state => _state;
|
StreamManagementState get state => _state;
|
||||||
|
|
||||||
bool get streamResumed => _streamResumed;
|
bool get streamResumed => _streamResumed;
|
||||||
|
|
||||||
@override
|
|
||||||
String getId() => smManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'StreamManagementManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<NonzaHandler> getNonzaHandlers() => [
|
List<NonzaHandler> getNonzaHandlers() => [
|
||||||
NonzaHandler(
|
NonzaHandler(
|
||||||
nonzaTag: 'r',
|
nonzaTag: 'r',
|
||||||
nonzaXmlns: smXmlns,
|
nonzaXmlns: smXmlns,
|
||||||
callback: _handleAckRequest,
|
callback: _handleAckRequest,
|
||||||
),
|
),
|
||||||
NonzaHandler(
|
NonzaHandler(
|
||||||
nonzaTag: 'a',
|
nonzaTag: 'a',
|
||||||
nonzaXmlns: smXmlns,
|
nonzaXmlns: smXmlns,
|
||||||
callback: _handleAckResponse,
|
callback: _handleAckResponse,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingPreStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
callback: _onServerStanzaReceived,
|
callback: _onServerStanzaReceived,
|
||||||
priority: 9999,
|
priority: 9999,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getOutgoingPostStanzaHandlers() => [
|
List<StanzaHandler> getOutgoingPostStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
callback: _onClientStanzaSent,
|
callback: _onClientStanzaSent,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onXmppEvent(XmppEvent event) async {
|
Future<void> onXmppEvent(XmppEvent event) async {
|
||||||
if (event is StreamResumedEvent) {
|
if (event is StreamResumedEvent) {
|
||||||
@@ -185,9 +184,18 @@ class StreamManagementManager extends XmppManagerBase {
|
|||||||
_disableStreamManagement();
|
_disableStreamManagement();
|
||||||
_streamResumed = false;
|
_streamResumed = false;
|
||||||
} else if (event is ConnectionStateChangedEvent) {
|
} else if (event is ConnectionStateChangedEvent) {
|
||||||
if (event.state == XmppConnectionState.connected) {
|
switch (event.state) {
|
||||||
// Push out all pending stanzas
|
case XmppConnectionState.connected:
|
||||||
await onStreamResumed(0);
|
// Push out all pending stanzas
|
||||||
|
await onStreamResumed(0);
|
||||||
|
break;
|
||||||
|
case XmppConnectionState.error:
|
||||||
|
case XmppConnectionState.notConnected:
|
||||||
|
_stopAckTimer();
|
||||||
|
break;
|
||||||
|
case XmppConnectionState.connecting:
|
||||||
|
// NOOP
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,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();
|
||||||
}
|
}
|
||||||
@@ -238,13 +247,13 @@ class StreamManagementManager extends XmppManagerBase {
|
|||||||
_startAckTimer();
|
_startAckTimer();
|
||||||
|
|
||||||
logger.fine('_pendingAcks is now at $_pendingAcks');
|
logger.fine('_pendingAcks is now at $_pendingAcks');
|
||||||
|
|
||||||
getAttributes().sendNonza(StreamManagementRequestNonza());
|
getAttributes().sendNonza(StreamManagementRequestNonza());
|
||||||
|
|
||||||
logger.fine('_sendAckRequest: Releasing lock...');
|
logger.fine('_sendAckRequest: Releasing lock...');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the enablement of stream management, but __NOT__ the internal state.
|
/// Resets the enablement of stream management, but __NOT__ the internal state.
|
||||||
/// This is to prevent ack requests being sent before we resume or re-enable
|
/// This is to prevent ack requests being sent before we resume or re-enable
|
||||||
/// stream management.
|
/// stream management.
|
||||||
@@ -252,13 +261,13 @@ class StreamManagementManager extends XmppManagerBase {
|
|||||||
_streamManagementEnabled = false;
|
_streamManagementEnabled = false;
|
||||||
logger.finest('Stream Management disabled');
|
logger.finest('Stream Management disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enables support for XEP-0198 stream management
|
/// Enables support for XEP-0198 stream management
|
||||||
void _enableStreamManagement() {
|
void _enableStreamManagement() {
|
||||||
_streamManagementEnabled = true;
|
_streamManagementEnabled = true;
|
||||||
logger.finest('Stream Management enabled');
|
logger.finest('Stream Management enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether XEP-0198 stream management is enabled
|
/// Returns whether XEP-0198 stream management is enabled
|
||||||
bool isStreamManagementEnabled() => _streamManagementEnabled;
|
bool isStreamManagementEnabled() => _streamManagementEnabled;
|
||||||
|
|
||||||
@@ -291,42 +300,44 @@ class StreamManagementManager extends XmppManagerBase {
|
|||||||
logger.fine('_pendingAcks is now at $_pendingAcks');
|
logger.fine('_pendingAcks is now at $_pendingAcks');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return early if we acked nothing.
|
// Return early if we acked nothing.
|
||||||
// Taken from slixmpp's stream management code
|
// Taken from slixmpp's stream management code
|
||||||
logger.fine('_handleAckResponse: Waiting to aquire lock...');
|
logger.fine('_handleAckResponse: Waiting to aquire lock...');
|
||||||
await _stateLock.synchronized(() async {
|
await _stateLock.synchronized(() async {
|
||||||
logger.fine('_handleAckResponse: Done...');
|
logger.fine('_handleAckResponse: Done...');
|
||||||
if (h == _state.c2s && _unackedStanzas.isEmpty) {
|
if (h == _state.c2s && _unackedStanzas.isEmpty) {
|
||||||
logger.fine('_handleAckResponse: Releasing lock...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final attrs = getAttributes();
|
|
||||||
final sequences = _unackedStanzas.keys.toList()..sort();
|
|
||||||
for (final height in sequences) {
|
|
||||||
// Do nothing if the ack does not concern this stanza
|
|
||||||
if (height > h) continue;
|
|
||||||
|
|
||||||
final stanza = _unackedStanzas[height]!;
|
|
||||||
_unackedStanzas.remove(height);
|
|
||||||
|
|
||||||
// Create a StanzaAckedEvent if the stanza is correct
|
|
||||||
if (shouldTriggerAckedEvent(stanza)) {
|
|
||||||
attrs.sendEvent(StanzaAckedEvent(stanza));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (h > _state.c2s) {
|
|
||||||
logger.info('C2S height jumped from ${_state.c2s} (local) to $h (remote).');
|
|
||||||
// ignore: cascade_invocations
|
|
||||||
logger.info('Proceeding with $h as local C2S counter.');
|
|
||||||
|
|
||||||
_state = _state.copyWith(c2s: h);
|
|
||||||
await commitState();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.fine('_handleAckResponse: Releasing lock...');
|
logger.fine('_handleAckResponse: Releasing lock...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final attrs = getAttributes();
|
||||||
|
final sequences = _unackedStanzas.keys.toList()..sort();
|
||||||
|
for (final height in sequences) {
|
||||||
|
// Do nothing if the ack does not concern this stanza
|
||||||
|
if (height > h) continue;
|
||||||
|
|
||||||
|
final stanza = _unackedStanzas[height]!;
|
||||||
|
_unackedStanzas.remove(height);
|
||||||
|
|
||||||
|
// Create a StanzaAckedEvent if the stanza is correct
|
||||||
|
if (shouldTriggerAckedEvent(stanza)) {
|
||||||
|
attrs.sendEvent(StanzaAckedEvent(stanza));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h > _state.c2s) {
|
||||||
|
logger.info(
|
||||||
|
'C2S height jumped from ${_state.c2s} (local) to $h (remote).',
|
||||||
|
);
|
||||||
|
// ignore: cascade_invocations
|
||||||
|
logger.info('Proceeding with $h as local C2S counter.');
|
||||||
|
|
||||||
|
_state = _state.copyWith(c2s: h);
|
||||||
|
await commitState();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.fine('_handleAckResponse: Releasing lock...');
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -336,33 +347,40 @@ class StreamManagementManager extends XmppManagerBase {
|
|||||||
Future<void> _incrementC2S() async {
|
Future<void> _incrementC2S() async {
|
||||||
logger.fine('_incrementC2S: Waiting to aquire lock...');
|
logger.fine('_incrementC2S: Waiting to aquire lock...');
|
||||||
await _stateLock.synchronized(() async {
|
await _stateLock.synchronized(() async {
|
||||||
logger.fine('_incrementC2S: Done');
|
logger.fine('_incrementC2S: Done');
|
||||||
_state = _state.copyWith(c2s: _state.c2s + 1 % xmlUintMax);
|
_state = _state.copyWith(c2s: _state.c2s + 1 % xmlUintMax);
|
||||||
await commitState();
|
await commitState();
|
||||||
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 {
|
||||||
logger.fine('_incrementS2C: Done');
|
logger.fine('_incrementS2C: Done');
|
||||||
_state = _state.copyWith(s2c: _state.s2c + 1 % xmlUintMax);
|
_state = _state.copyWith(s2c: _state.s2c + 1 % xmlUintMax);
|
||||||
await commitState();
|
await commitState();
|
||||||
logger.fine('_incrementS2C: Releasing lock...');
|
logger.fine('_incrementS2C: Releasing lock...');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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;
|
||||||
|
|
||||||
if (isStreamManagementEnabled()) {
|
if (isStreamManagementEnabled()) {
|
||||||
await _sendAckRequest();
|
await _sendAckRequest();
|
||||||
}
|
}
|
||||||
@@ -378,7 +396,7 @@ class StreamManagementManager extends XmppManagerBase {
|
|||||||
|
|
||||||
final stanzas = _unackedStanzas.values.toList();
|
final stanzas = _unackedStanzas.values.toList();
|
||||||
_unackedStanzas.clear();
|
_unackedStanzas.clear();
|
||||||
|
|
||||||
// Retransmit the rest of the queue
|
// Retransmit the rest of the queue
|
||||||
final attrs = getAttributes();
|
final attrs = getAttributes();
|
||||||
for (final stanza in stanzas) {
|
for (final stanza in stanzas) {
|
||||||
|
|||||||
@@ -8,33 +8,30 @@ import 'package:moxxmpp/src/stanza.dart';
|
|||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class DelayedDelivery {
|
class DelayedDelivery {
|
||||||
|
|
||||||
const DelayedDelivery(this.from, this.timestamp);
|
const DelayedDelivery(this.from, this.timestamp);
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
final String from;
|
final String from;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DelayedDeliveryManager extends XmppManagerBase {
|
class DelayedDeliveryManager extends XmppManagerBase {
|
||||||
|
DelayedDeliveryManager() : super(delayedDeliveryManager);
|
||||||
@override
|
|
||||||
String getId() => delayedDeliveryManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'DelayedDeliveryManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
callback: _onIncomingMessage,
|
callback: _onIncomingMessage,
|
||||||
priority: 200,
|
priority: 200,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
|||||||
@@ -12,38 +12,36 @@ import 'package:moxxmpp/src/stringxml.dart';
|
|||||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0297.dart';
|
import 'package:moxxmpp/src/xeps/xep_0297.dart';
|
||||||
|
|
||||||
|
/// This manager class implements support for XEP-0280.
|
||||||
class CarbonsManager extends XmppManagerBase {
|
class CarbonsManager extends XmppManagerBase {
|
||||||
|
CarbonsManager() : super(carbonsManager);
|
||||||
|
|
||||||
CarbonsManager() : _isEnabled = false, _supported = false, _gotSupported = false, super();
|
/// Indicates that message carbons are enabled.
|
||||||
bool _isEnabled;
|
bool _isEnabled = false;
|
||||||
bool _supported;
|
|
||||||
bool _gotSupported;
|
/// Indicates that the server supports message carbons.
|
||||||
|
bool _supported = false;
|
||||||
@override
|
|
||||||
String getId() => carbonsManager;
|
/// Indicates that we know that [CarbonsManager._supported] is accurate.
|
||||||
|
bool _gotSupported = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getName() => 'CarbonsManager';
|
List<StanzaHandler> getIncomingPreStanzaHandlers() => [
|
||||||
|
StanzaHandler(
|
||||||
@override
|
stanzaTag: 'message',
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
tagName: 'received',
|
||||||
StanzaHandler(
|
tagXmlns: carbonsXmlns,
|
||||||
stanzaTag: 'message',
|
callback: _onMessageReceived,
|
||||||
tagName: 'received',
|
priority: -98,
|
||||||
tagXmlns: carbonsXmlns,
|
),
|
||||||
callback: _onMessageReceived,
|
StanzaHandler(
|
||||||
// Before all managers the message manager depends on
|
stanzaTag: 'message',
|
||||||
priority: -98,
|
tagName: 'sent',
|
||||||
),
|
tagXmlns: carbonsXmlns,
|
||||||
StanzaHandler(
|
callback: _onMessageSent,
|
||||||
stanzaTag: 'message',
|
priority: -98,
|
||||||
tagName: 'sent',
|
)
|
||||||
tagXmlns: carbonsXmlns,
|
];
|
||||||
callback: _onMessageSent,
|
|
||||||
// Before all managers the message manager depends on
|
|
||||||
priority: -98,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async {
|
Future<bool> isSupported() async {
|
||||||
@@ -61,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...');
|
_gotSupported = false;
|
||||||
await enableCarbons();
|
_supported = false;
|
||||||
logger.finest('Message carbons enabled');
|
|
||||||
} else {
|
|
||||||
logger.info('Message carbons not supported.');
|
|
||||||
}
|
}
|
||||||
} else if (event is StreamResumeFailedEvent) {
|
|
||||||
_gotSupported = 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);
|
||||||
@@ -91,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);
|
||||||
@@ -104,10 +102,15 @@ class CarbonsManager extends XmppManagerBase {
|
|||||||
stanza: carbon,
|
stanza: carbon,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a request to the server, asking it to enable Message Carbons.
|
||||||
|
///
|
||||||
|
/// Returns true if carbons were enabled. False, if not.
|
||||||
Future<bool> enableCarbons() async {
|
Future<bool> enableCarbons() async {
|
||||||
final result = await getAttributes().sendStanza(
|
final attrs = getAttributes();
|
||||||
|
final result = await attrs.sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
|
to: attrs.getFullJID().toBare().toString(),
|
||||||
type: 'set',
|
type: 'set',
|
||||||
children: [
|
children: [
|
||||||
XMLNode.xmlns(
|
XMLNode.xmlns(
|
||||||
@@ -132,6 +135,9 @@ class CarbonsManager extends XmppManagerBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a request to the server, asking it to disable Message Carbons.
|
||||||
|
///
|
||||||
|
/// Returns true if carbons were disabled. False, if not.
|
||||||
Future<bool> disableCarbons() async {
|
Future<bool> disableCarbons() async {
|
||||||
final result = await getAttributes().sendStanza(
|
final result = await getAttributes().sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
@@ -154,17 +160,28 @@ class CarbonsManager extends XmppManagerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.fine('Successfully disabled message carbons');
|
logger.fine('Successfully disabled message carbons');
|
||||||
|
|
||||||
_isEnabled = false;
|
_isEnabled = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if Message Carbons are enabled. False, if not.
|
||||||
|
bool get isEnabled => _isEnabled;
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
void forceEnable() {
|
void forceEnable() {
|
||||||
_isEnabled = true;
|
_isEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a carbon sent by [senderJid] is valid to prevent vulnerabilities like
|
||||||
|
/// the ones listed at https://xmpp.org/extensions/xep-0280.html#security.
|
||||||
|
///
|
||||||
|
/// Returns true if the carbon is valid. Returns false if not.
|
||||||
bool isCarbonValid(JID senderJid) {
|
bool isCarbonValid(JID senderJid) {
|
||||||
return _isEnabled && senderJid == getAttributes().getConnectionSettings().jid.toBare();
|
return _isEnabled &&
|
||||||
|
getAttributes().getFullJID().bareCompare(
|
||||||
|
senderJid,
|
||||||
|
ensureBare: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -61,27 +61,26 @@ HashFunction hashFunctionFromName(String name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CryptographicHashManager extends XmppManagerBase {
|
class CryptographicHashManager extends XmppManagerBase {
|
||||||
@override
|
CryptographicHashManager() : super(cryptographicHashManager);
|
||||||
String getId() => cryptographicHashManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'CryptographicHashManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> isSupported() async => true;
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> getDiscoFeatures() => [
|
List<String> getDiscoFeatures() => [
|
||||||
'$hashFunctionNameBaseXmlns:$hashSha256',
|
'$hashFunctionNameBaseXmlns:$hashSha256',
|
||||||
'$hashFunctionNameBaseXmlns:$hashSha512',
|
'$hashFunctionNameBaseXmlns:$hashSha512',
|
||||||
//'$hashFunctionNameBaseXmlns:$hashSha3256',
|
//'$hashFunctionNameBaseXmlns:$hashSha3256',
|
||||||
//'$hashFunctionNameBaseXmlns:$hashSha3512',
|
//'$hashFunctionNameBaseXmlns:$hashSha3512',
|
||||||
//'$hashFunctionNameBaseXmlns:$hashBlake2b256',
|
//'$hashFunctionNameBaseXmlns:$hashBlake2b256',
|
||||||
'$hashFunctionNameBaseXmlns:$hashBlake2b512',
|
'$hashFunctionNameBaseXmlns:$hashBlake2b512',
|
||||||
];
|
];
|
||||||
|
|
||||||
static Future<List<int>> hashFromData(List<int> data, HashFunction function) async {
|
static Future<List<int>> hashFromData(
|
||||||
// TODO(PapaTutuWawa): Implemen the others as well
|
List<int> data,
|
||||||
|
HashFunction function,
|
||||||
|
) async {
|
||||||
|
// TODO(PapaTutuWawa): Implement the others as well
|
||||||
HashAlgorithm algo;
|
HashAlgorithm algo;
|
||||||
switch (function) {
|
switch (function) {
|
||||||
case HashFunction.sha256:
|
case HashFunction.sha256:
|
||||||
|
|||||||
49
packages/moxxmpp/lib/src/xeps/xep_0308.dart
Normal file
49
packages/moxxmpp/lib/src/xeps/xep_0308.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
|
import 'package:moxxmpp/src/managers/data.dart';
|
||||||
|
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||||
|
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||||
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
|
||||||
|
XMLNode makeLastMessageCorrectionEdit(String id) {
|
||||||
|
return XMLNode.xmlns(
|
||||||
|
tag: 'replace',
|
||||||
|
xmlns: lmcXmlns,
|
||||||
|
attributes: <String, String>{
|
||||||
|
'id': id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class LastMessageCorrectionManager extends XmppManagerBase {
|
||||||
|
LastMessageCorrectionManager() : super(lastMessageCorrectionManager);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<String> getDiscoFeatures() => [lmcXmlns];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
|
StanzaHandler(
|
||||||
|
stanzaTag: 'message',
|
||||||
|
tagName: 'replace',
|
||||||
|
tagXmlns: lmcXmlns,
|
||||||
|
callback: _onMessage,
|
||||||
|
// Before the message handler
|
||||||
|
priority: -99,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isSupported() async => true;
|
||||||
|
|
||||||
|
Future<StanzaHandlerData> _onMessage(
|
||||||
|
Stanza stanza,
|
||||||
|
StanzaHandlerData state,
|
||||||
|
) async {
|
||||||
|
final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!;
|
||||||
|
return state.copyWith(
|
||||||
|
lastMessageCorrectionSid: edit.attributes['id']! as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,52 +16,56 @@ 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},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatMarkerManager extends XmppManagerBase {
|
class ChatMarkerManager extends XmppManagerBase {
|
||||||
@override
|
ChatMarkerManager() : super(chatMarkerManager);
|
||||||
String getName() => 'ChatMarkerManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getId() => chatMarkerManager;
|
List<String> getDiscoFeatures() => [chatMarkersXmlns];
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> getDiscoFeatures() => [ chatMarkersXmlns ];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
tagXmlns: chatMarkersXmlns,
|
tagXmlns: chatMarkersXmlns,
|
||||||
callback: _onMessage,
|
callback: _onMessage,
|
||||||
// Before the message handler
|
// Before the message handler
|
||||||
priority: -99,
|
priority: -99,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@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
|
||||||
if (marker.tag == 'markable') return state.copyWith(isMarkable: true);
|
if (marker.tag == 'markable') return state.copyWith(isMarkable: true);
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -8,8 +8,22 @@ enum MessageProcessingHint {
|
|||||||
store,
|
store,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: We do not define a function for turning a Message Processing Hint element into
|
MessageProcessingHint messageProcessingHintFromXml(XMLNode element) {
|
||||||
/// an enum value since the elements do not concern us as a client.
|
switch (element.tag) {
|
||||||
|
case 'no-permanent-store':
|
||||||
|
return MessageProcessingHint.noPermanentStore;
|
||||||
|
case 'no-store':
|
||||||
|
return MessageProcessingHint.noStore;
|
||||||
|
case 'no-copy':
|
||||||
|
return MessageProcessingHint.noCopies;
|
||||||
|
case 'store':
|
||||||
|
return MessageProcessingHint.store;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(false, 'Invalid Message Processing Hint: ${element.tag}');
|
||||||
|
return MessageProcessingHint.noStore;
|
||||||
|
}
|
||||||
|
|
||||||
extension XmlExtension on MessageProcessingHint {
|
extension XmlExtension on MessageProcessingHint {
|
||||||
XMLNode toXml() {
|
XMLNode toXml() {
|
||||||
String tag;
|
String tag;
|
||||||
|
|||||||
@@ -4,39 +4,40 @@ import 'package:moxxmpp/src/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
|
|
||||||
class CSIActiveNonza extends XMLNode {
|
class CSIActiveNonza extends XMLNode {
|
||||||
CSIActiveNonza() : super(
|
CSIActiveNonza()
|
||||||
tag: 'active',
|
: super(
|
||||||
attributes: <String, String>{
|
tag: 'active',
|
||||||
'xmlns': csiXmlns
|
attributes: <String, String>{'xmlns': csiXmlns},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CSIInactiveNonza extends XMLNode {
|
class CSIInactiveNonza extends XMLNode {
|
||||||
CSIInactiveNonza() : super(
|
CSIInactiveNonza()
|
||||||
tag: 'inactive',
|
: super(
|
||||||
attributes: <String, String>{
|
tag: 'inactive',
|
||||||
'xmlns': csiXmlns
|
attributes: <String, String>{'xmlns': csiXmlns},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Stub negotiator that is just for "intercepting" the stream feature.
|
/// A Stub negotiator that is just for "intercepting" the stream feature.
|
||||||
class CSINegotiator extends XmppFeatureNegotiatorBase {
|
class CSINegotiator extends XmppFeatureNegotiatorBase {
|
||||||
CSINegotiator() : _supported = false, super(11, false, csiXmlns, csiNegotiator);
|
CSINegotiator() : super(11, false, csiXmlns, csiNegotiator);
|
||||||
|
|
||||||
/// True if CSI is supported. False otherwise.
|
/// True if CSI is supported. False otherwise.
|
||||||
bool _supported;
|
bool _supported = false;
|
||||||
bool get isSupported => _supported;
|
bool get isSupported => _supported;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> negotiate(XMLNode nonza) async {
|
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||||
|
XMLNode nonza,
|
||||||
|
) async {
|
||||||
// negotiate is only called when the negotiator matched, meaning the server
|
// negotiate is only called when the negotiator matched, meaning the server
|
||||||
// advertises CSI.
|
// advertises CSI.
|
||||||
_supported = true;
|
_supported = true;
|
||||||
state = NegotiatorState.done;
|
return const Result(NegotiatorState.done);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -49,21 +50,17 @@ class CSINegotiator extends XmppFeatureNegotiatorBase {
|
|||||||
|
|
||||||
/// The manager requires a CSINegotiator to be registered as a feature negotiator.
|
/// The manager requires a CSINegotiator to be registered as a feature negotiator.
|
||||||
class CSIManager extends XmppManagerBase {
|
class CSIManager extends XmppManagerBase {
|
||||||
|
CSIManager() : super(csiManager);
|
||||||
|
|
||||||
CSIManager() : _isActive = true, super();
|
bool _isActive = true;
|
||||||
bool _isActive;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getId() => csiManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'CSIManager';
|
|
||||||
|
|
||||||
@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
|
||||||
/// survive a stream resumption.
|
/// survive a stream resumption.
|
||||||
void restoreCSIState() {
|
void restoreCSIState() {
|
||||||
@@ -73,7 +70,7 @@ class CSIManager extends XmppManagerBase {
|
|||||||
setInactive();
|
setInactive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tells the server to top optimizing traffic
|
/// Tells the server to top optimizing traffic
|
||||||
Future<void> setActive() async {
|
Future<void> setActive() async {
|
||||||
_isActive = true;
|
_isActive = true;
|
||||||
|
|||||||
@@ -13,8 +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;
|
||||||
@@ -24,42 +23,49 @@ 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},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class StableIdManager extends XmppManagerBase {
|
class StableIdManager extends XmppManagerBase {
|
||||||
@override
|
StableIdManager() : super(stableIdManager);
|
||||||
String getName() => 'StableIdManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getId() => stableIdManager;
|
List<String> getDiscoFeatures() => [stableIdXmlns];
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> getDiscoFeatures() => [ stableIdXmlns ];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
stanzaTag: 'message',
|
stanzaTag: 'message',
|
||||||
callback: _onMessage,
|
callback: _onMessage,
|
||||||
// Before the MessageManager
|
// Before the MessageManager
|
||||||
priority: -99,
|
priority: -99,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@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;
|
||||||
String? stanzaIdBy;
|
String? stanzaIdBy;
|
||||||
final originIdTag = message.firstTag('origin-id', xmlns: stableIdXmlns);
|
final originIdTag = message.firstTag('origin-id', xmlns: stableIdXmlns);
|
||||||
final stanzaIdTag = message.firstTag('stanza-id', xmlns: stableIdXmlns);
|
final stanzaIdTag = message.firstTag('stanza-id', xmlns: stableIdXmlns);
|
||||||
if (originIdTag != null || stanzaIdTag != null) {
|
|
||||||
logger.finest('Found Unique and Stable Stanza Id tag');
|
// Process the origin id
|
||||||
|
if (originIdTag != null) {
|
||||||
|
logger.finest('Found origin Id tag');
|
||||||
|
originId = originIdTag.attributes['id']! as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the stanza id tag
|
||||||
|
if (stanzaIdTag != null) {
|
||||||
|
logger.finest('Found stanza Id tag');
|
||||||
final attrs = getAttributes();
|
final attrs = getAttributes();
|
||||||
final disco = attrs.getManagerById<DiscoManager>(discoManager)!;
|
final disco = attrs.getManagerById<DiscoManager>(discoManager)!;
|
||||||
final result = await disco.discoInfoQuery(from.toString());
|
final result = await disco.discoInfoQuery(from.toString());
|
||||||
@@ -68,20 +74,17 @@ class StableIdManager extends XmppManagerBase {
|
|||||||
logger.finest('Got info for ${from.toString()}');
|
logger.finest('Got info for ${from.toString()}');
|
||||||
if (info.features.contains(stableIdXmlns)) {
|
if (info.features.contains(stableIdXmlns)) {
|
||||||
logger.finest('${from.toString()} supports $stableIdXmlns.');
|
logger.finest('${from.toString()} supports $stableIdXmlns.');
|
||||||
|
stanzaId = stanzaIdTag.attributes['id']! as String;
|
||||||
if (originIdTag != null) {
|
stanzaIdBy = stanzaIdTag.attributes['by']! as String;
|
||||||
originId = originIdTag.attributes['id']! as String;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stanzaIdTag != null) {
|
|
||||||
stanzaId = stanzaIdTag.attributes['id']! as String;
|
|
||||||
stanzaIdBy = stanzaIdTag.attributes['by']! as String;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.finest('${from.toString()} does not support $stableIdXmlns. Ignoring... ');
|
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... ',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
packages/moxxmpp/lib/src/xeps/xep_0363/errors.dart
Normal file
10
packages/moxxmpp/lib/src/xeps/xep_0363/errors.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
abstract class HttpFileUploadError {}
|
||||||
|
|
||||||
|
/// Returned when we don't know what JID to ask for an upload slot
|
||||||
|
class NoEntityKnownError extends HttpFileUploadError {}
|
||||||
|
|
||||||
|
/// Returned when the file we want to upload is too big
|
||||||
|
class FileTooBigError extends HttpFileUploadError {}
|
||||||
|
|
||||||
|
/// Unspecified errors
|
||||||
|
class UnknownHttpFileUploadError extends HttpFileUploadError {}
|
||||||
@@ -7,19 +7,15 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/namespaces.dart';
|
import 'package:moxxmpp/src/namespaces.dart';
|
||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
import 'package:moxxmpp/src/types/error.dart';
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0363/errors.dart';
|
||||||
|
|
||||||
const errorNoUploadServer = 1;
|
const allowedHTTPHeaders = ['authorization', 'cookie', 'expires'];
|
||||||
const errorFileTooBig = 2;
|
|
||||||
const errorGeneric = 3;
|
|
||||||
|
|
||||||
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);
|
||||||
final String putUrl;
|
final String putUrl;
|
||||||
final String getUrl;
|
final String getUrl;
|
||||||
@@ -36,32 +32,36 @@ String _stripNewlinesFromString(String value) {
|
|||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
Map<String, String> prepareHeaders(Map<String, String> headers) {
|
Map<String, String> prepareHeaders(Map<String, String> headers) {
|
||||||
return headers.map((key, value) {
|
return headers.map((key, value) {
|
||||||
return MapEntry(
|
return MapEntry(
|
||||||
_stripNewlinesFromString(key),
|
_stripNewlinesFromString(key),
|
||||||
_stripNewlinesFromString(value),
|
_stripNewlinesFromString(value),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
..removeWhere((key, _) => !allowedHTTPHeaders.contains(key.toLowerCase()));
|
..removeWhere((key, _) => !allowedHTTPHeaders.contains(key.toLowerCase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpFileUploadManager extends XmppManagerBase {
|
class HttpFileUploadManager extends XmppManagerBase {
|
||||||
|
HttpFileUploadManager() : super(httpFileUploadManager);
|
||||||
|
|
||||||
HttpFileUploadManager() : _gotSupported = false, _supported = false, super();
|
/// The entity that we will request file uploads from, if discovered.
|
||||||
JID? _entityJid;
|
JID? _entityJid;
|
||||||
|
|
||||||
|
/// The maximum file upload file size, if advertised and discovered.
|
||||||
int? _maxUploadSize;
|
int? _maxUploadSize;
|
||||||
bool _gotSupported;
|
|
||||||
bool _supported;
|
|
||||||
|
|
||||||
@override
|
/// Flag, if we every tried to discover the upload entity.
|
||||||
String getId() => httpFileUploadManager;
|
bool _gotSupported = false;
|
||||||
|
|
||||||
@override
|
/// Flag, if we can use HTTP File Upload
|
||||||
String getName() => 'HttpFileUploadManager';
|
bool _supported = false;
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -80,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) {
|
||||||
_gotSupported = false;
|
final newStream = await isNewStream();
|
||||||
_supported = false;
|
if (newStream) {
|
||||||
_entityJid = null;
|
_gotSupported = false;
|
||||||
_maxUploadSize = null;
|
_supported = false;
|
||||||
|
_entityJid = 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;
|
||||||
@@ -102,8 +107,9 @@ 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) &&
|
||||||
logger.info('Discovered HTTP File Upload for ${info.jid}');
|
info.features.contains(httpFileUploadXmlns)) {
|
||||||
|
logger.info('Discovered HTTP File Upload for ${info.jid}');
|
||||||
|
|
||||||
_entityJid = info.jid;
|
_entityJid = info.jid;
|
||||||
_maxUploadSize = _getMaxFileSize(info);
|
_maxUploadSize = _getMaxFileSize(info);
|
||||||
@@ -119,19 +125,29 @@ 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<MayFail<HttpFileUploadSlot>> requestUploadSlot(String filename, int filesize, { String? contentType }) async {
|
Future<Result<HttpFileUploadSlot, HttpFileUploadError>> requestUploadSlot(
|
||||||
if (!(await isSupported())) return MayFail.failure(errorNoUploadServer);
|
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(
|
||||||
return MayFail.failure(errorNoUploadServer);
|
'Attempted to request HTTP File Upload slot but no entity is known to send this request to.',
|
||||||
|
);
|
||||||
|
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(
|
||||||
return MayFail.failure(errorFileTooBig);
|
'Attempted to request HTTP File Upload slot for a file that exceeds the filesize limit',
|
||||||
|
);
|
||||||
|
return Result(FileTooBigError());
|
||||||
}
|
}
|
||||||
|
|
||||||
final attrs = getAttributes();
|
final attrs = getAttributes();
|
||||||
final response = await attrs.sendStanza(
|
final response = await attrs.sendStanza(
|
||||||
Stanza.iq(
|
Stanza.iq(
|
||||||
@@ -144,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} : {}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -154,7 +170,7 @@ class HttpFileUploadManager extends XmppManagerBase {
|
|||||||
if (response.attributes['type']! != 'result') {
|
if (response.attributes['type']! != 'result') {
|
||||||
logger.severe('Failed to request HTTP File Upload slot.');
|
logger.severe('Failed to request HTTP File Upload slot.');
|
||||||
// TODO(Unknown): Be more precise
|
// TODO(Unknown): Be more precise
|
||||||
return MayFail.failure(errorGeneric);
|
return Result(UnknownHttpFileUploadError());
|
||||||
}
|
}
|
||||||
|
|
||||||
final slot = response.firstTag('slot', xmlns: httpFileUploadXmlns)!;
|
final slot = response.firstTag('slot', xmlns: httpFileUploadXmlns)!;
|
||||||
@@ -169,7 +185,7 @@ class HttpFileUploadManager extends XmppManagerBase {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return MayFail.success(
|
return Result(
|
||||||
HttpFileUploadSlot(
|
HttpFileUploadSlot(
|
||||||
putUrl,
|
putUrl,
|
||||||
getUrl,
|
getUrl,
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,33 +67,28 @@ XMLNode buildEmeElement(ExplicitEncryptionType type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class EmeManager extends XmppManagerBase {
|
class EmeManager extends XmppManagerBase {
|
||||||
|
EmeManager() : super(emeManager);
|
||||||
EmeManager() : super();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getId() => emeManager;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String getName() => 'EmeManager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
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() => [
|
||||||
StanzaHandler(
|
StanzaHandler(
|
||||||
tagName: 'encryption',
|
tagName: 'encryption',
|
||||||
tagXmlns: emeXmlns,
|
tagXmlns: emeXmlns,
|
||||||
callback: _onStanzaReceived,
|
callback: _onStanzaReceived,
|
||||||
// Before the message handler
|
// Before the message handler
|
||||||
priority: -99,
|
priority: -99,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
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(
|
||||||
|
|||||||
@@ -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() ==
|
||||||
encReceiver.toBare().toString() == ourJid.toBare().toString();
|
JID.fromString(sender).toBare().toString() &&
|
||||||
|
encReceiver.toBare().toString() == ourJid.toBare().toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ class InvalidAffixElementsException with Exception {}
|
|||||||
class OmemoNotSupportedForContactException extends OmemoError {}
|
class OmemoNotSupportedForContactException extends OmemoError {}
|
||||||
|
|
||||||
class EncryptionFailedException with Exception {}
|
class EncryptionFailedException with Exception {}
|
||||||
|
|
||||||
|
class InvalidEnvelopePayloadException with Exception {}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user