Compare commits
146 Commits
da591a552d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c61ddeb338 | |||
| e2515e25e4 | |||
| 09a849c6eb | |||
| 9eb94e5f48 | |||
| db77790bf4 | |||
| 7ceee48d31 | |||
| 941c3e4fd8 | |||
| 365ff2f238 | |||
| b3c8a6cd2f | |||
| d4166d087e | |||
| ddf781daff | |||
| 5973076b89 | |||
| 72cb76d1f6 | |||
| be7581e841 | |||
| 8a2435e4ad | |||
| 97f082b6f5 | |||
| f287d501ab | |||
| 93e9d6ca22 | |||
| 007cdce53d | |||
| 6d3a5e98de | |||
| e97d6e6517 | |||
| 882d20dc7a | |||
| 1f712151e4 | |||
| e7922668b1 | |||
| 87866bf3f5 | |||
| 41b789fa28 | |||
| 0a68f09fb4 | |||
| edf1d0b257 | |||
| 59b90307c2 | |||
| 49d3c6411b | |||
| 3a94dd9634 | |||
| fb4b4c71e2 | |||
| d9fbb9e102 | |||
| aba90f2e90 | |||
| 9211963390 | |||
| c7d58c3d3f | |||
| 6dbbf08be4 | |||
| 7ca648c478 | |||
| 814f99436b | |||
| 5bd2466c54 | |||
| 14b62cef96 | |||
| c3088f9046 | |||
| 64b93b536e | |||
| c1c48d0a83 | |||
| 4a681b9483 | |||
| c504afc944 | |||
| 76a9f7be7a | |||
| afa3927720 | |||
| 5f36289f50 | |||
| fbe3b90200 | |||
| d7c13abde6 | |||
| d4416c8a47 | |||
| 9666557655 | |||
| 1625f912b0 | |||
| 864cc0e747 | |||
| c9e817054d | |||
| d57bf2ef80 | |||
| 8bfdd5e54a | |||
| e58082bf38 | |||
| dbb945b424 | |||
| 2431eafa6c | |||
| 264ab130ee | |||
| 38dba0e6b7 | |||
| 94d6fe4925 | |||
| c8b903e5df | |||
| b14363319a | |||
| a18507cc3a | |||
| 93418f0127 | |||
| 1e7279e23b | |||
|
|
b2724aba0c | ||
|
|
d3742ea156 | ||
| b92e825bc1 | |||
|
|
8b00e85167 | ||
|
|
04dfc6d2ac | ||
|
|
9e70e802ef | ||
|
|
3ebd9b86ec | ||
| a928c5c877 | |||
| 77a1acb0e7 | |||
|
|
a873edb9ec | ||
|
|
e6bd6d05cd | ||
| 05e3d804a4 | |||
| b5efc2dfae | |||
|
|
b7d53b8f47 | ||
|
|
217c3ac236 | ||
| d35b955259 | |||
| 30dca67fb6 | |||
| 2db44e2f51 | |||
|
|
51bca6c25d | ||
| 4f9a0605c7 | |||
| 3621f2709a | |||
| 9da6d319a3 | |||
| e3ca83670a | |||
| fbbe413148 | |||
|
|
8728166a4d | ||
|
|
1f1321b269 | ||
| 9fd2daabb2 | |||
| 8252472fae | |||
| 3cb5a568ce | |||
| c2f62e2967 | |||
|
|
66195f66fa | ||
|
|
70fdfaf16d | ||
|
|
cd73f89e63 | ||
|
|
05c41d3185 | ||
|
|
64a8de6caa | ||
|
|
68809469f6 | ||
|
|
762cf1c77a | ||
| 29a5417b31 | |||
|
|
255d0f88e0 | ||
| fa2ce7c2d1 | |||
|
|
fa11a3a384 | ||
|
|
ac5bb9e461 | ||
| aa71d3ed5d | |||
| f2d8c6a009 | |||
| 88545e3308 | |||
| 925a46c0da | |||
| 327f695a40 | |||
| 8266765ff8 | |||
| e234c812ff | |||
| 4ff2992a03 | |||
| 09fd5845aa | |||
| 963f3f2cd9 | |||
| da1d28a6d6 | |||
| cbd90b1163 | |||
| f0538b0447 | |||
| 968604b0ba | |||
| 60279a84e0 | |||
| cf3287ccf4 | |||
| 1ab0ed856f | |||
| 10a5046431 | |||
| 4d76b9f57a | |||
| 0ec3777f44 | |||
| 6f5de9c4dc | |||
| 79d7e3ba64 | |||
| 8270185027 | |||
| 9e0f38154e | |||
| 1475cb542f | |||
| b949ec6ff5 | |||
| c3be199cca | |||
| 83ebe58c47 | |||
| 4db0ef6b34 | |||
| b95e75329d | |||
| 3163101f82 | |||
| bd4e1d28ea | |||
| b1da6e5a53 | |||
| c6552968d5 | |||
| 1d87c0ce95 |
2
.gitlint
2
.gitlint
@@ -7,7 +7,7 @@ line-length=72
|
||||
[title-trailing-punctuation]
|
||||
[title-hard-tab]
|
||||
[title-match-regex]
|
||||
regex=^((feat|fix|chore|refactor|docs|release|test)\((meta|tests|style|docs|xep|core|example)+(,(meta|tests|style|docs|xep|core|example))*\)|release): [A-Z0-9].*$
|
||||
regex=^((feat|fix|chore|refactor|docs|release|test)\((meta|tests|style|docs|xep|core|example|all|flake|ci)+(,(meta|tests|style|docs|xep|core|example|all|flake|ci))*\)|release): [A-Z0-9].*$
|
||||
|
||||
|
||||
[body-trailing-whitespace]
|
||||
|
||||
@@ -1,28 +1,49 @@
|
||||
when:
|
||||
branch: master
|
||||
|
||||
pipeline:
|
||||
# Check moxxmpp
|
||||
moxxmpp-lint:
|
||||
image: dart:2.18.1
|
||||
image: dart:3.0.7
|
||||
commands:
|
||||
- cd packages/moxxmpp
|
||||
- dart pub get
|
||||
# Proxy requests to pub.dev using pubcached
|
||||
- PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
|
||||
- dart analyze --fatal-infos --fatal-warnings
|
||||
|
||||
moxxmpp-test:
|
||||
image: dart:2.18.1
|
||||
image: dart:3.0.7
|
||||
commands:
|
||||
- cd packages/moxxmpp
|
||||
- dart pub get
|
||||
# Proxy requests to pub.dev using pubcached
|
||||
- PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
|
||||
- dart test
|
||||
|
||||
# Check moxxmpp_socket_tcp
|
||||
moxxmpp_socket_tcp-lint:
|
||||
image: dart:2.18.1
|
||||
image: dart:3.0.7
|
||||
commands:
|
||||
- cd packages/moxxmpp_socket_tcp
|
||||
- dart pub get
|
||||
# Proxy requests to pub.dev using pubcached
|
||||
- PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
|
||||
- dart analyze --fatal-infos --fatal-warnings
|
||||
|
||||
# moxxmpp-test:
|
||||
# image: dart:2.18.1
|
||||
# image: dart:3.0.7
|
||||
# commands:
|
||||
# - cd packages/moxxmpp
|
||||
# - dart pub get
|
||||
# # Proxy requests to pub.dev using pubcached
|
||||
# - PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
|
||||
# - dart test
|
||||
|
||||
notify:
|
||||
image: git.polynom.me/papatutuwawa/woodpecker-xmpp
|
||||
settings:
|
||||
xmpp_tls: 1
|
||||
xmpp_is_muc: 1
|
||||
xmpp_recipient: moxxy-build@muc.moxxy.org
|
||||
xmpp_alias: 2Bot
|
||||
secrets: [ xmpp_jid, xmpp_password, xmpp_server ]
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
|
||||
@@ -7,7 +7,7 @@ moxxmpp is a XMPP library written purely in Dart for usage in Moxxy.
|
||||
|
||||
This package contains the actual XMPP code that is platform-independent.
|
||||
|
||||
Documentation is available [here](https://moxxy.org/developers/docs/moxxmpp/).
|
||||
Documentation is available [here](https://docs.moxxy.org/moxxmpp/index.html).
|
||||
|
||||
### [moxxmpp_socket_tcp](./packages/moxxmpp_socket_tcp)
|
||||
|
||||
@@ -15,6 +15,10 @@ Documentation is available [here](https://moxxy.org/developers/docs/moxxmpp/).
|
||||
implements the RFC6120 connection algorithm and XEP-0368 direct TLS connections,
|
||||
if a DNS implementation is given, and supports StartTLS.
|
||||
|
||||
### moxxmpp_color
|
||||
|
||||
Implementation of [XEP-0392](https://xmpp.org/extensions/xep-0392.html).
|
||||
|
||||
## Development
|
||||
|
||||
To begin, use [melos](https://github.com/invertase/melos) to bootstrap the project: `melos bootstrap`. Then, the example
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
||||
|
||||
class TestingTCPSocketWrapper extends TCPSocketWrapper {
|
||||
TestingTCPSocketWrapper() : super(true);
|
||||
|
||||
@override
|
||||
bool onBadCertificate(dynamic certificate, String domain) {
|
||||
return true;
|
||||
@@ -30,24 +32,26 @@ class EchoMessageManager extends XmppManagerBase {
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final body = stanza.firstTag('body');
|
||||
if (body == null) return state.copyWith(done: true);
|
||||
if (body == null) return state..done = true;
|
||||
|
||||
final bodyText = body.innerText();
|
||||
|
||||
await getAttributes().sendStanza(
|
||||
Stanza.message(
|
||||
to: stanza.from,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'body',
|
||||
text: 'Hello, ${stanza.from}! You said "$bodyText"',
|
||||
),
|
||||
],
|
||||
StanzaDetails(
|
||||
Stanza.message(
|
||||
to: stanza.from,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'body',
|
||||
text: 'Hello, ${stanza.from}! You said "$bodyText"',
|
||||
),
|
||||
],
|
||||
),
|
||||
awaitable: false,
|
||||
),
|
||||
awaitable: false,
|
||||
);
|
||||
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
111
examples_dart/bin/muc_client.dart
Normal file
111
examples_dart/bin/muc_client.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'package:cli_repl/cli_repl.dart';
|
||||
import 'package:example_dart/arguments.dart';
|
||||
import 'package:example_dart/socket.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
|
||||
void main(List<String> args) async {
|
||||
// Set up logging
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
|
||||
);
|
||||
});
|
||||
|
||||
final parser = ArgumentParser()
|
||||
..parser.addOption('muc', help: 'The MUC to send messages to')
|
||||
..parser.addOption('nick', help: 'The nickname with which to join the MUC');
|
||||
final options = parser.handleArguments(args);
|
||||
if (options == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect
|
||||
final muc = JID.fromString(options['muc']! as String).toBare();
|
||||
final nick = options['nick']! as String;
|
||||
final connection = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
ClientToServerNegotiator(),
|
||||
ExampleTCPSocketWrapper(parser.srvRecord),
|
||||
)..connectionSettings = parser.connectionSettings;
|
||||
|
||||
// Register the managers and negotiators
|
||||
await connection.registerManagers([
|
||||
PresenceManager(),
|
||||
DiscoManager([]),
|
||||
PubSubManager(),
|
||||
MessageManager(),
|
||||
StableIdManager(),
|
||||
MUCManager(),
|
||||
]);
|
||||
await connection.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StartTlsNegotiator(),
|
||||
SaslScramNegotiator(10, '', '', ScramHashType.sha1),
|
||||
]);
|
||||
|
||||
// Connect
|
||||
Logger.root.info('Connecting...');
|
||||
final result =
|
||||
await connection.connect(shouldReconnect: false, waitUntilLogin: true);
|
||||
if (!result.isType<bool>()) {
|
||||
Logger.root.severe('Authentication failed!');
|
||||
return;
|
||||
}
|
||||
Logger.root.info('Connected.');
|
||||
|
||||
// Print received messages.
|
||||
connection
|
||||
.asBroadcastStream()
|
||||
.where((event) => event is MessageEvent)
|
||||
.listen((event) {
|
||||
event as MessageEvent;
|
||||
|
||||
// Ignore messages with no <body />
|
||||
final body = event.get<MessageBodyData>()?.body;
|
||||
if (body == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
print('=====> [${event.from}] $body');
|
||||
});
|
||||
|
||||
// Join room
|
||||
final mm = connection.getManagerById<MUCManager>(mucManager)!;
|
||||
await mm.joinRoom(
|
||||
muc,
|
||||
nick,
|
||||
maxHistoryStanzas: 0,
|
||||
);
|
||||
final state = (await mm.getRoomState(muc))!;
|
||||
|
||||
print('=====> ${state.members.length} users in room');
|
||||
print('=====> ${state.members.values.map((m) => m.nick).join(", ")}');
|
||||
|
||||
final repl = Repl(prompt: '> ');
|
||||
await for (final line in repl.runAsync()) {
|
||||
await connection
|
||||
.getManagerById<MessageManager>(messageManager)!
|
||||
.sendMessage(
|
||||
muc,
|
||||
TypedMap<StanzaHandlerExtension>.fromList([
|
||||
MessageBodyData(line),
|
||||
StableIdData(
|
||||
// NOTE: Don't do this. Use a UUID.
|
||||
DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
null,
|
||||
),
|
||||
]),
|
||||
type: 'groupchat');
|
||||
}
|
||||
|
||||
// Leave room
|
||||
await connection.getManagerById<MUCManager>(mucManager)!.leaveRoom(muc);
|
||||
|
||||
// Disconnect
|
||||
await connection.disconnect();
|
||||
}
|
||||
116
examples_dart/bin/omemo_client.dart
Normal file
116
examples_dart/bin/omemo_client.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'package:chalkdart/chalk.dart';
|
||||
import 'package:cli_repl/cli_repl.dart';
|
||||
import 'package:example_dart/arguments.dart';
|
||||
import 'package:example_dart/socket.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:omemo_dart/omemo_dart.dart' as omemo;
|
||||
|
||||
void main(List<String> args) async {
|
||||
// Set up logging
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
|
||||
);
|
||||
});
|
||||
|
||||
final parser = ArgumentParser()
|
||||
..parser.addOption('to', help: 'The JID to send messages to');
|
||||
final options = parser.handleArguments(args);
|
||||
if (options == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect
|
||||
final jid = parser.jid;
|
||||
final to = JID.fromString(options['to']! as String).toBare();
|
||||
final connection = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
ClientToServerNegotiator(),
|
||||
ExampleTCPSocketWrapper(parser.srvRecord, true),
|
||||
)..connectionSettings = parser.connectionSettings;
|
||||
|
||||
// Generate OMEMO data
|
||||
omemo.OmemoManager? oom;
|
||||
final moxxmppOmemo = OmemoManager(
|
||||
() async => oom!,
|
||||
(toJid, _) async => toJid == to,
|
||||
);
|
||||
oom = omemo.OmemoManager(
|
||||
await omemo.OmemoDevice.generateNewDevice(jid.toString(), opkAmount: 5),
|
||||
omemo.BlindTrustBeforeVerificationTrustManager(),
|
||||
moxxmppOmemo.sendEmptyMessageImpl,
|
||||
moxxmppOmemo.fetchDeviceList,
|
||||
moxxmppOmemo.fetchDeviceBundle,
|
||||
moxxmppOmemo.subscribeToDeviceListImpl,
|
||||
moxxmppOmemo.publishDeviceImpl,
|
||||
);
|
||||
final deviceId = await oom.getDeviceId();
|
||||
Logger.root.info('Our device id: $deviceId');
|
||||
|
||||
// Register the managers and negotiators
|
||||
await connection.registerManagers([
|
||||
PresenceManager(),
|
||||
DiscoManager([]),
|
||||
PubSubManager(),
|
||||
MessageManager(),
|
||||
moxxmppOmemo,
|
||||
]);
|
||||
await connection.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StartTlsNegotiator(),
|
||||
SaslScramNegotiator(10, '', '', ScramHashType.sha1),
|
||||
]);
|
||||
|
||||
// Set up event handlers
|
||||
connection.asBroadcastStream().listen((event) {
|
||||
if (event is MessageEvent) {
|
||||
Logger.root.info(event.id);
|
||||
Logger.root.info(event.extensions.keys.toList());
|
||||
|
||||
final body = event.encryptionError != null
|
||||
? chalk.red('Failed to decrypt message: ${event.encryptionError}')
|
||||
: chalk.green(event.get<MessageBodyData>()?.body ?? '');
|
||||
print('[${event.from.toString()}] $body');
|
||||
}
|
||||
});
|
||||
|
||||
// Connect
|
||||
Logger.root.info('Connecting...');
|
||||
final result =
|
||||
await connection.connect(shouldReconnect: false, waitUntilLogin: true);
|
||||
if (!result.isType<bool>()) {
|
||||
Logger.root.severe('Authentication failed!');
|
||||
return;
|
||||
}
|
||||
Logger.root.info('Connected.');
|
||||
|
||||
// Publish our bundle
|
||||
Logger.root.info('Publishing bundle');
|
||||
final device = await oom.getDevice();
|
||||
final omemoResult = await moxxmppOmemo.publishBundle(await device.toBundle());
|
||||
if (!omemoResult.isType<bool>()) {
|
||||
Logger.root.severe(
|
||||
'Failed to publish OMEMO bundle: ${omemoResult.get<OmemoError>()}');
|
||||
return;
|
||||
}
|
||||
|
||||
final repl = Repl(prompt: '> ');
|
||||
await for (final line in repl.runAsync()) {
|
||||
await connection
|
||||
.getManagerById<MessageManager>(messageManager)!
|
||||
.sendMessage(
|
||||
to,
|
||||
TypedMap<StanzaHandlerExtension>.fromList([
|
||||
MessageBodyData(line),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// Disconnect
|
||||
await connection.disconnect();
|
||||
}
|
||||
112
examples_dart/bin/simple_client.dart
Normal file
112
examples_dart/bin/simple_client.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
||||
|
||||
/// The JID we want to authenticate as.
|
||||
final xmppUser = JID.fromString('jane@example.com');
|
||||
|
||||
/// The password to authenticate with.
|
||||
const xmppPass = 'secret';
|
||||
|
||||
/// The [xmppHost]:[xmppPort] server address to connect to.
|
||||
/// In a real application, one might prefer to use [TCPSocketWrapper]
|
||||
/// with a custom DNS implementation to let moxxmpp resolve the XMPP
|
||||
/// server's address automatically. However, if we just provide a host
|
||||
/// and a port, then [TCPSocketWrapper] will just skip the resolution and
|
||||
/// immediately use the provided connection details.
|
||||
const xmppHost = 'localhost';
|
||||
const xmppPort = 5222;
|
||||
|
||||
void main(List<String> args) async {
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
print('${record.level.name}|${record.time}: ${record.message}');
|
||||
});
|
||||
|
||||
// This class manages every aspect of handling the XMPP stream.
|
||||
final connection = XmppConnection(
|
||||
// A reconnection policy tells the connection how to handle an error
|
||||
// while or after connecting to the server. The [TestingReconnectionPolicy]
|
||||
// immediately triggers a reconnection. In a real implementation, one might
|
||||
// prefer to use a smarter strategy, like using an exponential backoff.
|
||||
TestingReconnectionPolicy(),
|
||||
|
||||
// A connectivity manager tells the connection when it can connect. This is to
|
||||
// ensure that we're not constantly trying to reconnect because we have no
|
||||
// Internet connection. [AlwaysConnectedConnectivityManager] always says that
|
||||
// we're connected. In a real application, one might prefer to use a smarter
|
||||
// strategy, like using connectivity_plus to query the system's network connectivity
|
||||
// state.
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
|
||||
// This kind of negotiator tells the connection how to handle the stream
|
||||
// negotiations. The [ClientToServerNegotiator] allows to connect to the server
|
||||
// as a regular client. Another negotiator would be the [ComponentToServerNegotiator] that
|
||||
// allows for connections to the server where we're acting as a component.
|
||||
ClientToServerNegotiator(),
|
||||
|
||||
// A wrapper around any kind of connection. In this case, we use the [TCPSocketWrapper], which
|
||||
// uses a dart:io Socket/SecureSocket to connect to the server. If you want, you can also
|
||||
// provide your own socket to use, for example, WebSockets or any other connection
|
||||
// mechanism.
|
||||
TCPSocketWrapper(false),
|
||||
)..connectionSettings = ConnectionSettings(
|
||||
jid: xmppUser,
|
||||
password: xmppPass,
|
||||
host: xmppHost,
|
||||
port: xmppPort,
|
||||
);
|
||||
|
||||
// Register a set of "managers" that provide you with implementations of various
|
||||
// XEPs. Some have interdependencies, which need to be met. However, this example keeps
|
||||
// it simple and just registers a [MessageManager], which has no required dependencies.
|
||||
await connection.registerManagers([
|
||||
// The [MessageManager] handles receiving and sending <message /> stanzas.
|
||||
MessageManager(),
|
||||
]);
|
||||
|
||||
// Feature negotiators are objects that tell the connection negotiator what stream features
|
||||
// we can negotiate and enable. moxxmpp negotiators always try to enable their features.
|
||||
await connection.registerFeatureNegotiators([
|
||||
// This negotiator authenticates to the server using SASL PLAIN with the provided
|
||||
// credentials.
|
||||
SaslPlainNegotiator(),
|
||||
// This negotiator attempts to bind a resource. By default, it's always a random one.
|
||||
ResourceBindingNegotiator(),
|
||||
// This negotiator attempts to do StartTLS before authenticating.
|
||||
StartTlsNegotiator(),
|
||||
]);
|
||||
|
||||
// Set up a stream handler for the connection's event stream. Managers and negotiators
|
||||
// may trigger certain events. The [MessageManager], for example, triggers a [MessageEvent]
|
||||
// whenever a message is received. If other managers are registered that parse a message's
|
||||
// contents, then they can add their data to the event.
|
||||
connection.asBroadcastStream().listen((event) {
|
||||
if (event is! MessageEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The text body (contents of the <body /> element) are returned as a
|
||||
// [MessageBodyData] object. However, a message does not have to contain a
|
||||
// body, so it is nullable.
|
||||
final body = event.extensions.get<MessageBodyData>()?.body;
|
||||
print('[<-- ${event.from}] $body');
|
||||
});
|
||||
|
||||
// Connect to the server.
|
||||
final result = await connection.connect(
|
||||
// This flag indicates that we want to reconnect in case something happens.
|
||||
shouldReconnect: true,
|
||||
// This flag indicates that we want the returned Future to only resolve
|
||||
// once the stream negotiations are done and no negotiator has any feature left
|
||||
// to negotiate.
|
||||
waitUntilLogin: true,
|
||||
);
|
||||
|
||||
// Check if the connection was successful. [connection.connect] can return a boolean
|
||||
// to indicate success or a [XmppError] in case the connection attempt failed.
|
||||
if (!result.isType<bool>()) {
|
||||
print('Failed to connect to server');
|
||||
return;
|
||||
}
|
||||
}
|
||||
84
examples_dart/lib/arguments.dart
Normal file
84
examples_dart/lib/arguments.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:args/args.dart';
|
||||
import 'package:chalkdart/chalk.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
||||
|
||||
extension StringToInt on String {
|
||||
int toInt() => int.parse(this);
|
||||
}
|
||||
|
||||
/// A wrapper around [ArgParser] for providing convenience functions and standard parameters
|
||||
/// to the examples.
|
||||
class ArgumentParser {
|
||||
ArgumentParser() {
|
||||
parser
|
||||
..addOption('jid', help: 'The JID to connect as')
|
||||
..addOption('password', help: 'The password to use for authenticating')
|
||||
..addOption('host',
|
||||
help:
|
||||
'The host address to connect to (By default uses the domain part of the JID)')
|
||||
..addOption('port', help: 'The port to connect to')
|
||||
..addOption('xmpps-srv',
|
||||
help:
|
||||
'Inject a SRV record for _xmpps-client._tcp. Format: <priority>,<weight>,<target>,<port>')
|
||||
..addFlag('help',
|
||||
abbr: 'h',
|
||||
negatable: false,
|
||||
defaultsTo: false,
|
||||
help: 'Show this help text');
|
||||
}
|
||||
|
||||
/// The [ArgParser] that handles parsing the arguments.
|
||||
final ArgParser parser = ArgParser();
|
||||
|
||||
/// The parsed options. Only valid after calling [handleArguments].
|
||||
late ArgResults options;
|
||||
|
||||
ArgResults? handleArguments(List<String> args) {
|
||||
options = parser.parse(args);
|
||||
if (options['help']!) {
|
||||
print(parser.usage);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (options['jid'] == null) {
|
||||
print(chalk.red('No JID specified'));
|
||||
print(parser.usage);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (options['password'] == null) {
|
||||
print(chalk.red('No password specified'));
|
||||
print(parser.usage);
|
||||
return null;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/// The JID to connect as.
|
||||
JID get jid => JID.fromString(options['jid']!).toBare();
|
||||
|
||||
/// Construct connection settings from the parsed options.
|
||||
ConnectionSettings get connectionSettings => ConnectionSettings(
|
||||
jid: jid,
|
||||
password: options['password']!,
|
||||
host: options['host'],
|
||||
port: (options['port'] as String?)?.toInt(),
|
||||
);
|
||||
|
||||
/// Construct an xmpps-client SRV record for injection, if specified.
|
||||
MoxSrvRecord? get srvRecord {
|
||||
if (options['xmpps-srv'] == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final parts = options['xmpps-srv']!.split(',');
|
||||
return MoxSrvRecord(
|
||||
int.parse(parts[0]),
|
||||
int.parse(parts[1]),
|
||||
parts[2],
|
||||
int.parse(parts[3]),
|
||||
);
|
||||
}
|
||||
}
|
||||
22
examples_dart/lib/socket.dart
Normal file
22
examples_dart/lib/socket.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
||||
|
||||
/// A simple socket for examples that allows injection of SRV records (since
|
||||
/// we cannot use moxdns here).
|
||||
class ExampleTCPSocketWrapper extends TCPSocketWrapper {
|
||||
ExampleTCPSocketWrapper(this.srvRecord, bool logData) : super(logData);
|
||||
|
||||
/// A potential SRV record to inject for testing.
|
||||
final MoxSrvRecord? srvRecord;
|
||||
|
||||
@override
|
||||
bool onBadCertificate(dynamic certificate, String domain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MoxSrvRecord>> srvQuery(String domain, bool dnssec) async {
|
||||
return [
|
||||
if (srvRecord != null) srvRecord!,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,34 @@
|
||||
name: example_dart
|
||||
description: A sample command-line application.
|
||||
description: A collection of samples for moxxmpp.
|
||||
version: 1.0.0
|
||||
# homepage: https://www.example.com
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.0 <3.0.0'
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
args: 2.4.1
|
||||
chalkdart: 2.0.9
|
||||
cli_repl: 0.2.3
|
||||
logging: ^1.0.2
|
||||
moxxmpp:
|
||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||
version: 0.3.1
|
||||
version: 0.4.0
|
||||
moxxmpp_socket_tcp:
|
||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||
version: 0.3.1
|
||||
version: 0.4.0
|
||||
omemo_dart:
|
||||
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
|
||||
version: ^0.5.1
|
||||
|
||||
dependency_overrides:
|
||||
moxxmpp:
|
||||
path: ../packages/moxxmpp
|
||||
moxxmpp_socket_tcp:
|
||||
path: ../packages/moxxmpp_socket_tcp
|
||||
omemo_dart:
|
||||
git:
|
||||
url: https://github.com/PapaTutuWawa/omemo_dart.git
|
||||
rev: 49c7e114e6cf80dcde55fbbd218bba3182045862
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
|
||||
126
flake.lock
generated
126
flake.lock
generated
@@ -1,12 +1,74 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"android-nixpkgs": {
|
||||
"inputs": {
|
||||
"devshell": "devshell",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1678901627,
|
||||
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
|
||||
"lastModified": 1727554699,
|
||||
"narHash": "sha256-puBCNL5PW7Pej+6Srmi2YjEgNeE015NFe33hbkkLqeQ=",
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"rev": "bc34ef1c71fe9eafcfb1d637b431fca83d746625",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "tadfisher",
|
||||
"repo": "android-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"devshell": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"android-nixpkgs",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1722113426,
|
||||
"narHash": "sha256-Yo/3loq572A8Su6aY5GP56knpuKYRvM2a1meP9oJZCw=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "67cce7359e4cd3c45296fb4aaf6a19e2a9c757ae",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692799911,
|
||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -17,27 +79,27 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1676076353,
|
||||
"narHash": "sha256-mdUtE8Tp40cZETwcq5tCwwLqkJVV1ULJQ5GKRtbshag=",
|
||||
"owner": "AtaraxiaSjel",
|
||||
"lastModified": 1727348695,
|
||||
"narHash": "sha256-J+PeFKSDV+pHL7ukkfpVzCOO7mBSrrpJ3svwBFABbhI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5deb99bdccbbb97e7562dee4ba8a3ee3021688e6",
|
||||
"rev": "1925c603f17fc89f4c8f6bf6f631a802ad85d784",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "AtaraxiaSjel",
|
||||
"ref": "update/flutter",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1680273054,
|
||||
"narHash": "sha256-Bs6/5LpvYp379qVqGt9mXxxx9GSE789k3oFc+OAL07M=",
|
||||
"lastModified": 1727586919,
|
||||
"narHash": "sha256-e/YXG0tO5GWHDS8QQauj8aj4HhXEm602q9swrrlTlKQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3364b5b117f65fe1ce65a3cdd5612a078a3b31e3",
|
||||
"rev": "2dcd9c55e8914017226f5948ac22c53872a13ee2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -49,9 +111,39 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
||||
"android-nixpkgs": "android-nixpkgs",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
34
flake.nix
34
flake.nix
@@ -1,22 +1,20 @@
|
||||
{
|
||||
description = "moxxmpp";
|
||||
inputs = {
|
||||
nixpkgs.url = "github:AtaraxiaSjel/nixpkgs/update/flutter";
|
||||
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
android-nixpkgs.url = "github:tadfisher/android-nixpkgs";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import nixpkgs {
|
||||
outputs = { self, nixpkgs, android-nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config = {
|
||||
android_sdk.accept_license = true;
|
||||
allowUnfree = true;
|
||||
};
|
||||
};
|
||||
unstable = import nixpkgs-unstable {
|
||||
inherit system;
|
||||
};
|
||||
# Everything to make Flutter happy
|
||||
android = pkgs.androidenv.composeAndroidPackages {
|
||||
# TODO: Find a way to pin these
|
||||
#toolsVersion = "26.1.1";
|
||||
@@ -33,6 +31,7 @@
|
||||
useGoogleAPIs = false;
|
||||
useGoogleTVAddOns = false;
|
||||
};
|
||||
lib = pkgs.lib;
|
||||
pinnedJDK = pkgs.jdk17;
|
||||
|
||||
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
||||
@@ -51,7 +50,7 @@
|
||||
};
|
||||
|
||||
devShell = let
|
||||
prosody-newer-community-modules = unstable.prosody.overrideAttrs (old: {
|
||||
prosody-newer-community-modules = pkgs.prosody.overrideAttrs (old: {
|
||||
communityModules = pkgs.fetchhg {
|
||||
url = "https://hg.prosody.im/prosody-modules";
|
||||
rev = "e3a3a6c86a9f";
|
||||
@@ -103,7 +102,26 @@
|
||||
CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include";
|
||||
LD_LIBRARY_PATH = with pkgs; lib.makeLibraryPath [ atk cairo epoxy gdk-pixbuf glib gtk3 harfbuzz pango ];
|
||||
|
||||
ANDROID_SDK_ROOT = "${android.androidsdk}/share/android-sdk";
|
||||
ANDROID_HOME = "${android.androidsdk}/share/android-sdk";
|
||||
JAVA_HOME = pinnedJDK;
|
||||
|
||||
# Fix an issue with Flutter using an older version of aapt2, which does not know
|
||||
# an used parameter.
|
||||
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${android.androidsdk}/share/android-sdk/build-tools/34.0.0/aapt2";
|
||||
};
|
||||
|
||||
apps = {
|
||||
regenerateNixPackage = let
|
||||
script = pkgs.writeShellScript "regenerate-nix-package.sh" ''
|
||||
set -e
|
||||
${pythonEnv}/bin/python ./scripts/pubspec2lock.py ./packages/moxxmpp/pubspec.lock ./nix/moxxmpp.lock
|
||||
${pythonEnv}/bin/python ./scripts/lock2nix.py ./nix/moxxmpp.lock ./nix/pubcache.moxxmpp.nix moxxmpp
|
||||
'';
|
||||
in {
|
||||
type = "app";
|
||||
program = "${script}";
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
4
integration_tests/create_users.sh
Normal file
4
integration_tests/create_users.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
set -ex
|
||||
|
||||
prosodyctl --config ./prosody.cfg.lua register testuser1 localhost abc123
|
||||
prosodyctl --config ./prosody.cfg.lua register testuser2 localhost abc123
|
||||
@@ -3,14 +3,16 @@ description: A sample command-line application.
|
||||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.0 <3.0.0'
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
logging: ^1.0.2
|
||||
moxxmpp: 0.3.0
|
||||
moxxmpp_socket_tcp: 0.3.0
|
||||
logging: ^1.3.0
|
||||
moxxmpp:
|
||||
path: ../packages/moxxmpp
|
||||
moxxmpp_socket_tcp:
|
||||
path: ../packages/moxxmpp_socket_tcp
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
test: ^1.16.0
|
||||
very_good_analysis: ^3.0.1
|
||||
build_runner: ^2.4.13
|
||||
test: ^1.25.8
|
||||
very_good_analysis: ^6.0.0
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class TestingTCPSocketWrapper extends TCPSocketWrapper {
|
||||
TestingTCPSocketWrapper() : super(true);
|
||||
|
||||
@override
|
||||
bool onBadCertificate(dynamic certificate, String domain) {
|
||||
return true;
|
||||
|
||||
@@ -4,6 +4,8 @@ import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class TestingTCPSocketWrapper extends TCPSocketWrapper {
|
||||
TestingTCPSocketWrapper() : super(true);
|
||||
|
||||
@override
|
||||
bool onBadCertificate(dynamic certificate, String domain) {
|
||||
return true;
|
||||
@@ -27,7 +29,7 @@ void main() {
|
||||
ClientToServerNegotiator(),
|
||||
TestingTCPSocketWrapper(),
|
||||
)..connectionSettings = ConnectionSettings(
|
||||
jid: JID.fromString('testuser@localhost'),
|
||||
jid: JID.fromString('testuser1@localhost'),
|
||||
password: 'abc123',
|
||||
host: '127.0.0.1',
|
||||
port: 5222,
|
||||
@@ -40,17 +42,18 @@ void main() {
|
||||
]);
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
SaslScramNegotiator(9, '', '', ScramHashType.sha1),
|
||||
SaslScramNegotiator(10, '', '', ScramHashType.sha256),
|
||||
ResourceBindingNegotiator(),
|
||||
FASTSaslNegotiator(),
|
||||
Bind2Negotiator(),
|
||||
StartTlsNegotiator(),
|
||||
Sasl2Negotiator(
|
||||
userAgent: const UserAgent(
|
||||
Sasl2Negotiator()
|
||||
..userAgent = const UserAgent(
|
||||
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
|
||||
software: 'moxxmpp',
|
||||
device: "PapaTutuWawa's awesome device",
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
|
||||
609
nix/moxxmpp.lock
609
nix/moxxmpp.lock
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,30 @@
|
||||
# GENERATED BY LOCK2NIX.py
|
||||
# DO NOT EDIT BY HAND
|
||||
{fetchzip, runCommand} : rec {
|
||||
_fe_analyzer_shared = fetchzip {
|
||||
sha256 = "1hyd5pmjcfyvfwhsc0wq6k0229abmqq5zn95g31hh42bklb2gci5";
|
||||
url = "https://pub.dartlang.org/packages/_fe_analyzer_shared/versions/50.0.0.tar.gz";
|
||||
sha256 = "15fh9ka41dw4qsynv07msq4i243fibprcmafdygw5x88f7m55fq3";
|
||||
url = "https://pub.dartlang.org/packages/_fe_analyzer_shared/versions/61.0.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
analyzer = fetchzip {
|
||||
sha256 = "0niy5b3w39aywpjpw5a84pxdilhh3zzv1c22x8ywml756pybmj4r";
|
||||
url = "https://pub.dartlang.org/packages/analyzer/versions/5.2.0.tar.gz";
|
||||
sha256 = "0w604zngxwfx0xqxvhbxrhdh04wgm6ad6a1lbwnyvmk57amv44np";
|
||||
url = "https://pub.dartlang.org/packages/analyzer/versions/5.13.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
args = fetchzip {
|
||||
sha256 = "0c78zkzg2d2kzw1qrpiyrj1qvm4pr0yhnzapbqk347m780ha408g";
|
||||
url = "https://pub.dartlang.org/packages/args/versions/2.3.1.tar.gz";
|
||||
sha256 = "01ps253280c6dbx0vncw4wga4l2qp1zx779qjj2x06xzb3744zbz";
|
||||
url = "https://pub.dartlang.org/packages/args/versions/2.4.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
async = fetchzip {
|
||||
sha256 = "00hhylamsjcqmcbxlsrfimri63gb384l31r9mqvacn6c6bvk4yfx";
|
||||
url = "https://pub.dartlang.org/packages/async/versions/2.10.0.tar.gz";
|
||||
sha256 = "0hfgvjajp5c2mw68186hgrk9v5zjhhi149hlhl0fap274p2v1g3q";
|
||||
url = "https://pub.dartlang.org/packages/async/versions/2.11.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -49,29 +51,29 @@
|
||||
};
|
||||
|
||||
build_daemon = fetchzip {
|
||||
sha256 = "0b6hnwjc3gi5g7cnpy8xyiqigcrs0xp51c7y7v1pqn9v75g25w6j";
|
||||
url = "https://pub.dartlang.org/packages/build_daemon/versions/3.1.0.tar.gz";
|
||||
sha256 = "1wn7bq846vgdj62bkh9h25l95xdsndv0jdyw52nyr0591l3bpg3h";
|
||||
url = "https://pub.dartlang.org/packages/build_daemon/versions/3.1.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
build_resolvers = fetchzip {
|
||||
sha256 = "0fnrisgq6rnvbqsf8v43hb11kr1qq6azrxbsvx3wwimd37nxx8m5";
|
||||
url = "https://pub.dartlang.org/packages/build_resolvers/versions/2.1.0.tar.gz";
|
||||
sha256 = "00h9abhrfmnl0xxziyf6p68sxnbv2ww1c4dhgpnz00mzbmamnq5c";
|
||||
url = "https://pub.dartlang.org/packages/build_resolvers/versions/2.3.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
build_runner = fetchzip {
|
||||
sha256 = "0246bxl9rxgil55fhfzi7csd9a56blj9s1j1z79717hiyzsr60x6";
|
||||
url = "https://pub.dartlang.org/packages/build_runner/versions/2.3.2.tar.gz";
|
||||
sha256 = "0b5ha1l6k0gn2swqgqvfy2vl58klf81sxrjnmk0p7rj1wzbqjm7l";
|
||||
url = "https://pub.dartlang.org/packages/build_runner/versions/2.3.3.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
build_runner_core = fetchzip {
|
||||
sha256 = "0bpil0fw0dag3vbnin9p945ymi7xjgkiy7jrq9j52plljf7cnf5z";
|
||||
url = "https://pub.dartlang.org/packages/build_runner_core/versions/7.2.7.tar.gz";
|
||||
sha256 = "07r1kfy6ylm4i4xrb24ns8l26h4h1lgcskmnf8wvq2rd5d5hq790";
|
||||
url = "https://pub.dartlang.org/packages/build_runner_core/versions/7.2.7%2B1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -84,29 +86,29 @@
|
||||
};
|
||||
|
||||
built_value = fetchzip {
|
||||
sha256 = "0sslr4258snvcj8qhbdk6wapka174als0viyxddwqlnhs7dlci8i";
|
||||
url = "https://pub.dartlang.org/packages/built_value/versions/8.4.2.tar.gz";
|
||||
sha256 = "1y84imf9xqqy3gnd5zz9bcln6mycy7qx35r70b0izm31ismlbzkv";
|
||||
url = "https://pub.dartlang.org/packages/built_value/versions/8.6.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
checked_yaml = fetchzip {
|
||||
sha256 = "1gf7ankc5jb7mk17br87ajv05pfg6vb8nf35ay6c35w8jp70ra7k";
|
||||
url = "https://pub.dartlang.org/packages/checked_yaml/versions/2.0.1.tar.gz";
|
||||
sha256 = "1sn01yrmj0pkijn08g3v45c3zmyvdygk9svigkkzybgicdwlkpqs";
|
||||
url = "https://pub.dartlang.org/packages/checked_yaml/versions/2.0.3.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
code_builder = fetchzip {
|
||||
sha256 = "1vl9dl23yd0zjw52ndrazijs6dw83fg1rvyb2gfdpd6n1lj9nbhg";
|
||||
url = "https://pub.dartlang.org/packages/code_builder/versions/4.3.0.tar.gz";
|
||||
sha256 = "1shgl7mxiyv0hhw326yqj2b9jxi1h74qxmsnxf1d1xc6yz766p9a";
|
||||
url = "https://pub.dartlang.org/packages/code_builder/versions/4.6.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
collection = fetchzip {
|
||||
sha256 = "1iyl3v3j7mj3sxjf63b1kc182fwrwd04mjp5x2i61hic8ihfw545";
|
||||
url = "https://pub.dartlang.org/packages/collection/versions/1.17.0.tar.gz";
|
||||
sha256 = "1mr8j0078c4z9hhckiq8m735rggsazwfprm0w9gisil51vh7j2mk";
|
||||
url = "https://pub.dartlang.org/packages/collection/versions/1.18.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -119,29 +121,29 @@
|
||||
};
|
||||
|
||||
coverage = fetchzip {
|
||||
sha256 = "0akbg1yp2h4vprc8r9xvrpgvp5d26h7m80h5sbzgr5dlis1bcw0d";
|
||||
url = "https://pub.dartlang.org/packages/coverage/versions/1.6.1.tar.gz";
|
||||
sha256 = "1yy9bgkax5b6kk7qa07p452v82fyj4rl1j03fn366ywyvhfrh6lp";
|
||||
url = "https://pub.dartlang.org/packages/coverage/versions/1.6.3.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
crypto = fetchzip {
|
||||
sha256 = "1kjfb8fvdxazmv9ps2iqdhb8kcr31115h0nwn6v4xmr71k8jb8ds";
|
||||
url = "https://pub.dartlang.org/packages/crypto/versions/3.0.2.tar.gz";
|
||||
sha256 = "100ai8qa4p3dyvvd60c4xa9p0gm06yh0d68xgcfm3giraad8xmqj";
|
||||
url = "https://pub.dartlang.org/packages/crypto/versions/3.0.3.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
cryptography = fetchzip {
|
||||
sha256 = "0jqph45d9lbhdakprnb84c3qhk4aq05hhb1pmn8w23yhl41ypijs";
|
||||
url = "https://pub.dartlang.org/packages/cryptography/versions/2.0.5.tar.gz";
|
||||
sha256 = "1yxn9slqq93ri81fbr2nbsinz0mpk9wk39ny076ja8q31d4i8v3f";
|
||||
url = "https://pub.dartlang.org/packages/cryptography/versions/2.5.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
dart_style = fetchzip {
|
||||
sha256 = "01wg15kalbjlh4i3xbawc9zk8yrk28qhak7xp7mlwn2syhdckn7v";
|
||||
url = "https://pub.dartlang.org/packages/dart_style/versions/2.2.4.tar.gz";
|
||||
sha256 = "0cjhrb1hs8iw9smmfd0fgnxq3nm0w8sz17l6q6svyz6kif19wk9k";
|
||||
url = "https://pub.dartlang.org/packages/dart_style/versions/2.3.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -154,43 +156,29 @@
|
||||
};
|
||||
|
||||
fixnum = fetchzip {
|
||||
sha256 = "1m8cdfqp9d6w1cik3fwz9bai1wf9j11rjv2z0zlv7ich87q9kkjk";
|
||||
url = "https://pub.dartlang.org/packages/fixnum/versions/1.0.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
freezed = fetchzip {
|
||||
sha256 = "1i9s4djf4vlz56zqn8brcck3n7sk07qay23wmaan991cqydd10iq";
|
||||
url = "https://pub.dartlang.org/packages/freezed/versions/2.1.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
freezed_annotation = fetchzip {
|
||||
sha256 = "0ym120dh1lpfnb68gxh1finm8p9l445q5x10aw8269y469b9k9z3";
|
||||
url = "https://pub.dartlang.org/packages/freezed_annotation/versions/2.1.0.tar.gz";
|
||||
sha256 = "0nqrzj41ys8dpxf1x70r0kfj1avj0f2j2b7498k8kvc0i9c5asz7";
|
||||
url = "https://pub.dartlang.org/packages/fixnum/versions/1.1.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
frontend_server_client = fetchzip {
|
||||
sha256 = "0nv4avkv2if9hdcfzckz36f3mclv7vxchivrg8j3miaqhnjvv4bj";
|
||||
url = "https://pub.dartlang.org/packages/frontend_server_client/versions/3.1.0.tar.gz";
|
||||
sha256 = "096v7ycix5hgnk750s1qgykyghl2mymhdkg39jrlk3kbj6xygq5b";
|
||||
url = "https://pub.dartlang.org/packages/frontend_server_client/versions/3.2.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
glob = fetchzip {
|
||||
sha256 = "0a6gbwsbz6rkg35dkff0zv88rvcflqdmda90hdfpn7jp1z1w9rhs";
|
||||
url = "https://pub.dartlang.org/packages/glob/versions/2.1.0.tar.gz";
|
||||
sha256 = "0ffab3azx8zkma36mk6wnig8bn8g5g0vjrq2gl21y77rxgw9iqxj";
|
||||
url = "https://pub.dartlang.org/packages/glob/versions/2.1.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
graphs = fetchzip {
|
||||
sha256 = "0cr6dgs1a7ln2ir5gd0kiwpn787lk4dwhqfjv8876hkkr1rv80m9";
|
||||
url = "https://pub.dartlang.org/packages/graphs/versions/2.2.0.tar.gz";
|
||||
sha256 = "0fda0j8y6sq1rc9zpzglrzysl5h49y2ji1wq2lq0wx2c609dxm7f";
|
||||
url = "https://pub.dartlang.org/packages/graphs/versions/2.3.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -217,78 +205,78 @@
|
||||
};
|
||||
|
||||
io = fetchzip {
|
||||
sha256 = "1bp5l8hkrp6fjj7zw9af51hxyp52sjspc5558lq0lmi453l0czni";
|
||||
url = "https://pub.dartlang.org/packages/io/versions/1.0.3.tar.gz";
|
||||
sha256 = "101kd0rw26vglmr1m5p130kbrp3k7dk4p5nr77wsbwgg53w8c0d4";
|
||||
url = "https://pub.dartlang.org/packages/io/versions/1.0.4.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
js = fetchzip {
|
||||
sha256 = "13fbxgyg1v6bmzvxamg6494vk3923fn3mgxj6f4y476aqwk99n50";
|
||||
url = "https://pub.dartlang.org/packages/js/versions/0.6.5.tar.gz";
|
||||
sha256 = "124a9yqrjdw3p4nnirab9hm9ziwraldlw4q5cb3sr0dcrli74qpw";
|
||||
url = "https://pub.dartlang.org/packages/js/versions/0.6.7.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
json_annotation = fetchzip {
|
||||
sha256 = "1p9nvn33psx2zbalhyqjw8gr4agd76jj5jq0fdz0i584c7l77bby";
|
||||
url = "https://pub.dartlang.org/packages/json_annotation/versions/4.7.0.tar.gz";
|
||||
sha256 = "1jjw7p8qyqajgdq4jqvxipq5w0qrq9dpi1qmia70pk995akryh6m";
|
||||
url = "https://pub.dartlang.org/packages/json_annotation/versions/4.8.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
json_serializable = fetchzip {
|
||||
sha256 = "04d7laaxrbiybcgbv3y223hy8d6n9f84h5lv9sv79zd9ffzkb2hg";
|
||||
url = "https://pub.dartlang.org/packages/json_serializable/versions/6.5.4.tar.gz";
|
||||
sha256 = "1pmidql9x6s2pbhdx9x20pwqwvwpfkvrz0h0cm1f8pqis76c90hb";
|
||||
url = "https://pub.dartlang.org/packages/json_serializable/versions/6.6.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
logging = fetchzip {
|
||||
sha256 = "0hl1mjh662c44ci7z60x92i0jsyqg1zm6k6fc89n9pdcxsqdpwfs";
|
||||
url = "https://pub.dartlang.org/packages/logging/versions/1.0.2.tar.gz";
|
||||
sha256 = "124hfjs66r30p92ndfmy5fymgy66yk9in97h8sq6fi7r78pqyc7g";
|
||||
url = "https://pub.dartlang.org/packages/logging/versions/1.2.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
matcher = fetchzip {
|
||||
sha256 = "0pjgc38clnjbv124n8bh724db1wcc4kk125j7dxl0icz7clvm0p0";
|
||||
url = "https://pub.dartlang.org/packages/matcher/versions/0.12.13.tar.gz";
|
||||
sha256 = "0inznqkrxqnq09lcbwvda3xd07qfm1k3aa6dv1wy39gvci8hybss";
|
||||
url = "https://pub.dartlang.org/packages/matcher/versions/0.12.16.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
meta = fetchzip {
|
||||
sha256 = "01kqdd25nln5a219pr94s66p27m0kpqz0wpmwnm24kdy3ngif1v5";
|
||||
url = "https://pub.dartlang.org/packages/meta/versions/1.8.0.tar.gz";
|
||||
sha256 = "1l3zaz6q2s9mnm7s674xshsfqspy79p5kdbbnc99rf2l76avv4h3";
|
||||
url = "https://pub.dartlang.org/packages/meta/versions/1.9.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
mime = fetchzip {
|
||||
sha256 = "1dr3qikzvp10q1saka7azki5gk2kkf2v7k9wfqjsyxmza2zlv896";
|
||||
url = "https://pub.dartlang.org/packages/mime/versions/1.0.2.tar.gz";
|
||||
sha256 = "1dha9z64bsz8xhi0p62vmlyikr8xwbdlrw90hxghmm3rdgd9h25w";
|
||||
url = "https://pub.dartlang.org/packages/mime/versions/1.0.4.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
moxlib = fetchzip {
|
||||
sha256 = "1j52xglpwy8c7dbylc3f6vrh0p52xhhwqs4h0qcqk8c1rvjn5czq";
|
||||
url = "https://git.polynom.me/api/packages/moxxy/pub/api/packages/moxlib/files/0.1.5.tar.gz";
|
||||
sha256 = "1qaacmcqhq33grn2nq8sn23ki62dcmw0fqy589xm1zv6w0pzfmsk";
|
||||
url = "https://git.polynom.me/api/packages/moxxy/pub/api/packages/moxlib/files/0.2.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
node_preamble = fetchzip {
|
||||
sha256 = "0i0gfc2yqa09182vc01lj47qpq98kfm9m8h4n8c5fby0mjd0lvyx";
|
||||
url = "https://pub.dartlang.org/packages/node_preamble/versions/2.0.1.tar.gz";
|
||||
sha256 = "12ajg76r9aqmqkavvlxbnb3sszg1szcq3f30badkd0xc25mnhyh8";
|
||||
url = "https://pub.dartlang.org/packages/node_preamble/versions/2.0.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
omemo_dart = fetchzip {
|
||||
sha256 = "09x3jqa11hjdjp31nxnz91j6jssbc2f8a1lh44fmkc0d79hs8bbi";
|
||||
url = "https://git.polynom.me/api/packages/PapaTutuWawa/pub/api/packages/omemo_dart/files/0.4.3.tar.gz";
|
||||
sha256 = "0fhf89ic5mdyld25l6rfb37a1fk1f0f2b4d72xi4r7pvr0ddjhz8";
|
||||
url = "https://git.polynom.me/api/packages/PapaTutuWawa/pub/api/packages/omemo_dart/files/0.5.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -301,8 +289,8 @@
|
||||
};
|
||||
|
||||
path = fetchzip {
|
||||
sha256 = "16ggdh29ciy7h8sdshhwmxn6dd12sfbykf2j82c56iwhhlljq181";
|
||||
url = "https://pub.dartlang.org/packages/path/versions/1.8.2.tar.gz";
|
||||
sha256 = "1mjdhq2fsz6i9krhp2mnaks2bcw34sa4p7mg0v6njk8dgx2754iv";
|
||||
url = "https://pub.dartlang.org/packages/path/versions/1.8.3.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -315,8 +303,8 @@
|
||||
};
|
||||
|
||||
petitparser = fetchzip {
|
||||
sha256 = "1pqqqqiy9ald24qsi24q9qrr0zphgpsrnrv9rlx4vwr6xak7d8c0";
|
||||
url = "https://pub.dartlang.org/packages/petitparser/versions/5.1.0.tar.gz";
|
||||
sha256 = "19zqrpb1z77aw1k2s8rsxdfxczzv9934g2rdfj2jyiv3pqgdq8gh";
|
||||
url = "https://pub.dartlang.org/packages/petitparser/versions/5.4.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -335,16 +323,30 @@
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
protobuf = fetchzip {
|
||||
sha256 = "1jriyisf8bnvq5ygjk93mn2yzdlnii7xrhy6aabz54xr3y4dcy9x";
|
||||
url = "https://pub.dartlang.org/packages/protobuf/versions/2.1.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
protoc_plugin = fetchzip {
|
||||
sha256 = "0hjjd1xkv4s4g1d5n2aza0kdwlbfl2aivq99230m3yml7irn00jk";
|
||||
url = "https://pub.dartlang.org/packages/protoc_plugin/versions/20.0.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
pub_semver = fetchzip {
|
||||
sha256 = "1vsj5c1f2dza4l5zmjix4zh65lp8gsg6pw01h57pijx2id0g4bwi";
|
||||
url = "https://pub.dartlang.org/packages/pub_semver/versions/2.1.2.tar.gz";
|
||||
sha256 = "0wpcfz1crxipbjm18m71pl4vl2ra8vw1n93ff8snr54mmlyfb9z1";
|
||||
url = "https://pub.dartlang.org/packages/pub_semver/versions/2.1.4.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
pubspec_parse = fetchzip {
|
||||
sha256 = "19dmr9k4wsqjnhlzp1lbrw8dv7a1gnwmr8l5j9zlw407rmfg20d1";
|
||||
url = "https://pub.dartlang.org/packages/pubspec_parse/versions/1.2.1.tar.gz";
|
||||
sha256 = "0dj8sf1w61g7vh1ly3sl690z0nwllzjzbapxswmgsglr0ndcyrs1";
|
||||
url = "https://pub.dartlang.org/packages/pubspec_parse/versions/1.2.3.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -364,43 +366,43 @@
|
||||
};
|
||||
|
||||
shelf = fetchzip {
|
||||
sha256 = "0x2xl7glrnq0hdxpy2i94a4wxbdrd6dm46hvhzgjn8alsm8z0wz1";
|
||||
url = "https://pub.dartlang.org/packages/shelf/versions/1.4.0.tar.gz";
|
||||
sha256 = "10yk98nadrgj5d3r3241kdaywjjs1j10mg8gacv80kg1mhcfdrxp";
|
||||
url = "https://pub.dartlang.org/packages/shelf/versions/1.4.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
shelf_packages_handler = fetchzip {
|
||||
sha256 = "199rbdbifj46lg3iynznnsbs8zr4dfcw0s7wan8v73nvpqvli82q";
|
||||
url = "https://pub.dartlang.org/packages/shelf_packages_handler/versions/3.0.1.tar.gz";
|
||||
sha256 = "1h8s42nff9ar0xn7yb42m64lpvmqzq8wranqrkkixdnp7w3pmv1x";
|
||||
url = "https://pub.dartlang.org/packages/shelf_packages_handler/versions/3.0.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
shelf_static = fetchzip {
|
||||
sha256 = "1kqbaslz7bna9lldda3ibrjg0gczbzlwgm9cic8shg0bnl0v3s34";
|
||||
url = "https://pub.dartlang.org/packages/shelf_static/versions/1.1.1.tar.gz";
|
||||
sha256 = "1bcqynn2z2syrigmrclxgg8hjhd1x9742938i62cicbaga6vclaz";
|
||||
url = "https://pub.dartlang.org/packages/shelf_static/versions/1.1.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
shelf_web_socket = fetchzip {
|
||||
sha256 = "0rr87nx2wdf9alippxiidqlgi82fbprnsarr1jswg9qin0yy4jpn";
|
||||
url = "https://pub.dartlang.org/packages/shelf_web_socket/versions/1.0.3.tar.gz";
|
||||
sha256 = "110b5hrqwpnmq16shxxzjmcih5yfs5kh80dn8avfv0xj5iv7n94c";
|
||||
url = "https://pub.dartlang.org/packages/shelf_web_socket/versions/1.0.4.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
source_gen = fetchzip {
|
||||
sha256 = "1kxgx782lzpjhv736h0pz3lnxpcgiy05h0ysy0q77gix8q09i1hz";
|
||||
url = "https://pub.dartlang.org/packages/source_gen/versions/1.2.6.tar.gz";
|
||||
sha256 = "1jql5zccv4vnbbvwcpyyvz8l27pg1rviqbp4vrks5313nf4b0kjg";
|
||||
url = "https://pub.dartlang.org/packages/source_gen/versions/1.3.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
source_helper = fetchzip {
|
||||
sha256 = "044kzmzlfpx93s4raz5avijahizmvai0zvl0lbm4wi93ynhdp1pd";
|
||||
url = "https://pub.dartlang.org/packages/source_helper/versions/1.3.3.tar.gz";
|
||||
sha256 = "0mdd02vhcdcv9n58gzbx2q0bphwj0alz312ca1a8xpkf8jx3y8v4";
|
||||
url = "https://pub.dartlang.org/packages/source_helper/versions/1.3.4.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -413,29 +415,29 @@
|
||||
};
|
||||
|
||||
source_maps = fetchzip {
|
||||
sha256 = "18ixrlz3l2alk3hp0884qj0mcgzhxmjpg6nq0n1200pfy62pc4z6";
|
||||
url = "https://pub.dartlang.org/packages/source_maps/versions/0.10.11.tar.gz";
|
||||
sha256 = "004lcfka01agxjdw7zjhrffdkisvgx5s61b5gsl8qqk2jd1rswa7";
|
||||
url = "https://pub.dartlang.org/packages/source_maps/versions/0.10.12.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
source_span = fetchzip {
|
||||
sha256 = "1lq4sy7lw15qsv9cijf6l48p16qr19r7njzwr4pxn8vv1kh6rb86";
|
||||
url = "https://pub.dartlang.org/packages/source_span/versions/1.9.1.tar.gz";
|
||||
sha256 = "1nybnf7l5chslp4fczhqnrgrhymy844lw7qrj6y08i626dshrd46";
|
||||
url = "https://pub.dartlang.org/packages/source_span/versions/1.10.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
stack_trace = fetchzip {
|
||||
sha256 = "0bggqvvpkrfvqz24bnir4959k0c45azc3zivk4lyv3mvba6092na";
|
||||
url = "https://pub.dartlang.org/packages/stack_trace/versions/1.11.0.tar.gz";
|
||||
sha256 = "0xpk2cvmgdh46iwip9jsb54fqx13jnina8pk03akxkmsxvag5izb";
|
||||
url = "https://pub.dartlang.org/packages/stack_trace/versions/1.11.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
stream_channel = fetchzip {
|
||||
sha256 = "054by84c60yxphr3qgg6f82gg6d22a54aqjp265anlm8dwz1ji32";
|
||||
url = "https://pub.dartlang.org/packages/stream_channel/versions/2.1.1.tar.gz";
|
||||
sha256 = "0nrlw6zcscgnn6818krkbgs9qiv3f7q8pa7ljw1bqkrsb7xabm8s";
|
||||
url = "https://pub.dartlang.org/packages/stream_channel/versions/2.1.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -455,8 +457,8 @@
|
||||
};
|
||||
|
||||
synchronized = fetchzip {
|
||||
sha256 = "1j6108cq1hbcqpwhk9sah8q3gcidd7222bzhha2nk9syxhzqy82i";
|
||||
url = "https://pub.dartlang.org/packages/synchronized/versions/3.0.0%2B2.tar.gz";
|
||||
sha256 = "1fx1z1p5qkn4qnq24riw5s86vmq645ppg8f74iyv2fc9rvr301ar";
|
||||
url = "https://pub.dartlang.org/packages/synchronized/versions/3.1.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -469,36 +471,36 @@
|
||||
};
|
||||
|
||||
test = fetchzip {
|
||||
sha256 = "08kimbjvkdw3bkj7za36p3yqdr8dnlb5v30c250kvdncb7k09h4x";
|
||||
url = "https://pub.dartlang.org/packages/test/versions/1.22.0.tar.gz";
|
||||
sha256 = "002phlj2pg6nll5hv449izxbqk29zwmwc77d0jx2iimz18dgy2r5";
|
||||
url = "https://pub.dartlang.org/packages/test/versions/1.24.3.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
test_api = fetchzip {
|
||||
sha256 = "0mfyjpqkkmaqdh7xygrydx12591wq9ll816f61n80dc6rmkdx7px";
|
||||
url = "https://pub.dartlang.org/packages/test_api/versions/0.4.16.tar.gz";
|
||||
sha256 = "0as1xcywjrd2zax3cm56qmnac12shf8c1ynnzzjwnggm23f61dxb";
|
||||
url = "https://pub.dartlang.org/packages/test_api/versions/0.6.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
test_core = fetchzip {
|
||||
sha256 = "1r8dnvkxxvh55z1c8lrsja1m0dkf5i4lgwwqixcx0mqvxx5w3005";
|
||||
url = "https://pub.dartlang.org/packages/test_core/versions/0.4.20.tar.gz";
|
||||
sha256 = "1cx2rmz1xzk5z5yh8fpbsrsz4mgjanrw4xvnp0qzdnm2d7vhaq0y";
|
||||
url = "https://pub.dartlang.org/packages/test_core/versions/0.5.3.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
timing = fetchzip {
|
||||
sha256 = "0a02znvy0fbzr0n4ai67pp8in7w6m768aynkk1kp5lnmgy17ppsg";
|
||||
url = "https://pub.dartlang.org/packages/timing/versions/1.0.0.tar.gz";
|
||||
sha256 = "15jvxsw7v0gwbdlykma60l1qlhlzb3brh6m0sg2bgbfir4l5s9gw";
|
||||
url = "https://pub.dartlang.org/packages/timing/versions/1.0.1.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
typed_data = fetchzip {
|
||||
sha256 = "1x402bvyzdmdvmyqhyfamjxf54p9j8sa8ns2n5dwsdhnfqbw859g";
|
||||
url = "https://pub.dartlang.org/packages/typed_data/versions/1.3.1.tar.gz";
|
||||
sha256 = "0q6ggc52vfpr8kqaq69h757wy942hvgshhnsr2pjdinb4zk2sxl1";
|
||||
url = "https://pub.dartlang.org/packages/typed_data/versions/1.3.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -511,8 +513,8 @@
|
||||
};
|
||||
|
||||
uuid = fetchzip {
|
||||
sha256 = "12lsynr07lw9848jknmzxvzn3ia12xdj07iiva0vg0qjvpq7ladg";
|
||||
url = "https://pub.dartlang.org/packages/uuid/versions/3.0.5.tar.gz";
|
||||
sha256 = "1nh1hxfr6bhyadqqcxrpwrphmm75f1iq4rzfjdwa2486xwlh7vx3";
|
||||
url = "https://pub.dartlang.org/packages/uuid/versions/3.0.7.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -525,8 +527,8 @@
|
||||
};
|
||||
|
||||
vm_service = fetchzip {
|
||||
sha256 = "05xaxaxzyfls6jklw1hzws2jmina1cjk10gbl7a63djh1ghnzjb5";
|
||||
url = "https://pub.dartlang.org/packages/vm_service/versions/9.4.0.tar.gz";
|
||||
sha256 = "15ail7rbaq9ksg73cc0mw2k5imbiidl95yfd4v49k81gp5xmj92w";
|
||||
url = "https://pub.dartlang.org/packages/vm_service/versions/11.10.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -539,8 +541,8 @@
|
||||
};
|
||||
|
||||
web_socket_channel = fetchzip {
|
||||
sha256 = "147amn05v1f1a1grxjr7yzgshrczjwijwiywggsv6dgic8kxyj5a";
|
||||
url = "https://pub.dartlang.org/packages/web_socket_channel/versions/2.2.0.tar.gz";
|
||||
sha256 = "0f9441c4zifb5qadpjg319dcilimpkdhfacnkl543802bf8qjn4w";
|
||||
url = "https://pub.dartlang.org/packages/web_socket_channel/versions/2.4.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
@@ -553,262 +555,262 @@
|
||||
};
|
||||
|
||||
xml = fetchzip {
|
||||
sha256 = "0jwknkfcnb5svg6r01xjsj0aiw06mlx54pgay1ymaaqm2mjhyz01";
|
||||
url = "https://pub.dartlang.org/packages/xml/versions/6.2.0.tar.gz";
|
||||
sha256 = "120azx71gazvrrn07vd83vrffzrhsqnmf9rdjxl73rra9py8ixiy";
|
||||
url = "https://pub.dartlang.org/packages/xml/versions/6.3.0.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
yaml = fetchzip {
|
||||
sha256 = "0mqqmzn3c9rr38b5xm312fz1vyp6vb36lm477r9hak77bxzpp0iw";
|
||||
url = "https://pub.dartlang.org/packages/yaml/versions/3.1.1.tar.gz";
|
||||
sha256 = "0awh9dynbhrlq8zgszaiyxbyyn9b6wyps1zww4z2lx62nbma0pda";
|
||||
url = "https://pub.dartlang.org/packages/yaml/versions/3.1.2.tar.gz";
|
||||
stripRoot = false;
|
||||
extension = "tar.gz";
|
||||
};
|
||||
|
||||
pubCache = runCommand "moxxmpp-pub-cache" {} ''
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${_fe_analyzer_shared} $out/hosted/pub.dartlang.org/_fe_analyzer_shared-50.0.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${_fe_analyzer_shared} $out/hosted/pub.dev/_fe_analyzer_shared-61.0.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${analyzer} $out/hosted/pub.dartlang.org/analyzer-5.2.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${analyzer} $out/hosted/pub.dev/analyzer-5.13.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${args} $out/hosted/pub.dartlang.org/args-2.3.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${args} $out/hosted/pub.dev/args-2.4.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${async} $out/hosted/pub.dartlang.org/async-2.10.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${async} $out/hosted/pub.dev/async-2.11.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${boolean_selector} $out/hosted/pub.dartlang.org/boolean_selector-2.1.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${boolean_selector} $out/hosted/pub.dev/boolean_selector-2.1.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${build} $out/hosted/pub.dartlang.org/build-2.3.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${build} $out/hosted/pub.dev/build-2.3.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${build_config} $out/hosted/pub.dartlang.org/build_config-1.1.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${build_config} $out/hosted/pub.dev/build_config-1.1.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${build_daemon} $out/hosted/pub.dartlang.org/build_daemon-3.1.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${build_daemon} $out/hosted/pub.dev/build_daemon-3.1.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${build_resolvers} $out/hosted/pub.dartlang.org/build_resolvers-2.1.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${build_resolvers} $out/hosted/pub.dev/build_resolvers-2.3.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${build_runner} $out/hosted/pub.dartlang.org/build_runner-2.3.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${build_runner} $out/hosted/pub.dev/build_runner-2.3.3
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${build_runner_core} $out/hosted/pub.dartlang.org/build_runner_core-7.2.7
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${build_runner_core} $out/hosted/pub.dev/build_runner_core-7.2.7+1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${built_collection} $out/hosted/pub.dartlang.org/built_collection-5.1.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${built_collection} $out/hosted/pub.dev/built_collection-5.1.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${built_value} $out/hosted/pub.dartlang.org/built_value-8.4.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${built_value} $out/hosted/pub.dev/built_value-8.6.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${checked_yaml} $out/hosted/pub.dartlang.org/checked_yaml-2.0.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${checked_yaml} $out/hosted/pub.dev/checked_yaml-2.0.3
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${code_builder} $out/hosted/pub.dartlang.org/code_builder-4.3.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${code_builder} $out/hosted/pub.dev/code_builder-4.6.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${collection} $out/hosted/pub.dartlang.org/collection-1.17.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${collection} $out/hosted/pub.dev/collection-1.18.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${convert} $out/hosted/pub.dartlang.org/convert-3.1.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${convert} $out/hosted/pub.dev/convert-3.1.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${coverage} $out/hosted/pub.dartlang.org/coverage-1.6.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${coverage} $out/hosted/pub.dev/coverage-1.6.3
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${crypto} $out/hosted/pub.dartlang.org/crypto-3.0.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${crypto} $out/hosted/pub.dev/crypto-3.0.3
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${cryptography} $out/hosted/pub.dartlang.org/cryptography-2.0.5
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${cryptography} $out/hosted/pub.dev/cryptography-2.5.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${dart_style} $out/hosted/pub.dartlang.org/dart_style-2.2.4
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${dart_style} $out/hosted/pub.dev/dart_style-2.3.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${file} $out/hosted/pub.dartlang.org/file-6.1.4
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${file} $out/hosted/pub.dev/file-6.1.4
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${fixnum} $out/hosted/pub.dartlang.org/fixnum-1.0.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${fixnum} $out/hosted/pub.dev/fixnum-1.1.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${freezed} $out/hosted/pub.dartlang.org/freezed-2.1.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${frontend_server_client} $out/hosted/pub.dev/frontend_server_client-3.2.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${freezed_annotation} $out/hosted/pub.dartlang.org/freezed_annotation-2.1.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${glob} $out/hosted/pub.dev/glob-2.1.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${frontend_server_client} $out/hosted/pub.dartlang.org/frontend_server_client-3.1.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${graphs} $out/hosted/pub.dev/graphs-2.3.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${glob} $out/hosted/pub.dartlang.org/glob-2.1.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${hex} $out/hosted/pub.dev/hex-0.2.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${graphs} $out/hosted/pub.dartlang.org/graphs-2.2.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${http_multi_server} $out/hosted/pub.dev/http_multi_server-3.2.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${hex} $out/hosted/pub.dartlang.org/hex-0.2.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${http_parser} $out/hosted/pub.dev/http_parser-4.0.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${http_multi_server} $out/hosted/pub.dartlang.org/http_multi_server-3.2.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${io} $out/hosted/pub.dev/io-1.0.4
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${http_parser} $out/hosted/pub.dartlang.org/http_parser-4.0.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${js} $out/hosted/pub.dev/js-0.6.7
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${io} $out/hosted/pub.dartlang.org/io-1.0.3
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${json_annotation} $out/hosted/pub.dev/json_annotation-4.8.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${js} $out/hosted/pub.dartlang.org/js-0.6.5
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${json_serializable} $out/hosted/pub.dev/json_serializable-6.6.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${json_annotation} $out/hosted/pub.dartlang.org/json_annotation-4.7.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${logging} $out/hosted/pub.dev/logging-1.2.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${json_serializable} $out/hosted/pub.dartlang.org/json_serializable-6.5.4
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${matcher} $out/hosted/pub.dev/matcher-0.12.16
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${logging} $out/hosted/pub.dartlang.org/logging-1.0.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${meta} $out/hosted/pub.dev/meta-1.9.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${matcher} $out/hosted/pub.dartlang.org/matcher-0.12.13
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${meta} $out/hosted/pub.dartlang.org/meta-1.8.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${mime} $out/hosted/pub.dartlang.org/mime-1.0.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${mime} $out/hosted/pub.dev/mime-1.0.4
|
||||
|
||||
mkdir -p $out/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47
|
||||
ln -s ${moxlib} $out/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47/moxlib-0.1.5
|
||||
ln -s ${moxlib} $out/hosted/git.polynom.me%47api%47packages%47Moxxy%47pub%47/moxlib-0.2.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${node_preamble} $out/hosted/pub.dartlang.org/node_preamble-2.0.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${node_preamble} $out/hosted/pub.dev/node_preamble-2.0.2
|
||||
|
||||
mkdir -p $out/hosted/git.polynom.me%47api%47packages%47PapaTutuWawa%47pub%47
|
||||
ln -s ${omemo_dart} $out/hosted/git.polynom.me%47api%47packages%47PapaTutuWawa%47pub%47/omemo_dart-0.4.3
|
||||
ln -s ${omemo_dart} $out/hosted/git.polynom.me%47api%47packages%47PapaTutuWawa%47pub%47/omemo_dart-0.5.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${package_config} $out/hosted/pub.dartlang.org/package_config-2.1.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${package_config} $out/hosted/pub.dev/package_config-2.1.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${path} $out/hosted/pub.dartlang.org/path-1.8.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${path} $out/hosted/pub.dev/path-1.8.3
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${pedantic} $out/hosted/pub.dartlang.org/pedantic-1.11.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${pedantic} $out/hosted/pub.dev/pedantic-1.11.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${petitparser} $out/hosted/pub.dartlang.org/petitparser-5.1.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${petitparser} $out/hosted/pub.dev/petitparser-5.4.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${pinenacl} $out/hosted/pub.dartlang.org/pinenacl-0.5.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${pinenacl} $out/hosted/pub.dev/pinenacl-0.5.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${pool} $out/hosted/pub.dartlang.org/pool-1.5.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${pool} $out/hosted/pub.dev/pool-1.5.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${pub_semver} $out/hosted/pub.dartlang.org/pub_semver-2.1.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${protobuf} $out/hosted/pub.dev/protobuf-2.1.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${pubspec_parse} $out/hosted/pub.dartlang.org/pubspec_parse-1.2.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${protoc_plugin} $out/hosted/pub.dev/protoc_plugin-20.0.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${random_string} $out/hosted/pub.dartlang.org/random_string-2.3.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${pub_semver} $out/hosted/pub.dev/pub_semver-2.1.4
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${saslprep} $out/hosted/pub.dartlang.org/saslprep-1.0.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${pubspec_parse} $out/hosted/pub.dev/pubspec_parse-1.2.3
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${shelf} $out/hosted/pub.dartlang.org/shelf-1.4.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${random_string} $out/hosted/pub.dev/random_string-2.3.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${shelf_packages_handler} $out/hosted/pub.dartlang.org/shelf_packages_handler-3.0.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${saslprep} $out/hosted/pub.dev/saslprep-1.0.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${shelf_static} $out/hosted/pub.dartlang.org/shelf_static-1.1.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${shelf} $out/hosted/pub.dev/shelf-1.4.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${shelf_web_socket} $out/hosted/pub.dartlang.org/shelf_web_socket-1.0.3
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${shelf_packages_handler} $out/hosted/pub.dev/shelf_packages_handler-3.0.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${source_gen} $out/hosted/pub.dartlang.org/source_gen-1.2.6
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${shelf_static} $out/hosted/pub.dev/shelf_static-1.1.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${source_helper} $out/hosted/pub.dartlang.org/source_helper-1.3.3
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${shelf_web_socket} $out/hosted/pub.dev/shelf_web_socket-1.0.4
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${source_map_stack_trace} $out/hosted/pub.dartlang.org/source_map_stack_trace-2.1.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${source_gen} $out/hosted/pub.dev/source_gen-1.3.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${source_maps} $out/hosted/pub.dartlang.org/source_maps-0.10.11
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${source_helper} $out/hosted/pub.dev/source_helper-1.3.4
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${source_span} $out/hosted/pub.dartlang.org/source_span-1.9.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${source_map_stack_trace} $out/hosted/pub.dev/source_map_stack_trace-2.1.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${stack_trace} $out/hosted/pub.dartlang.org/stack_trace-1.11.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${source_maps} $out/hosted/pub.dev/source_maps-0.10.12
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${stream_channel} $out/hosted/pub.dartlang.org/stream_channel-2.1.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${source_span} $out/hosted/pub.dev/source_span-1.10.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${stream_transform} $out/hosted/pub.dartlang.org/stream_transform-2.1.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${stack_trace} $out/hosted/pub.dev/stack_trace-1.11.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${string_scanner} $out/hosted/pub.dartlang.org/string_scanner-1.2.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${stream_channel} $out/hosted/pub.dev/stream_channel-2.1.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${synchronized} $out/hosted/pub.dartlang.org/synchronized-3.0.0+2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${stream_transform} $out/hosted/pub.dev/stream_transform-2.1.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${term_glyph} $out/hosted/pub.dartlang.org/term_glyph-1.2.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${string_scanner} $out/hosted/pub.dev/string_scanner-1.2.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${test} $out/hosted/pub.dartlang.org/test-1.22.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${synchronized} $out/hosted/pub.dev/synchronized-3.1.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${test_api} $out/hosted/pub.dartlang.org/test_api-0.4.16
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${term_glyph} $out/hosted/pub.dev/term_glyph-1.2.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${test_core} $out/hosted/pub.dartlang.org/test_core-0.4.20
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${test} $out/hosted/pub.dev/test-1.24.3
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${timing} $out/hosted/pub.dartlang.org/timing-1.0.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${test_api} $out/hosted/pub.dev/test_api-0.6.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${typed_data} $out/hosted/pub.dartlang.org/typed_data-1.3.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${test_core} $out/hosted/pub.dev/test_core-0.5.3
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${unorm_dart} $out/hosted/pub.dartlang.org/unorm_dart-0.2.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${timing} $out/hosted/pub.dev/timing-1.0.1
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${uuid} $out/hosted/pub.dartlang.org/uuid-3.0.5
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${typed_data} $out/hosted/pub.dev/typed_data-1.3.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${very_good_analysis} $out/hosted/pub.dartlang.org/very_good_analysis-3.1.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${unorm_dart} $out/hosted/pub.dev/unorm_dart-0.2.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${vm_service} $out/hosted/pub.dartlang.org/vm_service-9.4.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${uuid} $out/hosted/pub.dev/uuid-3.0.7
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${watcher} $out/hosted/pub.dartlang.org/watcher-1.0.2
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${very_good_analysis} $out/hosted/pub.dev/very_good_analysis-3.1.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${web_socket_channel} $out/hosted/pub.dartlang.org/web_socket_channel-2.2.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${vm_service} $out/hosted/pub.dev/vm_service-11.10.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${webkit_inspection_protocol} $out/hosted/pub.dartlang.org/webkit_inspection_protocol-1.2.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${watcher} $out/hosted/pub.dev/watcher-1.0.2
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${xml} $out/hosted/pub.dartlang.org/xml-6.2.0
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${web_socket_channel} $out/hosted/pub.dev/web_socket_channel-2.4.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dartlang.org
|
||||
ln -s ${yaml} $out/hosted/pub.dartlang.org/yaml-3.1.1
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${webkit_inspection_protocol} $out/hosted/pub.dev/webkit_inspection_protocol-1.2.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${xml} $out/hosted/pub.dev/xml-6.3.0
|
||||
|
||||
mkdir -p $out/hosted/pub.dev
|
||||
ln -s ${yaml} $out/hosted/pub.dev/yaml-3.1.2
|
||||
'';
|
||||
|
||||
}
|
||||
@@ -1,7 +1,34 @@
|
||||
## 0.3.2
|
||||
## 0.4.1
|
||||
- Moved FAST from staging to xep_0484.dart
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- **BREAKING**: Remove `lastResource` from `XmppConnection`'s `connect` method. Instead, set the `StreamManagementNegotiator`'s `resource` attribute instead. Since the resource can only really be restored by stream management, this is no issue.
|
||||
- **BREAKING**: Changed order of parameters of `CryptographicHashManager.hashFromData`
|
||||
- **BREAKING**: Removed support for XEP-0414, as the (supported) hash computations are already implemented by `CryptographicHashManager.hashFromData`.
|
||||
- The `DiscoManager` now only handled entity capabilities if a `EntityCapabilityManager` is registered.
|
||||
- The `EntityCapabilityManager` now verifies and validates its data before caching.
|
||||
- **BREAKING**: Added the `resumed` parameter to `StreamNegotiationsDoneEvent`. Use this to check if the current stream is new or resumed instead of using the `ConnectionStateChangedEvent`.
|
||||
- **BREAKING**: Remove `DiscoManager.discoInfoCapHashQuery`.
|
||||
- **BREAKING**: The entity argument of `DiscoManager.discoInfoQuery` and `DiscoManager.discoItemsQuery` are now `JID` instead of `String`.
|
||||
- **BREAKING**: `PubSubManager` and `UserAvatarManager` now use `JID` instead of `String`.
|
||||
- **BREAKING**: `XmppConnection.sendStanza` not only takes a `StanzaDetails` argument.
|
||||
- Sent stanzas are now kept in a queue until sent.
|
||||
- **BREAKING**: `MessageManager.sendMessage` does not use `MessageDetails` anymore. Instead, use `TypedMap`.
|
||||
- `MessageManager` now allows registering callbacks for adding data whenever a message is sent.
|
||||
- **BREAKING**: `MessageEvent` now makes use of `TypedMap`.
|
||||
- **BREAKING**: Removed `PresenceReceivedEvent`. Use a manager registering handlers with priority greater than `[PresenceManager.presenceHandlerPriority]` instead.
|
||||
- **BREAKING**: `ChatState.toString()` is now `ChatState.toName()`
|
||||
- **BREAKING**: Overriding `BaseOmemoManager` is no longer required. `OmemoManager` now takes callback methods instead.
|
||||
- Removed `ErrorResponseDiscoError` from the possible XEP-0030 errors.
|
||||
- **BREAKING**: Removed "Extensible File Thumbnails" (The `Thumbnail` type).
|
||||
- *BREAKING*: Rename `UserAvatarManager`'s `getUserAvatar` to `getUserAvatarData`. It now also requires the id of the avatar to fetch
|
||||
- *BREAKING*: `UserAvatarManager`'s `getAvatarId` with `getLatestMetadata`.
|
||||
- The `PubSubManager` now supports PubSub's `max_items` in `getItems`.
|
||||
- *BREAKING*: `vCardManager`'s `VCardAvatarUpdatedEvent` no longer automatically requests the newest VCard avatar.
|
||||
- *BREAKING*: `XmppConnection` now tries to ensure that incoming data is processed in-order. The only exception are awaited stanzas as they are allowed to bypass the queue.
|
||||
- *BREAKING*: If a stanza handler causes an exception, the handler is simply skipped while processing.
|
||||
- Add better logging around what stanza handler is running and if they end processing early.
|
||||
|
||||
## 0.3.1
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ export 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
export 'package:moxxmpp/src/managers/priorities.dart';
|
||||
export 'package:moxxmpp/src/message.dart';
|
||||
export 'package:moxxmpp/src/namespaces.dart';
|
||||
export 'package:moxxmpp/src/negotiators/manager.dart';
|
||||
export 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
export 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
export 'package:moxxmpp/src/ping.dart';
|
||||
@@ -39,15 +38,17 @@ export 'package:moxxmpp/src/settings.dart';
|
||||
export 'package:moxxmpp/src/socket.dart';
|
||||
export 'package:moxxmpp/src/stanza.dart';
|
||||
export 'package:moxxmpp/src/stringxml.dart';
|
||||
export 'package:moxxmpp/src/types/result.dart';
|
||||
export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
|
||||
export 'package:moxxmpp/src/xeps/staging/fast.dart';
|
||||
export 'package:moxxmpp/src/util/typed_map.dart';
|
||||
export 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0030/helpers.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0045/events.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0045/types.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0045/xep_0045.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0054.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0060/helpers.dart';
|
||||
@@ -63,6 +64,7 @@ export 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0198/state.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0203.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0264.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0280.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0297.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0300.dart';
|
||||
@@ -85,7 +87,7 @@ export 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0414.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0421.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||
@@ -93,3 +95,4 @@ export 'package:moxxmpp/src/xeps/xep_0447.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0448.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0449.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0461.dart';
|
||||
export 'package:moxxmpp/src/xeps/xep_0484.dart';
|
||||
|
||||
@@ -1,35 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
/// A surrogate key for awaiting stanzas.
|
||||
@immutable
|
||||
class _StanzaSurrogateKey {
|
||||
const _StanzaSurrogateKey(this.sentTo, this.id, this.tag);
|
||||
/// (JID we sent a stanza to, the id of the sent stanza, the tag of the sent stanza).
|
||||
// ignore: avoid_private_typedef_functions
|
||||
typedef _StanzaCompositeKey = (String?, String, String);
|
||||
|
||||
/// The JID the original stanza was sent to. We expect the result to come from the
|
||||
/// same JID.
|
||||
final String sentTo;
|
||||
|
||||
/// The ID of the original stanza. We expect the result to have the same ID.
|
||||
final String id;
|
||||
|
||||
/// The tag name of the stanza.
|
||||
final String tag;
|
||||
|
||||
@override
|
||||
int get hashCode => sentTo.hashCode ^ id.hashCode ^ tag.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _StanzaSurrogateKey &&
|
||||
other.sentTo == sentTo &&
|
||||
other.id == id &&
|
||||
other.tag == tag;
|
||||
}
|
||||
}
|
||||
/// Callback function that returns the bare JID of the connection as a String.
|
||||
typedef GetBareJidCallback = String Function();
|
||||
|
||||
/// This class handles the await semantics for stanzas. Stanzas are given a "unique"
|
||||
/// key equal to the tuple (to, id, tag) with which their response is identified.
|
||||
@@ -40,8 +18,12 @@ class _StanzaSurrogateKey {
|
||||
///
|
||||
/// This class also handles some "edge cases" of RFC 6120, like an empty "from" attribute.
|
||||
class StanzaAwaiter {
|
||||
StanzaAwaiter(this._bareJidCallback);
|
||||
|
||||
final GetBareJidCallback _bareJidCallback;
|
||||
|
||||
/// The pending stanzas, identified by their surrogate key.
|
||||
final Map<_StanzaSurrogateKey, Completer<XMLNode>> _pending = {};
|
||||
final Map<_StanzaCompositeKey, Completer<XMLNode>> _pending = {};
|
||||
|
||||
/// The critical section for accessing [StanzaAwaiter._pending].
|
||||
final Lock _lock = Lock();
|
||||
@@ -52,30 +34,33 @@ class StanzaAwaiter {
|
||||
/// [tag] is the stanza's tag name.
|
||||
///
|
||||
/// Returns a future that might resolve to the response to the stanza.
|
||||
Future<Future<XMLNode>> addPending(String to, String id, String tag) async {
|
||||
Future<Future<XMLNode>> addPending(String? to, String id, String tag) async {
|
||||
// Check if we want to send a stanza to our bare JID and replace it with null.
|
||||
final processedTo = to != null && to == _bareJidCallback() ? null : to;
|
||||
|
||||
final completer = await _lock.synchronized(() {
|
||||
final completer = Completer<XMLNode>();
|
||||
_pending[_StanzaSurrogateKey(to, id, tag)] = completer;
|
||||
_pending[(processedTo, id, tag)] = completer;
|
||||
return completer;
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Checks if the stanza [stanza] is being awaited. [bareJid] is the bare JID of
|
||||
/// the connection.
|
||||
/// Checks if the stanza [stanza] is being awaited.
|
||||
/// If [stanza] is awaited, resolves the future and returns true. If not, returns
|
||||
/// false.
|
||||
Future<bool> onData(XMLNode stanza, JID bareJid) async {
|
||||
assert(bareJid.isBare(), 'bareJid must be bare');
|
||||
|
||||
Future<bool> onData(XMLNode stanza) async {
|
||||
final id = stanza.attributes['id'] as String?;
|
||||
if (id == null) return false;
|
||||
|
||||
final key = _StanzaSurrogateKey(
|
||||
// Section 8.1.2.1 § 3 of RFC 6120 says that an empty "from" indicates that the
|
||||
// attribute is implicitly from our own bare JID.
|
||||
stanza.attributes['from'] as String? ?? bareJid.toString(),
|
||||
// Check if we want to send a stanza to our bare JID and replace it with null.
|
||||
final from = stanza.attributes['from'] as String?;
|
||||
final processedFrom =
|
||||
from != null && from == _bareJidCallback() ? null : from;
|
||||
|
||||
final key = (
|
||||
processedFrom,
|
||||
id,
|
||||
stanza.tag,
|
||||
);
|
||||
@@ -91,4 +76,19 @@ class StanzaAwaiter {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/// Checks if [stanza] represents a stanza that is awaited. Returns true, if [stanza]
|
||||
/// is awaited. False, if not.
|
||||
Future<bool> isAwaited(XMLNode stanza) async {
|
||||
final id = stanza.attributes['id'] as String?;
|
||||
if (id == null) return false;
|
||||
|
||||
final key = (
|
||||
stanza.attributes['from'] as String?,
|
||||
id,
|
||||
stanza.tag,
|
||||
);
|
||||
|
||||
return _lock.synchronized(() => _pending.containsKey(key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ 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/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/parser.dart';
|
||||
import 'package:moxxmpp/src/presence.dart';
|
||||
@@ -26,12 +25,12 @@ import 'package:moxxmpp/src/settings.dart';
|
||||
import 'package:moxxmpp/src/socket.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/util/incoming_queue.dart';
|
||||
import 'package:moxxmpp/src/util/queue.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0352.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// The states the XmppConnection can be in
|
||||
@@ -50,16 +49,17 @@ enum XmppConnectionState {
|
||||
error
|
||||
}
|
||||
|
||||
/// Metadata for [XmppConnection.sendStanza].
|
||||
enum StanzaFromType {
|
||||
/// Add the full JID to the stanza as the from attribute
|
||||
full,
|
||||
/// (The actual stanza handler, Name of the owning manager).
|
||||
typedef _StanzaHandlerWrapper = (StanzaHandler, String);
|
||||
|
||||
/// Add the bare JID to the stanza as the from attribute
|
||||
bare,
|
||||
|
||||
/// Add no JID as the from attribute
|
||||
none,
|
||||
/// Wrapper around [stanzaHandlerSortComparator] for [_StanzaHandlerWrapper].
|
||||
int _stanzaHandlerWrapperSortComparator(
|
||||
_StanzaHandlerWrapper a,
|
||||
_StanzaHandlerWrapper b,
|
||||
) {
|
||||
final (ha, _) = a;
|
||||
final (hb, _) = b;
|
||||
return stanzaHandlerSortComparator(ha, hb);
|
||||
}
|
||||
|
||||
/// This class is a connection to the server.
|
||||
@@ -71,7 +71,11 @@ class XmppConnection {
|
||||
this._socket, {
|
||||
this.connectingTimeout = const Duration(minutes: 2),
|
||||
}) : _reconnectionPolicy = reconnectionPolicy,
|
||||
_connectivityManager = connectivityManager {
|
||||
_connectivityManager = connectivityManager,
|
||||
assert(
|
||||
_socket.getDataStream().isBroadcast,
|
||||
"The socket's data stream must be a broadcast stream",
|
||||
) {
|
||||
// Allow the reconnection policy to perform reconnections by itself
|
||||
_reconnectionPolicy.register(
|
||||
_attemptReconnection,
|
||||
@@ -90,10 +94,21 @@ class XmppConnection {
|
||||
},
|
||||
);
|
||||
|
||||
_stanzaAwaiter = StanzaAwaiter(
|
||||
() => connectionSettings.jid.toBare().toString(),
|
||||
);
|
||||
_incomingStanzaQueue = IncomingStanzaQueue(handleXmlStream, _stanzaAwaiter);
|
||||
_socketStream = _socket.getDataStream();
|
||||
// TODO(Unknown): Handle on done
|
||||
_socketStream.transform(_streamParser).forEach(handleXmlStream);
|
||||
_socket.getEventStream().listen(_handleSocketEvent);
|
||||
_socketStream
|
||||
.transform(_streamParser)
|
||||
.forEach(_incomingStanzaQueue.addStanza);
|
||||
_socketStream.listen(_handleOnDataCallbacks);
|
||||
_socket.getEventStream().listen(handleSocketEvent);
|
||||
|
||||
_stanzaQueue = AsyncStanzaQueue(
|
||||
_sendStanzaImpl,
|
||||
_canSendData,
|
||||
);
|
||||
}
|
||||
|
||||
/// The state that the connection is currently in
|
||||
@@ -117,16 +132,16 @@ class XmppConnection {
|
||||
final ConnectivityManager _connectivityManager;
|
||||
|
||||
/// A helper for handling await semantics with stanzas
|
||||
final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter();
|
||||
late final StanzaAwaiter _stanzaAwaiter;
|
||||
|
||||
/// Sorted list of handlers that we call or incoming and outgoing stanzas
|
||||
final List<StanzaHandler> _incomingStanzaHandlers =
|
||||
final List<_StanzaHandlerWrapper> _incomingStanzaHandlers =
|
||||
List.empty(growable: true);
|
||||
final List<StanzaHandler> _incomingPreStanzaHandlers =
|
||||
final List<_StanzaHandlerWrapper> _incomingPreStanzaHandlers =
|
||||
List.empty(growable: true);
|
||||
final List<StanzaHandler> _outgoingPreStanzaHandlers =
|
||||
final List<_StanzaHandlerWrapper> _outgoingPreStanzaHandlers =
|
||||
List.empty(growable: true);
|
||||
final List<StanzaHandler> _outgoingPostStanzaHandlers =
|
||||
final List<_StanzaHandlerWrapper> _outgoingPostStanzaHandlers =
|
||||
List.empty(growable: true);
|
||||
final StreamController<XmppEvent> _eventStreamController =
|
||||
StreamController.broadcast();
|
||||
@@ -165,10 +180,6 @@ class XmppConnection {
|
||||
T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) =>
|
||||
_negotiationsHandler.getNegotiatorById<T>(id);
|
||||
|
||||
/// Prevent data from being passed to _currentNegotiator.negotiator while the negotiator
|
||||
/// is still running.
|
||||
final Lock _negotiationLock = Lock();
|
||||
|
||||
/// The logger for the class
|
||||
final Logger _log = Logger('XmppConnection');
|
||||
|
||||
@@ -177,6 +188,10 @@ class XmppConnection {
|
||||
|
||||
bool get isAuthenticated => _isAuthenticated;
|
||||
|
||||
late final IncomingStanzaQueue _incomingStanzaQueue;
|
||||
|
||||
late final AsyncStanzaQueue _stanzaQueue;
|
||||
|
||||
/// Returns the JID we authenticate with and add the resource that we have bound.
|
||||
JID _getJidWithResource() {
|
||||
assert(_resource.isNotEmpty, 'The resource must not be empty');
|
||||
@@ -204,18 +219,25 @@ class XmppConnection {
|
||||
|
||||
_xmppManagers[manager.id] = manager;
|
||||
|
||||
_incomingStanzaHandlers.addAll(manager.getIncomingStanzaHandlers());
|
||||
_incomingPreStanzaHandlers.addAll(manager.getIncomingPreStanzaHandlers());
|
||||
_outgoingPreStanzaHandlers.addAll(manager.getOutgoingPreStanzaHandlers());
|
||||
_outgoingPostStanzaHandlers
|
||||
.addAll(manager.getOutgoingPostStanzaHandlers());
|
||||
_incomingStanzaHandlers.addAll(
|
||||
manager.getIncomingStanzaHandlers().map((h) => (h, manager.name)),
|
||||
);
|
||||
_incomingPreStanzaHandlers.addAll(
|
||||
manager.getIncomingPreStanzaHandlers().map((h) => (h, manager.name)),
|
||||
);
|
||||
_outgoingPreStanzaHandlers.addAll(
|
||||
manager.getOutgoingPreStanzaHandlers().map((h) => (h, manager.name)),
|
||||
);
|
||||
_outgoingPostStanzaHandlers.addAll(
|
||||
manager.getOutgoingPostStanzaHandlers().map((h) => (h, manager.name)),
|
||||
);
|
||||
}
|
||||
|
||||
// Sort them
|
||||
_incomingStanzaHandlers.sort(stanzaHandlerSortComparator);
|
||||
_incomingPreStanzaHandlers.sort(stanzaHandlerSortComparator);
|
||||
_outgoingPreStanzaHandlers.sort(stanzaHandlerSortComparator);
|
||||
_outgoingPostStanzaHandlers.sort(stanzaHandlerSortComparator);
|
||||
_incomingStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
|
||||
_incomingPreStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
|
||||
_outgoingPreStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
|
||||
_outgoingPostStanzaHandlers.sort(_stanzaHandlerWrapperSortComparator);
|
||||
|
||||
// Run the post register callbacks
|
||||
for (final manager in _xmppManagers.values) {
|
||||
@@ -296,6 +318,13 @@ class XmppConnection {
|
||||
return getManagerById(csiManager);
|
||||
}
|
||||
|
||||
/// Called whenever we receive data on the socket.
|
||||
Future<void> _handleOnDataCallbacks(String _) async {
|
||||
for (final manager in _xmppManagers.values) {
|
||||
unawaited(manager.onData());
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to reconnect to the server by following an exponential backoff.
|
||||
Future<void> _attemptReconnection() async {
|
||||
_log.finest('_attemptReconnection: Setting state to notConnected');
|
||||
@@ -318,7 +347,7 @@ class XmppConnection {
|
||||
|
||||
/// Called when a stream ending error has occurred
|
||||
Future<void> handleError(XmppError error) async {
|
||||
_log.severe('handleError called with ${error.toString()}');
|
||||
_log.severe('handleError called with $error');
|
||||
|
||||
// Whenever we encounter an error that would trigger a reconnection attempt while
|
||||
// the connection result is being awaited, don't attempt a reconnection but instead
|
||||
@@ -368,7 +397,8 @@ class XmppConnection {
|
||||
}
|
||||
|
||||
/// Called whenever the socket creates an event
|
||||
Future<void> _handleSocketEvent(XmppSocketEvent event) async {
|
||||
@visibleForTesting
|
||||
Future<void> handleSocketEvent(XmppSocketEvent event) async {
|
||||
if (event is XmppSocketErrorEvent) {
|
||||
await handleError(SocketError(event));
|
||||
} else if (event is XmppSocketClosureEvent) {
|
||||
@@ -410,137 +440,144 @@ class XmppConnection {
|
||||
|
||||
/// Returns true if we can send data through the socket.
|
||||
Future<bool> _canSendData() async {
|
||||
return [XmppConnectionState.connected, XmppConnectionState.connecting]
|
||||
.contains(await getConnectionState());
|
||||
return await getConnectionState() == XmppConnectionState.connected;
|
||||
}
|
||||
|
||||
/// Sends a [stanza] to the server. If stream management is enabled, then keeping track
|
||||
/// of the stanza is taken care of. Returns a Future that resolves when we receive a
|
||||
/// response to the stanza.
|
||||
///
|
||||
/// If addFrom is true, then a 'from' attribute will be added to the stanza if
|
||||
/// [stanza] has none.
|
||||
/// If addId is true, then an 'id' attribute will be added to the stanza if [stanza] has
|
||||
/// none.
|
||||
/// Sends a stanza described by [details] to the server. Until sent, the stanza is
|
||||
/// kept in a queue, that is flushed after going online again. If Stream Management
|
||||
/// is active, stanza's acknowledgement is tracked.
|
||||
// TODO(Unknown): if addId = false, the function crashes.
|
||||
Future<XMLNode> sendStanza(
|
||||
Stanza stanza, {
|
||||
StanzaFromType addFrom = StanzaFromType.full,
|
||||
bool addId = true,
|
||||
bool awaitable = true,
|
||||
bool encrypted = false,
|
||||
bool forceEncryption = false,
|
||||
}) async {
|
||||
Future<XMLNode?> sendStanza(StanzaDetails details) async {
|
||||
assert(
|
||||
implies(addId == false && stanza.id == null, !awaitable),
|
||||
'Cannot await a stanza with no id',
|
||||
implies(
|
||||
details.awaitable,
|
||||
details.stanza.id != null && details.stanza.id!.isNotEmpty ||
|
||||
details.addId,
|
||||
),
|
||||
'An awaitable stanza must have an id',
|
||||
);
|
||||
|
||||
// Add extra data in case it was not set
|
||||
var stanza_ = stanza;
|
||||
if (addId && (stanza_.id == null || stanza_.id == '')) {
|
||||
stanza_ = stanza.copyWith(id: generateId());
|
||||
final completer = details.awaitable ? Completer<XMLNode>() : null;
|
||||
final entry = StanzaQueueEntry(
|
||||
details,
|
||||
completer,
|
||||
);
|
||||
|
||||
if (details.bypassQueue) {
|
||||
await _sendStanzaImpl(entry);
|
||||
} else {
|
||||
await _stanzaQueue.enqueueStanza(entry);
|
||||
}
|
||||
if (addFrom != StanzaFromType.none &&
|
||||
(stanza_.from == null || stanza_.from == '')) {
|
||||
switch (addFrom) {
|
||||
case StanzaFromType.full:
|
||||
{
|
||||
stanza_ = stanza_.copyWith(
|
||||
from: _getJidWithResource().toString(),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case StanzaFromType.bare:
|
||||
{
|
||||
stanza_ = stanza_.copyWith(
|
||||
from: connectionSettings.jid.toBare().toString(),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case StanzaFromType.none:
|
||||
break;
|
||||
}
|
||||
|
||||
return completer?.future;
|
||||
}
|
||||
|
||||
Future<void> _sendStanzaImpl(StanzaQueueEntry entry) async {
|
||||
final details = entry.details;
|
||||
var newStanza = details.stanza;
|
||||
|
||||
// Generate an id, if requested
|
||||
if (details.addId && (newStanza.id == null || newStanza.id == '')) {
|
||||
newStanza = newStanza.copyWith(id: generateId());
|
||||
}
|
||||
stanza_ = stanza_.copyWith(
|
||||
|
||||
// NOTE: Originally, we handled adding a "from" attribute to the stanza here.
|
||||
// However, this is not neccessary as RFC 6120 states:
|
||||
//
|
||||
// > When a server receives an XML stanza from a connected client, the
|
||||
// > server MUST add a 'from' attribute to the stanza or override the
|
||||
// > 'from' attribute specified by the client, where the value of the
|
||||
// > 'from' attribute MUST be the full JID
|
||||
// > (<localpart@domainpart/resource>) determined by the server for
|
||||
// > the connected resource that generated the stanza (see
|
||||
// > Section 4.3.6), or the bare JID (<localpart@domainpart>) in the
|
||||
// > case of subscription-related presence stanzas (see [XMPP-IM]).
|
||||
//
|
||||
// This means that even if we add a "from" attribute, the server will discard
|
||||
// it. If we don't specify it, then the server will add the correct value
|
||||
// itself.
|
||||
|
||||
// Add the correct stanza namespace
|
||||
newStanza = newStanza.copyWith(
|
||||
xmlns: _negotiationsHandler.getStanzaNamespace(),
|
||||
);
|
||||
|
||||
// Run pre-send handlers
|
||||
_log.fine('Running pre stanza handlers..');
|
||||
final data = await _runOutgoingPreStanzaHandlers(
|
||||
stanza_,
|
||||
newStanza,
|
||||
initial: StanzaHandlerData(
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
stanza_,
|
||||
encrypted: encrypted,
|
||||
forceEncryption: forceEncryption,
|
||||
newStanza,
|
||||
details.extensions ?? TypedMap(),
|
||||
encrypted: details.encrypted,
|
||||
shouldEncrypt: details.shouldEncrypt,
|
||||
forceEncryption: details.forceEncryption,
|
||||
),
|
||||
);
|
||||
_log.fine('Done');
|
||||
|
||||
// Cancel sending, if the pre-send handlers indicated it.
|
||||
if (data.cancel) {
|
||||
_log.fine('A stanza handler indicated that it wants to cancel sending.');
|
||||
await _sendEvent(StanzaSendingCancelledEvent(data));
|
||||
return Stanza(
|
||||
tag: data.stanza.tag,
|
||||
to: data.stanza.from,
|
||||
from: data.stanza.to,
|
||||
attributes: <String, String>{
|
||||
'type': 'error',
|
||||
...data.stanza.id != null
|
||||
? {
|
||||
'id': data.stanza.id!,
|
||||
}
|
||||
: {},
|
||||
},
|
||||
);
|
||||
|
||||
// Resolve the future, if one was given.
|
||||
if (details.awaitable) {
|
||||
entry.completer!.complete(
|
||||
Stanza(
|
||||
tag: data.stanza.tag,
|
||||
to: data.stanza.from,
|
||||
from: data.stanza.to,
|
||||
attributes: <String, String>{
|
||||
'type': 'error',
|
||||
if (data.stanza.id != null) 'id': data.stanza.id!,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the (raw) stanza
|
||||
final prefix = data.encrypted ? '(Encrypted) ' : '';
|
||||
_log.finest('==> $prefix${stanza_.toXml()}');
|
||||
_log.finest('==> $prefix${newStanza.toXml()}');
|
||||
|
||||
final stanzaString = data.stanza.toXml();
|
||||
|
||||
// ignore: cascade_invocations
|
||||
_log.fine('Attempting to acquire lock for ${data.stanza.id}...');
|
||||
// TODO(PapaTutuWawa): Handle this much more graceful
|
||||
var future = Future.value(XMLNode(tag: 'not-used'));
|
||||
if (awaitable) {
|
||||
future = await _stanzaAwaiter.addPending(
|
||||
if (details.awaitable) {
|
||||
await _stanzaAwaiter
|
||||
.addPending(
|
||||
// A stanza with no to attribute is for direct processing by the server. As such,
|
||||
// we can correlate it by just *assuming* we have that attribute
|
||||
// (RFC 6120 Section 8.1.1.1)
|
||||
data.stanza.to ?? connectionSettings.jid.toBare().toString(),
|
||||
data.stanza.to,
|
||||
data.stanza.id!,
|
||||
data.stanza.tag,
|
||||
);
|
||||
)
|
||||
.then((result) {
|
||||
entry.completer!.complete(result);
|
||||
});
|
||||
}
|
||||
|
||||
// This uses the StreamManager to behave like a send queue
|
||||
if (await _canSendData()) {
|
||||
_socket.write(stanzaString);
|
||||
|
||||
// Try to ack every stanza
|
||||
// NOTE: Here we have send an Ack request nonza. This is now done by StreamManagementManager when receiving the StanzaSentEvent
|
||||
_socket.write(data.stanza.toXml());
|
||||
} else {
|
||||
_log.fine('_canSendData() returned false.');
|
||||
_log.fine('Not sending data as _canSendData() returned false.');
|
||||
}
|
||||
|
||||
// Run post-send handlers
|
||||
_log.fine('Running post stanza handlers..');
|
||||
await _runOutgoingPostStanzaHandlers(
|
||||
stanza_,
|
||||
newStanza,
|
||||
initial: StanzaHandlerData(
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
stanza_,
|
||||
newStanza,
|
||||
details.postSendExtensions ?? TypedMap<StanzaHandlerExtension>(),
|
||||
encrypted: data.encrypted,
|
||||
),
|
||||
);
|
||||
_log.fine('Done');
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/// Called when we timeout during connecting
|
||||
@@ -564,21 +601,29 @@ class XmppConnection {
|
||||
// Set the new routing state
|
||||
_updateRoutingState(RoutingState.handleStanzas);
|
||||
|
||||
// Set the connection state
|
||||
await _setConnectionState(XmppConnectionState.connected);
|
||||
|
||||
// Enable reconnections
|
||||
if (_enableReconnectOnSuccess) {
|
||||
await _reconnectionPolicy.setShouldReconnect(true);
|
||||
}
|
||||
|
||||
// Tell consumers of the event stream that we're done with stream feature
|
||||
// negotiations
|
||||
await _sendEvent(
|
||||
StreamNegotiationsDoneEvent(
|
||||
getManagerById<StreamManagementManager>(smManager)?.streamResumed ??
|
||||
false,
|
||||
),
|
||||
);
|
||||
|
||||
// Set the connection state
|
||||
await _setConnectionState(XmppConnectionState.connected);
|
||||
|
||||
// Resolve the connection completion future
|
||||
_connectionCompleter?.complete(const Result(true));
|
||||
_connectionCompleter = null;
|
||||
|
||||
// Tell consumers of the event stream that we're done with stream feature
|
||||
// negotiations
|
||||
await _sendEvent(StreamNegotiationsDoneEvent());
|
||||
// Flush the stanza send queue
|
||||
await _stanzaQueue.restart();
|
||||
}
|
||||
|
||||
/// Sets the connection state to [state] and triggers an event of type
|
||||
@@ -606,15 +651,10 @@ class XmppConnection {
|
||||
_destroyConnectingTimer();
|
||||
}
|
||||
|
||||
final sm =
|
||||
_negotiationsHandler.getNegotiatorById<StreamManagementNegotiator>(
|
||||
streamManagementNegotiator,
|
||||
);
|
||||
await _sendEvent(
|
||||
ConnectionStateChangedEvent(
|
||||
state,
|
||||
oldState,
|
||||
sm?.isResumed ?? false,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -645,15 +685,30 @@ class XmppConnection {
|
||||
/// call its callback and end the processing if the callback returned true; continue
|
||||
/// if it returned false.
|
||||
Future<StanzaHandlerData> _runStanzaHandlers(
|
||||
List<StanzaHandler> handlers,
|
||||
List<_StanzaHandlerWrapper> handlers,
|
||||
Stanza stanza, {
|
||||
StanzaHandlerData? initial,
|
||||
}) async {
|
||||
var state = initial ?? StanzaHandlerData(false, false, null, stanza);
|
||||
for (final handler in handlers) {
|
||||
var state = initial ?? StanzaHandlerData(false, false, stanza, TypedMap());
|
||||
for (final handlerRaw in handlers) {
|
||||
final (handler, managerName) = handlerRaw;
|
||||
if (handler.matches(state.stanza)) {
|
||||
state = await handler.callback(state.stanza, state);
|
||||
if (state.done || state.cancel) return state;
|
||||
_log.finest(
|
||||
'Running handler for ${stanza.tag} (${stanza.attributes["id"]}) of $managerName',
|
||||
);
|
||||
try {
|
||||
state = await handler.callback(state.stanza, state);
|
||||
} catch (ex) {
|
||||
_log.severe(
|
||||
'Handler from $managerName for ${stanza.tag} (${stanza.attributes["id"]}) failed with "$ex"',
|
||||
);
|
||||
}
|
||||
if (state.done || state.cancel) {
|
||||
_log.finest(
|
||||
'Processing ended early for ${stanza.tag} (${stanza.attributes["id"]}) by $managerName',
|
||||
);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,14 +779,20 @@ class XmppConnection {
|
||||
// it.
|
||||
final incomingPreHandlers = await _runIncomingPreStanzaHandlers(stanza);
|
||||
final prefix = incomingPreHandlers.encrypted &&
|
||||
incomingPreHandlers.other['encryption_error'] == null
|
||||
incomingPreHandlers.encryptionError == null
|
||||
? '(Encrypted) '
|
||||
: '';
|
||||
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
|
||||
|
||||
if (incomingPreHandlers.skip) {
|
||||
_log.fine(
|
||||
'Not processing stanza (${incomingPreHandlers.stanza.tag}, ${incomingPreHandlers.stanza.id}) due to skip=true.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final awaited = await _stanzaAwaiter.onData(
|
||||
incomingPreHandlers.stanza,
|
||||
connectionSettings.jid.toBare(),
|
||||
);
|
||||
if (awaited) {
|
||||
return;
|
||||
@@ -743,10 +804,11 @@ class XmppConnection {
|
||||
initial: StanzaHandlerData(
|
||||
false,
|
||||
incomingPreHandlers.cancel,
|
||||
incomingPreHandlers.cancelReason,
|
||||
incomingPreHandlers.stanza,
|
||||
incomingPreHandlers.extensions,
|
||||
encrypted: incomingPreHandlers.encrypted,
|
||||
other: incomingPreHandlers.other,
|
||||
encryptionError: incomingPreHandlers.encryptionError,
|
||||
cancelReason: incomingPreHandlers.cancelReason,
|
||||
),
|
||||
);
|
||||
if (!incomingHandlers.done) {
|
||||
@@ -789,14 +851,12 @@ class XmppConnection {
|
||||
// causing (a) the negotiator to become confused and (b) the stanzas/nonzas to be
|
||||
// missed. This causes the data to wait while the negotiator is running and thus
|
||||
// prevent this issue.
|
||||
await _negotiationLock.synchronized(() async {
|
||||
if (_routingState != RoutingState.negotiating) {
|
||||
unawaited(handleXmlStream(event));
|
||||
return;
|
||||
}
|
||||
if (_routingState != RoutingState.negotiating) {
|
||||
unawaited(handleXmlStream(event));
|
||||
return;
|
||||
}
|
||||
|
||||
await _negotiationsHandler.negotiate(event);
|
||||
});
|
||||
await _negotiationsHandler.negotiate(event);
|
||||
break;
|
||||
case RoutingState.handleStanzas:
|
||||
await _handleStanza(node);
|
||||
@@ -835,7 +895,7 @@ class XmppConnection {
|
||||
await _reconnectionPolicy.setShouldReconnect(false);
|
||||
|
||||
if (triggeredByUser) {
|
||||
getPresenceManager()?.sendUnavailablePresence();
|
||||
await getPresenceManager()?.sendUnavailablePresence();
|
||||
}
|
||||
|
||||
_socket.prepareDisconnect();
|
||||
|
||||
@@ -4,28 +4,20 @@ import 'package:moxxmpp/src/jid.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/util/typed_map.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_0066.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_0385.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0447.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0461.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0084.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0333.dart';
|
||||
|
||||
abstract class XmppEvent {}
|
||||
|
||||
/// Triggered when the connection state of the XmppConnection has
|
||||
/// changed.
|
||||
class ConnectionStateChangedEvent extends XmppEvent {
|
||||
ConnectionStateChangedEvent(this.state, this.before, this.resumed);
|
||||
ConnectionStateChangedEvent(this.state, this.before);
|
||||
final XmppConnectionState before;
|
||||
final XmppConnectionState state;
|
||||
final bool resumed;
|
||||
|
||||
/// Indicates whether the connection state switched from a not connected state to a
|
||||
/// connected state.
|
||||
@@ -75,58 +67,42 @@ class RosterUpdatedEvent extends XmppEvent {
|
||||
|
||||
/// Triggered when a message is received
|
||||
class MessageEvent extends XmppEvent {
|
||||
MessageEvent({
|
||||
required this.body,
|
||||
required this.fromJid,
|
||||
required this.toJid,
|
||||
required this.sid,
|
||||
required this.stanzaId,
|
||||
required this.isCarbon,
|
||||
required this.deliveryReceiptRequested,
|
||||
required this.isMarkable,
|
||||
required this.encrypted,
|
||||
required this.other,
|
||||
this.error,
|
||||
MessageEvent(
|
||||
this.from,
|
||||
this.to,
|
||||
this.encrypted,
|
||||
this.extensions, {
|
||||
this.id,
|
||||
this.type,
|
||||
this.oob,
|
||||
this.sfs,
|
||||
this.sims,
|
||||
this.reply,
|
||||
this.chatState,
|
||||
this.fun,
|
||||
this.funReplacement,
|
||||
this.funCancellation,
|
||||
this.messageRetraction,
|
||||
this.messageCorrectionId,
|
||||
this.messageReactions,
|
||||
this.messageProcessingHints,
|
||||
this.stickerPackId,
|
||||
this.error,
|
||||
this.encryptionError,
|
||||
});
|
||||
final StanzaError? error;
|
||||
final String body;
|
||||
final JID fromJid;
|
||||
final JID toJid;
|
||||
final String sid;
|
||||
|
||||
/// The from attribute of the message.
|
||||
final JID from;
|
||||
|
||||
/// The to attribute of the message.
|
||||
final JID to;
|
||||
|
||||
/// The id attribute of the message.
|
||||
final String? id;
|
||||
|
||||
/// The type attribute of the message.
|
||||
final String? type;
|
||||
final StableStanzaId stanzaId;
|
||||
final bool isCarbon;
|
||||
final bool deliveryReceiptRequested;
|
||||
final bool isMarkable;
|
||||
final OOBData? oob;
|
||||
final StatelessFileSharingData? sfs;
|
||||
final StatelessMediaSharingData? sims;
|
||||
final ReplyData? reply;
|
||||
final ChatState? chatState;
|
||||
final FileMetadataData? fun;
|
||||
final String? funReplacement;
|
||||
final String? funCancellation;
|
||||
|
||||
final StanzaError? error;
|
||||
|
||||
/// Flag indicating whether the message was 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;
|
||||
|
||||
/// The error in case an encryption error occurred.
|
||||
final Object? encryptionError;
|
||||
|
||||
/// Data added by other handlers.
|
||||
final TypedMap<StanzaHandlerExtension> extensions;
|
||||
|
||||
/// Shorthand for extensions.get<T>().
|
||||
T? get<T>() => extensions.get<T>();
|
||||
}
|
||||
|
||||
/// Triggered when a client responds to our delivery receipt request
|
||||
@@ -137,13 +113,19 @@ class DeliveryReceiptReceivedEvent extends XmppEvent {
|
||||
}
|
||||
|
||||
class ChatMarkerEvent extends XmppEvent {
|
||||
ChatMarkerEvent({
|
||||
required this.type,
|
||||
required this.from,
|
||||
required this.id,
|
||||
});
|
||||
ChatMarkerEvent(
|
||||
this.from,
|
||||
this.type,
|
||||
this.id,
|
||||
);
|
||||
|
||||
/// The entity that sent the chat marker.
|
||||
final JID from;
|
||||
final String type;
|
||||
|
||||
/// The type of chat marker that was sent.
|
||||
final ChatMarker type;
|
||||
|
||||
/// The id of the message that the marker applies to.
|
||||
final String id;
|
||||
}
|
||||
|
||||
@@ -167,13 +149,6 @@ class ResourceBoundEvent extends XmppEvent {
|
||||
final String resource;
|
||||
}
|
||||
|
||||
/// Triggered when we receive presence
|
||||
class PresenceReceivedEvent extends XmppEvent {
|
||||
PresenceReceivedEvent(this.jid, this.presence);
|
||||
final JID jid;
|
||||
final Stanza presence;
|
||||
}
|
||||
|
||||
/// Triggered when we are starting an connection attempt
|
||||
class ConnectingEvent extends XmppEvent {}
|
||||
|
||||
@@ -191,15 +166,31 @@ class SubscriptionRequestReceivedEvent extends XmppEvent {
|
||||
final JID from;
|
||||
}
|
||||
|
||||
/// Triggered when we receive a new or updated avatar
|
||||
class AvatarUpdatedEvent extends XmppEvent {
|
||||
AvatarUpdatedEvent({
|
||||
required this.jid,
|
||||
required this.base64,
|
||||
required this.hash,
|
||||
});
|
||||
final String jid;
|
||||
final String base64;
|
||||
/// Triggered when we receive a new or updated avatar via XEP-0084
|
||||
class UserAvatarUpdatedEvent extends XmppEvent {
|
||||
UserAvatarUpdatedEvent(
|
||||
this.jid,
|
||||
this.metadata,
|
||||
);
|
||||
|
||||
/// The JID of the user updating their avatar.
|
||||
final JID jid;
|
||||
|
||||
/// The metadata of the avatar.
|
||||
final List<UserAvatarMetadata> metadata;
|
||||
}
|
||||
|
||||
/// Triggered when we receive a new or updated avatar via XEP-0054
|
||||
class VCardAvatarUpdatedEvent extends XmppEvent {
|
||||
VCardAvatarUpdatedEvent(
|
||||
this.jid,
|
||||
this.hash,
|
||||
);
|
||||
|
||||
/// The JID of the entity that updated their avatar.
|
||||
final JID jid;
|
||||
|
||||
/// The SHA-1 hash of the avatar.
|
||||
final String hash;
|
||||
}
|
||||
|
||||
@@ -257,4 +248,10 @@ class NonRecoverableErrorEvent extends XmppEvent {
|
||||
}
|
||||
|
||||
/// Triggered when the stream negotiations are done.
|
||||
class StreamNegotiationsDoneEvent extends XmppEvent {}
|
||||
class StreamNegotiationsDoneEvent extends XmppEvent {
|
||||
StreamNegotiationsDoneEvent(this.resumed);
|
||||
|
||||
/// Flag indicating whether we resumed a previous stream (true) or are in a completely
|
||||
/// new stream (false).
|
||||
final bool resumed;
|
||||
}
|
||||
|
||||
@@ -23,9 +23,11 @@ Future<void> handleUnhandledStanza(
|
||||
);
|
||||
|
||||
await conn.sendStanza(
|
||||
stanza,
|
||||
awaitable: false,
|
||||
forceEncryption: data.encrypted,
|
||||
StanzaDetails(
|
||||
stanza,
|
||||
awaitable: false,
|
||||
forceEncryption: data.encrypted,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,14 +55,17 @@ class JID {
|
||||
|
||||
/// 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);
|
||||
|
||||
/// Convert the JID into the JID of the domain. For example, converts alice@example.org/abc123 to example.org.
|
||||
JID toDomain() {
|
||||
return JID('', domain, '');
|
||||
}
|
||||
|
||||
/// Compares the JID with [other]. This function assumes that JID and [other]
|
||||
/// 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.
|
||||
|
||||
@@ -23,14 +23,7 @@ class XmppManagerAttributes {
|
||||
});
|
||||
|
||||
/// Send a stanza whose response can be awaited.
|
||||
final Future<XMLNode> Function(
|
||||
Stanza stanza, {
|
||||
StanzaFromType addFrom,
|
||||
bool addId,
|
||||
bool awaitable,
|
||||
bool encrypted,
|
||||
bool forceEncryption,
|
||||
}) sendStanza;
|
||||
final Future<XMLNode?> Function(StanzaDetails) sendStanza;
|
||||
|
||||
/// Send a nonza.
|
||||
final void Function(XMLNode) sendNonza;
|
||||
|
||||
@@ -5,6 +5,7 @@ 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/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
@@ -45,8 +46,7 @@ abstract class XmppManagerBase {
|
||||
);
|
||||
|
||||
final result = await dm!.discoInfoQuery(
|
||||
_managerAttributes.getConnectionSettings().jid.domain,
|
||||
shouldEncrypt: false,
|
||||
_managerAttributes.getConnectionSettings().jid.toDomain(),
|
||||
);
|
||||
if (result.isType<DiscoError>()) {
|
||||
return false;
|
||||
@@ -80,6 +80,9 @@ abstract class XmppManagerBase {
|
||||
/// handler's priority, the earlier it is run.
|
||||
List<NonzaHandler> getNonzaHandlers() => [];
|
||||
|
||||
/// Whenever the socket receives data, this method is called, if it is non-null.
|
||||
Future<void> onData() async {}
|
||||
|
||||
/// Return a list of features that should be included in a disco response.
|
||||
List<String> getDiscoFeatures() => [];
|
||||
|
||||
@@ -165,9 +168,11 @@ abstract class XmppManagerBase {
|
||||
);
|
||||
|
||||
await getAttributes().sendStanza(
|
||||
stanza,
|
||||
awaitable: false,
|
||||
forceEncryption: data.encrypted,
|
||||
StanzaDetails(
|
||||
stanza,
|
||||
awaitable: false,
|
||||
forceEncryption: data.encrypted,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,59 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0066.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0085.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0203.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0380.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0385.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0447.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0461.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
part 'data.freezed.dart';
|
||||
abstract class StanzaHandlerExtension {}
|
||||
|
||||
@freezed
|
||||
class StanzaHandlerData with _$StanzaHandlerData {
|
||||
factory StanzaHandlerData(
|
||||
// Indicates to the runner that processing is now done. This means that all
|
||||
// pre-processing is done and no other handlers should be consulted.
|
||||
bool done,
|
||||
// Indicates to the runner that processing is to be cancelled and no further handlers
|
||||
// should run. The stanza also will not be sent.
|
||||
bool cancel,
|
||||
// The reason why we cancelled the processing and sending
|
||||
dynamic cancelReason,
|
||||
// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
|
||||
// necessary, e.g. with Message Carbons or OMEMO
|
||||
Stanza stanza, {
|
||||
// Whether the stanza is retransmitted. Only useful in the context of outgoing
|
||||
// stanza handlers. MUST NOT be overwritten.
|
||||
@Default(false) bool retransmitted,
|
||||
StatelessMediaSharingData? sims,
|
||||
StatelessFileSharingData? sfs,
|
||||
OOBData? oob,
|
||||
StableStanzaId? stableId,
|
||||
ReplyData? reply,
|
||||
ChatState? chatState,
|
||||
@Default(false) bool isCarbon,
|
||||
@Default(false) bool deliveryReceiptRequested,
|
||||
@Default(false) bool isMarkable,
|
||||
// File Upload Notifications
|
||||
// A notification
|
||||
FileMetadataData? fun,
|
||||
// The stanza id this replaces
|
||||
String? funReplacement,
|
||||
// The stanza id this cancels
|
||||
String? funCancellation,
|
||||
// Whether the stanza was received encrypted
|
||||
@Default(false) bool encrypted,
|
||||
// If true, forces the encryption manager to encrypt to the JID, even if it
|
||||
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
|
||||
// but forceEncryption is true, then the OMEMO manager will try to encrypt
|
||||
// to the JID anyway.
|
||||
@Default(false) bool forceEncryption,
|
||||
// The stated type of encryption used, if any was used
|
||||
ExplicitEncryptionType? encryptionType,
|
||||
// Delayed Delivery
|
||||
DelayedDelivery? delayedDelivery,
|
||||
// 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;
|
||||
class StanzaHandlerData {
|
||||
StanzaHandlerData(
|
||||
this.done,
|
||||
this.cancel,
|
||||
this.stanza,
|
||||
this.extensions, {
|
||||
this.cancelReason,
|
||||
this.encryptionError,
|
||||
this.encrypted = false,
|
||||
this.forceEncryption = false,
|
||||
this.shouldEncrypt = true,
|
||||
this.skip = false,
|
||||
});
|
||||
|
||||
/// Indicates to the runner that processing is now done. This means that all
|
||||
/// pre-processing is done and no other handlers should be consulted.
|
||||
bool done;
|
||||
|
||||
/// Only useful in combination with [done] = true: When [skip] is set to true and
|
||||
/// this [StanzaHandlerData] object is returned from a IncomingPreStanzaHandler, then
|
||||
/// moxxmpp will skip checking whether the stanza was awaited and will not run any actual
|
||||
/// IncomingStanzaHandler callbacks.
|
||||
bool skip;
|
||||
|
||||
/// Indicates to the runner that processing is to be cancelled and no further handlers
|
||||
/// should run. The stanza also will not be sent.
|
||||
bool cancel;
|
||||
|
||||
/// The reason why we cancelled the processing and sending.
|
||||
Object? cancelReason;
|
||||
|
||||
/// The reason why an encryption or decryption failed.
|
||||
Object? encryptionError;
|
||||
|
||||
/// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is
|
||||
/// absolutely necessary, e.g. with Message Carbons or OMEMO.
|
||||
Stanza stanza;
|
||||
|
||||
/// Whether the stanza is already encrypted
|
||||
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.
|
||||
bool forceEncryption;
|
||||
|
||||
/// Flag indicating whether a E2EE implementation should encrypt the stanza (true)
|
||||
/// or not (false).
|
||||
bool shouldEncrypt;
|
||||
|
||||
/// Additional data from other managers.
|
||||
final TypedMap<StanzaHandlerExtension> extensions;
|
||||
}
|
||||
|
||||
@@ -1,747 +0,0 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
|
||||
|
||||
part of 'data.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$StanzaHandlerData {
|
||||
// Indicates to the runner that processing is now done. This means that all
|
||||
// pre-processing is done and no other handlers should be consulted.
|
||||
bool get done =>
|
||||
throw _privateConstructorUsedError; // Indicates to the runner that processing is to be cancelled and no further handlers
|
||||
// should run. The stanza also will not be sent.
|
||||
bool get cancel =>
|
||||
throw _privateConstructorUsedError; // The reason why we cancelled the processing and sending
|
||||
dynamic get cancelReason =>
|
||||
throw _privateConstructorUsedError; // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
|
||||
// necessary, e.g. with Message Carbons or OMEMO
|
||||
Stanza get stanza =>
|
||||
throw _privateConstructorUsedError; // Whether the stanza is retransmitted. Only useful in the context of outgoing
|
||||
// stanza handlers. MUST NOT be overwritten.
|
||||
bool get retransmitted => throw _privateConstructorUsedError;
|
||||
StatelessMediaSharingData? get sims => throw _privateConstructorUsedError;
|
||||
StatelessFileSharingData? get sfs => throw _privateConstructorUsedError;
|
||||
OOBData? get oob => throw _privateConstructorUsedError;
|
||||
StableStanzaId? get stableId => throw _privateConstructorUsedError;
|
||||
ReplyData? get reply => throw _privateConstructorUsedError;
|
||||
ChatState? get chatState => throw _privateConstructorUsedError;
|
||||
bool get isCarbon => throw _privateConstructorUsedError;
|
||||
bool get deliveryReceiptRequested => throw _privateConstructorUsedError;
|
||||
bool get isMarkable =>
|
||||
throw _privateConstructorUsedError; // File Upload Notifications
|
||||
// A notification
|
||||
FileMetadataData? get fun =>
|
||||
throw _privateConstructorUsedError; // The stanza id this replaces
|
||||
String? get funReplacement =>
|
||||
throw _privateConstructorUsedError; // The stanza id this cancels
|
||||
String? get funCancellation =>
|
||||
throw _privateConstructorUsedError; // Whether the stanza was received encrypted
|
||||
bool get encrypted =>
|
||||
throw _privateConstructorUsedError; // If true, forces the encryption manager to encrypt to the JID, even if it
|
||||
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
|
||||
// but forceEncryption is true, then the OMEMO manager will try to encrypt
|
||||
// to the JID anyway.
|
||||
bool get forceEncryption =>
|
||||
throw _privateConstructorUsedError; // The stated type of encryption used, if any was used
|
||||
ExplicitEncryptionType? get encryptionType =>
|
||||
throw _privateConstructorUsedError; // Delayed Delivery
|
||||
DelayedDelivery? get delayedDelivery =>
|
||||
throw _privateConstructorUsedError; // This is for stanza handlers that are not part of the XMPP library but still need
|
||||
// pass data around.
|
||||
Map<String, dynamic> get other =>
|
||||
throw _privateConstructorUsedError; // If non-null, then it indicates the origin Id of the message that should be
|
||||
// retracted
|
||||
MessageRetractionData? get messageRetraction =>
|
||||
throw _privateConstructorUsedError; // If non-null, then the message is a correction for the specified stanza Id
|
||||
String? get lastMessageCorrectionSid =>
|
||||
throw _privateConstructorUsedError; // Reactions data
|
||||
MessageReactions? get messageReactions =>
|
||||
throw _privateConstructorUsedError; // The Id of the sticker pack this sticker belongs to
|
||||
String? get stickerPackId => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$StanzaHandlerDataCopyWith<StanzaHandlerData> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $StanzaHandlerDataCopyWith<$Res> {
|
||||
factory $StanzaHandlerDataCopyWith(
|
||||
StanzaHandlerData value, $Res Function(StanzaHandlerData) then) =
|
||||
_$StanzaHandlerDataCopyWithImpl<$Res>;
|
||||
$Res call(
|
||||
{bool done,
|
||||
bool cancel,
|
||||
dynamic cancelReason,
|
||||
Stanza stanza,
|
||||
bool retransmitted,
|
||||
StatelessMediaSharingData? sims,
|
||||
StatelessFileSharingData? sfs,
|
||||
OOBData? oob,
|
||||
StableStanzaId? stableId,
|
||||
ReplyData? reply,
|
||||
ChatState? chatState,
|
||||
bool isCarbon,
|
||||
bool deliveryReceiptRequested,
|
||||
bool isMarkable,
|
||||
FileMetadataData? fun,
|
||||
String? funReplacement,
|
||||
String? funCancellation,
|
||||
bool encrypted,
|
||||
bool forceEncryption,
|
||||
ExplicitEncryptionType? encryptionType,
|
||||
DelayedDelivery? delayedDelivery,
|
||||
Map<String, dynamic> other,
|
||||
MessageRetractionData? messageRetraction,
|
||||
String? lastMessageCorrectionSid,
|
||||
MessageReactions? messageReactions,
|
||||
String? stickerPackId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$StanzaHandlerDataCopyWithImpl<$Res>
|
||||
implements $StanzaHandlerDataCopyWith<$Res> {
|
||||
_$StanzaHandlerDataCopyWithImpl(this._value, this._then);
|
||||
|
||||
final StanzaHandlerData _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function(StanzaHandlerData) _then;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? done = freezed,
|
||||
Object? cancel = freezed,
|
||||
Object? cancelReason = freezed,
|
||||
Object? stanza = freezed,
|
||||
Object? retransmitted = freezed,
|
||||
Object? sims = freezed,
|
||||
Object? sfs = freezed,
|
||||
Object? oob = freezed,
|
||||
Object? stableId = freezed,
|
||||
Object? reply = freezed,
|
||||
Object? chatState = freezed,
|
||||
Object? isCarbon = freezed,
|
||||
Object? deliveryReceiptRequested = freezed,
|
||||
Object? isMarkable = freezed,
|
||||
Object? fun = freezed,
|
||||
Object? funReplacement = freezed,
|
||||
Object? funCancellation = freezed,
|
||||
Object? encrypted = freezed,
|
||||
Object? forceEncryption = freezed,
|
||||
Object? encryptionType = freezed,
|
||||
Object? delayedDelivery = freezed,
|
||||
Object? other = freezed,
|
||||
Object? messageRetraction = freezed,
|
||||
Object? lastMessageCorrectionSid = freezed,
|
||||
Object? messageReactions = freezed,
|
||||
Object? stickerPackId = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
done: done == freezed
|
||||
? _value.done
|
||||
: done // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
cancel: cancel == freezed
|
||||
? _value.cancel
|
||||
: cancel // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
cancelReason: cancelReason == freezed
|
||||
? _value.cancelReason
|
||||
: cancelReason // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
stanza: stanza == freezed
|
||||
? _value.stanza
|
||||
: stanza // ignore: cast_nullable_to_non_nullable
|
||||
as Stanza,
|
||||
retransmitted: retransmitted == freezed
|
||||
? _value.retransmitted
|
||||
: retransmitted // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
sims: sims == freezed
|
||||
? _value.sims
|
||||
: sims // ignore: cast_nullable_to_non_nullable
|
||||
as StatelessMediaSharingData?,
|
||||
sfs: sfs == freezed
|
||||
? _value.sfs
|
||||
: sfs // ignore: cast_nullable_to_non_nullable
|
||||
as StatelessFileSharingData?,
|
||||
oob: oob == freezed
|
||||
? _value.oob
|
||||
: oob // ignore: cast_nullable_to_non_nullable
|
||||
as OOBData?,
|
||||
stableId: stableId == freezed
|
||||
? _value.stableId
|
||||
: stableId // ignore: cast_nullable_to_non_nullable
|
||||
as StableStanzaId?,
|
||||
reply: reply == freezed
|
||||
? _value.reply
|
||||
: reply // ignore: cast_nullable_to_non_nullable
|
||||
as ReplyData?,
|
||||
chatState: chatState == freezed
|
||||
? _value.chatState
|
||||
: chatState // ignore: cast_nullable_to_non_nullable
|
||||
as ChatState?,
|
||||
isCarbon: isCarbon == freezed
|
||||
? _value.isCarbon
|
||||
: isCarbon // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
deliveryReceiptRequested: deliveryReceiptRequested == freezed
|
||||
? _value.deliveryReceiptRequested
|
||||
: deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isMarkable: isMarkable == freezed
|
||||
? _value.isMarkable
|
||||
: isMarkable // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
fun: fun == freezed
|
||||
? _value.fun
|
||||
: fun // ignore: cast_nullable_to_non_nullable
|
||||
as FileMetadataData?,
|
||||
funReplacement: funReplacement == freezed
|
||||
? _value.funReplacement
|
||||
: funReplacement // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
funCancellation: funCancellation == freezed
|
||||
? _value.funCancellation
|
||||
: funCancellation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
encrypted: encrypted == freezed
|
||||
? _value.encrypted
|
||||
: encrypted // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
forceEncryption: forceEncryption == freezed
|
||||
? _value.forceEncryption
|
||||
: forceEncryption // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
encryptionType: encryptionType == freezed
|
||||
? _value.encryptionType
|
||||
: encryptionType // ignore: cast_nullable_to_non_nullable
|
||||
as ExplicitEncryptionType?,
|
||||
delayedDelivery: delayedDelivery == freezed
|
||||
? _value.delayedDelivery
|
||||
: delayedDelivery // ignore: cast_nullable_to_non_nullable
|
||||
as DelayedDelivery?,
|
||||
other: other == freezed
|
||||
? _value.other
|
||||
: other // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
messageRetraction: messageRetraction == freezed
|
||||
? _value.messageRetraction
|
||||
: messageRetraction // ignore: cast_nullable_to_non_nullable
|
||||
as MessageRetractionData?,
|
||||
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
|
||||
? _value.lastMessageCorrectionSid
|
||||
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
messageReactions: messageReactions == freezed
|
||||
? _value.messageReactions
|
||||
: messageReactions // ignore: cast_nullable_to_non_nullable
|
||||
as MessageReactions?,
|
||||
stickerPackId: stickerPackId == freezed
|
||||
? _value.stickerPackId
|
||||
: stickerPackId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_StanzaHandlerDataCopyWith<$Res>
|
||||
implements $StanzaHandlerDataCopyWith<$Res> {
|
||||
factory _$$_StanzaHandlerDataCopyWith(_$_StanzaHandlerData value,
|
||||
$Res Function(_$_StanzaHandlerData) then) =
|
||||
__$$_StanzaHandlerDataCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call(
|
||||
{bool done,
|
||||
bool cancel,
|
||||
dynamic cancelReason,
|
||||
Stanza stanza,
|
||||
bool retransmitted,
|
||||
StatelessMediaSharingData? sims,
|
||||
StatelessFileSharingData? sfs,
|
||||
OOBData? oob,
|
||||
StableStanzaId? stableId,
|
||||
ReplyData? reply,
|
||||
ChatState? chatState,
|
||||
bool isCarbon,
|
||||
bool deliveryReceiptRequested,
|
||||
bool isMarkable,
|
||||
FileMetadataData? fun,
|
||||
String? funReplacement,
|
||||
String? funCancellation,
|
||||
bool encrypted,
|
||||
bool forceEncryption,
|
||||
ExplicitEncryptionType? encryptionType,
|
||||
DelayedDelivery? delayedDelivery,
|
||||
Map<String, dynamic> other,
|
||||
MessageRetractionData? messageRetraction,
|
||||
String? lastMessageCorrectionSid,
|
||||
MessageReactions? messageReactions,
|
||||
String? stickerPackId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_StanzaHandlerDataCopyWithImpl<$Res>
|
||||
extends _$StanzaHandlerDataCopyWithImpl<$Res>
|
||||
implements _$$_StanzaHandlerDataCopyWith<$Res> {
|
||||
__$$_StanzaHandlerDataCopyWithImpl(
|
||||
_$_StanzaHandlerData _value, $Res Function(_$_StanzaHandlerData) _then)
|
||||
: super(_value, (v) => _then(v as _$_StanzaHandlerData));
|
||||
|
||||
@override
|
||||
_$_StanzaHandlerData get _value => super._value as _$_StanzaHandlerData;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? done = freezed,
|
||||
Object? cancel = freezed,
|
||||
Object? cancelReason = freezed,
|
||||
Object? stanza = freezed,
|
||||
Object? retransmitted = freezed,
|
||||
Object? sims = freezed,
|
||||
Object? sfs = freezed,
|
||||
Object? oob = freezed,
|
||||
Object? stableId = freezed,
|
||||
Object? reply = freezed,
|
||||
Object? chatState = freezed,
|
||||
Object? isCarbon = freezed,
|
||||
Object? deliveryReceiptRequested = freezed,
|
||||
Object? isMarkable = freezed,
|
||||
Object? fun = freezed,
|
||||
Object? funReplacement = freezed,
|
||||
Object? funCancellation = freezed,
|
||||
Object? encrypted = freezed,
|
||||
Object? forceEncryption = freezed,
|
||||
Object? encryptionType = freezed,
|
||||
Object? delayedDelivery = freezed,
|
||||
Object? other = freezed,
|
||||
Object? messageRetraction = freezed,
|
||||
Object? lastMessageCorrectionSid = freezed,
|
||||
Object? messageReactions = freezed,
|
||||
Object? stickerPackId = freezed,
|
||||
}) {
|
||||
return _then(_$_StanzaHandlerData(
|
||||
done == freezed
|
||||
? _value.done
|
||||
: done // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
cancel == freezed
|
||||
? _value.cancel
|
||||
: cancel // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
cancelReason == freezed
|
||||
? _value.cancelReason
|
||||
: cancelReason // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
stanza == freezed
|
||||
? _value.stanza
|
||||
: stanza // ignore: cast_nullable_to_non_nullable
|
||||
as Stanza,
|
||||
retransmitted: retransmitted == freezed
|
||||
? _value.retransmitted
|
||||
: retransmitted // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
sims: sims == freezed
|
||||
? _value.sims
|
||||
: sims // ignore: cast_nullable_to_non_nullable
|
||||
as StatelessMediaSharingData?,
|
||||
sfs: sfs == freezed
|
||||
? _value.sfs
|
||||
: sfs // ignore: cast_nullable_to_non_nullable
|
||||
as StatelessFileSharingData?,
|
||||
oob: oob == freezed
|
||||
? _value.oob
|
||||
: oob // ignore: cast_nullable_to_non_nullable
|
||||
as OOBData?,
|
||||
stableId: stableId == freezed
|
||||
? _value.stableId
|
||||
: stableId // ignore: cast_nullable_to_non_nullable
|
||||
as StableStanzaId?,
|
||||
reply: reply == freezed
|
||||
? _value.reply
|
||||
: reply // ignore: cast_nullable_to_non_nullable
|
||||
as ReplyData?,
|
||||
chatState: chatState == freezed
|
||||
? _value.chatState
|
||||
: chatState // ignore: cast_nullable_to_non_nullable
|
||||
as ChatState?,
|
||||
isCarbon: isCarbon == freezed
|
||||
? _value.isCarbon
|
||||
: isCarbon // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
deliveryReceiptRequested: deliveryReceiptRequested == freezed
|
||||
? _value.deliveryReceiptRequested
|
||||
: deliveryReceiptRequested // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
isMarkable: isMarkable == freezed
|
||||
? _value.isMarkable
|
||||
: isMarkable // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
fun: fun == freezed
|
||||
? _value.fun
|
||||
: fun // ignore: cast_nullable_to_non_nullable
|
||||
as FileMetadataData?,
|
||||
funReplacement: funReplacement == freezed
|
||||
? _value.funReplacement
|
||||
: funReplacement // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
funCancellation: funCancellation == freezed
|
||||
? _value.funCancellation
|
||||
: funCancellation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
encrypted: encrypted == freezed
|
||||
? _value.encrypted
|
||||
: encrypted // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
forceEncryption: forceEncryption == freezed
|
||||
? _value.forceEncryption
|
||||
: forceEncryption // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
encryptionType: encryptionType == freezed
|
||||
? _value.encryptionType
|
||||
: encryptionType // ignore: cast_nullable_to_non_nullable
|
||||
as ExplicitEncryptionType?,
|
||||
delayedDelivery: delayedDelivery == freezed
|
||||
? _value.delayedDelivery
|
||||
: delayedDelivery // ignore: cast_nullable_to_non_nullable
|
||||
as DelayedDelivery?,
|
||||
other: other == freezed
|
||||
? _value._other
|
||||
: other // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>,
|
||||
messageRetraction: messageRetraction == freezed
|
||||
? _value.messageRetraction
|
||||
: messageRetraction // ignore: cast_nullable_to_non_nullable
|
||||
as MessageRetractionData?,
|
||||
lastMessageCorrectionSid: lastMessageCorrectionSid == freezed
|
||||
? _value.lastMessageCorrectionSid
|
||||
: lastMessageCorrectionSid // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
messageReactions: messageReactions == freezed
|
||||
? _value.messageReactions
|
||||
: messageReactions // ignore: cast_nullable_to_non_nullable
|
||||
as MessageReactions?,
|
||||
stickerPackId: stickerPackId == freezed
|
||||
? _value.stickerPackId
|
||||
: stickerPackId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$_StanzaHandlerData implements _StanzaHandlerData {
|
||||
_$_StanzaHandlerData(this.done, this.cancel, this.cancelReason, this.stanza,
|
||||
{this.retransmitted = false,
|
||||
this.sims,
|
||||
this.sfs,
|
||||
this.oob,
|
||||
this.stableId,
|
||||
this.reply,
|
||||
this.chatState,
|
||||
this.isCarbon = false,
|
||||
this.deliveryReceiptRequested = false,
|
||||
this.isMarkable = false,
|
||||
this.fun,
|
||||
this.funReplacement,
|
||||
this.funCancellation,
|
||||
this.encrypted = false,
|
||||
this.forceEncryption = false,
|
||||
this.encryptionType,
|
||||
this.delayedDelivery,
|
||||
final Map<String, dynamic> other = const <String, dynamic>{},
|
||||
this.messageRetraction,
|
||||
this.lastMessageCorrectionSid,
|
||||
this.messageReactions,
|
||||
this.stickerPackId})
|
||||
: _other = other;
|
||||
|
||||
// Indicates to the runner that processing is now done. This means that all
|
||||
// pre-processing is done and no other handlers should be consulted.
|
||||
@override
|
||||
final bool done;
|
||||
// Indicates to the runner that processing is to be cancelled and no further handlers
|
||||
// should run. The stanza also will not be sent.
|
||||
@override
|
||||
final bool cancel;
|
||||
// The reason why we cancelled the processing and sending
|
||||
@override
|
||||
final dynamic cancelReason;
|
||||
// The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
|
||||
// necessary, e.g. with Message Carbons or OMEMO
|
||||
@override
|
||||
final Stanza stanza;
|
||||
// Whether the stanza is retransmitted. Only useful in the context of outgoing
|
||||
// stanza handlers. MUST NOT be overwritten.
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool retransmitted;
|
||||
@override
|
||||
final StatelessMediaSharingData? sims;
|
||||
@override
|
||||
final StatelessFileSharingData? sfs;
|
||||
@override
|
||||
final OOBData? oob;
|
||||
@override
|
||||
final StableStanzaId? stableId;
|
||||
@override
|
||||
final ReplyData? reply;
|
||||
@override
|
||||
final ChatState? chatState;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isCarbon;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool deliveryReceiptRequested;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isMarkable;
|
||||
// File Upload Notifications
|
||||
// A notification
|
||||
@override
|
||||
final FileMetadataData? fun;
|
||||
// The stanza id this replaces
|
||||
@override
|
||||
final String? funReplacement;
|
||||
// The stanza id this cancels
|
||||
@override
|
||||
final String? funCancellation;
|
||||
// Whether the stanza was received encrypted
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool encrypted;
|
||||
// If true, forces the encryption manager to encrypt to the JID, even if it
|
||||
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
|
||||
// but forceEncryption is true, then the OMEMO manager will try to encrypt
|
||||
// to the JID anyway.
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool forceEncryption;
|
||||
// The stated type of encryption used, if any was used
|
||||
@override
|
||||
final ExplicitEncryptionType? encryptionType;
|
||||
// Delayed Delivery
|
||||
@override
|
||||
final DelayedDelivery? delayedDelivery;
|
||||
// This is for stanza handlers that are not part of the XMPP library but still need
|
||||
// pass data around.
|
||||
final Map<String, dynamic> _other;
|
||||
// This is for stanza handlers that are not part of the XMPP library but still need
|
||||
// pass data around.
|
||||
@override
|
||||
@JsonKey()
|
||||
Map<String, dynamic> get other {
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_other);
|
||||
}
|
||||
|
||||
// If non-null, then it indicates the origin Id of the message that should be
|
||||
// retracted
|
||||
@override
|
||||
final MessageRetractionData? messageRetraction;
|
||||
// If non-null, then the message is a correction for the specified stanza Id
|
||||
@override
|
||||
final String? lastMessageCorrectionSid;
|
||||
// Reactions data
|
||||
@override
|
||||
final MessageReactions? messageReactions;
|
||||
// The Id of the sticker pack this sticker belongs to
|
||||
@override
|
||||
final String? stickerPackId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StanzaHandlerData(done: $done, cancel: $cancel, cancelReason: $cancelReason, stanza: $stanza, retransmitted: $retransmitted, sims: $sims, sfs: $sfs, oob: $oob, stableId: $stableId, reply: $reply, chatState: $chatState, isCarbon: $isCarbon, deliveryReceiptRequested: $deliveryReceiptRequested, isMarkable: $isMarkable, fun: $fun, funReplacement: $funReplacement, funCancellation: $funCancellation, encrypted: $encrypted, forceEncryption: $forceEncryption, encryptionType: $encryptionType, delayedDelivery: $delayedDelivery, other: $other, messageRetraction: $messageRetraction, lastMessageCorrectionSid: $lastMessageCorrectionSid, messageReactions: $messageReactions, stickerPackId: $stickerPackId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_StanzaHandlerData &&
|
||||
const DeepCollectionEquality().equals(other.done, done) &&
|
||||
const DeepCollectionEquality().equals(other.cancel, cancel) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.cancelReason, cancelReason) &&
|
||||
const DeepCollectionEquality().equals(other.stanza, stanza) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.retransmitted, retransmitted) &&
|
||||
const DeepCollectionEquality().equals(other.sims, sims) &&
|
||||
const DeepCollectionEquality().equals(other.sfs, sfs) &&
|
||||
const DeepCollectionEquality().equals(other.oob, oob) &&
|
||||
const DeepCollectionEquality().equals(other.stableId, stableId) &&
|
||||
const DeepCollectionEquality().equals(other.reply, reply) &&
|
||||
const DeepCollectionEquality().equals(other.chatState, chatState) &&
|
||||
const DeepCollectionEquality().equals(other.isCarbon, isCarbon) &&
|
||||
const DeepCollectionEquality().equals(
|
||||
other.deliveryReceiptRequested, deliveryReceiptRequested) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.isMarkable, isMarkable) &&
|
||||
const DeepCollectionEquality().equals(other.fun, fun) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.funReplacement, funReplacement) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.funCancellation, funCancellation) &&
|
||||
const DeepCollectionEquality().equals(other.encrypted, encrypted) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.forceEncryption, forceEncryption) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.encryptionType, encryptionType) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.delayedDelivery, delayedDelivery) &&
|
||||
const DeepCollectionEquality().equals(other._other, this._other) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.messageRetraction, messageRetraction) &&
|
||||
const DeepCollectionEquality().equals(
|
||||
other.lastMessageCorrectionSid, lastMessageCorrectionSid) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.messageReactions, messageReactions) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.stickerPackId, stickerPackId));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(done),
|
||||
const DeepCollectionEquality().hash(cancel),
|
||||
const DeepCollectionEquality().hash(cancelReason),
|
||||
const DeepCollectionEquality().hash(stanza),
|
||||
const DeepCollectionEquality().hash(retransmitted),
|
||||
const DeepCollectionEquality().hash(sims),
|
||||
const DeepCollectionEquality().hash(sfs),
|
||||
const DeepCollectionEquality().hash(oob),
|
||||
const DeepCollectionEquality().hash(stableId),
|
||||
const DeepCollectionEquality().hash(reply),
|
||||
const DeepCollectionEquality().hash(chatState),
|
||||
const DeepCollectionEquality().hash(isCarbon),
|
||||
const DeepCollectionEquality().hash(deliveryReceiptRequested),
|
||||
const DeepCollectionEquality().hash(isMarkable),
|
||||
const DeepCollectionEquality().hash(fun),
|
||||
const DeepCollectionEquality().hash(funReplacement),
|
||||
const DeepCollectionEquality().hash(funCancellation),
|
||||
const DeepCollectionEquality().hash(encrypted),
|
||||
const DeepCollectionEquality().hash(forceEncryption),
|
||||
const DeepCollectionEquality().hash(encryptionType),
|
||||
const DeepCollectionEquality().hash(delayedDelivery),
|
||||
const DeepCollectionEquality().hash(_other),
|
||||
const DeepCollectionEquality().hash(messageRetraction),
|
||||
const DeepCollectionEquality().hash(lastMessageCorrectionSid),
|
||||
const DeepCollectionEquality().hash(messageReactions),
|
||||
const DeepCollectionEquality().hash(stickerPackId)
|
||||
]);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>
|
||||
__$$_StanzaHandlerDataCopyWithImpl<_$_StanzaHandlerData>(
|
||||
this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _StanzaHandlerData implements StanzaHandlerData {
|
||||
factory _StanzaHandlerData(final bool done, final bool cancel,
|
||||
final dynamic cancelReason, final Stanza stanza,
|
||||
{final bool retransmitted,
|
||||
final StatelessMediaSharingData? sims,
|
||||
final StatelessFileSharingData? sfs,
|
||||
final OOBData? oob,
|
||||
final StableStanzaId? stableId,
|
||||
final ReplyData? reply,
|
||||
final ChatState? chatState,
|
||||
final bool isCarbon,
|
||||
final bool deliveryReceiptRequested,
|
||||
final bool isMarkable,
|
||||
final FileMetadataData? fun,
|
||||
final String? funReplacement,
|
||||
final String? funCancellation,
|
||||
final bool encrypted,
|
||||
final bool forceEncryption,
|
||||
final ExplicitEncryptionType? encryptionType,
|
||||
final DelayedDelivery? delayedDelivery,
|
||||
final Map<String, dynamic> other,
|
||||
final MessageRetractionData? messageRetraction,
|
||||
final String? lastMessageCorrectionSid,
|
||||
final MessageReactions? messageReactions,
|
||||
final String? stickerPackId}) = _$_StanzaHandlerData;
|
||||
|
||||
@override // Indicates to the runner that processing is now done. This means that all
|
||||
// pre-processing is done and no other handlers should be consulted.
|
||||
bool get done;
|
||||
@override // Indicates to the runner that processing is to be cancelled and no further handlers
|
||||
// should run. The stanza also will not be sent.
|
||||
bool get cancel;
|
||||
@override // The reason why we cancelled the processing and sending
|
||||
dynamic get cancelReason;
|
||||
@override // The stanza that is being dealt with. SHOULD NOT be overwritten, unless it is absolutely
|
||||
// necessary, e.g. with Message Carbons or OMEMO
|
||||
Stanza get stanza;
|
||||
@override // Whether the stanza is retransmitted. Only useful in the context of outgoing
|
||||
// stanza handlers. MUST NOT be overwritten.
|
||||
bool get retransmitted;
|
||||
@override
|
||||
StatelessMediaSharingData? get sims;
|
||||
@override
|
||||
StatelessFileSharingData? get sfs;
|
||||
@override
|
||||
OOBData? get oob;
|
||||
@override
|
||||
StableStanzaId? get stableId;
|
||||
@override
|
||||
ReplyData? get reply;
|
||||
@override
|
||||
ChatState? get chatState;
|
||||
@override
|
||||
bool get isCarbon;
|
||||
@override
|
||||
bool get deliveryReceiptRequested;
|
||||
@override
|
||||
bool get isMarkable;
|
||||
@override // File Upload Notifications
|
||||
// A notification
|
||||
FileMetadataData? get fun;
|
||||
@override // The stanza id this replaces
|
||||
String? get funReplacement;
|
||||
@override // The stanza id this cancels
|
||||
String? get funCancellation;
|
||||
@override // Whether the stanza was received encrypted
|
||||
bool get encrypted;
|
||||
@override // If true, forces the encryption manager to encrypt to the JID, even if it
|
||||
// would not normally. In the case of OMEMO: If shouldEncrypt returns false
|
||||
// but forceEncryption is true, then the OMEMO manager will try to encrypt
|
||||
// to the JID anyway.
|
||||
bool get forceEncryption;
|
||||
@override // The stated type of encryption used, if any was used
|
||||
ExplicitEncryptionType? get encryptionType;
|
||||
@override // Delayed Delivery
|
||||
DelayedDelivery? get delayedDelivery;
|
||||
@override // This is for stanza handlers that are not part of the XMPP library but still need
|
||||
// pass data around.
|
||||
Map<String, dynamic> get other;
|
||||
@override // If non-null, then it indicates the origin Id of the message that should be
|
||||
// retracted
|
||||
MessageRetractionData? get messageRetraction;
|
||||
@override // If non-null, then the message is a correction for the specified stanza Id
|
||||
String? get lastMessageCorrectionSid;
|
||||
@override // Reactions data
|
||||
MessageReactions? get messageReactions;
|
||||
@override // The Id of the sticker pack this sticker belongs to
|
||||
String? get stickerPackId;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_StanzaHandlerDataCopyWith<_$_StanzaHandlerData> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
@@ -100,10 +100,10 @@ class StanzaHandler extends Handler {
|
||||
matches &= firstTag?.xmlns == tagXmlns;
|
||||
}
|
||||
} else if (tagXmlns != null) {
|
||||
matches &= listContains(
|
||||
node.children,
|
||||
(XMLNode node_) => node_.attributes['xmlns'] == tagXmlns,
|
||||
);
|
||||
matches &= node.children.firstWhereOrNull(
|
||||
(XMLNode node_) => node_.attributes['xmlns'] == tagXmlns,
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
|
||||
return matches;
|
||||
|
||||
@@ -31,3 +31,6 @@ const lastMessageCorrectionManager = 'org.moxxmpp.lastmessagecorrectionmanager';
|
||||
const messageReactionsManager = 'org.moxxmpp.messagereactionsmanager';
|
||||
const stickersManager = 'org.moxxmpp.stickersmanager';
|
||||
const entityCapabilitiesManager = 'org.moxxmpp.entitycapabilities';
|
||||
const messageProcessingHintManager = 'org.moxxmpp.messageprocessinghint';
|
||||
const occupantIdManager = 'org.moxxmpp.occupantidmanager';
|
||||
const mucManager = 'org.moxxmpp.mucmanager';
|
||||
|
||||
@@ -1,325 +1,153 @@
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0066.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0085.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0184.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0308.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0333.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0334.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0447.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0448.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0449.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 {
|
||||
const MessageDetails({
|
||||
required this.to,
|
||||
this.body,
|
||||
this.requestDeliveryReceipt = false,
|
||||
this.requestChatMarkers = true,
|
||||
this.id,
|
||||
this.originId,
|
||||
this.quoteBody,
|
||||
this.quoteId,
|
||||
this.quoteFrom,
|
||||
this.chatState,
|
||||
this.sfs,
|
||||
this.fun,
|
||||
this.funReplacement,
|
||||
this.funCancellation,
|
||||
this.shouldEncrypt = false,
|
||||
this.messageRetraction,
|
||||
this.lastMessageCorrectionId,
|
||||
this.messageReactions,
|
||||
this.messageProcessingHints,
|
||||
this.stickerPackId,
|
||||
this.setOOBFallbackBody = true,
|
||||
});
|
||||
final String to;
|
||||
/// A callback that is called whenever a message is sent using
|
||||
/// [MessageManager.sendMessage]. The input the typed map that is passed to
|
||||
/// sendMessage.
|
||||
typedef MessageSendingCallback = List<XMLNode> Function(
|
||||
TypedMap<StanzaHandlerExtension>,
|
||||
);
|
||||
|
||||
/// The raw content of the <body /> element.
|
||||
class MessageBodyData implements StanzaHandlerExtension {
|
||||
const MessageBodyData(this.body);
|
||||
|
||||
/// The content of the <body /> element.
|
||||
final String? body;
|
||||
final bool requestDeliveryReceipt;
|
||||
final bool requestChatMarkers;
|
||||
final String? id;
|
||||
final String? originId;
|
||||
final String? quoteBody;
|
||||
final String? quoteId;
|
||||
final String? quoteFrom;
|
||||
final ChatState? chatState;
|
||||
final StatelessFileSharingData? sfs;
|
||||
final FileMetadataData? fun;
|
||||
final String? funReplacement;
|
||||
final String? funCancellation;
|
||||
final bool shouldEncrypt;
|
||||
final MessageRetractionData? messageRetraction;
|
||||
final String? lastMessageCorrectionId;
|
||||
final MessageReactions? messageReactions;
|
||||
final String? stickerPackId;
|
||||
final List<MessageProcessingHint>? messageProcessingHints;
|
||||
final bool setOOBFallbackBody;
|
||||
|
||||
XMLNode toXML() {
|
||||
return XMLNode(
|
||||
tag: 'body',
|
||||
text: body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The id attribute of the message stanza.
|
||||
class MessageIdData implements StanzaHandlerExtension {
|
||||
const MessageIdData(this.id);
|
||||
|
||||
/// The id attribute of the stanza.
|
||||
final String id;
|
||||
}
|
||||
|
||||
class MessageManager extends XmppManagerBase {
|
||||
MessageManager() : super(messageManager);
|
||||
|
||||
/// The priority of the message handler. If a handler should run before this one,
|
||||
/// which emits the [MessageEvent] event and terminates processing, make sure it
|
||||
/// has a priority greater than [messageHandlerPriority].
|
||||
static int messageHandlerPriority = -100;
|
||||
|
||||
/// A list of callbacks that are called when a message is sent in order to add
|
||||
/// appropriate child elements.
|
||||
final List<MessageSendingCallback> _messageSendingCallbacks =
|
||||
List<MessageSendingCallback>.empty(growable: true);
|
||||
|
||||
void registerMessageSendingCallback(MessageSendingCallback callback) {
|
||||
_messageSendingCallbacks.add(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||
StanzaHandler(
|
||||
stanzaTag: 'message',
|
||||
callback: _onMessage,
|
||||
priority: -100,
|
||||
)
|
||||
priority: messageHandlerPriority,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<bool> isSupported() async => true;
|
||||
|
||||
Future<StanzaHandlerData> _onMessage(
|
||||
Stanza _,
|
||||
Stanza stanza,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final message = state.stanza;
|
||||
final body = message.firstTag('body');
|
||||
|
||||
final hints = List<MessageProcessingHint>.empty(growable: true);
|
||||
for (final element
|
||||
in message.findTagsByXmlns(messageProcessingHintsXmlns)) {
|
||||
hints.add(messageProcessingHintFromXml(element));
|
||||
final body = stanza.firstTag('body');
|
||||
if (body != null) {
|
||||
state.extensions.set(
|
||||
MessageBodyData(body.innerText()),
|
||||
);
|
||||
}
|
||||
|
||||
getAttributes().sendEvent(
|
||||
MessageEvent(
|
||||
body: body != null ? body.innerText() : '',
|
||||
fromJid: JID.fromString(message.attributes['from']! as String),
|
||||
toJid: JID.fromString(message.attributes['to']! as String),
|
||||
sid: message.attributes['id']! as String,
|
||||
stanzaId: state.stableId ?? const StableStanzaId(),
|
||||
isCarbon: state.isCarbon,
|
||||
deliveryReceiptRequested: state.deliveryReceiptRequested,
|
||||
isMarkable: state.isMarkable,
|
||||
type: message.attributes['type'] as String?,
|
||||
oob: state.oob,
|
||||
sfs: state.sfs,
|
||||
sims: state.sims,
|
||||
reply: state.reply,
|
||||
chatState: state.chatState,
|
||||
fun: state.fun,
|
||||
funReplacement: state.funReplacement,
|
||||
funCancellation: state.funCancellation,
|
||||
encrypted: state.encrypted,
|
||||
messageRetraction: state.messageRetraction,
|
||||
messageCorrectionId: state.lastMessageCorrectionSid,
|
||||
messageReactions: state.messageReactions,
|
||||
messageProcessingHints: hints.isEmpty ? null : hints,
|
||||
stickerPackId: state.stickerPackId,
|
||||
other: state.other,
|
||||
error: StanzaError.fromStanza(message),
|
||||
JID.fromString(state.stanza.attributes['from']! as String),
|
||||
JID.fromString(state.stanza.attributes['to']! as String),
|
||||
state.encrypted,
|
||||
state.extensions,
|
||||
id: state.stanza.attributes['id'] as String?,
|
||||
type: state.stanza.attributes['type'] as String?,
|
||||
error: StanzaError.fromStanza(state.stanza),
|
||||
encryptionError: state.encryptionError,
|
||||
),
|
||||
);
|
||||
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
/// Send a message to to with the content body. If deliveryRequest is true, then
|
||||
/// the message will also request a delivery receipt from the receiver.
|
||||
/// If id is non-null, then it will be the id of the message stanza.
|
||||
/// 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.
|
||||
void sendMessage(MessageDetails details) {
|
||||
assert(
|
||||
implies(
|
||||
details.quoteBody != null,
|
||||
details.quoteFrom != null && details.quoteId != null,
|
||||
/// Send an unawaitable message to [to]. [extensions] is a typed map that contains
|
||||
/// data for building the message.
|
||||
Future<void> sendMessage(
|
||||
JID to,
|
||||
TypedMap<StanzaHandlerExtension> extensions, {
|
||||
String type = 'chat',
|
||||
}) async {
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.message(
|
||||
to: to.toString(),
|
||||
id: extensions.get<MessageIdData>()?.id,
|
||||
type: type,
|
||||
children: _messageSendingCallbacks
|
||||
.map((c) => c(extensions))
|
||||
.flattened
|
||||
.toList(),
|
||||
),
|
||||
extensions: extensions,
|
||||
awaitable: false,
|
||||
),
|
||||
'When quoting a message, then quoteFrom and quoteId must also be non-null',
|
||||
);
|
||||
}
|
||||
|
||||
final stanza = Stanza.message(
|
||||
to: details.to,
|
||||
type: 'chat',
|
||||
id: details.id,
|
||||
children: [],
|
||||
);
|
||||
|
||||
if (details.quoteBody != null) {
|
||||
final quote = QuoteData.fromBodies(details.quoteBody!, details.body!);
|
||||
|
||||
stanza
|
||||
..addChild(
|
||||
XMLNode(tag: 'body', text: quote.body),
|
||||
)
|
||||
..addChild(
|
||||
XMLNode.xmlns(
|
||||
tag: 'reply',
|
||||
xmlns: replyXmlns,
|
||||
attributes: {'to': details.quoteFrom!, 'id': details.quoteId!},
|
||||
),
|
||||
)
|
||||
..addChild(
|
||||
XMLNode.xmlns(
|
||||
tag: 'fallback',
|
||||
xmlns: fallbackXmlns,
|
||||
attributes: {'for': replyXmlns},
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'body',
|
||||
attributes: <String, String>{
|
||||
'start': '0',
|
||||
'end': '${quote.fallbackLength}',
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
var body = details.body;
|
||||
if (details.sfs != null && details.setOOBFallbackBody) {
|
||||
// TODO(Unknown): Maybe find a better solution
|
||||
final firstSource = details.sfs!.sources.first;
|
||||
if (firstSource is StatelessFileSharingUrlSource) {
|
||||
body = firstSource.url;
|
||||
} else if (firstSource is StatelessFileSharingEncryptedSource) {
|
||||
body = firstSource.source.url;
|
||||
}
|
||||
} else if (details.messageRetraction?.fallback != null) {
|
||||
body = details.messageRetraction!.fallback;
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
stanza.addChild(
|
||||
XMLNode(tag: 'body', text: body),
|
||||
);
|
||||
}
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
if (extensions.get<ReplyData>() != null) {
|
||||
return [];
|
||||
}
|
||||
if (extensions.get<StickersData>() != null) {
|
||||
return [];
|
||||
}
|
||||
if (extensions.get<StatelessFileSharingData>() != null) {
|
||||
return [];
|
||||
}
|
||||
if (extensions.get<OOBData>() != null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (details.requestDeliveryReceipt) {
|
||||
stanza.addChild(makeMessageDeliveryRequest());
|
||||
}
|
||||
if (details.requestChatMarkers) {
|
||||
stanza.addChild(makeChatMarkerMarkable());
|
||||
}
|
||||
if (details.originId != null) {
|
||||
stanza.addChild(makeOriginIdElement(details.originId!));
|
||||
}
|
||||
final data = extensions.get<MessageBodyData>();
|
||||
return data != null ? [data.toXML()] : [];
|
||||
}
|
||||
|
||||
if (details.sfs != null) {
|
||||
stanza.addChild(details.sfs!.toXML());
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
final source = details.sfs!.sources.first;
|
||||
if (source is StatelessFileSharingUrlSource &&
|
||||
details.setOOBFallbackBody) {
|
||||
// SFS recommends OOB as a fallback
|
||||
stanza.addChild(constructOOBNode(OOBData(url: source.url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (details.chatState != null) {
|
||||
stanza.addChild(
|
||||
// TODO(Unknown): Move this into xep_0085.dart
|
||||
XMLNode.xmlns(
|
||||
tag: chatStateToString(details.chatState!),
|
||||
xmlns: chatStateXmlns,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (details.fun != null) {
|
||||
stanza.addChild(
|
||||
XMLNode.xmlns(
|
||||
tag: 'file-upload',
|
||||
xmlns: fileUploadNotificationXmlns,
|
||||
children: [
|
||||
details.fun!.toXML(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (details.funReplacement != null) {
|
||||
stanza.addChild(
|
||||
XMLNode.xmlns(
|
||||
tag: 'replaces',
|
||||
xmlns: fileUploadNotificationXmlns,
|
||||
attributes: <String, String>{
|
||||
'id': details.funReplacement!,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (details.messageRetraction != null) {
|
||||
stanza.addChild(
|
||||
XMLNode.xmlns(
|
||||
tag: 'apply-to',
|
||||
xmlns: fasteningXmlns,
|
||||
attributes: <String, String>{
|
||||
'id': details.messageRetraction!.id,
|
||||
},
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'retract',
|
||||
xmlns: messageRetractionXmlns,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (details.messageRetraction!.fallback != null) {
|
||||
stanza.addChild(
|
||||
XMLNode.xmlns(
|
||||
tag: 'fallback',
|
||||
xmlns: fallbackIndicationXmlns,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (details.lastMessageCorrectionId != null) {
|
||||
stanza.addChild(
|
||||
makeLastMessageCorrectionEdit(
|
||||
details.lastMessageCorrectionId!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (details.messageReactions != null) {
|
||||
stanza.addChild(details.messageReactions!.toXml());
|
||||
}
|
||||
|
||||
if (details.messageProcessingHints != null) {
|
||||
for (final hint in details.messageProcessingHints!) {
|
||||
stanza.addChild(hint.toXml());
|
||||
}
|
||||
}
|
||||
|
||||
if (details.stickerPackId != null) {
|
||||
stanza.addChild(
|
||||
XMLNode.xmlns(
|
||||
tag: 'sticker',
|
||||
xmlns: stickersXmlns,
|
||||
attributes: {
|
||||
'pack': details.stickerPackId!,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getAttributes().sendStanza(stanza, awaitable: false);
|
||||
// Register the sending callback
|
||||
registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ const fullStanzaXmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas';
|
||||
// RFC 6121
|
||||
const rosterXmlns = 'jabber:iq:roster';
|
||||
const rosterVersioningXmlns = 'urn:xmpp:features:rosterver';
|
||||
const subscriptionPreApprovalXmlns = 'urn:xmpp:features:pre-approval';
|
||||
|
||||
// XEP-0004
|
||||
const dataFormsXmlns = 'jabber:x:data';
|
||||
const formVarFormType = 'FORM_TYPE';
|
||||
|
||||
// XEP-0030
|
||||
const discoInfoXmlns = 'http://jabber.org/protocol/disco#info';
|
||||
@@ -20,6 +22,11 @@ const discoItemsXmlns = 'http://jabber.org/protocol/disco#items';
|
||||
// XEP-0033
|
||||
const extendedAddressingXmlns = 'http://jabber.org/protocol/address';
|
||||
|
||||
// XEP-0045
|
||||
const mucXmlns = 'http://jabber.org/protocol/muc';
|
||||
const mucUserXmlns = 'http://jabber.org/protocol/muc#user';
|
||||
const roomInfoFormType = 'http://jabber.org/protocol/muc#roominfo';
|
||||
|
||||
// XEP-0054
|
||||
const vCardTempXmlns = 'vcard-temp';
|
||||
const vCardTempUpdate = 'vcard-temp:x:update';
|
||||
@@ -65,6 +72,9 @@ const delayedDeliveryXmlns = 'urn:xmpp:delay';
|
||||
// XEP-0234
|
||||
const jingleFileTransferXmlns = 'urn:xmpp:jingle:apps:file-transfer:5';
|
||||
|
||||
// XEP-0264
|
||||
const jingleContentThumbnailXmlns = 'urn:xmpp:thumbs:1';
|
||||
|
||||
// XEP-0280
|
||||
const carbonsXmlns = 'urn:xmpp:carbons:2';
|
||||
|
||||
@@ -96,7 +106,7 @@ const httpFileUploadXmlns = 'urn:xmpp:http:upload:0';
|
||||
// XEP-0372
|
||||
const referenceXmlns = 'urn:xmpp:reference:0';
|
||||
|
||||
// XEP-380
|
||||
// XEP-0380
|
||||
const emeXmlns = 'urn:xmpp:eme:0';
|
||||
const emeOtr = 'urn:xmpp:otr:0';
|
||||
const emeLegacyOpenPGP = 'jabber:x:encrypted';
|
||||
@@ -122,6 +132,9 @@ const sasl2Xmlns = 'urn:xmpp:sasl:2';
|
||||
// XEP-0420
|
||||
const sceXmlns = 'urn:xmpp:sce:1';
|
||||
|
||||
// XEP-0421
|
||||
const occupantIdXmlns = 'urn:xmpp:occupant-id:0';
|
||||
|
||||
// XEP-0422
|
||||
const fasteningXmlns = 'urn:xmpp:fasten:0';
|
||||
|
||||
@@ -153,7 +166,6 @@ const stickersXmlns = 'urn:xmpp:stickers:0';
|
||||
|
||||
// XEP-0461
|
||||
const replyXmlns = 'urn:xmpp:reply:0';
|
||||
const fallbackXmlns = 'urn:xmpp:feature-fallback:0';
|
||||
|
||||
// ???
|
||||
const urlDataXmlns = 'http://jabber.org/protocol/url-data';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -11,3 +11,4 @@ const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2';
|
||||
const bind2Negotiator = 'org.moxxmpp.bind2';
|
||||
const saslFASTNegotiator = 'org.moxxmpp.sasl.fast';
|
||||
const carbonsNegotiator = 'org.moxxmpp.bind2.carbons';
|
||||
const presenceNegotiator = 'org.moxxmpp.core.presence';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/connection.dart';
|
||||
@@ -8,7 +9,6 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/settings.dart';
|
||||
import 'package:moxxmpp/src/socket.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
|
||||
/// The state a negotiator is currently in
|
||||
enum NegotiatorState {
|
||||
@@ -117,8 +117,7 @@ abstract class XmppFeatureNegotiatorBase {
|
||||
/// Returns true if a feature in [features], which are the children of the
|
||||
/// <stream:features /> nonza, can be negotiated. Otherwise, returns false.
|
||||
bool matchesFeature(List<XMLNode> features) {
|
||||
return firstWhereOrNull(
|
||||
features,
|
||||
return features.firstWhereOrNull(
|
||||
(XMLNode feature) => feature.attributes['xmlns'] == negotiatingXmlns,
|
||||
) !=
|
||||
null;
|
||||
|
||||
@@ -57,9 +57,10 @@ class _ChunkedConversionBuffer<S, T> {
|
||||
}
|
||||
|
||||
/// A buffer to put between a socket's input and a full XML stream.
|
||||
class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
|
||||
final StreamController<XMPPStreamObject> _streamController =
|
||||
StreamController<XMPPStreamObject>();
|
||||
class XMPPStreamParser
|
||||
extends StreamTransformerBase<String, List<XMPPStreamObject>> {
|
||||
final StreamController<List<XMPPStreamObject>> _streamController =
|
||||
StreamController<List<XMPPStreamObject>>();
|
||||
|
||||
/// Turns a String into a list of [XmlEvent]s in a chunked fashion.
|
||||
_ChunkedConversionBuffer<String, XmlEvent> _eventBuffer =
|
||||
@@ -117,13 +118,14 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<XMPPStreamObject> bind(Stream<String> stream) {
|
||||
Stream<List<XMPPStreamObject>> bind(Stream<String> stream) {
|
||||
// We do not want to use xml's toXmlEvents and toSubtreeEvents methods as they
|
||||
// create streams we cannot close. We need to be able to destroy and recreate an
|
||||
// XML parser whenever we start a new connection.
|
||||
stream.listen((input) {
|
||||
final events = _eventBuffer.convert(input);
|
||||
final streamHeaderEvents = _streamHeaderSelector.convert(events);
|
||||
final objects = List<XMPPStreamObject>.empty(growable: true);
|
||||
|
||||
// Process the stream header separately.
|
||||
for (final event in streamHeaderEvents) {
|
||||
@@ -135,7 +137,7 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
|
||||
continue;
|
||||
}
|
||||
|
||||
_streamController.add(
|
||||
objects.add(
|
||||
XMPPStreamHeader(
|
||||
Map<String, String>.fromEntries(
|
||||
event.attributes.map((attr) {
|
||||
@@ -151,13 +153,15 @@ class XMPPStreamParser extends StreamTransformerBase<String, XMPPStreamObject> {
|
||||
final children = _childBuffer.convert(childEvents);
|
||||
for (final node in children) {
|
||||
if (node.nodeType == XmlNodeType.ELEMENT) {
|
||||
_streamController.add(
|
||||
objects.add(
|
||||
XMPPStreamElement(
|
||||
XMLNode.fromXmlElement(node as XmlElement),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_streamController.add(objects);
|
||||
});
|
||||
|
||||
return _streamController.stream;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:moxxmpp/src/connection.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
@@ -8,15 +8,44 @@ import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
|
||||
|
||||
/// A function that will be called when presence, outside of subscription request
|
||||
/// management, will be sent. Useful for managers that want to add [XMLNode]s to said
|
||||
/// presence.
|
||||
typedef PresencePreSendCallback = Future<List<XMLNode>> Function();
|
||||
|
||||
/// A pseudo-negotiator that does not really negotiate anything. Instead, its purpose
|
||||
/// is to look for a stream feature indicating that we can pre-approve subscription
|
||||
/// requests, shown by [PresenceNegotiator.preApprovalSupported].
|
||||
class PresenceNegotiator extends XmppFeatureNegotiatorBase {
|
||||
PresenceNegotiator()
|
||||
: super(11, false, subscriptionPreApprovalXmlns, presenceNegotiator);
|
||||
|
||||
/// Flag indicating whether presence subscription pre-approval is supported
|
||||
bool _supported = false;
|
||||
bool get preApprovalSupported => _supported;
|
||||
|
||||
@override
|
||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||
XMLNode nonza,
|
||||
) async {
|
||||
_supported = true;
|
||||
return const Result(NegotiatorState.done);
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_supported = false;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// A mandatory manager that handles initial presence sending, sending of subscription
|
||||
/// request management requests and triggers events for incoming presence stanzas.
|
||||
class PresenceManager extends XmppManagerBase {
|
||||
@@ -26,12 +55,18 @@ class PresenceManager extends XmppManagerBase {
|
||||
final List<PresencePreSendCallback> _presenceCallbacks =
|
||||
List.empty(growable: true);
|
||||
|
||||
/// The priority of the presence handler. If a handler should run before this one,
|
||||
/// which terminates processing, make sure the handler has a priority greater than
|
||||
/// [presenceHandlerPriority].
|
||||
static int presenceHandlerPriority = -100;
|
||||
|
||||
@override
|
||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||
StanzaHandler(
|
||||
stanzaTag: 'presence',
|
||||
callback: _onPresence,
|
||||
)
|
||||
priority: presenceHandlerPriority,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -49,12 +84,8 @@ class PresenceManager extends XmppManagerBase {
|
||||
Future<void> onXmppEvent(XmppEvent event) async {
|
||||
if (event is StreamNegotiationsDoneEvent) {
|
||||
// Send initial presence only when we have not resumed the stream
|
||||
final sm = getAttributes().getNegotiatorById<StreamManagementNegotiator>(
|
||||
streamManagementNegotiator,
|
||||
);
|
||||
final isResumed = sm?.isResumed ?? false;
|
||||
if (!isResumed) {
|
||||
unawaited(sendInitialPresence());
|
||||
if (!event.resumed) {
|
||||
await sendInitialPresence();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +104,7 @@ class PresenceManager extends XmppManagerBase {
|
||||
from: JID.fromString(presence.from!),
|
||||
),
|
||||
);
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
@@ -82,10 +113,7 @@ class PresenceManager extends XmppManagerBase {
|
||||
if (presence.from != null) {
|
||||
logger.finest("Received presence from '${presence.from}'");
|
||||
|
||||
getAttributes().sendEvent(
|
||||
PresenceReceivedEvent(JID.fromString(presence.from!), presence),
|
||||
);
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
return state;
|
||||
@@ -108,66 +136,111 @@ class PresenceManager extends XmppManagerBase {
|
||||
|
||||
final attrs = getAttributes();
|
||||
await attrs.sendStanza(
|
||||
Stanza.presence(
|
||||
from: attrs.getFullJID().toString(),
|
||||
children: children,
|
||||
StanzaDetails(
|
||||
Stanza.presence(
|
||||
children: children,
|
||||
),
|
||||
awaitable: false,
|
||||
addId: false,
|
||||
),
|
||||
awaitable: false,
|
||||
addId: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Send an unavailable presence with no 'to' attribute.
|
||||
void sendUnavailablePresence() {
|
||||
getAttributes().sendStanza(
|
||||
Stanza.presence(
|
||||
type: 'unavailable',
|
||||
Future<void> sendUnavailablePresence() async {
|
||||
// Bypass the queue so that this get's sent immediately.
|
||||
// If we do it like this, we can also block the disconnection
|
||||
// until we're actually ready.
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.presence(
|
||||
type: 'unavailable',
|
||||
),
|
||||
awaitable: false,
|
||||
bypassQueue: true,
|
||||
postSendExtensions: TypedMap<StanzaHandlerExtension>.fromList([
|
||||
const StreamManagementData(true, null),
|
||||
]),
|
||||
),
|
||||
addFrom: StanzaFromType.full,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends a subscription request to [to].
|
||||
void sendSubscriptionRequest(String to) {
|
||||
getAttributes().sendStanza(
|
||||
Stanza.presence(
|
||||
type: 'subscribe',
|
||||
to: to,
|
||||
/// Similar to [requestSubscription], but it also tells the server to automatically
|
||||
/// accept a subscription request from [to], should it arrive.
|
||||
/// This requires a [PresenceNegotiator] to be registered as this feature is optional.
|
||||
///
|
||||
/// Returns true, when the stanza was sent. Returns false, when the stanza was not sent,
|
||||
/// for example because the server does not support subscription pre-approvals.
|
||||
Future<bool> preApproveSubscription(JID to) async {
|
||||
final negotiator = getAttributes()
|
||||
.getNegotiatorById<PresenceNegotiator>(presenceNegotiator);
|
||||
assert(negotiator != null, 'No PresenceNegotiator registered');
|
||||
|
||||
if (!negotiator!.preApprovalSupported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.presence(
|
||||
type: 'subscribed',
|
||||
to: to.toString(),
|
||||
),
|
||||
awaitable: false,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Sends a subscription request to [to].
|
||||
Future<void> requestSubscription(JID to) async {
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.presence(
|
||||
type: 'subscribe',
|
||||
to: to.toString(),
|
||||
),
|
||||
awaitable: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Accept a subscription request from [to].
|
||||
Future<void> acceptSubscriptionRequest(JID to) async {
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.presence(
|
||||
type: 'subscribed',
|
||||
to: to.toString(),
|
||||
),
|
||||
awaitable: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Send a subscription request rejection to [to].
|
||||
Future<void> rejectSubscriptionRequest(JID to) async {
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.presence(
|
||||
type: 'unsubscribed',
|
||||
to: to.toString(),
|
||||
),
|
||||
awaitable: false,
|
||||
),
|
||||
addFrom: StanzaFromType.none,
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends an unsubscription request to [to].
|
||||
void sendUnsubscriptionRequest(String to) {
|
||||
getAttributes().sendStanza(
|
||||
Stanza.presence(
|
||||
type: 'unsubscribe',
|
||||
to: to,
|
||||
Future<void> unsubscribe(JID to) async {
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.presence(
|
||||
type: 'unsubscribe',
|
||||
to: to.toString(),
|
||||
),
|
||||
awaitable: false,
|
||||
),
|
||||
addFrom: StanzaFromType.none,
|
||||
);
|
||||
}
|
||||
|
||||
/// Accept a presence subscription request for [to].
|
||||
void sendSubscriptionRequestApproval(String to) {
|
||||
getAttributes().sendStanza(
|
||||
Stanza.presence(
|
||||
type: 'subscribed',
|
||||
to: to,
|
||||
),
|
||||
addFrom: StanzaFromType.none,
|
||||
);
|
||||
}
|
||||
|
||||
/// Reject a presence subscription request for [to].
|
||||
void sendSubscriptionRequestRejection(String to) {
|
||||
getAttributes().sendStanza(
|
||||
Stanza.presence(
|
||||
type: 'unsubscribed',
|
||||
to: to,
|
||||
),
|
||||
addFrom: StanzaFromType.none,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
@@ -13,15 +13,13 @@ abstract class SaslNegotiator extends XmppFeatureNegotiatorBase {
|
||||
@override
|
||||
bool matchesFeature(List<XMLNode> features) {
|
||||
// Is SASL advertised?
|
||||
final mechanisms = firstWhereOrNull(
|
||||
features,
|
||||
final mechanisms = features.firstWhereOrNull(
|
||||
(XMLNode feature) => feature.attributes['xmlns'] == saslXmlns,
|
||||
);
|
||||
if (mechanisms == null) return false;
|
||||
|
||||
// Is SASL PLAIN advertised?
|
||||
return firstWhereOrNull(
|
||||
mechanisms.children,
|
||||
return mechanisms.children.firstWhereOrNull(
|
||||
(XMLNode mechanism) => mechanism.text == mechanismName,
|
||||
) !=
|
||||
null;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'dart:convert';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
import 'package:saslprep/saslprep.dart';
|
||||
@@ -96,7 +96,9 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
|
||||
|
||||
@override
|
||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
|
||||
state = NegotiatorState.done;
|
||||
if (pickedForSasl2) {
|
||||
state = NegotiatorState.done;
|
||||
}
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:math' show Random;
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
@@ -10,7 +11,6 @@ import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
import 'package:random_string/random_string.dart';
|
||||
@@ -246,6 +246,9 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
|
||||
bool _checkSignature(String base64Signature) {
|
||||
final signature =
|
||||
parseKeyValue(utf8.decode(base64.decode(base64Signature)));
|
||||
_log.finest(
|
||||
'Expecting signature: "$_serverSignature", got: "${signature["v"]}"',
|
||||
);
|
||||
return signature['v']! == _serverSignature;
|
||||
}
|
||||
|
||||
@@ -360,6 +363,11 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
|
||||
|
||||
@override
|
||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
|
||||
// Don't do anything if we have not been picked for SASL2.
|
||||
if (!pickedForSasl2) {
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
// When we're done with SASL2, check the additional data to verify the server
|
||||
// signature.
|
||||
state = NegotiatorState.done;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
|
||||
enum _StartTlsState { ready, requested }
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/attributes.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
@@ -14,7 +15,6 @@ import 'package:moxxmpp/src/roster/errors.dart';
|
||||
import 'package:moxxmpp/src/roster/state.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
|
||||
@immutable
|
||||
class XmppRosterItem {
|
||||
@@ -122,7 +122,7 @@ class RosterManager extends XmppManagerBase {
|
||||
tagName: 'query',
|
||||
tagXmlns: rosterXmlns,
|
||||
callback: _onRosterPush,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -145,7 +145,7 @@ class RosterManager extends XmppManagerBase {
|
||||
logger.warning(
|
||||
'Roster push invalid! Unexpected from attribute: ${stanza.toXml()}',
|
||||
);
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
final query = stanza.firstTag('query', xmlns: rosterXmlns)!;
|
||||
@@ -154,7 +154,7 @@ class RosterManager extends XmppManagerBase {
|
||||
|
||||
if (item == null) {
|
||||
logger.warning('Received empty roster push');
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
unawaited(
|
||||
@@ -177,13 +177,23 @@ class RosterManager extends XmppManagerBase {
|
||||
[],
|
||||
);
|
||||
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
/// Shared code between requesting rosters without and with roster versioning, if
|
||||
/// the server deems a regular roster response more efficient than n roster pushes.
|
||||
///
|
||||
/// [query] is the <query /> child of the iq, if available.
|
||||
///
|
||||
/// If roster versioning was used, then [requestedRosterVersion] is the version
|
||||
/// we requested the roster with.
|
||||
///
|
||||
/// Note that if roster versioning is used and the server returns us an empty iq,
|
||||
/// it means that the roster did not change since the last version. In that case,
|
||||
/// we do nothing and just return. The roster state manager will not be notified.
|
||||
Future<Result<RosterRequestResult, RosterError>> _handleRosterResponse(
|
||||
XMLNode? query,
|
||||
String? requestedRosterVersion,
|
||||
) async {
|
||||
final List<XmppRosterItem> items;
|
||||
String? rosterVersion;
|
||||
@@ -204,6 +214,14 @@ class RosterManager extends XmppManagerBase {
|
||||
.toList();
|
||||
|
||||
rosterVersion = query.attributes['ver'] as String?;
|
||||
} else if (requestedRosterVersion != null) {
|
||||
// Skip the handleRosterFetch call since nothing changed.
|
||||
return Result(
|
||||
RosterRequestResult(
|
||||
[],
|
||||
requestedRosterVersion,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
logger.warning(
|
||||
'Server response to roster request without roster versioning does not contain a <query /> element, while the type is not error. This violates RFC6121',
|
||||
@@ -223,26 +241,34 @@ class RosterManager extends XmppManagerBase {
|
||||
return Result(result);
|
||||
}
|
||||
|
||||
/// Requests the roster following RFC 6121.
|
||||
Future<Result<RosterRequestResult, RosterError>> requestRoster() async {
|
||||
/// Requests the roster following RFC 6121. If [useRosterVersion] is set to false, then
|
||||
/// roster versioning will not be used, even if the server supports it and we have a last
|
||||
/// known roster version.
|
||||
Future<Result<RosterRequestResult, RosterError>> requestRoster({
|
||||
bool useRosterVersion = true,
|
||||
}) async {
|
||||
final attrs = getAttributes();
|
||||
final query = XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: rosterXmlns,
|
||||
);
|
||||
final rosterVersion = await _stateManager.getRosterVersion();
|
||||
if (rosterVersion != null && rosterVersioningAvailable()) {
|
||||
if (rosterVersion != null &&
|
||||
rosterVersioningAvailable() &&
|
||||
useRosterVersion) {
|
||||
query.attributes['ver'] = rosterVersion;
|
||||
}
|
||||
|
||||
final response = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
children: [
|
||||
query,
|
||||
],
|
||||
final response = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
children: [
|
||||
query,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
if (response.attributes['type'] != 'result') {
|
||||
logger.warning('Error requesting roster: ${response.toXml()}');
|
||||
@@ -250,7 +276,7 @@ class RosterManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
final responseQuery = response.firstTag('query', xmlns: rosterXmlns);
|
||||
return _handleRosterResponse(responseQuery);
|
||||
return _handleRosterResponse(responseQuery, rosterVersion);
|
||||
}
|
||||
|
||||
/// Requests a series of roster pushes according to RFC6121. Requires that the server
|
||||
@@ -258,20 +284,23 @@ class RosterManager extends XmppManagerBase {
|
||||
Future<Result<RosterRequestResult?, RosterError>>
|
||||
requestRosterPushes() async {
|
||||
final attrs = getAttributes();
|
||||
final result = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: rosterXmlns,
|
||||
attributes: {
|
||||
'ver': await _stateManager.getRosterVersion() ?? '',
|
||||
},
|
||||
)
|
||||
],
|
||||
final rosterVersion = await _stateManager.getRosterVersion();
|
||||
final result = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: rosterXmlns,
|
||||
attributes: {
|
||||
'ver': rosterVersion ?? '',
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
if (result.attributes['type'] != 'result') {
|
||||
logger.warning('Requesting roster pushes failed: ${result.toXml()}');
|
||||
@@ -279,7 +308,7 @@ class RosterManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
final query = result.firstTag('query', xmlns: rosterXmlns);
|
||||
return _handleRosterResponse(query);
|
||||
return _handleRosterResponse(query, rosterVersion);
|
||||
}
|
||||
|
||||
bool rosterVersioningAvailable() {
|
||||
@@ -296,31 +325,31 @@ class RosterManager extends XmppManagerBase {
|
||||
List<String>? groups,
|
||||
}) async {
|
||||
final attrs = getAttributes();
|
||||
final response = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: rosterXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: <String, String>{
|
||||
'jid': jid,
|
||||
...title == jid.split('@')[0]
|
||||
? <String, String>{}
|
||||
: <String, String>{'name': title}
|
||||
},
|
||||
children: (groups ?? [])
|
||||
.map((group) => XMLNode(tag: 'group', text: group))
|
||||
.toList(),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
final response = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: rosterXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: <String, String>{
|
||||
'jid': jid,
|
||||
if (title == jid.split('@')[0]) 'name': title,
|
||||
},
|
||||
children: (groups ?? [])
|
||||
.map((group) => XMLNode(tag: 'group', text: group))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
if (response.attributes['type'] != 'result') {
|
||||
logger.severe('Error adding $jid to roster: $response');
|
||||
@@ -334,26 +363,28 @@ class RosterManager extends XmppManagerBase {
|
||||
/// false otherwise.
|
||||
Future<RosterRemovalResult> removeFromRoster(String jid) async {
|
||||
final attrs = getAttributes();
|
||||
final response = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: rosterXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: <String, String>{
|
||||
'jid': jid,
|
||||
'subscription': 'remove'
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
final response = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: rosterXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: {
|
||||
'jid': jid,
|
||||
'subscription': 'remove',
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
if (response.attributes['type'] != 'result') {
|
||||
logger.severe('Failed to remove roster item: ${response.toXml()}');
|
||||
|
||||
@@ -1,28 +1,109 @@
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
/// A simple description of the <error /> element that may be inside a stanza
|
||||
class StanzaError {
|
||||
StanzaError(this.type, this.error);
|
||||
String type;
|
||||
String error;
|
||||
/// A description of a stanza to send.
|
||||
class StanzaDetails {
|
||||
const StanzaDetails(
|
||||
this.stanza, {
|
||||
this.extensions,
|
||||
this.addId = true,
|
||||
this.awaitable = true,
|
||||
this.shouldEncrypt = true,
|
||||
this.encrypted = false,
|
||||
this.forceEncryption = false,
|
||||
this.bypassQueue = false,
|
||||
this.postSendExtensions,
|
||||
});
|
||||
|
||||
/// The stanza to send.
|
||||
final Stanza stanza;
|
||||
|
||||
/// The extension data used for constructing the stanza.
|
||||
final TypedMap<StanzaHandlerExtension>? extensions;
|
||||
|
||||
/// Flag indicating whether a stanza id should be added before sending.
|
||||
final bool addId;
|
||||
|
||||
/// Track the stanza to allow awaiting its response.
|
||||
final bool awaitable;
|
||||
|
||||
final bool forceEncryption;
|
||||
|
||||
/// Flag indicating whether the stanza that is sent is already encrypted (true)
|
||||
/// or not (false). This is only useful for E2EE implementations that have to
|
||||
/// send heartbeats that must bypass themselves.
|
||||
final bool encrypted;
|
||||
|
||||
/// Tells an E2EE implementation, if available, to encrypt the stanza (true) or
|
||||
/// ignore the stanza (false).
|
||||
final bool shouldEncrypt;
|
||||
|
||||
/// Bypasses being put into the queue. Useful for sending stanzas that must go out
|
||||
/// now, where it's okay if it does not get sent.
|
||||
/// This should never have to be set to true.
|
||||
final bool bypassQueue;
|
||||
|
||||
/// This makes the Stream Management implementation, when available, ignore the stanza,
|
||||
/// meaning that it gets counted but excluded from resending.
|
||||
/// This should never have to be set to true.
|
||||
final TypedMap<StanzaHandlerExtension>? postSendExtensions;
|
||||
}
|
||||
|
||||
/// A general error type for errors.
|
||||
abstract class StanzaError {
|
||||
static StanzaError? fromXMLNode(XMLNode node) {
|
||||
final error = node.firstTag('error');
|
||||
if (error == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final specificError = error.firstTagByXmlns(fullStanzaXmlns);
|
||||
if (specificError == null) {
|
||||
return UnknownStanzaError();
|
||||
}
|
||||
|
||||
switch (specificError.tag) {
|
||||
case RemoteServerNotFoundError.tag:
|
||||
return RemoteServerNotFoundError();
|
||||
case RemoteServerTimeoutError.tag:
|
||||
return RemoteServerTimeoutError();
|
||||
case ServiceUnavailableError.tag:
|
||||
return ServiceUnavailableError();
|
||||
}
|
||||
|
||||
return UnknownStanzaError();
|
||||
}
|
||||
|
||||
/// Returns a StanzaError if [stanza] contains a <error /> element. If not, returns
|
||||
/// null.
|
||||
static StanzaError? fromStanza(Stanza stanza) {
|
||||
final error = stanza.firstTag('error');
|
||||
if (error == null) return null;
|
||||
|
||||
final stanzaError = error.firstTagByXmlns(fullStanzaXmlns);
|
||||
if (stanzaError == null) return null;
|
||||
|
||||
return StanzaError(
|
||||
error.attributes['type']! as String,
|
||||
stanzaError.tag,
|
||||
);
|
||||
return fromXMLNode(stanza);
|
||||
}
|
||||
}
|
||||
|
||||
/// Recipient does not provide a given service.
|
||||
/// https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-service-unavailable
|
||||
class ServiceUnavailableError extends StanzaError {
|
||||
static const tag = 'service-unavailable';
|
||||
}
|
||||
|
||||
/// Could not connect to the remote server.
|
||||
/// https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-remote-server-not-found
|
||||
class RemoteServerNotFoundError extends StanzaError {
|
||||
static const tag = 'remote-server-not-found';
|
||||
}
|
||||
|
||||
/// The connection to the remote server timed out.
|
||||
/// https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions-remote-server-timeout
|
||||
class RemoteServerTimeoutError extends StanzaError {
|
||||
static const tag = 'remote-server-timeout';
|
||||
}
|
||||
|
||||
/// An unknown error.
|
||||
class UnknownStanzaError extends StanzaError {}
|
||||
|
||||
const _stanzaNotDefined = Object();
|
||||
|
||||
class Stanza extends XMLNode {
|
||||
// ignore: use_super_parameters
|
||||
Stanza({
|
||||
@@ -137,7 +218,7 @@ class Stanza extends XMLNode {
|
||||
|
||||
Stanza copyWith({
|
||||
String? id,
|
||||
String? from,
|
||||
Object? from = _stanzaNotDefined,
|
||||
String? to,
|
||||
String? type,
|
||||
List<XMLNode>? children,
|
||||
@@ -146,7 +227,7 @@ class Stanza extends XMLNode {
|
||||
return Stanza(
|
||||
tag: tag,
|
||||
to: to ?? this.to,
|
||||
from: from ?? this.from,
|
||||
from: from != _stanzaNotDefined ? from as String? : this.from,
|
||||
id: id ?? this.id,
|
||||
type: type ?? this.type,
|
||||
children: children ?? this.children,
|
||||
@@ -169,15 +250,14 @@ XMLNode buildErrorElement(String type, String condition, {String? text}) {
|
||||
XMLNode.xmlns(
|
||||
tag: condition,
|
||||
xmlns: fullStanzaXmlns,
|
||||
children: text != null
|
||||
? [
|
||||
XMLNode.xmlns(
|
||||
tag: 'text',
|
||||
xmlns: fullStanzaXmlns,
|
||||
text: text,
|
||||
)
|
||||
]
|
||||
: [],
|
||||
children: [
|
||||
if (text != null)
|
||||
XMLNode.xmlns(
|
||||
tag: 'text',
|
||||
xmlns: fullStanzaXmlns,
|
||||
text: text,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
class Result<T, V> {
|
||||
const Result(this._data)
|
||||
: assert(
|
||||
_data is T || _data is V,
|
||||
'Invalid data type: Must be either $T or $V',
|
||||
);
|
||||
final dynamic _data;
|
||||
|
||||
bool isType<S>() => _data is S;
|
||||
|
||||
S get<S>() {
|
||||
assert(_data is S, 'Data is not $S');
|
||||
|
||||
return _data as S;
|
||||
}
|
||||
}
|
||||
97
packages/moxxmpp/lib/src/util/incoming_queue.dart
Normal file
97
packages/moxxmpp/lib/src/util/incoming_queue.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/src/awaiter.dart';
|
||||
import 'package:moxxmpp/src/parser.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
/// A queue for incoming [XMPPStreamObject]s to ensure "in order"
|
||||
/// processing (except for stanzas that are awaited).
|
||||
class IncomingStanzaQueue {
|
||||
IncomingStanzaQueue(this._callback, this._stanzaAwaiter);
|
||||
|
||||
/// The queue for storing the completer of each
|
||||
/// incoming stanza (or stream object to be precise).
|
||||
/// Only access while holding the lock [_lock].
|
||||
final Queue<Completer<void>> _queue = Queue();
|
||||
|
||||
/// Flag indicating whether a callback is already running (true)
|
||||
/// or not. "a callback" and not "the callback" because awaited stanzas
|
||||
/// are allowed to bypass the queue.
|
||||
/// Only access while holding the lock [_lock].
|
||||
bool _isRunning = false;
|
||||
|
||||
/// The function to call to process an incoming stream object.
|
||||
final Future<void> Function(XMPPStreamObject) _callback;
|
||||
|
||||
/// Lock guarding both [_queue] and [_isRunning].
|
||||
final Lock _lock = Lock();
|
||||
|
||||
/// Logger.
|
||||
final Logger _log = Logger('IncomingStanzaQueue');
|
||||
|
||||
final StanzaAwaiter _stanzaAwaiter;
|
||||
|
||||
Future<void> _processStreamObject(
|
||||
Future<void>? future,
|
||||
XMPPStreamObject object,
|
||||
) async {
|
||||
if (future == null) {
|
||||
if (object is XMPPStreamElement) {
|
||||
_log.finest(
|
||||
'Bypassing queue for ${object.node.tag} (${object.node.attributes["id"]})',
|
||||
);
|
||||
}
|
||||
return _callback(object);
|
||||
}
|
||||
|
||||
// Wait for our turn.
|
||||
await future;
|
||||
|
||||
// Run the callback.
|
||||
await _callback(object);
|
||||
|
||||
// Run the next entry.
|
||||
await _lock.synchronized(() {
|
||||
if (_queue.isNotEmpty) {
|
||||
_queue.removeFirst().complete();
|
||||
} else {
|
||||
_isRunning = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addStanza(List<XMPPStreamObject> objects) async {
|
||||
await _lock.synchronized(() async {
|
||||
for (final object in objects) {
|
||||
if (await canBypassQueue(object)) {
|
||||
unawaited(
|
||||
_processStreamObject(null, object),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
final completer = Completer<void>();
|
||||
if (_isRunning) {
|
||||
_queue.add(completer);
|
||||
} else {
|
||||
_isRunning = true;
|
||||
completer.complete();
|
||||
}
|
||||
|
||||
unawaited(
|
||||
_processStreamObject(completer.future, object),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> canBypassQueue(XMPPStreamObject object) async {
|
||||
if (object is XMPPStreamHeader) {
|
||||
return false;
|
||||
}
|
||||
|
||||
object as XMPPStreamElement;
|
||||
return _stanzaAwaiter.isAwaited(object.node);
|
||||
}
|
||||
}
|
||||
5
packages/moxxmpp/lib/src/util/list.dart
Normal file
5
packages/moxxmpp/lib/src/util/list.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
extension ListItemCountExtension<T> on List<T> {
|
||||
int count(bool Function(T) matches) {
|
||||
return where(matches).length;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,61 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
/// A job to be submitted to an [AsyncQueue].
|
||||
typedef AsyncQueueJob = Future<void> Function();
|
||||
class StanzaQueueEntry {
|
||||
const StanzaQueueEntry(
|
||||
this.details,
|
||||
this.completer,
|
||||
);
|
||||
|
||||
/// The actual data to send.
|
||||
final StanzaDetails details;
|
||||
|
||||
/// The [Completer] to resolve when the response is received.
|
||||
final Completer<XMLNode>? completer;
|
||||
}
|
||||
|
||||
/// A function that is executed when a job is popped from the queue.
|
||||
typedef SendStanzaFunction = Future<void> Function(StanzaQueueEntry);
|
||||
|
||||
/// A function that is called before popping a queue item. Should return true when
|
||||
/// the [SendStanzaFunction] can be executed.
|
||||
typedef CanSendCallback = Future<bool> Function();
|
||||
|
||||
/// A (hopefully) async-safe queue that attempts to force
|
||||
/// in-order execution of its jobs.
|
||||
class AsyncQueue {
|
||||
/// The lock for accessing [AsyncQueue._lock] and [AsyncQueue._running].
|
||||
class AsyncStanzaQueue {
|
||||
AsyncStanzaQueue(
|
||||
this._sendStanzaFunction,
|
||||
this._canSendCallback,
|
||||
);
|
||||
|
||||
/// The lock for accessing [AsyncStanzaQueue._queue].
|
||||
final Lock _lock = Lock();
|
||||
|
||||
/// The actual job queue.
|
||||
final Queue<AsyncQueueJob> _queue = Queue<AsyncQueueJob>();
|
||||
final Queue<StanzaQueueEntry> _queue = Queue<StanzaQueueEntry>();
|
||||
|
||||
/// Indicates whether we are currently executing a job.
|
||||
bool _running = false;
|
||||
/// Sends the stanza when we can pop from the queue.
|
||||
final SendStanzaFunction _sendStanzaFunction;
|
||||
|
||||
final CanSendCallback _canSendCallback;
|
||||
|
||||
@visibleForTesting
|
||||
Queue<AsyncQueueJob> get queue => _queue;
|
||||
Queue<StanzaQueueEntry> get queue => _queue;
|
||||
|
||||
@visibleForTesting
|
||||
bool get isRunning => _running;
|
||||
/// Adds a job [entry] to the queue.
|
||||
Future<void> enqueueStanza(StanzaQueueEntry entry) async {
|
||||
await _lock.synchronized(() async {
|
||||
_queue.add(entry);
|
||||
|
||||
/// 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());
|
||||
if (_queue.isNotEmpty && await _canSendCallback()) {
|
||||
unawaited(
|
||||
_runJob(_queue.removeFirst()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -40,16 +64,26 @@ class AsyncQueue {
|
||||
await _lock.synchronized(_queue.clear);
|
||||
}
|
||||
|
||||
Future<void> _popJob() async {
|
||||
final job = _queue.removeFirst();
|
||||
final future = job();
|
||||
await future;
|
||||
Future<void> _runJob(StanzaQueueEntry details) async {
|
||||
await _sendStanzaFunction(details);
|
||||
|
||||
await _lock.synchronized(() async {
|
||||
if (_queue.isNotEmpty && await _canSendCallback()) {
|
||||
unawaited(
|
||||
_runJob(_queue.removeFirst()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> restart() async {
|
||||
if (!(await _canSendCallback())) return;
|
||||
|
||||
await _lock.synchronized(() {
|
||||
if (_queue.isNotEmpty) {
|
||||
unawaited(_popJob());
|
||||
} else {
|
||||
_running = false;
|
||||
unawaited(
|
||||
_runJob(_queue.removeFirst()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
25
packages/moxxmpp/lib/src/util/typed_map.dart
Normal file
25
packages/moxxmpp/lib/src/util/typed_map.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
/// A map, similar to Map, but always uses the type of the value as the key.
|
||||
class TypedMap<B> {
|
||||
/// Create an empty typed map.
|
||||
TypedMap();
|
||||
|
||||
/// Create a typed map from a list of values.
|
||||
TypedMap.fromList(List<B> items) {
|
||||
for (final item in items) {
|
||||
_data[item.runtimeType] = item;
|
||||
}
|
||||
}
|
||||
|
||||
/// The internal mapping of type -> data
|
||||
final Map<Object, B> _data = {};
|
||||
|
||||
/// Associate the type of [value] with [value] in the map.
|
||||
void set<T extends B>(T value) {
|
||||
_data[T] = value;
|
||||
}
|
||||
|
||||
/// Return the object of type [T] from the map, if it has been stored.
|
||||
T? get<T>() => _data[T] as T?;
|
||||
|
||||
Iterable<Object> get keys => _data.keys;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
/// NOTE: Specified by https://codeberg.org/moxxy/custom-xeps/src/branch/master/xep-xxxx-extensible-file-thumbnails.md
|
||||
|
||||
const fileThumbnailsXmlns = 'proto:urn:xmpp:eft:0';
|
||||
const blurhashThumbnailType = '$fileThumbnailsXmlns:blurhash';
|
||||
|
||||
abstract class Thumbnail {}
|
||||
|
||||
class BlurhashThumbnail extends Thumbnail {
|
||||
BlurhashThumbnail(this.hash);
|
||||
final String hash;
|
||||
}
|
||||
|
||||
Thumbnail? parseFileThumbnailElement(XMLNode node) {
|
||||
assert(
|
||||
node.attributes['xmlns'] == fileThumbnailsXmlns,
|
||||
'Invalid element xmlns',
|
||||
);
|
||||
assert(node.tag == 'file-thumbnail', 'Invalid element name');
|
||||
|
||||
switch (node.attributes['type']!) {
|
||||
case blurhashThumbnailType:
|
||||
{
|
||||
final hash = node.firstTag('blurhash')!.innerText();
|
||||
return BlurhashThumbnail(hash);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
XMLNode? _fromThumbnail(Thumbnail thumbnail) {
|
||||
if (thumbnail is BlurhashThumbnail) {
|
||||
return XMLNode(
|
||||
tag: 'blurhash',
|
||||
text: thumbnail.hash,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
XMLNode constructFileThumbnailElement(Thumbnail thumbnail) {
|
||||
final node = _fromThumbnail(thumbnail)!;
|
||||
var type = '';
|
||||
if (thumbnail is BlurhashThumbnail) {
|
||||
type = blurhashThumbnailType;
|
||||
}
|
||||
|
||||
return XMLNode.xmlns(
|
||||
tag: 'file-thumbnail',
|
||||
xmlns: fileThumbnailsXmlns,
|
||||
attributes: {'type': type},
|
||||
children: [node],
|
||||
);
|
||||
}
|
||||
@@ -2,14 +2,70 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0446.dart';
|
||||
|
||||
/// NOTE: Specified by https://github.com/PapaTutuWawa/custom-xeps/blob/master/xep-xxxx-file-upload-notifications.md
|
||||
|
||||
const fileUploadNotificationXmlns = 'proto:urn:xmpp:fun:0';
|
||||
|
||||
/// Indicates a file upload notification.
|
||||
class FileUploadNotificationData implements StanzaHandlerExtension {
|
||||
const FileUploadNotificationData(this.metadata);
|
||||
|
||||
/// The file metadata indicated in the upload notification.
|
||||
final FileMetadataData metadata;
|
||||
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'file-upload',
|
||||
xmlns: fileUploadNotificationXmlns,
|
||||
children: [
|
||||
metadata.toXML(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates that a file upload has been cancelled.
|
||||
class FileUploadNotificationCancellationData implements StanzaHandlerExtension {
|
||||
const FileUploadNotificationCancellationData(this.id);
|
||||
|
||||
/// The id of the upload notifiaction that is cancelled.
|
||||
final String id;
|
||||
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'cancelled',
|
||||
xmlns: fileUploadNotificationXmlns,
|
||||
attributes: {
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates that a file upload has been completed.
|
||||
class FileUploadNotificationReplacementData implements StanzaHandlerExtension {
|
||||
const FileUploadNotificationReplacementData(this.id);
|
||||
|
||||
/// The id of the upload notifiaction that is replaced.
|
||||
final String id;
|
||||
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'replaces',
|
||||
xmlns: fileUploadNotificationXmlns,
|
||||
attributes: {
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FileUploadNotificationManager extends XmppManagerBase {
|
||||
FileUploadNotificationManager() : super(fileUploadNotificationManager);
|
||||
|
||||
@@ -47,11 +103,14 @@ class FileUploadNotificationManager extends XmppManagerBase {
|
||||
) async {
|
||||
final funElement =
|
||||
message.firstTag('file-upload', xmlns: fileUploadNotificationXmlns)!;
|
||||
return state.copyWith(
|
||||
fun: FileMetadataData.fromXML(
|
||||
funElement.firstTag('file', xmlns: fileMetadataXmlns)!,
|
||||
),
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
FileUploadNotificationData(
|
||||
FileMetadataData.fromXML(
|
||||
funElement.firstTag('file', xmlns: fileMetadataXmlns)!,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onFileUploadNotificationReplacementReceived(
|
||||
@@ -60,9 +119,12 @@ class FileUploadNotificationManager extends XmppManagerBase {
|
||||
) async {
|
||||
final element =
|
||||
message.firstTag('replaces', xmlns: fileUploadNotificationXmlns)!;
|
||||
return state.copyWith(
|
||||
funReplacement: element.attributes['id']! as String,
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
FileUploadNotificationReplacementData(
|
||||
element.attributes['id']! as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onFileUploadNotificationCancellationReceived(
|
||||
@@ -71,8 +133,42 @@ class FileUploadNotificationManager extends XmppManagerBase {
|
||||
) async {
|
||||
final element =
|
||||
message.firstTag('cancels', xmlns: fileUploadNotificationXmlns)!;
|
||||
return state.copyWith(
|
||||
funCancellation: element.attributes['id']! as String,
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
FileUploadNotificationCancellationData(
|
||||
element.attributes['id']! as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final fun = extensions.get<FileUploadNotificationData>();
|
||||
if (fun != null) {
|
||||
return [fun.toXML()];
|
||||
}
|
||||
|
||||
final cancel = extensions.get<FileUploadNotificationCancellationData>();
|
||||
if (cancel != null) {
|
||||
return [cancel.toXML()];
|
||||
}
|
||||
|
||||
final replace = extensions.get<FileUploadNotificationReplacementData>();
|
||||
if (replace != null) {
|
||||
return [replace.toXML()];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
@@ -10,14 +10,14 @@ class DataFormOption {
|
||||
XMLNode toXml() {
|
||||
return XMLNode(
|
||||
tag: 'option',
|
||||
attributes: label != null
|
||||
? <String, dynamic>{'label': label}
|
||||
: <String, dynamic>{},
|
||||
attributes: {
|
||||
if (label != null) 'label': label,
|
||||
},
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'value',
|
||||
text: value,
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -45,19 +45,22 @@ class DataFormField {
|
||||
return XMLNode(
|
||||
tag: 'field',
|
||||
attributes: <String, dynamic>{
|
||||
...varAttr != null
|
||||
? <String, dynamic>{'var': varAttr}
|
||||
: <String, dynamic>{},
|
||||
...type != null ? <String, dynamic>{'type': type} : <String, dynamic>{},
|
||||
...label != null
|
||||
? <String, dynamic>{'label': label}
|
||||
: <String, dynamic>{}
|
||||
if (varAttr != null) 'var': varAttr,
|
||||
if (type != null) 'type': type,
|
||||
if (label != null) 'label': label,
|
||||
},
|
||||
children: [
|
||||
...description != null ? [XMLNode(tag: 'desc', text: description)] : [],
|
||||
...isRequired ? [XMLNode(tag: 'required')] : [],
|
||||
if (description != null)
|
||||
XMLNode(
|
||||
tag: 'desc',
|
||||
text: description,
|
||||
),
|
||||
if (isRequired)
|
||||
XMLNode(
|
||||
tag: 'required',
|
||||
),
|
||||
...values.map((value) => XMLNode(tag: 'value', text: value)),
|
||||
...options.map((option) => option.toXml())
|
||||
...options.map((option) => option.toXml()),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -80,7 +83,7 @@ class DataForm {
|
||||
final List<List<DataFormField>> items;
|
||||
|
||||
DataFormField? getFieldByVar(String varAttr) {
|
||||
return firstWhereOrNull(fields, (field) => field.varAttr == varAttr);
|
||||
return fields.firstWhereOrNull((field) => field.varAttr == varAttr);
|
||||
}
|
||||
|
||||
XMLNode toXml() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
|
||||
@internal
|
||||
@immutable
|
||||
@@ -6,7 +7,7 @@ class DiscoCacheKey {
|
||||
const DiscoCacheKey(this.jid, this.node);
|
||||
|
||||
/// The JID we're requesting disco data from.
|
||||
final String jid;
|
||||
final JID jid;
|
||||
|
||||
/// Optionally the node we are requesting from.
|
||||
final String? node;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
abstract class DiscoError {}
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
|
||||
/// Base type for disco-related errors.
|
||||
abstract class DiscoError extends StanzaError {}
|
||||
|
||||
/// An unspecified error that is not covered by another [DiscoError].
|
||||
class UnknownDiscoError extends DiscoError {}
|
||||
|
||||
/// The received disco response is invalid in some shape or form.
|
||||
class InvalidResponseDiscoError extends DiscoError {}
|
||||
|
||||
class ErrorResponseDiscoError extends DiscoError {}
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
// TODO(PapaTutuWawa): Move types into types.dart
|
||||
|
||||
Stanza buildDiscoInfoQueryStanza(String entity, String? node) {
|
||||
Stanza buildDiscoInfoQueryStanza(JID entity, String? node) {
|
||||
return Stanza.iq(
|
||||
to: entity,
|
||||
to: entity.toString(),
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: discoInfoXmlns,
|
||||
attributes: node != null ? {'node': node} : {},
|
||||
)
|
||||
attributes: {
|
||||
if (node != null) 'node': node,
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Stanza buildDiscoItemsQueryStanza(String entity, {String? node}) {
|
||||
Stanza buildDiscoItemsQueryStanza(JID entity, {String? node}) {
|
||||
return Stanza.iq(
|
||||
to: entity,
|
||||
to: entity.toString(),
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'query',
|
||||
xmlns: discoItemsXmlns,
|
||||
attributes: node != null ? {'node': node} : {},
|
||||
)
|
||||
attributes: {
|
||||
if (node != null) 'node': node,
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,13 +19,11 @@ class Identity {
|
||||
XMLNode toXMLNode() {
|
||||
return XMLNode(
|
||||
tag: 'identity',
|
||||
attributes: <String, dynamic>{
|
||||
attributes: {
|
||||
'category': category,
|
||||
'type': type,
|
||||
'name': name,
|
||||
...lang == null
|
||||
? <String, dynamic>{}
|
||||
: <String, dynamic>{'xml:lang': lang}
|
||||
if (lang != null) 'xml:lang': lang,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -108,13 +106,13 @@ class DiscoInfo {
|
||||
@immutable
|
||||
class DiscoItem {
|
||||
const DiscoItem({required this.jid, this.node, this.name});
|
||||
final String jid;
|
||||
final JID jid;
|
||||
final String? node;
|
||||
final String? name;
|
||||
|
||||
XMLNode toXml() {
|
||||
final attributes = {
|
||||
'jid': jid,
|
||||
'jid': jid.toString(),
|
||||
};
|
||||
if (node != null) {
|
||||
attributes['node'] = node!;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/connection.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
@@ -10,7 +10,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/util/wait.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||
@@ -41,21 +40,15 @@ class DiscoManager extends XmppManagerBase {
|
||||
/// Disco identities that we advertise
|
||||
final List<Identity> _identities;
|
||||
|
||||
/// Map full JID to Capability hashes
|
||||
final Map<String, CapabilityHashInfo> _capHashCache = {};
|
||||
|
||||
/// Map capability hash to the disco info
|
||||
final Map<String, DiscoInfo> _capHashInfoCache = {};
|
||||
|
||||
/// Map full JID to Disco Info
|
||||
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
|
||||
|
||||
/// The tracker for tracking disco#info queries that are in flight.
|
||||
final WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>>
|
||||
final WaitForTracker<DiscoCacheKey, Result<StanzaError, DiscoInfo>>
|
||||
_discoInfoTracker = WaitForTracker();
|
||||
|
||||
/// The tracker for tracking disco#info queries that are in flight.
|
||||
final WaitForTracker<DiscoCacheKey, Result<DiscoError, List<DiscoItem>>>
|
||||
final WaitForTracker<DiscoCacheKey, Result<StanzaError, List<DiscoItem>>>
|
||||
_discoItemsTracker = WaitForTracker();
|
||||
|
||||
/// Cache lock
|
||||
@@ -74,7 +67,7 @@ class DiscoManager extends XmppManagerBase {
|
||||
List<String> get features => _features;
|
||||
|
||||
@visibleForTesting
|
||||
WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>>
|
||||
WaitForTracker<DiscoCacheKey, Result<StanzaError, DiscoInfo>>
|
||||
get infoTracker => _discoInfoTracker;
|
||||
|
||||
@override
|
||||
@@ -101,13 +94,7 @@ class DiscoManager extends XmppManagerBase {
|
||||
|
||||
@override
|
||||
Future<void> onXmppEvent(XmppEvent event) async {
|
||||
if (event is PresenceReceivedEvent) {
|
||||
await _onPresence(event.jid, event.presence);
|
||||
} else if (event is ConnectionStateChangedEvent) {
|
||||
// TODO(Unknown): This handling is stupid. We should have an event that is
|
||||
// triggered when we cannot guarantee that everything is as
|
||||
// it was before.
|
||||
if (event.state != XmppConnectionState.connected) return;
|
||||
if (event is StreamNegotiationsDoneEvent) {
|
||||
if (event.resumed) return;
|
||||
|
||||
// Cancel all waiting requests
|
||||
@@ -135,6 +122,15 @@ class DiscoManager extends XmppManagerBase {
|
||||
_discoItemsCallbacks[node] = callback;
|
||||
}
|
||||
|
||||
/// Add a [DiscoCacheKey]-[DiscoInfo] pair [discoInfoEntry] to the internal cache.
|
||||
Future<void> addCachedDiscoInfo(
|
||||
MapEntry<DiscoCacheKey, DiscoInfo> discoInfoEntry,
|
||||
) async {
|
||||
await _cacheLock.synchronized(() {
|
||||
_discoInfoCache[discoInfoEntry.key] = discoInfoEntry.value;
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds a list of features to the possible disco info response.
|
||||
/// This function only adds features that are not already present in the disco features.
|
||||
void addFeatures(List<String> features) {
|
||||
@@ -155,39 +151,6 @@ class DiscoManager extends XmppManagerBase {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onPresence(JID from, Stanza presence) async {
|
||||
final c = presence.firstTag('c', xmlns: capsXmlns);
|
||||
if (c == null) return;
|
||||
|
||||
final info = CapabilityHashInfo(
|
||||
c.attributes['ver']! as String,
|
||||
c.attributes['node']! as String,
|
||||
c.attributes['hash']! as String,
|
||||
);
|
||||
|
||||
// Check if we already know of that cache
|
||||
var cached = false;
|
||||
await _cacheLock.synchronized(() async {
|
||||
if (!_capHashCache.containsKey(info.ver)) {
|
||||
cached = true;
|
||||
}
|
||||
});
|
||||
if (cached) return;
|
||||
|
||||
// Request the cap hash
|
||||
logger.finest(
|
||||
"Received capability hash we don't know about. Requesting it...",
|
||||
);
|
||||
final result =
|
||||
await discoInfoQuery(from.toString(), node: '${info.node}#${info.ver}');
|
||||
if (result.isType<DiscoError>()) return;
|
||||
|
||||
await _cacheLock.synchronized(() async {
|
||||
_capHashCache[from.toString()] = info;
|
||||
_capHashInfoCache[info.ver] = result.get<DiscoInfo>();
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the [DiscoInfo] object that would be used as the response to a disco#info
|
||||
/// query against our bare JID with no node. The results node attribute is set
|
||||
/// to [node].
|
||||
@@ -221,7 +184,7 @@ class DiscoManager extends XmppManagerBase {
|
||||
],
|
||||
);
|
||||
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
await reply(
|
||||
@@ -232,7 +195,7 @@ class DiscoManager extends XmppManagerBase {
|
||||
],
|
||||
);
|
||||
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onDiscoItemsRequest(
|
||||
@@ -260,7 +223,7 @@ class DiscoManager extends XmppManagerBase {
|
||||
],
|
||||
);
|
||||
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
return state;
|
||||
@@ -268,11 +231,12 @@ class DiscoManager extends XmppManagerBase {
|
||||
|
||||
Future<void> _exitDiscoInfoCriticalSection(
|
||||
DiscoCacheKey key,
|
||||
Result<DiscoError, DiscoInfo> result,
|
||||
Result<StanzaError, DiscoInfo> result,
|
||||
bool shouldCache,
|
||||
) async {
|
||||
await _cacheLock.synchronized(() async {
|
||||
// Add to cache if it is a result
|
||||
if (result.isType<DiscoInfo>()) {
|
||||
if (result.isType<DiscoInfo>() && shouldCache) {
|
||||
_discoInfoCache[key] = result.get<DiscoInfo>();
|
||||
}
|
||||
});
|
||||
@@ -280,22 +244,40 @@ class DiscoManager extends XmppManagerBase {
|
||||
await _discoInfoTracker.resolve(key, result);
|
||||
}
|
||||
|
||||
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node].
|
||||
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(
|
||||
String entity, {
|
||||
/// Send a disco#info query to [entity]. If [node] is specified, then the disco#info
|
||||
/// request will be directed against that one node of [entity].
|
||||
///
|
||||
/// [shouldEncrypt] indicates to possible end-to-end encryption implementations whether
|
||||
/// the request should be encrypted (true) or not (false).
|
||||
///
|
||||
/// [shouldCache] indicates whether the successful result of the disco#info query
|
||||
/// should be cached (true) or not(false).
|
||||
Future<Result<StanzaError, DiscoInfo>> discoInfoQuery(
|
||||
JID entity, {
|
||||
String? node,
|
||||
bool shouldEncrypt = true,
|
||||
bool shouldEncrypt = false,
|
||||
bool shouldCache = true,
|
||||
}) async {
|
||||
final cacheKey = DiscoCacheKey(entity, node);
|
||||
DiscoInfo? info;
|
||||
final cacheKey = DiscoCacheKey(entity, node);
|
||||
final ecm = getAttributes()
|
||||
.getManagerById<EntityCapabilitiesManager>(entityCapabilitiesManager);
|
||||
final ffuture = await _cacheLock
|
||||
.synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>(
|
||||
.synchronized<Future<Future<Result<StanzaError, DiscoInfo>>?>?>(
|
||||
() async {
|
||||
// Check if we already know what the JID supports
|
||||
if (_discoInfoCache.containsKey(cacheKey)) {
|
||||
info = _discoInfoCache[cacheKey];
|
||||
return null;
|
||||
} else {
|
||||
// Check if we know entity capabilities
|
||||
if (ecm != null && node == null) {
|
||||
info = await ecm.getCachedDiscoInfoFromJid(entity);
|
||||
if (info != null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return _discoInfoTracker.waitFor(cacheKey);
|
||||
}
|
||||
});
|
||||
@@ -309,39 +291,43 @@ class DiscoManager extends XmppManagerBase {
|
||||
}
|
||||
}
|
||||
|
||||
final stanza = await getAttributes().sendStanza(
|
||||
buildDiscoInfoQueryStanza(entity, node),
|
||||
encrypted: !shouldEncrypt,
|
||||
);
|
||||
final query = stanza.firstTag('query');
|
||||
if (query == null) {
|
||||
final result = Result<DiscoError, DiscoInfo>(InvalidResponseDiscoError());
|
||||
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
||||
final stanza = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
buildDiscoInfoQueryStanza(entity, node),
|
||||
shouldEncrypt: shouldEncrypt,
|
||||
),
|
||||
))!;
|
||||
|
||||
// Error handling
|
||||
if (stanza.attributes['type'] == 'error') {
|
||||
final result =
|
||||
Result<StanzaError, DiscoInfo>(StanzaError.fromXMLNode(stanza));
|
||||
await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (stanza.attributes['type'] == 'error') {
|
||||
//final error = stanza.firstTag('error');
|
||||
final result = Result<DiscoError, DiscoInfo>(ErrorResponseDiscoError());
|
||||
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
||||
final query = stanza.firstTag('query');
|
||||
if (query == null) {
|
||||
final result = Result<DiscoError, DiscoInfo>(InvalidResponseDiscoError());
|
||||
await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
|
||||
return result;
|
||||
}
|
||||
|
||||
final result = Result<DiscoError, DiscoInfo>(
|
||||
DiscoInfo.fromQuery(
|
||||
query,
|
||||
JID.fromString(entity),
|
||||
entity,
|
||||
),
|
||||
);
|
||||
await _exitDiscoInfoCriticalSection(cacheKey, result);
|
||||
await _exitDiscoInfoCriticalSection(cacheKey, result, shouldCache);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
|
||||
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(
|
||||
String entity, {
|
||||
Future<Result<StanzaError, List<DiscoItem>>> discoItemsQuery(
|
||||
JID entity, {
|
||||
String? node,
|
||||
bool shouldEncrypt = true,
|
||||
bool shouldEncrypt = false,
|
||||
}) async {
|
||||
final key = DiscoCacheKey(entity, node);
|
||||
final future = await _discoItemsTracker.waitFor(key);
|
||||
@@ -349,10 +335,20 @@ class DiscoManager extends XmppManagerBase {
|
||||
return future;
|
||||
}
|
||||
|
||||
final stanza = await getAttributes().sendStanza(
|
||||
buildDiscoItemsQueryStanza(entity, node: node),
|
||||
encrypted: !shouldEncrypt,
|
||||
) as Stanza;
|
||||
final stanza = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
buildDiscoItemsQueryStanza(entity, node: node),
|
||||
encrypted: !shouldEncrypt,
|
||||
),
|
||||
))!;
|
||||
|
||||
// Error handling
|
||||
if (stanza.attributes['type'] == 'error') {
|
||||
final result =
|
||||
Result<StanzaError, List<DiscoItem>>(StanzaError.fromXMLNode(stanza));
|
||||
await _discoItemsTracker.resolve(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
final query = stanza.firstTag('query');
|
||||
if (query == null) {
|
||||
@@ -362,20 +358,11 @@ class DiscoManager extends XmppManagerBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (stanza.type == 'error') {
|
||||
//final error = stanza.firstTag('error');
|
||||
//print("Disco Items error: " + error.toXml());
|
||||
final result =
|
||||
Result<DiscoError, List<DiscoItem>>(ErrorResponseDiscoError());
|
||||
await _discoItemsTracker.resolve(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
final items = query
|
||||
.findTags('item')
|
||||
.map(
|
||||
(node) => DiscoItem(
|
||||
jid: node.attributes['jid']! as String,
|
||||
jid: JID.fromString(node.attributes['jid']! as String),
|
||||
node: node.attributes['node'] as String?,
|
||||
name: node.attributes['name'] as String?,
|
||||
),
|
||||
@@ -387,18 +374,9 @@ class DiscoManager extends XmppManagerBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Queries information about a jid based on its node and capability hash.
|
||||
Future<Result<DiscoError, DiscoInfo>> discoInfoCapHashQuery(
|
||||
String jid,
|
||||
String node,
|
||||
String ver,
|
||||
) async {
|
||||
return discoInfoQuery(jid, node: '$node#$ver');
|
||||
}
|
||||
|
||||
Future<Result<DiscoError, List<DiscoInfo>>> performDiscoSweep() async {
|
||||
final attrs = getAttributes();
|
||||
final serverJid = attrs.getConnectionSettings().jid.domain;
|
||||
final serverJid = attrs.getConnectionSettings().jid.toDomain();
|
||||
final infoResults = List<DiscoInfo>.empty(growable: true);
|
||||
final result = await discoInfoQuery(serverJid);
|
||||
if (result.isType<DiscoInfo>()) {
|
||||
@@ -441,8 +419,8 @@ class DiscoManager extends XmppManagerBase {
|
||||
/// A wrapper function around discoInfoQuery: Returns true if the entity with JID
|
||||
/// [entity] supports the disco feature [feature]. If not, returns false.
|
||||
Future<bool> supportsFeature(JID entity, String feature) async {
|
||||
final info = await discoInfoQuery(entity.toString());
|
||||
if (info.isType<DiscoError>()) return false;
|
||||
final info = await discoInfoQuery(entity);
|
||||
if (info.isType<StanzaError>()) return false;
|
||||
|
||||
return info.get<DiscoInfo>().features.contains(feature);
|
||||
}
|
||||
|
||||
25
packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart
Normal file
25
packages/moxxmpp/lib/src/xeps/xep_0045/errors.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
/// Represents an error related to Multi-User Chat (MUC).
|
||||
abstract class MUCError {}
|
||||
|
||||
/// Error indicating an invalid (non-supported) stanza received while going
|
||||
/// through normal operation/flow of an MUC.
|
||||
class InvalidStanzaFormat extends MUCError {}
|
||||
|
||||
/// Represents an error indicating an abnormal condition while parsing
|
||||
/// the DiscoInfo response stanza in Multi-User Chat (MUC).
|
||||
class InvalidDiscoInfoResponse extends MUCError {}
|
||||
|
||||
/// Returned when no nickname was specified from the client side while trying to
|
||||
/// perform some actions on the MUC, such as joining the room.
|
||||
class NoNicknameSpecified extends MUCError {}
|
||||
|
||||
/// This error occurs when a user attempts to perform an action that requires
|
||||
/// them to be a member of a room, but they are not currently joined to
|
||||
/// that room.
|
||||
class RoomNotJoinedError extends MUCError {}
|
||||
|
||||
/// Indicates that the MUC forbids us from joining, i.e. when we're banned.
|
||||
class JoinForbiddenError extends MUCError {}
|
||||
|
||||
/// Indicates that an unspecific error occurred while joining.
|
||||
class MUCUnspecificError extends MUCError {}
|
||||
72
packages/moxxmpp/lib/src/xeps/xep_0045/events.dart
Normal file
72
packages/moxxmpp/lib/src/xeps/xep_0045/events.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
|
||||
|
||||
/// Triggered when the MUC changes our nickname.
|
||||
class OwnDataChangedEvent extends XmppEvent {
|
||||
OwnDataChangedEvent(
|
||||
this.roomJid,
|
||||
this.nick,
|
||||
this.affiliation,
|
||||
this.role,
|
||||
);
|
||||
|
||||
/// The JID of the room.
|
||||
final JID roomJid;
|
||||
|
||||
/// Our nickname.
|
||||
final String nick;
|
||||
|
||||
/// Our affiliation.
|
||||
final Affiliation affiliation;
|
||||
|
||||
/// Our role.
|
||||
final Role role;
|
||||
}
|
||||
|
||||
/// Triggered when an entity joins the MUC.
|
||||
class MemberJoinedEvent extends XmppEvent {
|
||||
MemberJoinedEvent(this.roomJid, this.member);
|
||||
|
||||
/// The JID of the room.
|
||||
final JID roomJid;
|
||||
|
||||
/// The new member.
|
||||
final RoomMember member;
|
||||
}
|
||||
|
||||
/// Triggered when an entity changes their presence in the MUC.
|
||||
class MemberChangedEvent extends XmppEvent {
|
||||
MemberChangedEvent(this.roomJid, this.member);
|
||||
|
||||
/// The JID of the room.
|
||||
final JID roomJid;
|
||||
|
||||
/// The new member.
|
||||
final RoomMember member;
|
||||
}
|
||||
|
||||
/// Triggered when an entity leaves the MUC.
|
||||
class MemberLeftEvent extends XmppEvent {
|
||||
MemberLeftEvent(this.roomJid, this.nick);
|
||||
|
||||
/// The JID of the room.
|
||||
final JID roomJid;
|
||||
|
||||
/// The nick of the user who left.
|
||||
final String nick;
|
||||
}
|
||||
|
||||
/// Triggered when an entity changes their nick.
|
||||
class MemberChangedNickEvent extends XmppEvent {
|
||||
MemberChangedNickEvent(this.roomJid, this.oldNick, this.newNick);
|
||||
|
||||
/// The JID of the room.
|
||||
final JID roomJid;
|
||||
|
||||
/// The original nick.
|
||||
final String oldNick;
|
||||
|
||||
/// The new nick.
|
||||
final String newNick;
|
||||
}
|
||||
2
packages/moxxmpp/lib/src/xeps/xep_0045/status_codes.dart
Normal file
2
packages/moxxmpp/lib/src/xeps/xep_0045/status_codes.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
const selfPresenceStatus = '110';
|
||||
const nicknameChangedStatus = '303';
|
||||
163
packages/moxxmpp/lib/src/xeps/xep_0045/types.dart
Normal file
163
packages/moxxmpp/lib/src/xeps/xep_0045/types.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
|
||||
class InvalidAffiliationException implements Exception {}
|
||||
|
||||
class InvalidRoleException implements Exception {}
|
||||
|
||||
enum Affiliation {
|
||||
owner('owner'),
|
||||
admin('admin'),
|
||||
member('member'),
|
||||
outcast('outcast'),
|
||||
none('none');
|
||||
|
||||
const Affiliation(this.value);
|
||||
|
||||
factory Affiliation.fromString(String value) {
|
||||
switch (value) {
|
||||
case 'owner':
|
||||
return Affiliation.owner;
|
||||
case 'admin':
|
||||
return Affiliation.admin;
|
||||
case 'member':
|
||||
return Affiliation.member;
|
||||
case 'outcast':
|
||||
return Affiliation.outcast;
|
||||
case 'none':
|
||||
return Affiliation.none;
|
||||
default:
|
||||
throw InvalidAffiliationException();
|
||||
}
|
||||
}
|
||||
|
||||
/// The value to use for an attribute referring to this affiliation.
|
||||
final String value;
|
||||
}
|
||||
|
||||
enum Role {
|
||||
moderator('moderator'),
|
||||
participant('participant'),
|
||||
visitor('visitor'),
|
||||
none('none');
|
||||
|
||||
const Role(this.value);
|
||||
|
||||
factory Role.fromString(String value) {
|
||||
switch (value) {
|
||||
case 'moderator':
|
||||
return Role.moderator;
|
||||
case 'participant':
|
||||
return Role.participant;
|
||||
case 'visitor':
|
||||
return Role.visitor;
|
||||
case 'none':
|
||||
return Role.none;
|
||||
default:
|
||||
throw InvalidRoleException();
|
||||
}
|
||||
}
|
||||
|
||||
/// The value to use for an attribute referring to this role.
|
||||
final String value;
|
||||
}
|
||||
|
||||
class RoomInformation {
|
||||
/// Represents information about a Multi-User Chat (MUC) room.
|
||||
RoomInformation({
|
||||
required this.jid,
|
||||
required this.features,
|
||||
required this.name,
|
||||
this.roomInfo,
|
||||
});
|
||||
|
||||
/// Constructs a [RoomInformation] object from a [DiscoInfo] object.
|
||||
/// The [DiscoInfo] object contains the necessary information to populate
|
||||
/// the [RoomInformation] fields.
|
||||
factory RoomInformation.fromDiscoInfo({
|
||||
required DiscoInfo discoInfo,
|
||||
}) =>
|
||||
RoomInformation(
|
||||
jid: discoInfo.jid!,
|
||||
features: discoInfo.features,
|
||||
name: discoInfo.identities
|
||||
.firstWhere((i) => i.category == 'conference')
|
||||
.name!,
|
||||
roomInfo: discoInfo.extendedInfo.firstWhereOrNull((form) {
|
||||
final field = form.getFieldByVar(formVarFormType);
|
||||
return field?.type == 'hidden' &&
|
||||
field?.values.first == roomInfoFormType;
|
||||
}),
|
||||
);
|
||||
|
||||
/// The JID of the Multi-User Chat (MUC) room.
|
||||
final JID jid;
|
||||
|
||||
/// A list of features supported by the Multi-User Chat (MUC) room.
|
||||
final List<String> features;
|
||||
|
||||
/// The name or title of the Multi-User Chat (MUC) room.
|
||||
final String name;
|
||||
|
||||
/// The data form containing room information.
|
||||
final DataForm? roomInfo;
|
||||
}
|
||||
|
||||
/// The used message-id and an optional origin-id.
|
||||
typedef PendingMessage = (String, String?);
|
||||
|
||||
/// An entity inside a MUC room. The name "member" here does not refer to an affiliation of member.
|
||||
class RoomMember {
|
||||
const RoomMember(this.nick, this.affiliation, this.role);
|
||||
|
||||
/// The entity's nickname.
|
||||
final String nick;
|
||||
|
||||
/// The assigned affiliation.
|
||||
final Affiliation affiliation;
|
||||
|
||||
/// The assigned role.
|
||||
final Role role;
|
||||
|
||||
RoomMember copyWith({
|
||||
String? nick,
|
||||
Affiliation? affiliation,
|
||||
Role? role,
|
||||
}) {
|
||||
return RoomMember(
|
||||
nick ?? this.nick,
|
||||
affiliation ?? this.affiliation,
|
||||
role ?? this.role,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomState {
|
||||
RoomState({required this.roomJid, this.nick, required this.joined}) {
|
||||
pendingMessages = List<PendingMessage>.empty(growable: true);
|
||||
}
|
||||
|
||||
/// The JID of the room.
|
||||
final JID roomJid;
|
||||
|
||||
/// The nick we're joined with.
|
||||
String? nick;
|
||||
|
||||
/// Flag whether we're joined and can process messages
|
||||
bool joined;
|
||||
|
||||
/// Our own affiliation inside the MUC.
|
||||
Affiliation? affiliation;
|
||||
|
||||
/// Our own role inside the MUC.
|
||||
Role? role;
|
||||
|
||||
/// The list of messages that we sent and are waiting for their echo.
|
||||
late final List<PendingMessage> pendingMessages;
|
||||
|
||||
/// "List" of entities inside the MUC.
|
||||
final Map<String, RoomMember> members = {};
|
||||
}
|
||||
512
packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart
Normal file
512
packages/moxxmpp/lib/src/xeps/xep_0045/xep_0045.dart
Normal file
@@ -0,0 +1,512 @@
|
||||
import 'dart:async';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/presence.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0045/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0045/events.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0045/status_codes.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0045/types.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0359.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
/// (Room JID, nickname)
|
||||
typedef MUCRoomJoin = (JID, String);
|
||||
|
||||
class MUCManager extends XmppManagerBase {
|
||||
MUCManager() : super(mucManager);
|
||||
|
||||
@override
|
||||
Future<bool> isSupported() async => true;
|
||||
|
||||
/// Map a room's JID to its RoomState
|
||||
final Map<JID, RoomState> _mucRoomCache = {};
|
||||
|
||||
/// Mapp a room's JID to a completer waiting for the completion of the join process.
|
||||
final Map<JID, Completer<Result<bool, MUCError>>> _mucRoomJoinCompleter = {};
|
||||
|
||||
/// Cache lock
|
||||
final Lock _cacheLock = Lock();
|
||||
|
||||
/// Flag indicating whether we joined the rooms added to the room list with
|
||||
/// [prepareRoomList].
|
||||
bool _joinedPreparedRooms = true;
|
||||
|
||||
@override
|
||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||
StanzaHandler(
|
||||
stanzaTag: 'message',
|
||||
callback: _onMessage,
|
||||
// Before the message handler
|
||||
priority: -99,
|
||||
),
|
||||
StanzaHandler(
|
||||
stanzaTag: 'presence',
|
||||
callback: _onPresence,
|
||||
tagName: 'x',
|
||||
tagXmlns: mucUserXmlns,
|
||||
// Before the PresenceManager
|
||||
priority: PresenceManager.presenceHandlerPriority + 1,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
List<StanzaHandler> getOutgoingPreStanzaHandlers() => [
|
||||
StanzaHandler(
|
||||
stanzaTag: 'message',
|
||||
callback: _onMessageSent,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> onXmppEvent(XmppEvent event) async {
|
||||
if (event is! StreamNegotiationsDoneEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only attempt rejoining if we did not resume the stream and all
|
||||
// prepared rooms are already joined.
|
||||
if (event.resumed && _joinedPreparedRooms) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mucJoins = List<MUCRoomJoin>.empty(growable: true);
|
||||
await _cacheLock.synchronized(() async {
|
||||
// Mark all groupchats as not joined.
|
||||
for (final jid in _mucRoomCache.keys) {
|
||||
_mucRoomCache[jid]!.joined = false;
|
||||
_mucRoomJoinCompleter[jid] = Completer();
|
||||
|
||||
// Re-join all MUCs.
|
||||
final state = _mucRoomCache[jid]!;
|
||||
mucJoins.add((jid, state.nick!));
|
||||
}
|
||||
});
|
||||
|
||||
for (final join in mucJoins) {
|
||||
final (jid, nick) = join;
|
||||
await _sendMucJoin(
|
||||
jid,
|
||||
nick,
|
||||
0,
|
||||
);
|
||||
}
|
||||
_joinedPreparedRooms = true;
|
||||
}
|
||||
|
||||
/// Prepares the internal room list to ensure that the rooms
|
||||
/// [rooms] are joined once we are connected.
|
||||
Future<void> prepareRoomList(List<MUCRoomJoin> rooms) async {
|
||||
assert(
|
||||
rooms.isNotEmpty,
|
||||
'The room list should not be empty',
|
||||
);
|
||||
|
||||
await _cacheLock.synchronized(() {
|
||||
_joinedPreparedRooms = false;
|
||||
for (final room in rooms) {
|
||||
final (roomJid, nick) = room;
|
||||
_mucRoomCache[roomJid] = RoomState(
|
||||
roomJid: roomJid,
|
||||
nick: nick,
|
||||
joined: false,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Queries the information of a Multi-User Chat room.
|
||||
///
|
||||
/// Retrieves the information about the specified MUC room by performing a
|
||||
/// disco info query. Returns a [Result] with the [RoomInformation] on success
|
||||
/// or an appropriate [MUCError] on failure.
|
||||
Future<Result<RoomInformation, MUCError>> queryRoomInformation(
|
||||
JID roomJID,
|
||||
) async {
|
||||
final result = await getAttributes()
|
||||
.getManagerById<DiscoManager>(discoManager)!
|
||||
.discoInfoQuery(roomJID);
|
||||
if (result.isType<StanzaError>()) {
|
||||
return Result(InvalidStanzaFormat());
|
||||
}
|
||||
try {
|
||||
final roomInformation = RoomInformation.fromDiscoInfo(
|
||||
discoInfo: result.get<DiscoInfo>(),
|
||||
);
|
||||
return Result(roomInformation);
|
||||
} catch (e) {
|
||||
logger.warning('Invalid disco information: $e');
|
||||
return Result(InvalidDiscoInfoResponse());
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins a Multi-User Chat room.
|
||||
///
|
||||
/// Joins the specified MUC room using the provided nickname. Sends a presence
|
||||
/// stanza with the appropriate attributes to join the room. Returns a [Result]
|
||||
/// with a boolean value indicating success or failure, or an [MUCError]
|
||||
/// if applicable.
|
||||
Future<Result<bool, MUCError>> joinRoom(
|
||||
JID roomJid,
|
||||
String nick, {
|
||||
int? maxHistoryStanzas,
|
||||
}) async {
|
||||
if (nick.isEmpty) {
|
||||
return Result(NoNicknameSpecified());
|
||||
}
|
||||
|
||||
final completer =
|
||||
await _cacheLock.synchronized<Completer<Result<bool, MUCError>>>(
|
||||
() {
|
||||
_mucRoomCache[roomJid] = RoomState(
|
||||
roomJid: roomJid,
|
||||
nick: nick,
|
||||
joined: false,
|
||||
);
|
||||
|
||||
final completer = Completer<Result<bool, MUCError>>();
|
||||
_mucRoomJoinCompleter[roomJid] = completer;
|
||||
return completer;
|
||||
},
|
||||
);
|
||||
|
||||
await _sendMucJoin(roomJid, nick, maxHistoryStanzas);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> _sendMucJoin(
|
||||
JID roomJid,
|
||||
String nick,
|
||||
int? maxHistoryStanzas,
|
||||
) async {
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.presence(
|
||||
to: roomJid.withResource(nick).toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'x',
|
||||
xmlns: mucXmlns,
|
||||
children: [
|
||||
if (maxHistoryStanzas != null)
|
||||
XMLNode(
|
||||
tag: 'history',
|
||||
attributes: {
|
||||
'maxstanzas': maxHistoryStanzas.toString(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
awaitable: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Leaves a Multi-User Chat room.
|
||||
///
|
||||
/// Leaves the specified MUC room by sending an 'unavailable' presence stanza.
|
||||
/// Removes the corresponding room entry from the cache. Returns a [Result]
|
||||
/// with a boolean value indicating success or failure, or an [MUCError]
|
||||
/// if applicable.
|
||||
Future<Result<bool, MUCError>> leaveRoom(
|
||||
JID roomJid,
|
||||
) async {
|
||||
final nick = await _cacheLock.synchronized(() {
|
||||
final nick = _mucRoomCache[roomJid]?.nick;
|
||||
_mucRoomCache.remove(roomJid);
|
||||
return nick;
|
||||
});
|
||||
if (nick == null) {
|
||||
return Result(RoomNotJoinedError());
|
||||
}
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.presence(
|
||||
to: roomJid.withResource(nick).toString(),
|
||||
type: 'unavailable',
|
||||
),
|
||||
awaitable: false,
|
||||
),
|
||||
);
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
Future<RoomState?> getRoomState(JID roomJid) async {
|
||||
return _cacheLock.synchronized(() => _mucRoomCache[roomJid]);
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onPresence(
|
||||
Stanza presence,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
if (presence.from == null) {
|
||||
logger.finest('Ignoring presence as it has no from attribute');
|
||||
return state;
|
||||
}
|
||||
|
||||
final from = JID.fromString(presence.from!);
|
||||
final bareFrom = from.toBare();
|
||||
return _cacheLock.synchronized(() {
|
||||
logger.finest('Lock aquired for presence from ${presence.from}');
|
||||
final room = _mucRoomCache[bareFrom];
|
||||
if (room == null) {
|
||||
logger.finest('Ignoring presence as it does not belong to a room');
|
||||
return state;
|
||||
}
|
||||
|
||||
if (from.resource.isEmpty) {
|
||||
// TODO(Unknown): Handle presence from the room itself.
|
||||
logger.finest('Ignoring presence as it has no resource');
|
||||
return state;
|
||||
}
|
||||
|
||||
if (presence.type == 'error') {
|
||||
final errorTag = presence.firstTag('error')!;
|
||||
final error = errorTag.firstTagByXmlns(fullStanzaXmlns)!;
|
||||
Result<bool, MUCError> result;
|
||||
if (error.tag == 'forbidden') {
|
||||
result = Result(JoinForbiddenError());
|
||||
} else {
|
||||
result = Result(MUCUnspecificError());
|
||||
}
|
||||
|
||||
_mucRoomCache.remove(bareFrom);
|
||||
_mucRoomJoinCompleter[bareFrom]!.complete(result);
|
||||
_mucRoomJoinCompleter.remove(bareFrom);
|
||||
return StanzaHandlerData(
|
||||
true,
|
||||
false,
|
||||
presence,
|
||||
state.extensions,
|
||||
);
|
||||
}
|
||||
|
||||
final x = presence.firstTag('x', xmlns: mucUserXmlns)!;
|
||||
final item = x.firstTag('item')!;
|
||||
final statuses = x
|
||||
.findTags('status')
|
||||
.map((s) => s.attributes['code']! as String)
|
||||
.toList();
|
||||
final role = Role.fromString(
|
||||
item.attributes['role']! as String,
|
||||
);
|
||||
final affiliation = Affiliation.fromString(
|
||||
item.attributes['affiliation']! as String,
|
||||
);
|
||||
|
||||
if (statuses.contains(selfPresenceStatus)) {
|
||||
if (room.joined) {
|
||||
if (room.nick != from.resource ||
|
||||
room.affiliation != affiliation ||
|
||||
room.role != role) {
|
||||
// Notify us of the changed data.
|
||||
getAttributes().sendEvent(
|
||||
OwnDataChangedEvent(
|
||||
bareFrom,
|
||||
from.resource,
|
||||
affiliation,
|
||||
role,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the data to make sure we're in sync with the MUC.
|
||||
room
|
||||
..nick = from.resource
|
||||
..affiliation = affiliation
|
||||
..role = role;
|
||||
logger.finest('Self-presence handled');
|
||||
return StanzaHandlerData(
|
||||
true,
|
||||
false,
|
||||
presence,
|
||||
state.extensions,
|
||||
);
|
||||
}
|
||||
|
||||
if (presence.attributes['type'] == 'unavailable') {
|
||||
if (role == Role.none) {
|
||||
// Cannot happen while joining, so we assume we are joined
|
||||
assert(
|
||||
room.joined,
|
||||
'Should not receive unavailable with role="none" while joining',
|
||||
);
|
||||
room.members.remove(from.resource);
|
||||
getAttributes().sendEvent(
|
||||
MemberLeftEvent(
|
||||
bareFrom,
|
||||
from.resource,
|
||||
),
|
||||
);
|
||||
} else if (statuses.contains(nicknameChangedStatus)) {
|
||||
assert(
|
||||
room.joined,
|
||||
'Should not receive nick change while joining',
|
||||
);
|
||||
final newNick = item.attributes['nick']! as String;
|
||||
final member = RoomMember(
|
||||
newNick,
|
||||
Affiliation.fromString(
|
||||
item.attributes['affiliation']! as String,
|
||||
),
|
||||
role,
|
||||
);
|
||||
|
||||
// Remove the old member.
|
||||
room.members.remove(from.resource);
|
||||
|
||||
// Add the "new" member".
|
||||
room.members[newNick] = member;
|
||||
|
||||
// Trigger an event.
|
||||
getAttributes().sendEvent(
|
||||
MemberChangedNickEvent(
|
||||
bareFrom,
|
||||
from.resource,
|
||||
newNick,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final member = RoomMember(
|
||||
from.resource,
|
||||
Affiliation.fromString(
|
||||
item.attributes['affiliation']! as String,
|
||||
),
|
||||
role,
|
||||
);
|
||||
logger.finest('Got presence from ${from.resource} in $bareFrom');
|
||||
if (room.joined) {
|
||||
if (room.members.containsKey(from.resource)) {
|
||||
getAttributes().sendEvent(
|
||||
MemberChangedEvent(
|
||||
bareFrom,
|
||||
member,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
getAttributes().sendEvent(
|
||||
MemberJoinedEvent(
|
||||
bareFrom,
|
||||
member,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
room.members[from.resource] = member;
|
||||
logger.finest('${from.resource} added to the member list');
|
||||
}
|
||||
|
||||
logger.finest('Ran through');
|
||||
return StanzaHandlerData(
|
||||
true,
|
||||
false,
|
||||
presence,
|
||||
state.extensions,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onMessageSent(
|
||||
Stanza message,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
if (message.to == null) {
|
||||
return state;
|
||||
}
|
||||
final toJid = JID.fromString(message.to!);
|
||||
|
||||
return _cacheLock.synchronized(() {
|
||||
if (!_mucRoomCache.containsKey(toJid)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
_mucRoomCache[toJid]!.pendingMessages.add(
|
||||
(message.id!, state.extensions.get<StableIdData>()?.originId),
|
||||
);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onMessage(
|
||||
Stanza message,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final fromJid = JID.fromString(message.from!);
|
||||
final roomJid = fromJid.toBare();
|
||||
return _cacheLock.synchronized(() {
|
||||
logger.finest('Lock aquired for message from ${message.from}');
|
||||
final roomState = _mucRoomCache[roomJid];
|
||||
if (roomState == null) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (message.type == 'groupchat' && message.firstTag('subject') != null) {
|
||||
// The room subject marks the end of the join flow.
|
||||
if (!roomState.joined) {
|
||||
// Mark the room as joined.
|
||||
_mucRoomCache[roomJid]!.joined = true;
|
||||
_mucRoomJoinCompleter[roomJid]!.complete(
|
||||
const Result(true),
|
||||
);
|
||||
_mucRoomJoinCompleter.remove(roomJid);
|
||||
logger.finest('$roomJid is now joined');
|
||||
}
|
||||
|
||||
// TODO(Unknown): Signal the subject?
|
||||
|
||||
return StanzaHandlerData(
|
||||
true,
|
||||
false,
|
||||
message,
|
||||
state.extensions,
|
||||
);
|
||||
} else {
|
||||
if (!roomState.joined) {
|
||||
// Ignore the discussion history.
|
||||
return StanzaHandlerData(
|
||||
true,
|
||||
false,
|
||||
message,
|
||||
state.extensions,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if this is the message reflection.
|
||||
if (message.id == null) {
|
||||
return state;
|
||||
}
|
||||
final pending =
|
||||
(message.id!, state.extensions.get<StableIdData>()?.originId);
|
||||
if (fromJid.resource == roomState.nick &&
|
||||
roomState.pendingMessages.contains(pending)) {
|
||||
// Silently drop the message.
|
||||
roomState.pendingMessages.remove(pending);
|
||||
|
||||
// TODO(Unknown): Maybe send an event stating that we received the reflection.
|
||||
return StanzaHandlerData(
|
||||
true,
|
||||
false,
|
||||
message,
|
||||
state.extensions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
@@ -7,7 +8,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
|
||||
abstract class VCardError {}
|
||||
|
||||
@@ -38,7 +38,7 @@ class VCardManager extends XmppManagerBase {
|
||||
tagName: 'x',
|
||||
tagXmlns: vCardTempUpdate,
|
||||
callback: _onPresence,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -56,31 +56,13 @@ class VCardManager extends XmppManagerBase {
|
||||
final x = presence.firstTag('x', xmlns: vCardTempUpdate)!;
|
||||
final hash = x.firstTag('photo')!.innerText();
|
||||
|
||||
final from = JID.fromString(presence.from!).toBare().toString();
|
||||
final lastHash = _lastHash[from];
|
||||
if (lastHash != hash) {
|
||||
_lastHash[from] = hash;
|
||||
final vcardResult = await requestVCard(from);
|
||||
|
||||
if (vcardResult.isType<VCard>()) {
|
||||
final binval = vcardResult.get<VCard>().photo?.binval;
|
||||
if (binval != null) {
|
||||
getAttributes().sendEvent(
|
||||
AvatarUpdatedEvent(
|
||||
jid: from,
|
||||
base64: binval,
|
||||
hash: hash,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
logger.warning('No avatar data found');
|
||||
}
|
||||
} else {
|
||||
logger.warning('Failed to retrieve vCard for $from');
|
||||
}
|
||||
}
|
||||
|
||||
return state.copyWith(done: true);
|
||||
getAttributes().sendEvent(
|
||||
VCardAvatarUpdatedEvent(
|
||||
JID.fromString(presence.from!),
|
||||
hash,
|
||||
),
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
VCardPhoto? _parseVCardPhoto(XMLNode? node) {
|
||||
@@ -102,20 +84,22 @@ class VCardManager extends XmppManagerBase {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Result<VCardError, VCard>> requestVCard(String jid) async {
|
||||
final result = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
to: jid,
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'vCard',
|
||||
xmlns: vCardTempXmlns,
|
||||
)
|
||||
],
|
||||
Future<Result<VCardError, VCard>> requestVCard(JID jid) async {
|
||||
final result = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
to: jid.toString(),
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'vCard',
|
||||
xmlns: vCardTempXmlns,
|
||||
),
|
||||
],
|
||||
),
|
||||
encrypted: true,
|
||||
),
|
||||
encrypted: true,
|
||||
);
|
||||
))!;
|
||||
|
||||
if (result.attributes['type'] != 'result') {
|
||||
return Result(UnknownVCardError());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
@@ -9,7 +9,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
@@ -39,26 +38,20 @@ class PubSubPublishOptions {
|
||||
varAttr: 'FORM_TYPE',
|
||||
type: 'hidden',
|
||||
),
|
||||
...accessModel != null
|
||||
? [
|
||||
DataFormField(
|
||||
options: [],
|
||||
isRequired: false,
|
||||
values: [accessModel!],
|
||||
varAttr: 'pubsub#access_model',
|
||||
)
|
||||
]
|
||||
: [],
|
||||
...maxItems != null
|
||||
? [
|
||||
DataFormField(
|
||||
options: [],
|
||||
isRequired: false,
|
||||
values: [maxItems!],
|
||||
varAttr: 'pubsub#max_items',
|
||||
),
|
||||
]
|
||||
: [],
|
||||
if (accessModel != null)
|
||||
DataFormField(
|
||||
options: [],
|
||||
isRequired: false,
|
||||
values: [accessModel!],
|
||||
varAttr: 'pubsub#access_model',
|
||||
),
|
||||
if (maxItems != null)
|
||||
DataFormField(
|
||||
options: [],
|
||||
isRequired: false,
|
||||
values: [maxItems!],
|
||||
varAttr: 'pubsub#max_items',
|
||||
),
|
||||
],
|
||||
).toXml();
|
||||
}
|
||||
@@ -88,7 +81,7 @@ class PubSubManager extends XmppManagerBase {
|
||||
tagName: 'event',
|
||||
tagXmlns: pubsubEventXmlns,
|
||||
callback: _onPubsubMessage,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -114,10 +107,10 @@ class PubSubManager extends XmppManagerBase {
|
||||
),
|
||||
);
|
||||
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
Future<int> _getNodeItemCount(String jid, String node) async {
|
||||
Future<int> _getNodeItemCount(JID jid, String node) async {
|
||||
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
|
||||
final response = await dm.discoItemsQuery(jid, node: node);
|
||||
var count = 0;
|
||||
@@ -136,7 +129,7 @@ class PubSubManager extends XmppManagerBase {
|
||||
// with the requested configuration.
|
||||
@visibleForTesting
|
||||
Future<PubSubPublishOptions> preprocessPublishOptions(
|
||||
String jid,
|
||||
JID jid,
|
||||
String node,
|
||||
PubSubPublishOptions options,
|
||||
) async {
|
||||
@@ -179,29 +172,32 @@ class PubSubManager extends XmppManagerBase {
|
||||
return options;
|
||||
}
|
||||
|
||||
Future<Result<PubSubError, bool>> subscribe(String jid, String node) async {
|
||||
Future<Result<PubSubError, bool>> subscribe(JID jid, String node) async {
|
||||
final attrs = getAttributes();
|
||||
final result = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: jid,
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'subscribe',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
'jid': attrs.getFullJID().toBare().toString(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
final result = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: jid.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'subscribe',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
'jid': attrs.getFullJID().toBare().toString(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shouldEncrypt: false,
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
if (result.attributes['type'] != 'result') {
|
||||
return Result(UnknownPubSubError());
|
||||
@@ -220,29 +216,32 @@ class PubSubManager extends XmppManagerBase {
|
||||
return Result(subscription.attributes['subscription'] == 'subscribed');
|
||||
}
|
||||
|
||||
Future<Result<PubSubError, bool>> unsubscribe(String jid, String node) async {
|
||||
Future<Result<PubSubError, bool>> unsubscribe(JID jid, String node) async {
|
||||
final attrs = getAttributes();
|
||||
final result = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: jid,
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'unsubscribe',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
'jid': attrs.getFullJID().toBare().toString(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
final result = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: jid.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'unsubscribe',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
'jid': attrs.getFullJID().toBare().toString(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shouldEncrypt: false,
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
if (result.attributes['type'] != 'result') {
|
||||
return Result(UnknownPubSubError());
|
||||
@@ -264,7 +263,7 @@ class PubSubManager extends XmppManagerBase {
|
||||
/// Publish [payload] to the PubSub node [node] on JID [jid]. Returns true if it
|
||||
/// was successful. False otherwise.
|
||||
Future<Result<PubSubError, bool>> publish(
|
||||
String jid,
|
||||
JID jid,
|
||||
String node,
|
||||
XMLNode payload, {
|
||||
String? id,
|
||||
@@ -280,7 +279,7 @@ class PubSubManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
Future<Result<PubSubError, bool>> _publish(
|
||||
String jid,
|
||||
JID jid,
|
||||
String node,
|
||||
XMLNode payload, {
|
||||
String? id,
|
||||
@@ -293,38 +292,41 @@ class PubSubManager extends XmppManagerBase {
|
||||
pubOptions = await preprocessPublishOptions(jid, node, options);
|
||||
}
|
||||
|
||||
final result = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: jid,
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'publish',
|
||||
attributes: <String, String>{'node': node},
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: id != null
|
||||
? <String, String>{'id': id}
|
||||
: <String, String>{},
|
||||
children: [payload],
|
||||
)
|
||||
],
|
||||
),
|
||||
if (pubOptions != null)
|
||||
final result = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: jid.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'publish-options',
|
||||
children: [pubOptions.toXml()],
|
||||
tag: 'publish',
|
||||
attributes: <String, String>{'node': node},
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: {
|
||||
if (id != null) 'id': id,
|
||||
},
|
||||
children: [payload],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
if (pubOptions != null)
|
||||
XMLNode(
|
||||
tag: 'publish-options',
|
||||
children: [pubOptions.toXml()],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shouldEncrypt: false,
|
||||
),
|
||||
);
|
||||
))!;
|
||||
if (result.attributes['type'] != 'result') {
|
||||
final error = getPubSubError(result);
|
||||
|
||||
@@ -392,24 +394,34 @@ class PubSubManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
Future<Result<PubSubError, List<PubSubItem>>> getItems(
|
||||
String jid,
|
||||
String node,
|
||||
) async {
|
||||
final result = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
to: jid,
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(tag: 'items', attributes: <String, String>{'node': node}),
|
||||
],
|
||||
)
|
||||
],
|
||||
JID jid,
|
||||
String node, {
|
||||
int? maxItems,
|
||||
}) async {
|
||||
final result = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
to: jid.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'items',
|
||||
attributes: {
|
||||
'node': node,
|
||||
if (maxItems != null) 'max_items': maxItems.toString(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shouldEncrypt: false,
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
if (result.attributes['type'] != 'result') {
|
||||
return Result(getPubSubError(result));
|
||||
@@ -432,34 +444,37 @@ class PubSubManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
Future<Result<PubSubError, PubSubItem>> getItem(
|
||||
String jid,
|
||||
JID jid,
|
||||
String node,
|
||||
String id,
|
||||
) async {
|
||||
final result = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
to: jid,
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'items',
|
||||
attributes: <String, String>{'node': node},
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: <String, String>{'id': id},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
final result = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
to: jid.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'items',
|
||||
attributes: <String, String>{'node': node},
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: <String, String>{'id': id},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shouldEncrypt: false,
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
if (result.attributes['type'] != 'result') {
|
||||
return Result(getPubSubError(result));
|
||||
@@ -481,60 +496,66 @@ class PubSubManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
Future<Result<PubSubError, bool>> configure(
|
||||
String jid,
|
||||
JID jid,
|
||||
String node,
|
||||
PubSubPublishOptions options,
|
||||
) async {
|
||||
final attrs = getAttributes();
|
||||
|
||||
// Request the form
|
||||
final form = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
to: jid,
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubOwnerXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'configure',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
final form = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
to: jid.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubOwnerXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'configure',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shouldEncrypt: false,
|
||||
),
|
||||
);
|
||||
))!;
|
||||
if (form.attributes['type'] != 'result') {
|
||||
return Result(getPubSubError(form));
|
||||
}
|
||||
|
||||
final submit = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: jid,
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubOwnerXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'configure',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
},
|
||||
children: [
|
||||
options.toXml(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
final submit = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: jid.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubOwnerXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'configure',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
},
|
||||
children: [
|
||||
options.toXml(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shouldEncrypt: false,
|
||||
),
|
||||
);
|
||||
))!;
|
||||
if (submit.attributes['type'] != 'result') {
|
||||
return Result(getPubSubError(form));
|
||||
}
|
||||
@@ -543,28 +564,31 @@ class PubSubManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
Future<Result<PubSubError, bool>> delete(JID host, String node) async {
|
||||
final request = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: host.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubOwnerXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'delete',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
final request = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: host.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubOwnerXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'delete',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shouldEncrypt: false,
|
||||
),
|
||||
) as Stanza;
|
||||
))!;
|
||||
|
||||
if (request.type != 'result') {
|
||||
if (request.attributes['type'] != 'result') {
|
||||
// TODO(Unknown): Be more specific
|
||||
return Result(UnknownPubSubError());
|
||||
}
|
||||
@@ -577,36 +601,39 @@ class PubSubManager extends XmppManagerBase {
|
||||
String node,
|
||||
String itemId,
|
||||
) async {
|
||||
final request = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: host.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'retract',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
},
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: <String, String>{
|
||||
'id': itemId,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
final request = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
to: host.toString(),
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'pubsub',
|
||||
xmlns: pubsubXmlns,
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'retract',
|
||||
attributes: <String, String>{
|
||||
'node': node,
|
||||
},
|
||||
children: [
|
||||
XMLNode(
|
||||
tag: 'item',
|
||||
attributes: <String, String>{
|
||||
'id': itemId,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
shouldEncrypt: false,
|
||||
),
|
||||
) as Stanza;
|
||||
))!;
|
||||
|
||||
if (request.type != 'result') {
|
||||
if (request.attributes['type'] != 'result') {
|
||||
// TODO(Unknown): Be more specific
|
||||
return Result(UnknownPubSubError());
|
||||
}
|
||||
|
||||
@@ -2,32 +2,32 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
/// A data class representing the jabber:x:oob tag.
|
||||
class OOBData {
|
||||
const OOBData({this.url, this.desc});
|
||||
class OOBData implements StanzaHandlerExtension {
|
||||
const OOBData(this.url, this.desc);
|
||||
|
||||
/// The communicated URL of the OOB data
|
||||
final String? url;
|
||||
|
||||
/// The description of the url.
|
||||
final String? desc;
|
||||
}
|
||||
|
||||
XMLNode constructOOBNode(OOBData data) {
|
||||
final children = List<XMLNode>.empty(growable: true);
|
||||
|
||||
if (data.url != null) {
|
||||
children.add(XMLNode(tag: 'url', text: data.url));
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'x',
|
||||
xmlns: oobDataXmlns,
|
||||
children: [
|
||||
if (url != null) XMLNode(tag: 'url', text: url),
|
||||
if (desc != null) XMLNode(tag: 'desc', text: desc),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (data.desc != null) {
|
||||
children.add(XMLNode(tag: 'desc', text: data.desc));
|
||||
}
|
||||
|
||||
return XMLNode.xmlns(
|
||||
tag: 'x',
|
||||
xmlns: oobDataXmlns,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
class OOBManager extends XmppManagerBase {
|
||||
@@ -45,7 +45,7 @@ class OOBManager extends XmppManagerBase {
|
||||
callback: _onMessage,
|
||||
// Before the message manager
|
||||
priority: -99,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -59,11 +59,33 @@ class OOBManager extends XmppManagerBase {
|
||||
final url = x.firstTag('url');
|
||||
final desc = x.firstTag('desc');
|
||||
|
||||
return state.copyWith(
|
||||
oob: OOBData(
|
||||
url: url?.innerText(),
|
||||
desc: desc?.innerText(),
|
||||
),
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
OOBData(
|
||||
url?.innerText(),
|
||||
desc?.innerText(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final data = extensions.get<OOBData>();
|
||||
return data != null
|
||||
? [
|
||||
data.toXML(),
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0060/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0060/xep_0060.dart';
|
||||
|
||||
@@ -14,10 +13,20 @@ abstract class AvatarError {}
|
||||
|
||||
class UnknownAvatarError extends AvatarError {}
|
||||
|
||||
class UserAvatar {
|
||||
const UserAvatar({required this.base64, required this.hash});
|
||||
/// The result of a successful query of a users avatar.
|
||||
class UserAvatarData {
|
||||
const UserAvatarData(this.base64, this.hash);
|
||||
|
||||
/// The base64-encoded avatar data.
|
||||
final String base64;
|
||||
|
||||
/// The SHA-1 hash of the raw avatar data.
|
||||
final String hash;
|
||||
|
||||
/// The raw avatar data.
|
||||
/// NOTE: Remove newlines because "Line feeds SHOULD NOT be added but MUST be accepted"
|
||||
/// (https://xmpp.org/extensions/xep-0084.html#proto-data).
|
||||
List<int> get data => base64Decode(base64.replaceAll('\n', ''));
|
||||
}
|
||||
|
||||
class UserAvatarMetadata {
|
||||
@@ -26,21 +35,40 @@ class UserAvatarMetadata {
|
||||
this.length,
|
||||
this.width,
|
||||
this.height,
|
||||
this.mime,
|
||||
this.type,
|
||||
this.url,
|
||||
);
|
||||
|
||||
/// The amount of bytes in the file
|
||||
factory UserAvatarMetadata.fromXML(XMLNode node) {
|
||||
assert(node.tag == 'info', 'node must be an <info /> element');
|
||||
|
||||
final width = node.attributes['width'] as String?;
|
||||
final height = node.attributes['height'] as String?;
|
||||
return UserAvatarMetadata(
|
||||
node.attributes['id']! as String,
|
||||
int.parse(node.attributes['bytes']! as String),
|
||||
width != null ? int.parse(width) : null,
|
||||
height != null ? int.parse(height) : null,
|
||||
node.attributes['type']! as String,
|
||||
node.attributes['url'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// The amount of bytes in the file.
|
||||
final int length;
|
||||
|
||||
/// The identifier of the avatar
|
||||
/// The identifier of the avatar.
|
||||
final String id;
|
||||
|
||||
/// Image proportions
|
||||
final int width;
|
||||
final int height;
|
||||
/// Image proportions.
|
||||
final int? width;
|
||||
final int? height;
|
||||
|
||||
/// The MIME type of the avatar
|
||||
final String mime;
|
||||
/// The URL where the avatar can be found.
|
||||
final String? url;
|
||||
|
||||
/// The MIME type of the avatar.
|
||||
final String type;
|
||||
}
|
||||
|
||||
/// NOTE: This class requires a PubSubManager
|
||||
@@ -50,13 +78,18 @@ class UserAvatarManager extends XmppManagerBase {
|
||||
PubSubManager _getPubSubManager() =>
|
||||
getAttributes().getManagerById(pubsubManager)! as PubSubManager;
|
||||
|
||||
@override
|
||||
List<String> getDiscoFeatures() => [
|
||||
'$userAvatarMetadataXmlns+notify',
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> onXmppEvent(XmppEvent event) async {
|
||||
if (event is PubSubNotificationEvent) {
|
||||
if (event.item.node != userAvatarDataXmlns) return;
|
||||
if (event.item.node != userAvatarMetadataXmlns) return;
|
||||
|
||||
if (event.item.payload.tag != 'data' ||
|
||||
event.item.payload.attributes['xmlns'] != userAvatarDataXmlns) {
|
||||
if (event.item.payload.tag != 'metadata' ||
|
||||
event.item.payload.attributes['xmlns'] != userAvatarMetadataXmlns) {
|
||||
logger.warning(
|
||||
'Received avatar update from ${event.from} but the payload is invalid. Ignoring...',
|
||||
);
|
||||
@@ -64,10 +97,12 @@ class UserAvatarManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
getAttributes().sendEvent(
|
||||
AvatarUpdatedEvent(
|
||||
jid: event.from,
|
||||
base64: event.item.payload.innerText(),
|
||||
hash: event.item.id,
|
||||
UserAvatarUpdatedEvent(
|
||||
JID.fromString(event.from),
|
||||
event.item.payload
|
||||
.findTags('metadata', xmlns: userAvatarMetadataXmlns)
|
||||
.map(UserAvatarMetadata.fromXML)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -79,20 +114,44 @@ class UserAvatarManager extends XmppManagerBase {
|
||||
|
||||
/// Requests the avatar from [jid]. Returns the avatar data if the request was
|
||||
/// successful. Null otherwise
|
||||
Future<Result<AvatarError, UserAvatar>> getUserAvatar(String jid) async {
|
||||
Future<Result<AvatarError, UserAvatarData>> getUserAvatarData(
|
||||
JID jid,
|
||||
String id,
|
||||
) async {
|
||||
final pubsub = _getPubSubManager();
|
||||
final resultsRaw = await pubsub.getItems(jid, userAvatarDataXmlns);
|
||||
final resultRaw = await pubsub.getItem(jid, userAvatarDataXmlns, id);
|
||||
if (resultRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
|
||||
|
||||
final result = resultRaw.get<PubSubItem>();
|
||||
return Result(
|
||||
UserAvatarData(
|
||||
result.payload.innerText(),
|
||||
id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Attempts to fetch the latest item from the User Avatar metadata node. Returns the list of
|
||||
/// metadata contained within it. The list may be empty.
|
||||
///
|
||||
/// If an error occured, returns an [AvatarError].
|
||||
Future<Result<AvatarError, List<UserAvatarMetadata>>> getLatestMetadata(
|
||||
JID jid,
|
||||
) async {
|
||||
final resultsRaw = await _getPubSubManager()
|
||||
.getItems(jid, userAvatarMetadataXmlns, maxItems: 1);
|
||||
if (resultsRaw.isType<PubSubError>()) return Result(UnknownAvatarError());
|
||||
|
||||
final results = resultsRaw.get<List<PubSubItem>>();
|
||||
if (results.isEmpty) return Result(UnknownAvatarError());
|
||||
if (results.isEmpty) {
|
||||
return Result(UnknownAvatarError());
|
||||
}
|
||||
|
||||
final item = results[0];
|
||||
return Result(
|
||||
UserAvatar(
|
||||
base64: item.payload.innerText(),
|
||||
hash: item.id,
|
||||
),
|
||||
results.first.payload
|
||||
.findTags('info')
|
||||
.map(UserAvatarMetadata.fromXML)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,7 +165,7 @@ class UserAvatarManager extends XmppManagerBase {
|
||||
) async {
|
||||
final pubsub = _getPubSubManager();
|
||||
final result = await pubsub.publish(
|
||||
getAttributes().getFullJID().toBare().toString(),
|
||||
getAttributes().getFullJID().toBare(),
|
||||
userAvatarDataXmlns,
|
||||
XMLNode.xmlns(
|
||||
tag: 'data',
|
||||
@@ -133,7 +192,7 @@ class UserAvatarManager extends XmppManagerBase {
|
||||
) async {
|
||||
final pubsub = _getPubSubManager();
|
||||
final result = await pubsub.publish(
|
||||
getAttributes().getFullJID().toBare().toString(),
|
||||
getAttributes().getFullJID().toBare(),
|
||||
userAvatarMetadataXmlns,
|
||||
XMLNode.xmlns(
|
||||
tag: 'metadata',
|
||||
@@ -145,7 +204,7 @@ class UserAvatarManager extends XmppManagerBase {
|
||||
'bytes': metadata.length.toString(),
|
||||
'height': metadata.height.toString(),
|
||||
'width': metadata.width.toString(),
|
||||
'type': metadata.mime,
|
||||
'type': metadata.type,
|
||||
'id': metadata.id,
|
||||
},
|
||||
),
|
||||
@@ -162,34 +221,16 @@ class UserAvatarManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
/// Subscribe the data and metadata node of [jid].
|
||||
Future<Result<AvatarError, bool>> subscribe(String jid) async {
|
||||
Future<Result<AvatarError, bool>> subscribe(JID jid) async {
|
||||
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
|
||||
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
/// Unsubscribe the data and metadata node of [jid].
|
||||
Future<Result<AvatarError, bool>> unsubscribe(String jid) async {
|
||||
Future<Result<AvatarError, bool>> unsubscribe(JID jid) async {
|
||||
await _getPubSubManager().subscribe(jid, userAvatarMetadataXmlns);
|
||||
|
||||
return const Result(true);
|
||||
}
|
||||
|
||||
/// Returns the PubSub Id of an avatar after doing a disco#items query.
|
||||
/// Note that this assumes that there is only one (1) item published on
|
||||
/// the node.
|
||||
Future<Result<AvatarError, String>> getAvatarId(String jid) async {
|
||||
final disco = getAttributes().getManagerById(discoManager)! as DiscoManager;
|
||||
final response = await disco.discoItemsQuery(
|
||||
jid,
|
||||
node: userAvatarDataXmlns,
|
||||
shouldEncrypt: false,
|
||||
);
|
||||
if (response.isType<DiscoError>()) return Result(UnknownAvatarError());
|
||||
|
||||
final items = response.get<List<DiscoItem>>();
|
||||
if (items.isEmpty) return Result(UnknownAvatarError());
|
||||
|
||||
return Result(items.first.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,43 +2,59 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
enum ChatState { active, composing, paused, inactive, gone }
|
||||
enum ChatState implements StanzaHandlerExtension {
|
||||
active,
|
||||
composing,
|
||||
paused,
|
||||
inactive,
|
||||
gone;
|
||||
|
||||
ChatState chatStateFromString(String raw) {
|
||||
switch (raw) {
|
||||
case 'active':
|
||||
{
|
||||
factory ChatState.fromName(String state) {
|
||||
switch (state) {
|
||||
case 'active':
|
||||
return ChatState.active;
|
||||
}
|
||||
case 'composing':
|
||||
{
|
||||
case 'composing':
|
||||
return ChatState.composing;
|
||||
}
|
||||
case 'paused':
|
||||
{
|
||||
case 'paused':
|
||||
return ChatState.paused;
|
||||
}
|
||||
case 'inactive':
|
||||
{
|
||||
case 'inactive':
|
||||
return ChatState.inactive;
|
||||
}
|
||||
case 'gone':
|
||||
{
|
||||
case 'gone':
|
||||
return ChatState.gone;
|
||||
}
|
||||
default:
|
||||
{
|
||||
return ChatState.gone;
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception('Invalid chat state $state');
|
||||
}
|
||||
|
||||
String toName() {
|
||||
switch (this) {
|
||||
case ChatState.active:
|
||||
return 'active';
|
||||
case ChatState.composing:
|
||||
return 'composing';
|
||||
case ChatState.paused:
|
||||
return 'paused';
|
||||
case ChatState.inactive:
|
||||
return 'inactive';
|
||||
case ChatState.gone:
|
||||
return 'gone';
|
||||
}
|
||||
}
|
||||
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: toName(),
|
||||
xmlns: chatStateXmlns,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String chatStateToString(ChatState state) => state.toString().split('.').last;
|
||||
|
||||
class ChatStateManager extends XmppManagerBase {
|
||||
ChatStateManager() : super(chatStateManager);
|
||||
|
||||
@@ -53,7 +69,7 @@ class ChatStateManager extends XmppManagerBase {
|
||||
callback: _onChatStateReceived,
|
||||
// Before the message handler
|
||||
priority: -99,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -64,58 +80,55 @@ class ChatStateManager extends XmppManagerBase {
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final element = state.stanza.firstTagByXmlns(chatStateXmlns)!;
|
||||
ChatState? chatState;
|
||||
|
||||
switch (element.tag) {
|
||||
case 'active':
|
||||
{
|
||||
chatState = ChatState.active;
|
||||
}
|
||||
break;
|
||||
case 'composing':
|
||||
{
|
||||
chatState = ChatState.composing;
|
||||
}
|
||||
break;
|
||||
case 'paused':
|
||||
{
|
||||
chatState = ChatState.paused;
|
||||
}
|
||||
break;
|
||||
case 'inactive':
|
||||
{
|
||||
chatState = ChatState.inactive;
|
||||
}
|
||||
break;
|
||||
case 'gone':
|
||||
{
|
||||
chatState = ChatState.gone;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
{
|
||||
logger.warning("Received invalid chat state '${element.tag}'");
|
||||
}
|
||||
try {
|
||||
state.extensions.set(ChatState.fromName(element.tag));
|
||||
} catch (_) {
|
||||
logger.finest('Ignoring invalid chat state ${element.tag}');
|
||||
}
|
||||
|
||||
return state.copyWith(chatState: chatState);
|
||||
return state;
|
||||
}
|
||||
|
||||
/// Send a chat state notification to [to]. You can specify the type attribute
|
||||
/// of the message with [messageType].
|
||||
void sendChatState(
|
||||
Future<void> sendChatState(
|
||||
ChatState state,
|
||||
String to, {
|
||||
String messageType = 'chat',
|
||||
}) {
|
||||
final tagName = state.toString().split('.').last;
|
||||
|
||||
getAttributes().sendStanza(
|
||||
Stanza.message(
|
||||
to: to,
|
||||
type: messageType,
|
||||
children: [XMLNode.xmlns(tag: tagName, xmlns: chatStateXmlns)],
|
||||
}) async {
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.message(
|
||||
to: to,
|
||||
type: messageType,
|
||||
children: [
|
||||
state.toXML(),
|
||||
],
|
||||
),
|
||||
awaitable: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final data = extensions.get<ChatState>();
|
||||
return data != null
|
||||
? [
|
||||
data.toXML(),
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/presence.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_4790.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/list.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0004.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0414.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0300.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
@immutable
|
||||
class CapabilityHashInfo {
|
||||
const CapabilityHashInfo(this.ver, this.node, this.hash);
|
||||
final String ver;
|
||||
final String node;
|
||||
final String hash;
|
||||
}
|
||||
/// Given an identity [i], compute the string according to XEP-0115 § 5.1 step 2.
|
||||
String _identityString(Identity i) =>
|
||||
'${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}';
|
||||
|
||||
/// Calculates the Entitiy Capability hash according to XEP-0115 based on the
|
||||
/// disco information.
|
||||
Future<String> calculateCapabilityHash(
|
||||
HashFunction algorithm,
|
||||
DiscoInfo info,
|
||||
HashAlgorithm algorithm,
|
||||
) async {
|
||||
final buffer = StringBuffer();
|
||||
final identitiesSorted = info.identities
|
||||
.map(
|
||||
(Identity i) =>
|
||||
'${i.category}/${i.type}/${i.lang ?? ""}/${i.name ?? ""}',
|
||||
)
|
||||
.toList();
|
||||
final identitiesSorted = info.identities.map(_identityString).toList();
|
||||
// ignore: cascade_invocations
|
||||
identitiesSorted.sort(ioctetSortComparator);
|
||||
buffer.write('${identitiesSorted.join("<")}<');
|
||||
@@ -72,8 +72,11 @@ Future<String> calculateCapabilityHash(
|
||||
}
|
||||
}
|
||||
|
||||
return base64
|
||||
.encode((await algorithm.hash(utf8.encode(buffer.toString()))).bytes);
|
||||
final rawHash = await CryptographicHashManager.hashFromData(
|
||||
algorithm,
|
||||
utf8.encode(buffer.toString()),
|
||||
);
|
||||
return base64.encode(rawHash);
|
||||
}
|
||||
|
||||
/// A manager implementing the advertising of XEP-0115. It responds to the
|
||||
@@ -91,20 +94,42 @@ class EntityCapabilitiesManager extends XmppManagerBase {
|
||||
/// The cached capability hash.
|
||||
String? _capabilityHash;
|
||||
|
||||
/// Cache the mapping between the full JID and the capability hash string.
|
||||
final Map<String, String> _jidToCapHashCache = {};
|
||||
|
||||
/// Cache the mapping between capability hash string and the resulting disco#info.
|
||||
final Map<String, DiscoInfo> _capHashCache = {};
|
||||
|
||||
/// A lock guarding access to the capability hash cache.
|
||||
final Lock _cacheLock = Lock();
|
||||
|
||||
@override
|
||||
Future<bool> isSupported() async => true;
|
||||
|
||||
@override
|
||||
List<String> getDiscoFeatures() => [capsXmlns];
|
||||
List<String> getDiscoFeatures() => [
|
||||
capsXmlns,
|
||||
];
|
||||
|
||||
@override
|
||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||
StanzaHandler(
|
||||
stanzaTag: 'presence',
|
||||
tagName: 'c',
|
||||
tagXmlns: capsXmlns,
|
||||
callback: onPresence,
|
||||
priority: PresenceManager.presenceHandlerPriority + 1,
|
||||
),
|
||||
];
|
||||
|
||||
/// Computes, if required, the capability hash of the data provided by
|
||||
/// the DiscoManager.
|
||||
Future<String> getCapabilityHash() async {
|
||||
_capabilityHash ??= await calculateCapabilityHash(
|
||||
HashFunction.sha1,
|
||||
getAttributes()
|
||||
.getManagerById<DiscoManager>(discoManager)!
|
||||
.getDiscoInfo(null),
|
||||
getHashByName('sha-1')!,
|
||||
);
|
||||
|
||||
return _capabilityHash!;
|
||||
@@ -135,20 +160,220 @@ class EntityCapabilitiesManager extends XmppManagerBase {
|
||||
];
|
||||
}
|
||||
|
||||
/// If we know of [jid]'s capability hash, look up the [DiscoInfo] associated with
|
||||
/// that capability hash. If we don't know of [jid]'s capability hash, return null.
|
||||
Future<DiscoInfo?> getCachedDiscoInfoFromJid(JID jid) async {
|
||||
return _cacheLock.synchronized(() {
|
||||
final capHash = _jidToCapHashCache[jid.toString()];
|
||||
if (capHash == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _capHashCache[capHash];
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _performQuery(
|
||||
Stanza presence,
|
||||
String ver,
|
||||
String hashFunctionName,
|
||||
String capabilityNode,
|
||||
JID from,
|
||||
) async {
|
||||
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
|
||||
final discoRequest = await dm.discoInfoQuery(
|
||||
from,
|
||||
node: capabilityNode,
|
||||
);
|
||||
if (discoRequest.isType<StanzaError>()) {
|
||||
return;
|
||||
}
|
||||
final discoInfo = discoRequest.get<DiscoInfo>();
|
||||
|
||||
final hashFunction = HashFunction.maybeFromName(hashFunctionName);
|
||||
if (hashFunction == null) {
|
||||
await dm.addCachedDiscoInfo(
|
||||
MapEntry<DiscoCacheKey, DiscoInfo>(
|
||||
DiscoCacheKey(
|
||||
from,
|
||||
null,
|
||||
),
|
||||
discoInfo,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the disco#info result according to XEP-0115 § 5.4
|
||||
// > If the response includes more than one service discovery identity with the
|
||||
// > same category/type/lang/name, consider the entire response to be ill-formed.
|
||||
for (final identity in discoInfo.identities) {
|
||||
final identityString = _identityString(identity);
|
||||
if (discoInfo.identities
|
||||
.count((i) => _identityString(i) == identityString) >
|
||||
1) {
|
||||
logger.warning(
|
||||
'Malformed disco#info response: More than one equal identity',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// > If the response includes more than one service discovery feature with the same
|
||||
// > XML character data, consider the entire response to be ill-formed.
|
||||
for (final feature in discoInfo.features) {
|
||||
if (discoInfo.features.count((f) => f == feature) > 1) {
|
||||
logger.warning(
|
||||
'Malformed disco#info response: More than one equal feature',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// > If the response includes more than one extended service discovery information
|
||||
// > form with the same FORM_TYPE or the FORM_TYPE field contains more than one
|
||||
// > <value/> element with different XML character data, consider the entire response
|
||||
// > to be ill-formed.
|
||||
// >
|
||||
// > If the response includes an extended service discovery information form where
|
||||
// > the FORM_TYPE field is not of type "hidden" or the form does not include a
|
||||
// > FORM_TYPE field, ignore the form but continue processing.
|
||||
final validExtendedInfoItems = List<DataForm>.empty(growable: true);
|
||||
for (final extendedInfo in discoInfo.extendedInfo) {
|
||||
final formType = extendedInfo.getFieldByVar('FORM_TYPE');
|
||||
|
||||
// Form should have a FORM_TYPE field
|
||||
if (formType == null) {
|
||||
logger.fine('Skipping extended info as it contains no FORM_TYPE field');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we only have one unique FORM_TYPE value
|
||||
if (formType.values.length > 1) {
|
||||
if (Set<String>.from(formType.values).length > 1) {
|
||||
logger.warning(
|
||||
'Malformed disco#info response: Extended Info FORM_TYPE contains more than one value(s) of different value.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have more than one extended info forms of the same type
|
||||
final sameFormTypeFormsNumber = discoInfo.extendedInfo.count((form) {
|
||||
final type = form.getFieldByVar('FORM_TYPE')?.values.first;
|
||||
if (type == null) return false;
|
||||
|
||||
return type == formType.values.first;
|
||||
});
|
||||
if (sameFormTypeFormsNumber > 1) {
|
||||
logger.warning(
|
||||
'Malformed disco#info response: More than one Extended Disco Info forms with the same FORM_TYPE value',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the field type is hidden
|
||||
if (formType.type != 'hidden') {
|
||||
logger.fine(
|
||||
'Skipping extended info as the FORM_TYPE field is not of type "hidden"',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
validExtendedInfoItems.add(extendedInfo);
|
||||
}
|
||||
|
||||
// Validate the capability hash
|
||||
final newDiscoInfo = DiscoInfo(
|
||||
discoInfo.features,
|
||||
discoInfo.identities,
|
||||
validExtendedInfoItems,
|
||||
discoInfo.node,
|
||||
discoInfo.jid,
|
||||
);
|
||||
final computedCapabilityHash = await calculateCapabilityHash(
|
||||
hashFunction,
|
||||
newDiscoInfo,
|
||||
);
|
||||
|
||||
if (computedCapabilityHash == ver) {
|
||||
await _cacheLock.synchronized(() {
|
||||
_jidToCapHashCache[from.toString()] = ver;
|
||||
_capHashCache[ver] = newDiscoInfo;
|
||||
});
|
||||
} else {
|
||||
logger.warning(
|
||||
'Capability hash mismatch from $from: Received "$ver", expected "$computedCapabilityHash".',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<StanzaHandlerData> onPresence(
|
||||
Stanza stanza,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
if (stanza.from == null) {
|
||||
return state;
|
||||
}
|
||||
|
||||
final from = JID.fromString(stanza.from!);
|
||||
final c = stanza.firstTag('c', xmlns: capsXmlns)!;
|
||||
|
||||
final hashFunctionName = c.attributes['hash'] as String?;
|
||||
final capabilityNode = c.attributes['node'] as String?;
|
||||
final ver = c.attributes['ver'] as String?;
|
||||
if (hashFunctionName == null || capabilityNode == null || ver == null) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Check if we know of the hash
|
||||
final isCached =
|
||||
await _cacheLock.synchronized(() => _capHashCache.containsKey(ver));
|
||||
if (isCached) {
|
||||
return state;
|
||||
}
|
||||
|
||||
unawaited(
|
||||
_performQuery(
|
||||
stanza,
|
||||
ver,
|
||||
hashFunctionName,
|
||||
capabilityNode,
|
||||
from,
|
||||
),
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void injectIntoCache(JID jid, String ver, DiscoInfo info) {
|
||||
_jidToCapHashCache[jid.toString()] = ver;
|
||||
_capHashCache[ver] = info;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onXmppEvent(XmppEvent event) async {
|
||||
if (event is StreamNegotiationsDoneEvent) {
|
||||
// Clear the JID to cap. hash mapping.
|
||||
await _cacheLock.synchronized(_jidToCapHashCache.clear);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
getAttributes()
|
||||
.getManagerById<DiscoManager>(discoManager)!
|
||||
.registerInfoCallback(
|
||||
.getManagerById<DiscoManager>(discoManager)
|
||||
?.registerInfoCallback(
|
||||
await _getNode(),
|
||||
_onInfoQuery,
|
||||
);
|
||||
|
||||
getAttributes()
|
||||
.getManagerById<PresenceManager>(presenceManager)!
|
||||
.registerPreSendCallback(
|
||||
.getManagerById<PresenceManager>(presenceManager)
|
||||
?.registerPreSendCallback(
|
||||
_prePresenceSent,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,23 +4,43 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
XMLNode makeMessageDeliveryRequest() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'request',
|
||||
xmlns: deliveryXmlns,
|
||||
);
|
||||
class MessageDeliveryReceiptData implements StanzaHandlerExtension {
|
||||
const MessageDeliveryReceiptData(this.receiptRequested);
|
||||
|
||||
/// Indicates whether a delivery receipt is requested or not.
|
||||
final bool receiptRequested;
|
||||
|
||||
XMLNode toXML() {
|
||||
assert(
|
||||
receiptRequested,
|
||||
'This method makes little sense with receiptRequested == false',
|
||||
);
|
||||
return XMLNode.xmlns(
|
||||
tag: 'request',
|
||||
xmlns: deliveryXmlns,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
XMLNode makeMessageDeliveryResponse(String id) {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'received',
|
||||
xmlns: deliveryXmlns,
|
||||
attributes: {'id': id},
|
||||
);
|
||||
class MessageDeliveryReceivedData implements StanzaHandlerExtension {
|
||||
const MessageDeliveryReceivedData(this.id);
|
||||
|
||||
/// The stanza id of the message we received.
|
||||
final String id;
|
||||
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'received',
|
||||
xmlns: deliveryXmlns,
|
||||
attributes: {'id': id},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageDeliveryReceiptManager extends XmppManagerBase {
|
||||
@@ -46,7 +66,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
|
||||
callback: _onDeliveryRequestReceived,
|
||||
// Before the message handler
|
||||
priority: -99,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -56,7 +76,7 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
|
||||
Stanza message,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
return state.copyWith(deliveryReceiptRequested: true);
|
||||
return state..extensions.set(const MessageDeliveryReceiptData(true));
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onDeliveryReceiptReceived(
|
||||
@@ -64,16 +84,16 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final received = message.firstTag('received', xmlns: deliveryXmlns)!;
|
||||
for (final item in message.children) {
|
||||
if (!['origin-id', 'stanza-id', 'delay', 'store', 'received']
|
||||
.contains(item.tag)) {
|
||||
logger.info(
|
||||
"Won't handle stanza as delivery receipt because we found an '${item.tag}' element",
|
||||
);
|
||||
// for (final item in message.children) {
|
||||
// if (!['origin-id', 'stanza-id', 'delay', 'store', 'received']
|
||||
// .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);
|
||||
// }
|
||||
// }
|
||||
|
||||
getAttributes().sendEvent(
|
||||
DeliveryReceiptReceivedEvent(
|
||||
@@ -81,6 +101,27 @@ class MessageDeliveryReceiptManager extends XmppManagerBase {
|
||||
id: received.attributes['id']! as String,
|
||||
),
|
||||
);
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final data = extensions.get<MessageDeliveryReceivedData>();
|
||||
return data != null
|
||||
? [
|
||||
data.toXML(),
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class BlockingManager extends XmppManagerBase {
|
||||
tagName: 'block',
|
||||
tagXmlns: blockingXmlns,
|
||||
callback: _blockPush,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -70,7 +70,7 @@ class BlockingManager extends XmppManagerBase {
|
||||
),
|
||||
);
|
||||
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _unblockPush(
|
||||
@@ -92,43 +92,49 @@ class BlockingManager extends XmppManagerBase {
|
||||
);
|
||||
}
|
||||
|
||||
return state.copyWith(done: true);
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
Future<bool> block(List<String> items) async {
|
||||
final result = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'block',
|
||||
xmlns: blockingXmlns,
|
||||
children: items.map((item) {
|
||||
return XMLNode(
|
||||
tag: 'item',
|
||||
attributes: <String, String>{'jid': item},
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
],
|
||||
final result = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'block',
|
||||
xmlns: blockingXmlns,
|
||||
children: items.map((item) {
|
||||
return XMLNode(
|
||||
tag: 'item',
|
||||
attributes: {
|
||||
'jid': item,
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
return result.attributes['type'] == 'result';
|
||||
}
|
||||
|
||||
Future<bool> unblockAll() async {
|
||||
final result = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'unblock',
|
||||
xmlns: blockingXmlns,
|
||||
)
|
||||
],
|
||||
final result = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'unblock',
|
||||
xmlns: blockingXmlns,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
return result.attributes['type'] == 'result';
|
||||
}
|
||||
@@ -136,41 +142,47 @@ class BlockingManager extends XmppManagerBase {
|
||||
Future<bool> unblock(List<String> items) async {
|
||||
assert(items.isNotEmpty, 'The list of items to unblock must be non-empty');
|
||||
|
||||
final result = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'unblock',
|
||||
xmlns: blockingXmlns,
|
||||
children: items
|
||||
.map(
|
||||
(item) => XMLNode(
|
||||
tag: 'item',
|
||||
attributes: <String, String>{'jid': item},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
],
|
||||
final result = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'unblock',
|
||||
xmlns: blockingXmlns,
|
||||
children: items
|
||||
.map(
|
||||
(item) => XMLNode(
|
||||
tag: 'item',
|
||||
attributes: {
|
||||
'jid': item,
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
return result.attributes['type'] == 'result';
|
||||
}
|
||||
|
||||
Future<List<String>> getBlocklist() async {
|
||||
final result = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'blocklist',
|
||||
xmlns: blockingXmlns,
|
||||
)
|
||||
],
|
||||
final result = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'blocklist',
|
||||
xmlns: blockingXmlns,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
final blocklist = result.firstTag('blocklist', xmlns: blockingXmlns)!;
|
||||
return blocklist
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||
@@ -113,7 +113,7 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator
|
||||
// We have to do this because we otherwise get a stanza stuck in the queue,
|
||||
// thus spamming the server on every <a /> nonza we receive.
|
||||
// ignore: cascade_invocations
|
||||
await sm.setState(StreamManagementState(0, 0));
|
||||
await sm.setState(const StreamManagementState(0, 0));
|
||||
await sm.commitState();
|
||||
|
||||
_resumeFailed = true;
|
||||
|
||||
@@ -5,7 +5,10 @@ class StreamManagementEnableNonza extends XMLNode {
|
||||
StreamManagementEnableNonza()
|
||||
: super(
|
||||
tag: 'enable',
|
||||
attributes: <String, String>{'xmlns': smXmlns, 'resume': 'true'},
|
||||
attributes: {
|
||||
'xmlns': smXmlns,
|
||||
'resume': 'true',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,10 +16,10 @@ class StreamManagementResumeNonza extends XMLNode {
|
||||
StreamManagementResumeNonza(String id, int h)
|
||||
: super(
|
||||
tag: 'resume',
|
||||
attributes: <String, String>{
|
||||
attributes: {
|
||||
'xmlns': smXmlns,
|
||||
'previd': id,
|
||||
'h': h.toString()
|
||||
'h': h.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -25,7 +28,10 @@ class StreamManagementAckNonza extends XMLNode {
|
||||
StreamManagementAckNonza(int h)
|
||||
: super(
|
||||
tag: 'a',
|
||||
attributes: <String, String>{'xmlns': smXmlns, 'h': h.toString()},
|
||||
attributes: {
|
||||
'xmlns': smXmlns,
|
||||
'h': h.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +39,7 @@ class StreamManagementRequestNonza extends XMLNode {
|
||||
StreamManagementRequestNonza()
|
||||
: super(
|
||||
tag: 'r',
|
||||
attributes: <String, String>{
|
||||
attributes: {
|
||||
'xmlns': smXmlns,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,18 +1,43 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'state.freezed.dart';
|
||||
part 'state.g.dart';
|
||||
const _smNotSpecified = Object();
|
||||
|
||||
@freezed
|
||||
class StreamManagementState with _$StreamManagementState {
|
||||
factory StreamManagementState(
|
||||
int c2s,
|
||||
int s2c, {
|
||||
String? streamResumptionLocation,
|
||||
String? streamResumptionId,
|
||||
}) = _StreamManagementState;
|
||||
@immutable
|
||||
class StreamManagementState {
|
||||
const StreamManagementState(
|
||||
this.c2s,
|
||||
this.s2c, {
|
||||
this.streamResumptionLocation,
|
||||
this.streamResumptionId,
|
||||
});
|
||||
|
||||
// JSON
|
||||
factory StreamManagementState.fromJson(Map<String, dynamic> json) =>
|
||||
_$StreamManagementStateFromJson(json);
|
||||
/// The counter of stanzas sent from the client to the server.
|
||||
final int c2s;
|
||||
|
||||
/// The counter of stanzas sent from the server to the client.
|
||||
final int s2c;
|
||||
|
||||
/// If set, the server's preferred location for resumption.
|
||||
final String? streamResumptionLocation;
|
||||
|
||||
/// If set, the token to allow using stream resumption.
|
||||
final String? streamResumptionId;
|
||||
|
||||
StreamManagementState copyWith({
|
||||
Object c2s = _smNotSpecified,
|
||||
Object s2c = _smNotSpecified,
|
||||
Object? streamResumptionLocation = _smNotSpecified,
|
||||
Object? streamResumptionId = _smNotSpecified,
|
||||
}) {
|
||||
return StreamManagementState(
|
||||
c2s != _smNotSpecified ? c2s as int : this.c2s,
|
||||
s2c != _smNotSpecified ? s2c as int : this.s2c,
|
||||
streamResumptionLocation: streamResumptionLocation != _smNotSpecified
|
||||
? streamResumptionLocation as String?
|
||||
: this.streamResumptionLocation,
|
||||
streamResumptionId: streamResumptionId != _smNotSpecified
|
||||
? streamResumptionId as String?
|
||||
: this.streamResumptionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
|
||||
|
||||
part of 'state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
StreamManagementState _$StreamManagementStateFromJson(
|
||||
Map<String, dynamic> json) {
|
||||
return _StreamManagementState.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$StreamManagementState {
|
||||
int get c2s => throw _privateConstructorUsedError;
|
||||
int get s2c => throw _privateConstructorUsedError;
|
||||
String? get streamResumptionLocation => throw _privateConstructorUsedError;
|
||||
String? get streamResumptionId => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$StreamManagementStateCopyWith<StreamManagementState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $StreamManagementStateCopyWith<$Res> {
|
||||
factory $StreamManagementStateCopyWith(StreamManagementState value,
|
||||
$Res Function(StreamManagementState) then) =
|
||||
_$StreamManagementStateCopyWithImpl<$Res>;
|
||||
$Res call(
|
||||
{int c2s,
|
||||
int s2c,
|
||||
String? streamResumptionLocation,
|
||||
String? streamResumptionId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$StreamManagementStateCopyWithImpl<$Res>
|
||||
implements $StreamManagementStateCopyWith<$Res> {
|
||||
_$StreamManagementStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
final StreamManagementState _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function(StreamManagementState) _then;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? c2s = freezed,
|
||||
Object? s2c = freezed,
|
||||
Object? streamResumptionLocation = freezed,
|
||||
Object? streamResumptionId = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
c2s: c2s == freezed
|
||||
? _value.c2s
|
||||
: c2s // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
s2c: s2c == freezed
|
||||
? _value.s2c
|
||||
: s2c // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
streamResumptionLocation: streamResumptionLocation == freezed
|
||||
? _value.streamResumptionLocation
|
||||
: streamResumptionLocation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
streamResumptionId: streamResumptionId == freezed
|
||||
? _value.streamResumptionId
|
||||
: streamResumptionId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$_StreamManagementStateCopyWith<$Res>
|
||||
implements $StreamManagementStateCopyWith<$Res> {
|
||||
factory _$$_StreamManagementStateCopyWith(_$_StreamManagementState value,
|
||||
$Res Function(_$_StreamManagementState) then) =
|
||||
__$$_StreamManagementStateCopyWithImpl<$Res>;
|
||||
@override
|
||||
$Res call(
|
||||
{int c2s,
|
||||
int s2c,
|
||||
String? streamResumptionLocation,
|
||||
String? streamResumptionId});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$_StreamManagementStateCopyWithImpl<$Res>
|
||||
extends _$StreamManagementStateCopyWithImpl<$Res>
|
||||
implements _$$_StreamManagementStateCopyWith<$Res> {
|
||||
__$$_StreamManagementStateCopyWithImpl(_$_StreamManagementState _value,
|
||||
$Res Function(_$_StreamManagementState) _then)
|
||||
: super(_value, (v) => _then(v as _$_StreamManagementState));
|
||||
|
||||
@override
|
||||
_$_StreamManagementState get _value =>
|
||||
super._value as _$_StreamManagementState;
|
||||
|
||||
@override
|
||||
$Res call({
|
||||
Object? c2s = freezed,
|
||||
Object? s2c = freezed,
|
||||
Object? streamResumptionLocation = freezed,
|
||||
Object? streamResumptionId = freezed,
|
||||
}) {
|
||||
return _then(_$_StreamManagementState(
|
||||
c2s == freezed
|
||||
? _value.c2s
|
||||
: c2s // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
s2c == freezed
|
||||
? _value.s2c
|
||||
: s2c // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
streamResumptionLocation: streamResumptionLocation == freezed
|
||||
? _value.streamResumptionLocation
|
||||
: streamResumptionLocation // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
streamResumptionId: streamResumptionId == freezed
|
||||
? _value.streamResumptionId
|
||||
: streamResumptionId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$_StreamManagementState implements _StreamManagementState {
|
||||
_$_StreamManagementState(this.c2s, this.s2c,
|
||||
{this.streamResumptionLocation, this.streamResumptionId});
|
||||
|
||||
factory _$_StreamManagementState.fromJson(Map<String, dynamic> json) =>
|
||||
_$$_StreamManagementStateFromJson(json);
|
||||
|
||||
@override
|
||||
final int c2s;
|
||||
@override
|
||||
final int s2c;
|
||||
@override
|
||||
final String? streamResumptionLocation;
|
||||
@override
|
||||
final String? streamResumptionId;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StreamManagementState(c2s: $c2s, s2c: $s2c, streamResumptionLocation: $streamResumptionLocation, streamResumptionId: $streamResumptionId)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_StreamManagementState &&
|
||||
const DeepCollectionEquality().equals(other.c2s, c2s) &&
|
||||
const DeepCollectionEquality().equals(other.s2c, s2c) &&
|
||||
const DeepCollectionEquality().equals(
|
||||
other.streamResumptionLocation, streamResumptionLocation) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.streamResumptionId, streamResumptionId));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(c2s),
|
||||
const DeepCollectionEquality().hash(s2c),
|
||||
const DeepCollectionEquality().hash(streamResumptionLocation),
|
||||
const DeepCollectionEquality().hash(streamResumptionId));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
_$$_StreamManagementStateCopyWith<_$_StreamManagementState> get copyWith =>
|
||||
__$$_StreamManagementStateCopyWithImpl<_$_StreamManagementState>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$_StreamManagementStateToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _StreamManagementState implements StreamManagementState {
|
||||
factory _StreamManagementState(final int c2s, final int s2c,
|
||||
{final String? streamResumptionLocation,
|
||||
final String? streamResumptionId}) = _$_StreamManagementState;
|
||||
|
||||
factory _StreamManagementState.fromJson(Map<String, dynamic> json) =
|
||||
_$_StreamManagementState.fromJson;
|
||||
|
||||
@override
|
||||
int get c2s;
|
||||
@override
|
||||
int get s2c;
|
||||
@override
|
||||
String? get streamResumptionLocation;
|
||||
@override
|
||||
String? get streamResumptionId;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_StreamManagementStateCopyWith<_$_StreamManagementState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$_StreamManagementState _$$_StreamManagementStateFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$_StreamManagementState(
|
||||
json['c2s'] as int,
|
||||
json['s2c'] as int,
|
||||
streamResumptionLocation: json['streamResumptionLocation'] as String?,
|
||||
streamResumptionId: json['streamResumptionId'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_StreamManagementStateToJson(
|
||||
_$_StreamManagementState instance) =>
|
||||
<String, dynamic>{
|
||||
'c2s': instance.c2s,
|
||||
's2c': instance.s2c,
|
||||
'streamResumptionLocation': instance.streamResumptionLocation,
|
||||
'streamResumptionId': instance.streamResumptionId,
|
||||
};
|
||||
39
packages/moxxmpp/lib/src/xeps/xep_0198/types.dart
Normal file
39
packages/moxxmpp/lib/src/xeps/xep_0198/types.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
|
||||
class StreamManagementData implements StanzaHandlerExtension {
|
||||
const StreamManagementData(this.exclude, this.queueId);
|
||||
|
||||
/// Whether the stanza should be exluded from the StreamManagement's resend queue.
|
||||
final bool exclude;
|
||||
|
||||
/// The ID to use when queuing the stanza.
|
||||
final int? queueId;
|
||||
|
||||
/// If we resend a stanza, then we will have [queueId] set, so we should skip
|
||||
/// incrementing the C2S counter.
|
||||
bool get shouldCountStanza => queueId == null;
|
||||
}
|
||||
|
||||
/// A queue element for keeping track of stanzas to (potentially) resend.
|
||||
@immutable
|
||||
class SMQueueEntry {
|
||||
const SMQueueEntry(this.stanza, this.encrypted);
|
||||
|
||||
/// The actual stanza.
|
||||
final Stanza stanza;
|
||||
|
||||
/// Flag indicating whether the stanza was encrypted before sending.
|
||||
final bool encrypted;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SMQueueEntry &&
|
||||
other.stanza == stanza &&
|
||||
other.encrypted == encrypted;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => stanza.hashCode ^ encrypted.hashCode;
|
||||
}
|
||||
@@ -11,10 +11,12 @@ import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0198/types.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
|
||||
const xmlUintMax = 4294967296; // 2**32
|
||||
@@ -27,10 +29,10 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
}) : super(smManager);
|
||||
|
||||
/// The queue of stanzas that are not (yet) acked
|
||||
final Map<int, Stanza> _unackedStanzas = {};
|
||||
final Map<int, SMQueueEntry> _unackedStanzas = {};
|
||||
|
||||
/// Commitable state of the StreamManagementManager
|
||||
StreamManagementState _state = StreamManagementState(0, 0);
|
||||
StreamManagementState _state = const StreamManagementState(0, 0);
|
||||
|
||||
/// Mutex lock for _state
|
||||
final Lock _stateLock = Lock();
|
||||
@@ -60,7 +62,7 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
|
||||
/// Functions for testing
|
||||
@visibleForTesting
|
||||
Map<int, Stanza> getUnackedStanzas() => _unackedStanzas;
|
||||
Map<int, SMQueueEntry> getUnackedStanzas() => _unackedStanzas;
|
||||
|
||||
@visibleForTesting
|
||||
Future<int> getPendingAcks() async {
|
||||
@@ -73,6 +75,17 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
return acks;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onData() async {
|
||||
// The ack timer does not matter if we are currently in the middle of receiving
|
||||
// data.
|
||||
await _ackLock.synchronized(() {
|
||||
if (_pendingAcks > 0) {
|
||||
_resetAckTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Called when a stanza has been acked to decide whether we should trigger a
|
||||
/// StanzaAckedEvent.
|
||||
///
|
||||
@@ -138,7 +151,7 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
nonzaTag: 'a',
|
||||
nonzaXmlns: smXmlns,
|
||||
callback: _handleAckResponse,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -146,14 +159,14 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
StanzaHandler(
|
||||
callback: _onServerStanzaReceived,
|
||||
priority: 9999,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
List<StanzaHandler> getOutgoingPostStanzaHandlers() => [
|
||||
StanzaHandler(
|
||||
callback: _onClientStanzaSent,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -223,6 +236,12 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
_ackTimer = null;
|
||||
}
|
||||
|
||||
/// Resets the ack timer.
|
||||
void _resetAckTimer() {
|
||||
_stopAckTimer();
|
||||
_startAckTimer();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<void> handleAckTimeout() async {
|
||||
_stopAckTimer();
|
||||
@@ -305,12 +324,15 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
if (_pendingAcks > 0) {
|
||||
// Prevent diff from becoming negative
|
||||
final diff = max(_state.c2s - h, 0);
|
||||
|
||||
logger.finest(
|
||||
'Setting _pendingAcks to $diff (was $_pendingAcks before): max(${_state.c2s} - $h, 0)',
|
||||
);
|
||||
_pendingAcks = diff;
|
||||
|
||||
// Reset the timer
|
||||
if (_pendingAcks > 0) {
|
||||
_stopAckTimer();
|
||||
_startAckTimer();
|
||||
_resetAckTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,15 +357,18 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
final attrs = getAttributes();
|
||||
final sequences = _unackedStanzas.keys.toList()..sort();
|
||||
for (final height in sequences) {
|
||||
logger.finest('Unacked stanza: height $height, h $h');
|
||||
|
||||
// Do nothing if the ack does not concern this stanza
|
||||
if (height > h) continue;
|
||||
|
||||
final stanza = _unackedStanzas[height]!;
|
||||
logger.finest('Removing stanza with height $height');
|
||||
final entry = _unackedStanzas[height]!;
|
||||
_unackedStanzas.remove(height);
|
||||
|
||||
// Create a StanzaAckedEvent if the stanza is correct
|
||||
if (shouldTriggerAckedEvent(stanza)) {
|
||||
attrs.sendEvent(StanzaAckedEvent(stanza));
|
||||
if (shouldTriggerAckedEvent(entry.stanza)) {
|
||||
attrs.sendEvent(StanzaAckedEvent(entry.stanza));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,10 +424,30 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
Stanza stanza,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
await _incrementC2S();
|
||||
_unackedStanzas[_state.c2s] = stanza;
|
||||
|
||||
if (isStreamManagementEnabled()) {
|
||||
final smData = state.extensions.get<StreamManagementData>();
|
||||
logger.finest('Should count stanza: ${smData?.shouldCountStanza}');
|
||||
if (smData?.shouldCountStanza ?? true) {
|
||||
await _incrementC2S();
|
||||
}
|
||||
|
||||
if (smData?.exclude ?? false) {
|
||||
return state;
|
||||
}
|
||||
|
||||
int queueId;
|
||||
if (smData?.queueId != null) {
|
||||
logger.finest('Reusing queue id ${smData!.queueId}');
|
||||
queueId = smData.queueId!;
|
||||
} else {
|
||||
queueId = await _stateLock.synchronized(() => _state.c2s);
|
||||
}
|
||||
|
||||
_unackedStanzas[queueId] = SMQueueEntry(
|
||||
stanza,
|
||||
// Prevent an E2EE message being encrypted again
|
||||
state.encrypted,
|
||||
);
|
||||
await _sendAckRequest();
|
||||
}
|
||||
|
||||
@@ -410,11 +455,25 @@ class StreamManagementManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
Future<void> _resendStanzas() async {
|
||||
final stanzas = _unackedStanzas.values.toList();
|
||||
_unackedStanzas.clear();
|
||||
|
||||
for (final stanza in stanzas) {
|
||||
await getAttributes().sendStanza(stanza, awaitable: false);
|
||||
final queueCopy = _unackedStanzas.entries.toList();
|
||||
for (final entry in queueCopy) {
|
||||
logger.finest(
|
||||
'Resending ${entry.value.stanza.tag} with id ${entry.value.stanza.attributes["id"]}',
|
||||
);
|
||||
await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
entry.value.stanza,
|
||||
postSendExtensions: TypedMap<StanzaHandlerExtension>.fromList([
|
||||
StreamManagementData(
|
||||
false,
|
||||
entry.key,
|
||||
),
|
||||
]),
|
||||
awaitable: false,
|
||||
// Prevent an E2EE message being encrypted again
|
||||
encrypted: entry.value.encrypted,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
@@ -7,10 +8,14 @@ import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
|
||||
@immutable
|
||||
class DelayedDelivery {
|
||||
const DelayedDelivery(this.from, this.timestamp);
|
||||
class DelayedDeliveryData implements StanzaHandlerExtension {
|
||||
const DelayedDeliveryData(this.from, this.timestamp);
|
||||
|
||||
/// The timestamp the message was originally sent.
|
||||
final DateTime timestamp;
|
||||
final String from;
|
||||
|
||||
/// The JID that originally sent the message.
|
||||
final JID from;
|
||||
}
|
||||
|
||||
class DelayedDeliveryManager extends XmppManagerBase {
|
||||
@@ -23,6 +28,8 @@ class DelayedDeliveryManager extends XmppManagerBase {
|
||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||
StanzaHandler(
|
||||
stanzaTag: 'message',
|
||||
tagName: 'delay',
|
||||
tagXmlns: delayedDeliveryXmlns,
|
||||
callback: _onIncomingMessage,
|
||||
priority: 200,
|
||||
),
|
||||
@@ -32,14 +39,14 @@ class DelayedDeliveryManager extends XmppManagerBase {
|
||||
Stanza stanza,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns);
|
||||
if (delay == null) return state;
|
||||
final delay = stanza.firstTag('delay', xmlns: delayedDeliveryXmlns)!;
|
||||
|
||||
return state.copyWith(
|
||||
delayedDelivery: DelayedDelivery(
|
||||
delay.attributes['from']! as String,
|
||||
DateTime.parse(delay.attributes['stamp']! as String),
|
||||
),
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
DelayedDeliveryData(
|
||||
JID.fromString(delay.attributes['from']! as String),
|
||||
DateTime.parse(delay.attributes['stamp']! as String),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
59
packages/moxxmpp/lib/src/xeps/xep_0264.dart
Normal file
59
packages/moxxmpp/lib/src/xeps/xep_0264.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
extension _StringToInt on String {
|
||||
int toInt() => int.parse(this);
|
||||
}
|
||||
|
||||
class JingleContentThumbnail {
|
||||
const JingleContentThumbnail(
|
||||
this.uri,
|
||||
this.mediaType,
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
|
||||
factory JingleContentThumbnail.fromXML(XMLNode thumbnail) {
|
||||
assert(
|
||||
thumbnail.tag == 'thumbnail',
|
||||
'thumbnail must be Jingle Content Thumbnail',
|
||||
);
|
||||
assert(
|
||||
thumbnail.attributes['xmlns'] == jingleContentThumbnailXmlns,
|
||||
'thumbnail must be Jingle Content Thumbnail',
|
||||
);
|
||||
|
||||
return JingleContentThumbnail(
|
||||
Uri.parse(thumbnail.attributes['uri']! as String),
|
||||
thumbnail.attributes['media-type'] as String?,
|
||||
(thumbnail.attributes['width'] as String?)?.toInt(),
|
||||
(thumbnail.attributes['height'] as String?)?.toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
/// The URI of the thumbnail data.
|
||||
final Uri uri;
|
||||
|
||||
/// The MIME type of the thumbnail
|
||||
final String? mediaType;
|
||||
|
||||
/// The width of the thumbnail.
|
||||
final int? width;
|
||||
|
||||
/// The height of the thumbnail.
|
||||
final int? height;
|
||||
|
||||
/// Convert the thumbnail to its XML representation.
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'thumbnail',
|
||||
xmlns: jingleContentThumbnailXmlns,
|
||||
attributes: {
|
||||
'uri': uri.toString(),
|
||||
if (mediaType != null) 'media-type': mediaType!,
|
||||
if (width != null) 'width': width.toString(),
|
||||
if (height != null) 'height': height.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/connection.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
@@ -15,6 +14,13 @@ import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0297.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0386.dart';
|
||||
|
||||
class CarbonsData implements StanzaHandlerExtension {
|
||||
const CarbonsData(this.isCarbon);
|
||||
|
||||
/// Indicates whether this message is a carbon.
|
||||
final bool isCarbon;
|
||||
}
|
||||
|
||||
/// This manager class implements support for XEP-0280.
|
||||
class CarbonsManager extends XmppManagerBase {
|
||||
CarbonsManager() : super(carbonsManager);
|
||||
@@ -43,7 +49,7 @@ class CarbonsManager extends XmppManagerBase {
|
||||
tagXmlns: carbonsXmlns,
|
||||
callback: _onMessageSent,
|
||||
priority: -98,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -78,15 +84,14 @@ class CarbonsManager extends XmppManagerBase {
|
||||
) async {
|
||||
final from = JID.fromString(message.attributes['from']! as String);
|
||||
final received = message.firstTag('received', xmlns: carbonsXmlns)!;
|
||||
if (!isCarbonValid(from)) return state.copyWith(done: true);
|
||||
if (!isCarbonValid(from)) return state..done = true;
|
||||
|
||||
final forwarded = received.firstTag('forwarded', xmlns: forwardedXmlns)!;
|
||||
final carbon = unpackForwarded(forwarded);
|
||||
|
||||
return state.copyWith(
|
||||
isCarbon: true,
|
||||
stanza: carbon,
|
||||
);
|
||||
return state
|
||||
..extensions.set(const CarbonsData(true))
|
||||
..stanza = carbon;
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onMessageSent(
|
||||
@@ -95,15 +100,14 @@ class CarbonsManager extends XmppManagerBase {
|
||||
) async {
|
||||
final from = JID.fromString(message.attributes['from']! as String);
|
||||
final sent = message.firstTag('sent', xmlns: carbonsXmlns)!;
|
||||
if (!isCarbonValid(from)) return state.copyWith(done: true);
|
||||
if (!isCarbonValid(from)) return state..done = true;
|
||||
|
||||
final forwarded = sent.firstTag('forwarded', xmlns: forwardedXmlns)!;
|
||||
final carbon = unpackForwarded(forwarded);
|
||||
|
||||
return state.copyWith(
|
||||
isCarbon: true,
|
||||
stanza: carbon,
|
||||
);
|
||||
return state
|
||||
..extensions.set(const CarbonsData(true))
|
||||
..stanza = carbon;
|
||||
}
|
||||
|
||||
/// Send a request to the server, asking it to enable Message Carbons.
|
||||
@@ -111,20 +115,20 @@ class CarbonsManager extends XmppManagerBase {
|
||||
/// Returns true if carbons were enabled. False, if not.
|
||||
Future<bool> enableCarbons() async {
|
||||
final attrs = getAttributes();
|
||||
final result = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
to: attrs.getFullJID().toBare().toString(),
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'enable',
|
||||
xmlns: carbonsXmlns,
|
||||
)
|
||||
],
|
||||
final result = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
to: attrs.getFullJID().toBare().toString(),
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'enable',
|
||||
xmlns: carbonsXmlns,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
addFrom: StanzaFromType.full,
|
||||
addId: true,
|
||||
);
|
||||
))!;
|
||||
|
||||
if (result.attributes['type'] != 'result') {
|
||||
logger.warning('Failed to enable message carbons');
|
||||
@@ -142,19 +146,19 @@ class CarbonsManager extends XmppManagerBase {
|
||||
///
|
||||
/// Returns true if carbons were disabled. False, if not.
|
||||
Future<bool> disableCarbons() async {
|
||||
final result = await getAttributes().sendStanza(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'disable',
|
||||
xmlns: carbonsXmlns,
|
||||
)
|
||||
],
|
||||
final result = (await getAttributes().sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
type: 'set',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'disable',
|
||||
xmlns: carbonsXmlns,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
addFrom: StanzaFromType.full,
|
||||
addId: true,
|
||||
);
|
||||
))!;
|
||||
|
||||
if (result.attributes['type'] != 'result') {
|
||||
logger.warning('Failed to disable message carbons');
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
/// Hash names
|
||||
const _hashSha1 = 'sha-1';
|
||||
const _hashSha256 = 'sha-256';
|
||||
const _hashSha512 = 'sha-512';
|
||||
const _hashSha3256 = 'sha3-256';
|
||||
@@ -23,6 +24,9 @@ XMLNode constructHashElement(HashFunction hash, String value) {
|
||||
}
|
||||
|
||||
enum HashFunction {
|
||||
/// SHA-1
|
||||
sha1,
|
||||
|
||||
/// SHA-256
|
||||
sha256,
|
||||
|
||||
@@ -46,6 +50,8 @@ enum HashFunction {
|
||||
/// - XEP-0300
|
||||
factory HashFunction.fromName(String name) {
|
||||
switch (name) {
|
||||
case _hashSha1:
|
||||
return HashFunction.sha1;
|
||||
case _hashSha256:
|
||||
return HashFunction.sha256;
|
||||
case _hashSha512:
|
||||
@@ -60,12 +66,36 @@ enum HashFunction {
|
||||
return HashFunction.blake2b512;
|
||||
}
|
||||
|
||||
throw Exception();
|
||||
throw Exception('Invalid hash function $name');
|
||||
}
|
||||
|
||||
/// Like [HashFunction.fromName], but returns null if the hash function is unknown
|
||||
static HashFunction? maybeFromName(String name) {
|
||||
switch (name) {
|
||||
case _hashSha1:
|
||||
return HashFunction.sha1;
|
||||
case _hashSha256:
|
||||
return HashFunction.sha256;
|
||||
case _hashSha512:
|
||||
return HashFunction.sha512;
|
||||
case _hashSha3256:
|
||||
return HashFunction.sha3_256;
|
||||
case _hashSha3512:
|
||||
return HashFunction.sha3_512;
|
||||
case _hashBlake2b256:
|
||||
return HashFunction.blake2b256;
|
||||
case _hashBlake2b512:
|
||||
return HashFunction.blake2b512;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Return the hash function's name according to IANA's hash name register or XEP-0300.
|
||||
String toName() {
|
||||
switch (this) {
|
||||
case HashFunction.sha1:
|
||||
return _hashSha1;
|
||||
case HashFunction.sha256:
|
||||
return _hashSha256;
|
||||
case HashFunction.sha512:
|
||||
@@ -88,6 +118,9 @@ class CryptographicHashManager extends XmppManagerBase {
|
||||
@override
|
||||
Future<bool> isSupported() async => true;
|
||||
|
||||
/// NOTE: We intentionally do not advertise support for SHA-1, as it is marked as
|
||||
/// MUST NOT. Sha-1 support is only for providing a wrapper over its hash
|
||||
/// function, for example for XEP-0115.
|
||||
@override
|
||||
List<String> getDiscoFeatures() => [
|
||||
'$hashFunctionNameBaseXmlns:$_hashSha256',
|
||||
@@ -107,6 +140,9 @@ class CryptographicHashManager extends XmppManagerBase {
|
||||
// TODO(PapaTutuWawa): Implement the others as well
|
||||
HashAlgorithm algo;
|
||||
switch (function) {
|
||||
case HashFunction.sha1:
|
||||
algo = Sha1();
|
||||
break;
|
||||
case HashFunction.sha256:
|
||||
algo = Sha256();
|
||||
break;
|
||||
|
||||
@@ -2,18 +2,27 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
XMLNode makeLastMessageCorrectionEdit(String id) {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'replace',
|
||||
xmlns: lmcXmlns,
|
||||
attributes: <String, String>{
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
class LastMessageCorrectionData implements StanzaHandlerExtension {
|
||||
const LastMessageCorrectionData(this.id);
|
||||
|
||||
/// The id the LMC applies to.
|
||||
final String id;
|
||||
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'replace',
|
||||
xmlns: lmcXmlns,
|
||||
attributes: {
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LastMessageCorrectionManager extends XmppManagerBase {
|
||||
@@ -31,7 +40,7 @@ class LastMessageCorrectionManager extends XmppManagerBase {
|
||||
callback: _onMessage,
|
||||
// Before the message handler
|
||||
priority: -99,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -42,8 +51,30 @@ class LastMessageCorrectionManager extends XmppManagerBase {
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final edit = stanza.firstTag('replace', xmlns: lmcXmlns)!;
|
||||
return state.copyWith(
|
||||
lastMessageCorrectionSid: edit.attributes['id']! as String,
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
LastMessageCorrectionData(edit.attributes['id']! as String),
|
||||
);
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final data = extensions.get<LastMessageCorrectionData>();
|
||||
return data != null
|
||||
? [
|
||||
data.toXML(),
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,27 +4,86 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
XMLNode makeChatMarkerMarkable() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'markable',
|
||||
xmlns: chatMarkersXmlns,
|
||||
);
|
||||
enum ChatMarker {
|
||||
received,
|
||||
displayed,
|
||||
acknowledged;
|
||||
|
||||
factory ChatMarker.fromName(String name) {
|
||||
switch (name) {
|
||||
case 'received':
|
||||
return ChatMarker.received;
|
||||
case 'displayed':
|
||||
return ChatMarker.displayed;
|
||||
case 'acknowledged':
|
||||
return ChatMarker.acknowledged;
|
||||
}
|
||||
|
||||
throw Exception('Invalid chat marker $name');
|
||||
}
|
||||
|
||||
XMLNode toXML() {
|
||||
String tag;
|
||||
switch (this) {
|
||||
case ChatMarker.received:
|
||||
tag = 'received';
|
||||
break;
|
||||
case ChatMarker.displayed:
|
||||
tag = 'displayed';
|
||||
break;
|
||||
case ChatMarker.acknowledged:
|
||||
tag = 'acknowledged';
|
||||
break;
|
||||
}
|
||||
|
||||
return XMLNode.xmlns(
|
||||
tag: tag,
|
||||
xmlns: chatMarkersXmlns,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
XMLNode makeChatMarker(String tag, String id) {
|
||||
assert(
|
||||
['received', 'displayed', 'acknowledged'].contains(tag),
|
||||
'Invalid chat marker',
|
||||
);
|
||||
return XMLNode.xmlns(
|
||||
tag: tag,
|
||||
xmlns: chatMarkersXmlns,
|
||||
attributes: {'id': id},
|
||||
);
|
||||
class MarkableData implements StanzaHandlerExtension {
|
||||
const MarkableData(this.isMarkable);
|
||||
|
||||
/// Indicates whether the message can be replied to with a chat marker.
|
||||
final bool isMarkable;
|
||||
|
||||
XMLNode toXML() {
|
||||
assert(isMarkable, '');
|
||||
|
||||
return XMLNode.xmlns(
|
||||
tag: 'markable',
|
||||
xmlns: chatMarkersXmlns,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMarkerData implements StanzaHandlerExtension {
|
||||
const ChatMarkerData(this.marker, this.id);
|
||||
|
||||
/// The actual chat state
|
||||
final ChatMarker marker;
|
||||
|
||||
/// The ID the chat marker applies to
|
||||
final String id;
|
||||
|
||||
XMLNode toXML() {
|
||||
final tag = marker.toXML();
|
||||
return XMLNode.xmlns(
|
||||
tag: tag.tag,
|
||||
xmlns: chatMarkersXmlns,
|
||||
attributes: {
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMarkerManager extends XmppManagerBase {
|
||||
@@ -41,7 +100,7 @@ class ChatMarkerManager extends XmppManagerBase {
|
||||
callback: _onMessage,
|
||||
// Before the message handler
|
||||
priority: -99,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -51,23 +110,52 @@ class ChatMarkerManager extends XmppManagerBase {
|
||||
Stanza message,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final marker = message.firstTagByXmlns(chatMarkersXmlns)!;
|
||||
final element = message.firstTagByXmlns(chatMarkersXmlns)!;
|
||||
|
||||
// Handle the <markable /> explicitly
|
||||
if (marker.tag == 'markable') return state.copyWith(isMarkable: true);
|
||||
|
||||
if (!['received', 'displayed', 'acknowledged'].contains(marker.tag)) {
|
||||
logger.warning("Unknown message marker '${marker.tag}' found.");
|
||||
} else {
|
||||
getAttributes().sendEvent(
|
||||
ChatMarkerEvent(
|
||||
from: JID.fromString(message.from!),
|
||||
type: marker.tag,
|
||||
id: marker.attributes['id']! as String,
|
||||
),
|
||||
);
|
||||
if (element.tag == 'markable') {
|
||||
return state..extensions.set(const MarkableData(true));
|
||||
}
|
||||
|
||||
return state.copyWith(done: true);
|
||||
try {
|
||||
getAttributes().sendEvent(
|
||||
ChatMarkerEvent(
|
||||
JID.fromString(message.from!),
|
||||
ChatMarker.fromName(element.tag),
|
||||
element.attributes['id']! as String,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
logger.warning("Unknown message marker '${element.tag}' found.");
|
||||
}
|
||||
|
||||
return state..done = true;
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final children = List<XMLNode>.empty(growable: true);
|
||||
final marker = extensions.get<ChatMarkerData>();
|
||||
if (marker != null) {
|
||||
children.add(marker.toXML());
|
||||
}
|
||||
|
||||
final markable = extensions.get<MarkableData>();
|
||||
if (markable != null) {
|
||||
children.add(markable.toXML());
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
enum MessageProcessingHint {
|
||||
noPermanentStore,
|
||||
noStore,
|
||||
noCopies,
|
||||
store,
|
||||
}
|
||||
store;
|
||||
|
||||
MessageProcessingHint messageProcessingHintFromXml(XMLNode element) {
|
||||
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;
|
||||
factory MessageProcessingHint.fromName(String name) {
|
||||
switch (name) {
|
||||
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: $name');
|
||||
return MessageProcessingHint.noStore;
|
||||
}
|
||||
|
||||
assert(false, 'Invalid Message Processing Hint: ${element.tag}');
|
||||
return MessageProcessingHint.noStore;
|
||||
}
|
||||
|
||||
extension XmlExtension on MessageProcessingHint {
|
||||
XMLNode toXml() {
|
||||
XMLNode toXML() {
|
||||
String tag;
|
||||
switch (this) {
|
||||
case MessageProcessingHint.noPermanentStore:
|
||||
@@ -48,3 +53,60 @@ extension XmlExtension on MessageProcessingHint {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageProcessingHintData implements StanzaHandlerExtension {
|
||||
const MessageProcessingHintData(this.hints);
|
||||
|
||||
/// The attached message processing hints.
|
||||
final List<MessageProcessingHint> hints;
|
||||
}
|
||||
|
||||
class MessageProcessingHintManager extends XmppManagerBase {
|
||||
MessageProcessingHintManager() : super(messageProcessingHintManager);
|
||||
|
||||
@override
|
||||
Future<bool> isSupported() async => true;
|
||||
|
||||
@override
|
||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||
StanzaHandler(
|
||||
stanzaTag: 'message',
|
||||
tagXmlns: messageProcessingHintsXmlns,
|
||||
callback: _onMessage,
|
||||
// Before the message handler
|
||||
priority: -99,
|
||||
),
|
||||
];
|
||||
|
||||
Future<StanzaHandlerData> _onMessage(
|
||||
Stanza stanza,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final elements = stanza.findTagsByXmlns(messageProcessingHintsXmlns);
|
||||
return state
|
||||
..extensions.set(
|
||||
MessageProcessingHintData(
|
||||
elements
|
||||
.map((element) => MessageProcessingHint.fromName(element.tag))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final data = extensions.get<MessageProcessingHintData>();
|
||||
return data != null ? data.hints.map((hint) => hint.toXML()).toList() : [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0386.dart';
|
||||
|
||||
class CSIActiveNonza extends XMLNode {
|
||||
|
||||
@@ -3,28 +3,69 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
/// Represents data provided by XEP-0359.
|
||||
/// NOTE: [StableStanzaId.stanzaId] must not be confused with the actual id attribute of
|
||||
/// the message stanza.
|
||||
class StableStanzaId {
|
||||
const StableStanzaId({this.originId, this.stanzaId, this.stanzaIdBy});
|
||||
final String? originId;
|
||||
final String? stanzaId;
|
||||
final String? stanzaIdBy;
|
||||
/// Representation of a <stanza-id /> element.
|
||||
class StanzaId {
|
||||
const StanzaId(
|
||||
this.id,
|
||||
this.by,
|
||||
);
|
||||
|
||||
/// The unique stanza id.
|
||||
final String id;
|
||||
|
||||
/// The JID the id was generated by.
|
||||
final JID by;
|
||||
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'stanza-id',
|
||||
xmlns: stableIdXmlns,
|
||||
attributes: {
|
||||
'id': id,
|
||||
'by': by.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
XMLNode makeOriginIdElement(String id) {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'origin-id',
|
||||
xmlns: stableIdXmlns,
|
||||
attributes: {'id': id},
|
||||
);
|
||||
class StableIdData implements StanzaHandlerExtension {
|
||||
const StableIdData(this.originId, this.stanzaIds);
|
||||
|
||||
/// <origin-id />
|
||||
final String? originId;
|
||||
|
||||
/// Stanza ids
|
||||
final List<StanzaId>? stanzaIds;
|
||||
|
||||
XMLNode toOriginIdElement() {
|
||||
assert(
|
||||
originId != null,
|
||||
'Can only build the XML element if originId != null',
|
||||
);
|
||||
return XMLNode.xmlns(
|
||||
tag: 'origin-id',
|
||||
xmlns: stableIdXmlns,
|
||||
attributes: {'id': originId!},
|
||||
);
|
||||
}
|
||||
|
||||
List<XMLNode> toXML() {
|
||||
return [
|
||||
if (originId != null)
|
||||
XMLNode.xmlns(
|
||||
tag: 'origin-id',
|
||||
xmlns: stableIdXmlns,
|
||||
attributes: {'id': originId!},
|
||||
),
|
||||
if (stanzaIds != null) ...stanzaIds!.map((s) => s.toXML()),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class StableIdManager extends XmppManagerBase {
|
||||
@@ -40,7 +81,7 @@ class StableIdManager extends XmppManagerBase {
|
||||
callback: _onMessage,
|
||||
// Before the MessageManager
|
||||
priority: -99,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -50,50 +91,58 @@ class StableIdManager extends XmppManagerBase {
|
||||
Stanza message,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final from = JID.fromString(message.attributes['from']! as String);
|
||||
String? originId;
|
||||
String? stanzaId;
|
||||
String? stanzaIdBy;
|
||||
final originIdTag = message.firstTag('origin-id', xmlns: stableIdXmlns);
|
||||
final stanzaIdTag = message.firstTag('stanza-id', xmlns: stableIdXmlns);
|
||||
List<StanzaId>? stanzaIds;
|
||||
final originIdElement = message.firstTag('origin-id', xmlns: stableIdXmlns);
|
||||
final stanzaIdElements =
|
||||
message.findTags('stanza-id', xmlns: stableIdXmlns);
|
||||
|
||||
// Process the origin id
|
||||
if (originIdTag != null) {
|
||||
logger.finest('Found origin Id tag');
|
||||
originId = originIdTag.attributes['id']! as String;
|
||||
if (originIdElement != null) {
|
||||
originId = originIdElement.attributes['id']! as String;
|
||||
}
|
||||
|
||||
// Process the stanza id tag
|
||||
if (stanzaIdTag != null) {
|
||||
logger.finest('Found stanza Id tag');
|
||||
final attrs = getAttributes();
|
||||
final disco = attrs.getManagerById<DiscoManager>(discoManager)!;
|
||||
final result = await disco.discoInfoQuery(from.toString());
|
||||
if (result.isType<DiscoInfo>()) {
|
||||
final info = result.get<DiscoInfo>();
|
||||
logger.finest('Got info for ${from.toString()}');
|
||||
if (info.features.contains(stableIdXmlns)) {
|
||||
logger.finest('${from.toString()} supports $stableIdXmlns.');
|
||||
stanzaId = stanzaIdTag.attributes['id']! as String;
|
||||
stanzaIdBy = stanzaIdTag.attributes['by']! as String;
|
||||
} else {
|
||||
logger.finest(
|
||||
'${from.toString()} does not support $stableIdXmlns. Ignoring stanza id... ',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.finest(
|
||||
'Failed to find out if ${from.toString()} supports $stableIdXmlns. Ignoring... ',
|
||||
);
|
||||
}
|
||||
if (stanzaIdElements.isNotEmpty) {
|
||||
stanzaIds = stanzaIdElements
|
||||
.map(
|
||||
(element) => StanzaId(
|
||||
element.attributes['id']! as String,
|
||||
JID.fromString(element.attributes['by']! as String),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return state.copyWith(
|
||||
stableId: StableStanzaId(
|
||||
originId: originId,
|
||||
stanzaId: stanzaId,
|
||||
stanzaIdBy: stanzaIdBy,
|
||||
),
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
StableIdData(
|
||||
originId,
|
||||
stanzaIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final data = extensions.get<StableIdData>();
|
||||
if (data?.originId != null) {
|
||||
return [
|
||||
data!.toOriginIdElement(),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
@@ -7,7 +8,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
@@ -58,10 +58,10 @@ class HttpFileUploadManager extends XmppManagerBase {
|
||||
/// Returns whether the entity provided an identity that tells us that we can ask it
|
||||
/// for an HTTP upload slot.
|
||||
bool _containsFileUploadIdentity(DiscoInfo info) {
|
||||
return listContains(
|
||||
info.identities,
|
||||
(Identity id) => id.category == 'store' && id.type == 'file',
|
||||
);
|
||||
return info.identities.firstWhereOrNull(
|
||||
(Identity id) => id.category == 'store' && id.type == 'file',
|
||||
) !=
|
||||
null;
|
||||
}
|
||||
|
||||
/// Extract the maximum filesize in octets from the disco response. Returns null
|
||||
@@ -149,23 +149,25 @@ class HttpFileUploadManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
final attrs = getAttributes();
|
||||
final response = await attrs.sendStanza(
|
||||
Stanza.iq(
|
||||
to: _entityJid.toString(),
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'request',
|
||||
xmlns: httpFileUploadXmlns,
|
||||
attributes: {
|
||||
'filename': filename,
|
||||
'size': filesize.toString(),
|
||||
...contentType != null ? {'content-type': contentType} : {}
|
||||
},
|
||||
)
|
||||
],
|
||||
final response = (await attrs.sendStanza(
|
||||
StanzaDetails(
|
||||
Stanza.iq(
|
||||
to: _entityJid.toString(),
|
||||
type: 'get',
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'request',
|
||||
xmlns: httpFileUploadXmlns,
|
||||
attributes: {
|
||||
'filename': filename,
|
||||
'size': filesize.toString(),
|
||||
if (contentType != null) 'content-type': contentType,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
))!;
|
||||
|
||||
if (response.attributes['type']! != 'result') {
|
||||
logger.severe('Failed to request HTTP File Upload slot.');
|
||||
|
||||
@@ -6,64 +6,64 @@ import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
enum ExplicitEncryptionType {
|
||||
enum ExplicitEncryptionType implements StanzaHandlerExtension {
|
||||
otr,
|
||||
legacyOpenPGP,
|
||||
openPGP,
|
||||
omemo,
|
||||
omemo1,
|
||||
omemo2,
|
||||
unknown,
|
||||
}
|
||||
unknown;
|
||||
|
||||
String _explicitEncryptionTypeToString(ExplicitEncryptionType type) {
|
||||
switch (type) {
|
||||
case ExplicitEncryptionType.otr:
|
||||
return emeOtr;
|
||||
case ExplicitEncryptionType.legacyOpenPGP:
|
||||
return emeLegacyOpenPGP;
|
||||
case ExplicitEncryptionType.openPGP:
|
||||
return emeOpenPGP;
|
||||
case ExplicitEncryptionType.omemo:
|
||||
return emeOmemo;
|
||||
case ExplicitEncryptionType.omemo1:
|
||||
return emeOmemo1;
|
||||
case ExplicitEncryptionType.omemo2:
|
||||
return emeOmemo2;
|
||||
case ExplicitEncryptionType.unknown:
|
||||
return '';
|
||||
factory ExplicitEncryptionType.fromNamespace(String namespace) {
|
||||
switch (namespace) {
|
||||
case emeOtr:
|
||||
return ExplicitEncryptionType.otr;
|
||||
case emeLegacyOpenPGP:
|
||||
return ExplicitEncryptionType.legacyOpenPGP;
|
||||
case emeOpenPGP:
|
||||
return ExplicitEncryptionType.openPGP;
|
||||
case emeOmemo:
|
||||
return ExplicitEncryptionType.omemo;
|
||||
case emeOmemo1:
|
||||
return ExplicitEncryptionType.omemo1;
|
||||
case emeOmemo2:
|
||||
return ExplicitEncryptionType.omemo2;
|
||||
default:
|
||||
return ExplicitEncryptionType.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExplicitEncryptionType _explicitEncryptionTypeFromString(String str) {
|
||||
switch (str) {
|
||||
case emeOtr:
|
||||
return ExplicitEncryptionType.otr;
|
||||
case emeLegacyOpenPGP:
|
||||
return ExplicitEncryptionType.legacyOpenPGP;
|
||||
case emeOpenPGP:
|
||||
return ExplicitEncryptionType.openPGP;
|
||||
case emeOmemo:
|
||||
return ExplicitEncryptionType.omemo;
|
||||
case emeOmemo1:
|
||||
return ExplicitEncryptionType.omemo1;
|
||||
case emeOmemo2:
|
||||
return ExplicitEncryptionType.omemo2;
|
||||
default:
|
||||
return ExplicitEncryptionType.unknown;
|
||||
String toNamespace() {
|
||||
switch (this) {
|
||||
case ExplicitEncryptionType.otr:
|
||||
return emeOtr;
|
||||
case ExplicitEncryptionType.legacyOpenPGP:
|
||||
return emeLegacyOpenPGP;
|
||||
case ExplicitEncryptionType.openPGP:
|
||||
return emeOpenPGP;
|
||||
case ExplicitEncryptionType.omemo:
|
||||
return emeOmemo;
|
||||
case ExplicitEncryptionType.omemo1:
|
||||
return emeOmemo1;
|
||||
case ExplicitEncryptionType.omemo2:
|
||||
return emeOmemo2;
|
||||
case ExplicitEncryptionType.unknown:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an <encryption /> element with [type] indicating which type of encryption was
|
||||
/// used.
|
||||
XMLNode buildEmeElement(ExplicitEncryptionType type) {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'encryption',
|
||||
xmlns: emeXmlns,
|
||||
attributes: <String, String>{
|
||||
'namespace': _explicitEncryptionTypeToString(type),
|
||||
},
|
||||
);
|
||||
/// Create an <encryption /> element with an xmlns indicating what type of encryption was
|
||||
/// used.
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'encryption',
|
||||
xmlns: emeXmlns,
|
||||
attributes: <String, String>{
|
||||
'namespace': toNamespace(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmeManager extends XmppManagerBase {
|
||||
@@ -91,10 +91,11 @@ class EmeManager extends XmppManagerBase {
|
||||
) async {
|
||||
final encryption = message.firstTag('encryption', xmlns: emeXmlns)!;
|
||||
|
||||
return state.copyWith(
|
||||
encryptionType: _explicitEncryptionTypeFromString(
|
||||
encryption.attributes['namespace']! as String,
|
||||
),
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
ExplicitEncryptionType.fromNamespace(
|
||||
encryption.attributes['namespace']! as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ abstract class OmemoError {}
|
||||
|
||||
class UnknownOmemoError extends OmemoError {}
|
||||
|
||||
class InvalidAffixElementsException with Exception {}
|
||||
class InvalidAffixElementsException implements Exception {}
|
||||
|
||||
/// Internal exception that is returned when the device list cannot be
|
||||
/// fetched because the returned list is empty.
|
||||
class EmptyDeviceListException implements OmemoError {}
|
||||
|
||||
class OmemoNotSupportedForContactException extends OmemoError {}
|
||||
|
||||
class EncryptionFailedException with Exception {}
|
||||
class EncryptionFailedException implements Exception {}
|
||||
|
||||
class InvalidEnvelopePayloadException with Exception {}
|
||||
class InvalidEnvelopePayloadException implements Exception {}
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:omemo_dart/omemo_dart.dart';
|
||||
|
||||
/// A simple wrapper class for defining elements that should not be encrypted.
|
||||
class DoNotEncrypt {
|
||||
const DoNotEncrypt(this.tag, this.xmlns);
|
||||
|
||||
/// The tag of the element.
|
||||
final String tag;
|
||||
|
||||
/// The xmlns attribute of the element.
|
||||
final String xmlns;
|
||||
}
|
||||
|
||||
/// An encryption error caused by OMEMO.
|
||||
class OmemoEncryptionError {
|
||||
const OmemoEncryptionError(this.deviceEncryptionErrors);
|
||||
|
||||
/// See omemo_dart's EncryptionResult for info on this field.
|
||||
final Map<String, List<EncryptToJidError>> deviceEncryptionErrors;
|
||||
}
|
||||
|
||||
class OmemoData extends StanzaHandlerExtension {
|
||||
OmemoData(this.newRatchets, this.replacedRatchets);
|
||||
|
||||
final Map<String, List<int>> newRatchets;
|
||||
|
||||
final Map<String, List<int>> replacedRatchets;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
@@ -10,7 +11,6 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
|
||||
@@ -23,9 +23,21 @@ import 'package:moxxmpp/src/xeps/xep_0384/crypto.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0384/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0384/helpers.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0384/types.dart';
|
||||
import 'package:omemo_dart/omemo_dart.dart';
|
||||
import 'package:omemo_dart/omemo_dart.dart' as omemo;
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
/// A callback that is executed whenever we need to acquire the OmemoManager backing
|
||||
/// the manager.
|
||||
typedef GetOmemoManagerCallback = Future<omemo.OmemoManager> Function();
|
||||
|
||||
/// A callback for figuring out whether a stanza should be encrypted or not. Note that
|
||||
/// returning true here does not necessarily mean that a stanza gets encrypted because
|
||||
/// handlers can indicate that a stanza should not be encrypted, e.g. PubSub.
|
||||
typedef ShouldEncryptStanzaCallback = Future<bool> Function(
|
||||
JID toJid,
|
||||
Stanza stanza,
|
||||
);
|
||||
|
||||
const _doNotEncryptList = [
|
||||
// XEP-0033
|
||||
DoNotEncrypt('addresses', extendedAddressingXmlns),
|
||||
@@ -42,8 +54,15 @@ const _doNotEncryptList = [
|
||||
DoNotEncrypt('stanza-id', stableIdXmlns),
|
||||
];
|
||||
|
||||
abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
BaseOmemoManager() : super(omemoManager);
|
||||
class OmemoManager extends XmppManagerBase {
|
||||
OmemoManager(this._getOmemoManager, this._shouldEncryptStanza)
|
||||
: super(omemoManager);
|
||||
|
||||
/// Callback for getting the [omemo.OmemoManager].
|
||||
final GetOmemoManagerCallback _getOmemoManager;
|
||||
|
||||
/// Callback for checking whether a stanza should be encrypted or not.
|
||||
final ShouldEncryptStanzaCallback _shouldEncryptStanza;
|
||||
|
||||
// TODO(Unknown): Technically, this is not always true
|
||||
@override
|
||||
@@ -112,22 +131,19 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
// Tell the OmemoManager
|
||||
(await getOmemoManager()).onDeviceListUpdate(jid.toString(), ids);
|
||||
await (await _getOmemoManager()).onDeviceListUpdate(jid.toString(), ids);
|
||||
|
||||
// Generate an event
|
||||
getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids));
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForOverriding
|
||||
Future<OmemoManager> getOmemoManager();
|
||||
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
|
||||
Future<int> _getDeviceId() async => (await _getOmemoManager()).getDeviceId();
|
||||
|
||||
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
|
||||
Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId();
|
||||
|
||||
/// Wrapper around using getSessionManager and then calling getDeviceId on it.
|
||||
Future<OmemoBundle> _getDeviceBundle() async {
|
||||
final om = await getOmemoManager();
|
||||
Future<omemo.OmemoBundle> _getDeviceBundle() async {
|
||||
final om = await _getOmemoManager();
|
||||
final device = await om.getDevice();
|
||||
return device.toBundle();
|
||||
}
|
||||
@@ -198,53 +214,45 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
XMLNode _buildEncryptedElement(
|
||||
EncryptionResult result,
|
||||
omemo.EncryptionResult result,
|
||||
String recipientJid,
|
||||
int deviceId,
|
||||
) {
|
||||
final keyElements = <String, List<XMLNode>>{};
|
||||
for (final key in result.encryptedKeys) {
|
||||
final keyElement = XMLNode(
|
||||
tag: 'key',
|
||||
attributes: <String, String>{
|
||||
'rid': '${key.rid}',
|
||||
'kex': key.kex ? 'true' : 'false',
|
||||
},
|
||||
text: key.value,
|
||||
);
|
||||
|
||||
if (keyElements.containsKey(key.jid)) {
|
||||
keyElements[key.jid]!.add(keyElement);
|
||||
} else {
|
||||
keyElements[key.jid] = [keyElement];
|
||||
}
|
||||
for (final keys in result.encryptedKeys.entries) {
|
||||
keyElements[keys.key] = keys.value
|
||||
.map(
|
||||
(ek) => XMLNode(
|
||||
tag: 'key',
|
||||
attributes: {
|
||||
'rid': ek.rid.toString(),
|
||||
if (ek.kex) 'kex': 'true',
|
||||
},
|
||||
text: ek.value,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final keysElements = keyElements.entries.map((entry) {
|
||||
return XMLNode(
|
||||
tag: 'keys',
|
||||
attributes: <String, String>{
|
||||
attributes: {
|
||||
'jid': entry.key,
|
||||
},
|
||||
children: entry.value,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
var payloadElement = <XMLNode>[];
|
||||
if (result.ciphertext != null) {
|
||||
payloadElement = [
|
||||
XMLNode(
|
||||
tag: 'payload',
|
||||
text: base64.encode(result.ciphertext!),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return XMLNode.xmlns(
|
||||
tag: 'encrypted',
|
||||
xmlns: omemoXmlns,
|
||||
children: [
|
||||
...payloadElement,
|
||||
if (result.ciphertext != null)
|
||||
XMLNode(
|
||||
tag: 'payload',
|
||||
text: base64Encode(result.ciphertext!),
|
||||
),
|
||||
XMLNode(
|
||||
tag: 'header',
|
||||
attributes: <String, String>{
|
||||
@@ -258,34 +266,36 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
|
||||
/// For usage with omemo_dart's OmemoManager.
|
||||
Future<void> sendEmptyMessageImpl(
|
||||
EncryptionResult result,
|
||||
omemo.EncryptionResult result,
|
||||
String toJid,
|
||||
) async {
|
||||
await getAttributes().sendStanza(
|
||||
Stanza.message(
|
||||
to: toJid,
|
||||
type: 'chat',
|
||||
children: [
|
||||
_buildEncryptedElement(
|
||||
result,
|
||||
toJid,
|
||||
await _getDeviceId(),
|
||||
),
|
||||
StanzaDetails(
|
||||
Stanza.message(
|
||||
to: toJid,
|
||||
type: 'chat',
|
||||
children: [
|
||||
_buildEncryptedElement(
|
||||
result,
|
||||
toJid,
|
||||
await _getDeviceId(),
|
||||
),
|
||||
|
||||
// Add a storage hint in case this is a message
|
||||
// Taken from the example at
|
||||
// https://xmpp.org/extensions/xep-0384.html#message-structure-description.
|
||||
MessageProcessingHint.store.toXml(),
|
||||
],
|
||||
// Add a storage hint in case this is a message
|
||||
// Taken from the example at
|
||||
// https://xmpp.org/extensions/xep-0384.html#message-structure-description.
|
||||
MessageProcessingHint.store.toXML(),
|
||||
],
|
||||
),
|
||||
awaitable: false,
|
||||
encrypted: true,
|
||||
),
|
||||
awaitable: false,
|
||||
encrypted: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Send a heartbeat message to [jid].
|
||||
Future<void> sendOmemoHeartbeat(String jid) async {
|
||||
final om = await getOmemoManager();
|
||||
final om = await _getOmemoManager();
|
||||
await om.sendOmemoHeartbeat(jid);
|
||||
}
|
||||
|
||||
@@ -298,17 +308,22 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
/// For usage with omemo_dart's OmemoManager
|
||||
Future<OmemoBundle?> fetchDeviceBundle(String jid, int id) async {
|
||||
Future<omemo.OmemoBundle?> fetchDeviceBundle(String jid, int id) async {
|
||||
final result = await retrieveDeviceBundle(JID.fromString(jid), id);
|
||||
if (result.isType<OmemoError>()) return null;
|
||||
|
||||
return result.get<OmemoBundle>();
|
||||
return result.get<omemo.OmemoBundle>();
|
||||
}
|
||||
|
||||
Future<StanzaHandlerData> _onOutgoingStanza(
|
||||
Stanza stanza,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
if (!state.shouldEncrypt) {
|
||||
logger.finest('Not encrypting since state.shouldEncrypt is false');
|
||||
return state;
|
||||
}
|
||||
|
||||
if (state.encrypted) {
|
||||
logger.finest('Not encrypting since state.encrypted is true');
|
||||
return state;
|
||||
@@ -321,7 +336,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
final toJid = JID.fromString(stanza.to!).toBare();
|
||||
final shouldEncryptResult = await shouldEncryptStanza(toJid, stanza);
|
||||
final shouldEncryptResult = await _shouldEncryptStanza(toJid, stanza);
|
||||
if (!shouldEncryptResult && !state.forceEncryption) {
|
||||
logger.finest(
|
||||
'Not encrypting stanza for $toJid: Both shouldEncryptStanza and forceEncryption are false.',
|
||||
@@ -348,32 +363,31 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
.getManagerById<CarbonsManager>(carbonsManager)
|
||||
?.isEnabled ??
|
||||
false;
|
||||
final om = await getOmemoManager();
|
||||
final om = await _getOmemoManager();
|
||||
final encryptToJids = [
|
||||
toJid.toString(),
|
||||
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
|
||||
];
|
||||
final result = await om.onOutgoingStanza(
|
||||
OmemoOutgoingStanza(
|
||||
[
|
||||
toJid.toString(),
|
||||
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
|
||||
],
|
||||
omemo.OmemoOutgoingStanza(
|
||||
encryptToJids,
|
||||
_buildEnvelope(toEncrypt, toJid.toString()),
|
||||
),
|
||||
);
|
||||
logger.finest('Encryption done');
|
||||
|
||||
if (!result.isSuccess(2)) {
|
||||
final other = Map<String, dynamic>.from(state.other);
|
||||
other['encryption_error_jids'] = result.jidEncryptionErrors;
|
||||
other['encryption_error_devices'] = result.deviceEncryptionErrors;
|
||||
return state.copyWith(
|
||||
other: other,
|
||||
if (!result.canSend) {
|
||||
return state
|
||||
..cancel = true
|
||||
// If we have no device list for toJid, then the contact most likely does not
|
||||
// support OMEMO:2
|
||||
cancelReason: result.jidEncryptionErrors[toJid.toString()]
|
||||
is NoKeyMaterialAvailableException
|
||||
..cancelReason = result.deviceEncryptionErrors[toJid.toString()]!.first
|
||||
.error is omemo.NoKeyMaterialAvailableError
|
||||
? OmemoNotSupportedForContactException()
|
||||
: UnknownOmemoError(),
|
||||
cancel: true,
|
||||
);
|
||||
: UnknownOmemoError()
|
||||
..encryptionError = OmemoEncryptionError(
|
||||
result.deviceEncryptionErrors,
|
||||
);
|
||||
}
|
||||
|
||||
final encrypted = _buildEncryptedElement(
|
||||
@@ -387,72 +401,63 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
if (stanza.tag == 'message') {
|
||||
children
|
||||
// Add EME data
|
||||
..add(buildEmeElement(ExplicitEncryptionType.omemo2))
|
||||
..add(ExplicitEncryptionType.omemo2.toXML())
|
||||
// Add a storage hint in case this is a message
|
||||
// Taken from the example at
|
||||
// https://xmpp.org/extensions/xep-0384.html#message-structure-description.
|
||||
..add(MessageProcessingHint.store.toXml());
|
||||
..add(MessageProcessingHint.store.toXML());
|
||||
}
|
||||
|
||||
return state.copyWith(
|
||||
stanza: state.stanza.copyWith(
|
||||
children: children,
|
||||
),
|
||||
encrypted: true,
|
||||
);
|
||||
return state
|
||||
..stanza = state.stanza.copyWith(children: children)
|
||||
..encrypted = true;
|
||||
}
|
||||
|
||||
/// This function is called whenever a message is to be encrypted. If it returns true,
|
||||
/// then the message will be encrypted. If it returns false, the message won't be
|
||||
/// encrypted.
|
||||
@visibleForOverriding
|
||||
Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza);
|
||||
|
||||
Future<StanzaHandlerData> _onIncomingStanza(
|
||||
Stanza stanza,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns);
|
||||
if (encrypted == null) return state;
|
||||
if (stanza.from == null) return state;
|
||||
|
||||
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns)!;
|
||||
final fromJid = JID.fromString(stanza.from!).toBare();
|
||||
final header = encrypted.firstTag('header')!;
|
||||
final payloadElement = encrypted.firstTag('payload');
|
||||
final keys = List<EncryptedKey>.empty(growable: true);
|
||||
final ourJid = getAttributes().getFullJID();
|
||||
final ourJidString = ourJid.toBare().toString();
|
||||
final keys = List<omemo.EncryptedKey>.empty(growable: true);
|
||||
for (final keysElement in header.findTags('keys')) {
|
||||
// We only care about our own JID
|
||||
final jid = keysElement.attributes['jid']! as String;
|
||||
for (final key in keysElement.findTags('key')) {
|
||||
keys.add(
|
||||
EncryptedKey(
|
||||
jid,
|
||||
int.parse(key.attributes['rid']! as String),
|
||||
key.innerText(),
|
||||
key.attributes['kex'] == 'true',
|
||||
),
|
||||
);
|
||||
if (jid != ourJidString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
keys.addAll(
|
||||
keysElement.findTags('key').map(
|
||||
(key) => omemo.EncryptedKey(
|
||||
int.parse(key.attributes['rid']! as String),
|
||||
key.innerText(),
|
||||
key.attributes['kex'] == 'true',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final ourJid = getAttributes().getFullJID();
|
||||
final sid = int.parse(header.attributes['sid']! as String);
|
||||
|
||||
final om = await getOmemoManager();
|
||||
final om = await _getOmemoManager();
|
||||
final result = await om.onIncomingStanza(
|
||||
OmemoIncomingStanza(
|
||||
omemo.OmemoIncomingStanza(
|
||||
fromJid.toString(),
|
||||
sid,
|
||||
state.delayedDelivery?.timestamp.millisecondsSinceEpoch ??
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
keys,
|
||||
payloadElement?.innerText(),
|
||||
encrypted.firstTag('payload')?.innerText(),
|
||||
false,
|
||||
),
|
||||
);
|
||||
|
||||
final other = Map<String, dynamic>.from(state.other);
|
||||
var children = stanza.children;
|
||||
if (result.error != null) {
|
||||
other['encryption_error'] = result.error;
|
||||
state.encryptionError = result.error;
|
||||
} else {
|
||||
children = stanza.children
|
||||
.where(
|
||||
@@ -463,17 +468,16 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
.toList();
|
||||
}
|
||||
|
||||
logger.finest('Got payload: ${result.payload != null}');
|
||||
if (result.payload != null) {
|
||||
XMLNode envelope;
|
||||
try {
|
||||
envelope = XMLNode.fromString(result.payload!);
|
||||
} on XmlParserException catch (_) {
|
||||
logger.warning('Failed to parse envelope payload: ${result.payload!}');
|
||||
other['encryption_error'] = InvalidEnvelopePayloadException();
|
||||
return state.copyWith(
|
||||
encrypted: true,
|
||||
other: other,
|
||||
);
|
||||
return state
|
||||
..encrypted = true
|
||||
..encryptionError = InvalidEnvelopePayloadException();
|
||||
}
|
||||
|
||||
final envelopeChildren = envelope.firstTag('content')?.children;
|
||||
@@ -482,18 +486,29 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
// Do not add forbidden elements from the envelope
|
||||
envelopeChildren.where(shouldEncryptElement),
|
||||
);
|
||||
|
||||
logger.finest('Adding children: ${envelopeChildren.map((c) => c.tag)}');
|
||||
} else {
|
||||
logger.warning('Invalid envelope element: No <content /> element');
|
||||
}
|
||||
|
||||
if (!checkAffixElements(envelope, stanza.from!, ourJid)) {
|
||||
other['encryption_error'] = InvalidAffixElementsException();
|
||||
state.encryptionError = InvalidAffixElementsException();
|
||||
}
|
||||
}
|
||||
|
||||
return state.copyWith(
|
||||
encrypted: true,
|
||||
stanza: Stanza(
|
||||
// Ignore heartbeat messages
|
||||
if (stanza.tag == 'message' && encrypted.firstTag('payload') == null) {
|
||||
logger.finest('Received empty OMEMO message. Ending processing early.');
|
||||
return state
|
||||
..encrypted = true
|
||||
..skip = true
|
||||
..done = true;
|
||||
}
|
||||
|
||||
return state
|
||||
..encrypted = true
|
||||
..stanza = Stanza(
|
||||
to: stanza.to,
|
||||
from: stanza.from,
|
||||
id: stanza.id,
|
||||
@@ -501,9 +516,13 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
children: children,
|
||||
tag: stanza.tag,
|
||||
attributes: Map<String, String>.from(stanza.attributes),
|
||||
),
|
||||
other: other,
|
||||
);
|
||||
)
|
||||
..extensions.set<OmemoData>(
|
||||
OmemoData(
|
||||
result.newRatchets,
|
||||
result.replacedRatchets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convenience function that attempts to retrieve the raw XML payload from the
|
||||
@@ -514,10 +533,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
JID jid,
|
||||
) async {
|
||||
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
|
||||
final result =
|
||||
await pm.getItems(jid.toBare().toString(), omemoDevicesXmlns);
|
||||
final result = await pm.getItems(jid.toBare(), omemoDevicesXmlns);
|
||||
if (result.isType<PubSubError>()) return Result(UnknownOmemoError());
|
||||
return Result(result.get<List<PubSubItem>>().first.payload);
|
||||
|
||||
final itemList = result.get<List<PubSubItem>>();
|
||||
if (itemList.isEmpty) return Result(EmptyDeviceListException());
|
||||
return Result(itemList.first.payload);
|
||||
}
|
||||
|
||||
/// Retrieves the OMEMO device list from [jid].
|
||||
@@ -536,12 +557,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
/// Retrieve all device bundles for the JID [jid].
|
||||
///
|
||||
/// On success, returns a list of devices. On failure, returns am OmemoError.
|
||||
Future<Result<OmemoError, List<OmemoBundle>>> retrieveDeviceBundles(
|
||||
Future<Result<OmemoError, List<omemo.OmemoBundle>>> retrieveDeviceBundles(
|
||||
JID jid,
|
||||
) async {
|
||||
// TODO(Unknown): Should we query the device list first?
|
||||
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
|
||||
final bundlesRaw = await pm.getItems(jid.toString(), omemoBundlesXmlns);
|
||||
final bundlesRaw = await pm.getItems(jid, omemoBundlesXmlns);
|
||||
if (bundlesRaw.isType<PubSubError>()) return Result(UnknownOmemoError());
|
||||
|
||||
final bundles = bundlesRaw
|
||||
@@ -557,12 +578,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
/// Retrieves a bundle from entity [jid] with the device id [deviceId].
|
||||
///
|
||||
/// On success, returns the device bundle. On failure, returns an OmemoError.
|
||||
Future<Result<OmemoError, OmemoBundle>> retrieveDeviceBundle(
|
||||
Future<Result<OmemoError, omemo.OmemoBundle>> retrieveDeviceBundle(
|
||||
JID jid,
|
||||
int deviceId,
|
||||
) async {
|
||||
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
|
||||
final bareJid = jid.toBare().toString();
|
||||
final bareJid = jid.toBare();
|
||||
final item = await pm.getItem(bareJid, omemoBundlesXmlns, '$deviceId');
|
||||
if (item.isType<PubSubError>()) return Result(UnknownOmemoError());
|
||||
|
||||
@@ -573,7 +594,9 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
/// nodes.
|
||||
///
|
||||
/// On success, returns true. On failure, returns an OmemoError.
|
||||
Future<Result<OmemoError, bool>> publishBundle(OmemoBundle bundle) async {
|
||||
Future<Result<OmemoError, bool>> publishBundle(
|
||||
omemo.OmemoBundle bundle,
|
||||
) async {
|
||||
final attrs = getAttributes();
|
||||
final pm = attrs.getManagerById<PubSubManager>(pubsubManager)!;
|
||||
final bareJid = attrs.getFullJID().toBare();
|
||||
@@ -609,7 +632,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
);
|
||||
|
||||
final deviceListPublish = await pm.publish(
|
||||
bareJid.toString(),
|
||||
bareJid,
|
||||
omemoDevicesXmlns,
|
||||
newDeviceList,
|
||||
id: 'current',
|
||||
@@ -621,7 +644,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
}
|
||||
|
||||
final deviceBundlePublish = await pm.publish(
|
||||
bareJid.toString(),
|
||||
bareJid,
|
||||
omemoBundlesXmlns,
|
||||
bundleToXML(bundle),
|
||||
id: '${bundle.id}',
|
||||
@@ -637,7 +660,12 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
/// Subscribes to the device list PubSub node of [jid].
|
||||
Future<void> subscribeToDeviceListImpl(String jid) async {
|
||||
final pm = getAttributes().getManagerById<PubSubManager>(pubsubManager)!;
|
||||
await pm.subscribe(jid, omemoDevicesXmlns);
|
||||
await pm.subscribe(JID.fromString(jid), omemoDevicesXmlns);
|
||||
}
|
||||
|
||||
/// Implementation for publishing our device [device].
|
||||
Future<void> publishDeviceImpl(omemo.OmemoDevice device) async {
|
||||
await publishBundle(await device.toBundle());
|
||||
}
|
||||
|
||||
/// Attempts to find out if [jid] supports omemo:2.
|
||||
@@ -646,7 +674,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
/// On failure, returns an OmemoError.
|
||||
Future<Result<OmemoError, bool>> supportsOmemo(JID jid) async {
|
||||
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
|
||||
final items = await dm.discoItemsQuery(jid.toBare().toString());
|
||||
final items = await dm.discoItemsQuery(jid.toBare());
|
||||
|
||||
if (items.isType<DiscoError>()) return Result(UnknownOmemoError());
|
||||
|
||||
@@ -686,7 +714,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
|
||||
.toList(),
|
||||
);
|
||||
final publishResult = await pm.publish(
|
||||
jid.toString(),
|
||||
jid,
|
||||
omemoDevicesXmlns,
|
||||
newPayload,
|
||||
id: 'current',
|
||||
|
||||
@@ -5,9 +5,9 @@ import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0264.dart';
|
||||
|
||||
class StatelessMediaSharingData {
|
||||
class StatelessMediaSharingData implements StanzaHandlerExtension {
|
||||
const StatelessMediaSharingData({
|
||||
required this.mediaType,
|
||||
required this.size,
|
||||
@@ -20,7 +20,7 @@ class StatelessMediaSharingData {
|
||||
final int size;
|
||||
final String description;
|
||||
final Map<String, String> hashes; // algo -> hash value
|
||||
final List<Thumbnail> thumbnails;
|
||||
final List<JingleContentThumbnail> thumbnails;
|
||||
|
||||
final String url;
|
||||
}
|
||||
@@ -48,16 +48,11 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
|
||||
break;
|
||||
}
|
||||
|
||||
final thumbnails = List<Thumbnail>.empty(growable: true);
|
||||
for (final child in file.children) {
|
||||
// TODO(Unknown): Handle other thumbnails
|
||||
if (child.tag == 'file-thumbnail' &&
|
||||
child.attributes['xmlns'] == fileThumbnailsXmlns) {
|
||||
final thumb = parseFileThumbnailElement(child);
|
||||
if (thumb != null) {
|
||||
thumbnails.add(thumb);
|
||||
}
|
||||
}
|
||||
// Thumbnails
|
||||
final thumbnails = List<JingleContentThumbnail>.empty(growable: true);
|
||||
for (final i
|
||||
in file.findTags('thumbnail', xmlns: jingleContentThumbnailXmlns)) {
|
||||
thumbnails.add(JingleContentThumbnail.fromXML(i));
|
||||
}
|
||||
|
||||
return StatelessMediaSharingData(
|
||||
@@ -70,7 +65,9 @@ StatelessMediaSharingData parseSIMSElement(XMLNode node) {
|
||||
);
|
||||
}
|
||||
|
||||
@Deprecated('Not maintained')
|
||||
class SIMSManager extends XmppManagerBase {
|
||||
@Deprecated('Not maintained')
|
||||
SIMSManager() : super(simsManager);
|
||||
|
||||
@override
|
||||
@@ -85,7 +82,7 @@ class SIMSManager extends XmppManagerBase {
|
||||
tagXmlns: referenceXmlns,
|
||||
// Before the message handler
|
||||
priority: -99,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -98,7 +95,7 @@ class SIMSManager extends XmppManagerBase {
|
||||
final references = message.findTags('reference', xmlns: referenceXmlns);
|
||||
for (final ref in references) {
|
||||
final sims = ref.firstTag('media-sharing', xmlns: simsXmlns);
|
||||
if (sims != null) return state.copyWith(sims: parseSIMSElement(sims));
|
||||
if (sims != null) return state..extensions.set(parseSIMSElement(sims));
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
|
||||
/// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2.
|
||||
abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
/// A data class describing the user agent. See https://dyn.eightysoft.de/final/xep-0388.html#initiation
|
||||
/// A data class describing the user agent. See https://xmpp.org/extensions/xep-0388.html#initiation.
|
||||
class UserAgent {
|
||||
const UserAgent({
|
||||
this.id,
|
||||
@@ -24,11 +24,9 @@ class UserAgent {
|
||||
);
|
||||
return XMLNode(
|
||||
tag: 'user-agent',
|
||||
attributes: id != null
|
||||
? {
|
||||
'id': id,
|
||||
}
|
||||
: {},
|
||||
attributes: {
|
||||
if (id != null) 'id': id,
|
||||
},
|
||||
children: [
|
||||
if (software != null)
|
||||
XMLNode(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||
import 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
|
||||
@@ -20,12 +20,10 @@ enum Sasl2State {
|
||||
/// A negotiator that implements XEP-0388 SASL2. Alone, it does nothing. Has to be
|
||||
/// registered with other negotiators that register themselves against this one.
|
||||
class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
|
||||
Sasl2Negotiator({
|
||||
this.userAgent,
|
||||
}) : super(100, false, sasl2Xmlns, sasl2Negotiator);
|
||||
Sasl2Negotiator() : super(100, false, sasl2Xmlns, sasl2Negotiator);
|
||||
|
||||
/// The user agent data that will be sent to the server when authenticating.
|
||||
final UserAgent? userAgent;
|
||||
UserAgent? userAgent;
|
||||
|
||||
/// List of callbacks that are registered against us. Will be called once we get
|
||||
/// SASL2 features.
|
||||
|
||||
61
packages/moxxmpp/lib/src/xeps/xep_0421.dart
Normal file
61
packages/moxxmpp/lib/src/xeps/xep_0421.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'dart:async';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
/// Representation of a <occupant-id /> element.
|
||||
class OccupantIdData implements StanzaHandlerExtension {
|
||||
const OccupantIdData(
|
||||
this.id,
|
||||
);
|
||||
|
||||
/// The unique occupant id.
|
||||
final String id;
|
||||
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'occupant-id',
|
||||
xmlns: occupantIdXmlns,
|
||||
attributes: {
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OccupantIdManager extends XmppManagerBase {
|
||||
OccupantIdManager() : super(occupantIdManager);
|
||||
|
||||
@override
|
||||
List<String> getDiscoFeatures() => [
|
||||
occupantIdXmlns,
|
||||
];
|
||||
|
||||
@override
|
||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||
StanzaHandler(
|
||||
stanzaTag: 'message',
|
||||
tagName: 'occupant-id',
|
||||
tagXmlns: occupantIdXmlns,
|
||||
callback: _onMessage,
|
||||
// Before the MessageManager
|
||||
priority: MessageManager.messageHandlerPriority + 1,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<bool> isSupported() async => true;
|
||||
|
||||
Future<StanzaHandlerData> _onMessage(
|
||||
Stanza stanza,
|
||||
StanzaHandlerData state,
|
||||
) async {
|
||||
return state
|
||||
..extensions.set(OccupantIdData(stanza.attributes['id']! as String));
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,19 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
class MessageRetractionData {
|
||||
class MessageRetractionData implements StanzaHandlerExtension {
|
||||
MessageRetractionData(this.id, this.fallback);
|
||||
|
||||
/// A potential fallback message to set the body to when retracting.
|
||||
final String? fallback;
|
||||
|
||||
/// The id of the message that is retracted.
|
||||
final String id;
|
||||
}
|
||||
|
||||
@@ -24,7 +31,7 @@ class MessageRetractionManager extends XmppManagerBase {
|
||||
callback: _onMessage,
|
||||
// Before the MessageManager
|
||||
priority: -99,
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -47,11 +54,55 @@ class MessageRetractionManager extends XmppManagerBase {
|
||||
final isFallbackBody =
|
||||
message.firstTag('fallback', xmlns: fallbackIndicationXmlns) != null;
|
||||
|
||||
return state.copyWith(
|
||||
messageRetraction: MessageRetractionData(
|
||||
applyTo.attributes['id']! as String,
|
||||
isFallbackBody ? message.firstTag('body')?.innerText() : null,
|
||||
),
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
MessageRetractionData(
|
||||
applyTo.attributes['id']! as String,
|
||||
isFallbackBody ? message.firstTag('body')?.innerText() : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final data = extensions.get<MessageRetractionData>();
|
||||
return data != null
|
||||
? [
|
||||
XMLNode.xmlns(
|
||||
tag: 'apply-to',
|
||||
xmlns: fasteningXmlns,
|
||||
attributes: <String, String>{
|
||||
'id': data.id,
|
||||
},
|
||||
children: [
|
||||
XMLNode.xmlns(
|
||||
tag: 'retract',
|
||||
xmlns: messageRetractionXmlns,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (data.fallback != null)
|
||||
XMLNode(
|
||||
tag: 'body',
|
||||
text: data.fallback,
|
||||
),
|
||||
if (data.fallback != null)
|
||||
XMLNode.xmlns(
|
||||
tag: 'fallback',
|
||||
xmlns: fallbackIndicationXmlns,
|
||||
),
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,18 @@ import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/message.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/stanza.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/util/typed_map.dart';
|
||||
|
||||
class MessageReactions {
|
||||
const MessageReactions(this.messageId, this.emojis);
|
||||
class MessageReactionsData implements StanzaHandlerExtension {
|
||||
const MessageReactionsData(this.messageId, this.emojis);
|
||||
final String messageId;
|
||||
final List<String> emojis;
|
||||
|
||||
XMLNode toXml() {
|
||||
XMLNode toXML() {
|
||||
return XMLNode.xmlns(
|
||||
tag: 'reactions',
|
||||
xmlns: messageReactionsXmlns,
|
||||
@@ -55,14 +57,36 @@ class MessageReactionsManager extends XmppManagerBase {
|
||||
) async {
|
||||
final reactionsElement =
|
||||
message.firstTag('reactions', xmlns: messageReactionsXmlns)!;
|
||||
return state.copyWith(
|
||||
messageReactions: MessageReactions(
|
||||
reactionsElement.attributes['id']! as String,
|
||||
reactionsElement.children
|
||||
.where((c) => c.tag == 'reaction')
|
||||
.map((c) => c.innerText())
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
return state
|
||||
..extensions.set(
|
||||
MessageReactionsData(
|
||||
reactionsElement.attributes['id']! as String,
|
||||
reactionsElement.children
|
||||
.where((c) => c.tag == 'reaction')
|
||||
.map((c) => c.innerText())
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<XMLNode> _messageSendingCallback(
|
||||
TypedMap<StanzaHandlerExtension> extensions,
|
||||
) {
|
||||
final data = extensions.get<MessageReactionsData>();
|
||||
return data != null
|
||||
? [
|
||||
data.toXML(),
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
await super.postRegisterCallback();
|
||||
|
||||
// Register the sending callback
|
||||
getAttributes()
|
||||
.getManagerById<MessageManager>(messageManager)
|
||||
?.registerMessageSendingCallback(_messageSendingCallback);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user