Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing Caps Extension #20

Merged
merged 14 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions lib/extensions/caps/README.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Entity Capabilities

This extension provides support for handling capabilities in the XMPP protocol using Caps (short for **Capabilities**) feature. Allows a client to advertise its capabilities to other entities and discover the capabilities of other entities in the XMPP network.

## Features

- Advertise and discover capabilities of XMPP entities.
- Efficiently handle capabilities updates to avoid unnecessary queries.
- Automatic capabilities exchange during XMPP connection establishment.

## Limitations

Please note that you need to use `echo.disco.addFeature` and `echo.disco.addIdentity` methods to add capabilities features and identity to the client. This extension does not provide built-in methods for adding features and identities to the disco extension.

## Embedding

This extension comes built-in to the client. It means you do not need to attach this extension as you did on other extensions. You can not disable or enable this feature in any way.

## API

This code snippet demonstrates how to use this extension for the client.

```dart

import 'dart:async';
import 'dart:developer';

import 'package:echo/echo.dart';

Future<void> main() async {
final echo = Echo(service: 'ws://example.com:5443/ws');

await echo.connect(
jid: 'vsevex@example.com',
password: 'randompasswordwhichisgoingtobeyourpassword',
callback: (status) async {
if (status == EchoStatus.connected) {
log('Connection Established');
echo.disco.addFeature('someFeature');
echo.caps.sendPresence();
} else if (status == EchoStatus.disconnected) {
log('Connection Terminated');
} else if (status == EchoStatus.register) {
registrationCompleter.complete(true);
}
},
);
}

```
182 changes: 182 additions & 0 deletions lib/extensions/caps/caps_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import 'package:echo/echo.dart';
import 'package:echo/src/constants.dart';

/// Represents Caps (short for Entity Capabilities) plugin.
///
/// CapsExtension is an implementation of the [Extension] class that provides
/// support for handling capabilities in the XMPP protocol using the
/// Capabilities feature.
///
/// This extension allows the client to advertise its capabilities to other
/// entities and discover the capabilities of other entities.
class CapsExtension extends Extension {
/// Creates an instance of the [CapsExtension] with the provided extension
/// name.
///
/// For more information about this extension please refer to [CapsExtension]
/// or `Readme`.
CapsExtension() : super('caps-extension');

/// A [Map] to store known capabilities of other entities identified by the
/// `verification` attribute received in the presence stanzas.
final _knownCapabilities = <String, List<Map<String, dynamic>>>{};

/// A [Map] to store the `verification` attribute received in the presence
/// from different entities, indexed by their respective JIDs.
final _jidIndex = <String, String>{};

/// Called when the connection status changes.
@override
void changeStatus(EchoStatus status, String? condition) {
/// If the status is in `connected` state, it sends the client's presence
/// with its capabilities.
if (status == EchoStatus.connected) {
sendPresence();
}
}

@override
void initialize(Echo echo) {
/// CAPS feature namespace
echo.addNamespace('CAPS', 'http://jabber.org/protocol/caps');

/// Add the `CAPS` feature and client identity to the disco extension.
echo.disco.addFeature(ns['CAPS']!);
echo.disco.addIdentity(category: 'client', type: 'mobile', name: 'echo');

/// Set up presence handler to process the incoming presence stanzas.
echo.addHandler(
(stanza) {
final from = stanza.getAttribute('from');
final c = stanza.findAllElements('c').first;
final ver = c.getAttribute('ver');
final node = c.getAttribute('node');

if (!_knownCapabilities.containsKey(ver)) {
/// If the capabilities are not known, request capabilities from the
/// entity.
return _requestCapabilities(to: from!, node: node!, ver: ver!);
} else {
_jidIndex[from!] = ver!;
}
if (!_jidIndex.containsKey(from) || _jidIndex[from] != ver) {
_jidIndex[from] = ver;
}
return true;
},
namespace: ns['CAPS'],
name: 'presence',
);

super.echo = echo;
}

/// Requests capabilities from the given entity (identified by `to` JID) with
/// the provided `node` and `ver` attributes.
///
/// * @param to The Jabber Identifier to indicate who the request is going to.
/// * @param node A Unique Identifier for the capabilities being queried. It
/// helps in distinguishing between different sets of caps provided by the
/// same entity.
/// * @param ver Stands for verification string and helps preventing poisoning
/// of entity capabilities information.
Future<bool> _requestCapabilities({
required String to,
required String node,
required String ver,
}) async {
if (to != echo!.jid) {
await echo!.disco
.info(to, node: '$node#$ver', resultCallback: _handleDiscoInfoReply);
}
return true;
}

/// Handles the reply to the disco#info query and updates the known
/// capabilities for the entity identified by 'from' JID.
bool _handleDiscoInfoReply(XmlElement stanza) {
final query = stanza.findAllElements('query').first;
final node = query.getAttribute('node')!.split('#');
final ver = node.first;
final from = stanza.getAttribute('from');

if (!_knownCapabilities.containsKey(ver) ||
_knownCapabilities[ver] == null) {
/// If the capabilities are not known, add them to the knownCapabilities
/// [Map].
final nodes = query.descendantElements.toList();
_knownCapabilities[ver] = [];
for (int i = 0; i < nodes.length; i++) {
final node = nodes[i];
_knownCapabilities[ver]!
.add({'name': node.name.local, 'attributes': node.attributes});
}
_jidIndex[from!] = ver;
} else if (_jidIndex[from] == null || _jidIndex[from] != ver) {
_jidIndex[from!] = ver;
}

return false;
}

/// Generates the attributes for the 'c' (capabilities) node in the client's
/// presence.
Map<String, String> get _generateCapsAttributes => {
'xmlns': ns['CAPS']!,
'hash': 'sha-1',
'node': 'echo 0.0.6<',
'ver': _generateVerificationString,
};

/// Creates the 'c' (capabilities) node for the client's presence.
XmlElement? get _createCapsNode =>
EchoBuilder('c', _generateCapsAttributes).nodeTree;

void sendPresence() => echo!.send(EchoBuilder.pres().cnode(_createCapsNode!));

/// Generates the verification string for the client's capabilities based on
/// the identities and features supported by the client.
///
/// For more information about this string please refer to the documentation:
/// https://xmpp.org/extensions/xep-0115.html#ver
String get _generateVerificationString {
final verificationStringBuffer = StringBuffer();
final identities = echo!.disco.identities;
_sort(identities, 'category');
_sort(identities, 'type');
_sort(identities, 'language');
final features = echo!.disco.features..sort();

for (int i = 0; i < identities.length; i++) {
final id = identities[i];
verificationStringBuffer
..writeAll([id.category, id.type, id.language], '/')
..write(id.name)
..write('<');
}

for (int i = 0; i < features.length; i++) {
verificationStringBuffer
..write(features[i])
..write('<');
}

return Echotils.btoa(
Echotils.utf16to8(verificationStringBuffer.toString()),
);
}

/// Sorts the list of [DiscoIdentity] objects based on the specified
/// 'property'. The sorting is done in-place and returns the sorted list of
/// identities.
List<DiscoIdentity> _sort(List<DiscoIdentity> identities, String property) {
if (property == 'category') {
identities.sort((i1, i2) => i1.category.compareTo(i2.category));
} else if (property == 'type') {
identities.sort((i1, i2) => i1.type.compareTo(i2.type));
} else if (property == 'language') {
identities.sort((i1, i2) => i1.language!.compareTo(i2.language!));
}
return identities;
}
}
54 changes: 27 additions & 27 deletions lib/extensions/disco/disco_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ class DiscoExtension extends Extension {
late final _items = <DiscoItem>[];

/// The list of feature strings.
late final _features = <String>[];
late final features = <String>[];

/// The list of [DiscoIdentity] objects.
late final _identities = <DiscoIdentity>[];
late final identities = <DiscoIdentity>[];

/// This method is not implemented and will not be affected in the use of this
/// extension.
Expand Down Expand Up @@ -72,16 +72,16 @@ class DiscoExtension extends Extension {
String name = '',
String language = '',
}) {
for (int i = 0; i < _identities.length; i++) {
if (_identities[i].category == category &&
_identities[i].type == type &&
_identities[i].name == name &&
_identities[i].language == language) {
for (int i = 0; i < identities.length; i++) {
if (identities[i].category == category &&
identities[i].type == type &&
identities[i].name == name &&
identities[i].language == language) {
return false;
}
}

_identities.add(
identities.add(
DiscoIdentity(
category: category,
type: type,
Expand Down Expand Up @@ -110,22 +110,22 @@ class DiscoExtension extends Extension {
FutureOr<void> Function(EchoException)? errorCallback,
int? timeout,
}) async {
final id = super.echo!.getUniqueId('info');
final attributes = <String, String>{'xmlns': ns['DISCO_INFO']!};

if (node != null) {
attributes['node'] = node;
}

final info = EchoBuilder.iq(
attributes: {'from': echo!.jid, 'to': jid, 'type': 'get'},
attributes: {'from': super.echo!.jid, 'to': jid, 'type': 'get', 'id': id},
).c('query', attributes: attributes);

return echo!.sendIQ(
element: info.nodeTree!,
resultCallback: resultCallback,
errorCallback: errorCallback,
waitForResult: true,
timeout: timeout,
);
}

Expand Down Expand Up @@ -171,12 +171,12 @@ class DiscoExtension extends Extension {
/// * @return `true` if the feature was added successfully, or `false` if the
/// feature already exists.
bool addFeature(String variableName) {
for (int i = 0; i < _features.length; i++) {
if (_features[i] == variableName) {
for (int i = 0; i < features.length; i++) {
if (features[i] == variableName) {
return false;
}
}
_features.add(variableName);
features.add(variableName);
return true;
}

Expand All @@ -186,9 +186,9 @@ class DiscoExtension extends Extension {
/// * @return `true` feature was removed successfully, or `false` if the
/// feature does not exist.
bool removeFeature(String variableName) {
for (int i = 0; i < _features.length; i++) {
if (_features[i] == variableName) {
_features.removeAt(i);
for (int i = 0; i < features.length; i++) {
if (features[i] == variableName) {
features.removeAt(i);
return true;
}
}
Expand All @@ -209,25 +209,25 @@ class DiscoExtension extends Extension {
final iqResult =
_buildIQResult(stanza: stanza, queryAttributes: attributes);

for (int i = 0; i < _identities.length; i++) {
for (int i = 0; i < identities.length; i++) {
attributes = {
'category': _identities[i].category,
'type': _identities[i].type
'category': identities[i].category,
'type': identities[i].type
};
if (_identities[i].name != null) {
attributes['name'] = _identities[i].name!;
if (identities[i].name != null) {
attributes['name'] = identities[i].name!;
}
if (_identities[i].language != null) {
attributes['language'] = _identities[i].language!;
if (identities[i].language != null) {
attributes['language'] = identities[i].language!;
}
iqResult.c('identity', attributes: attributes).up();
}

for (int i = 0; i < _features.length; i++) {
iqResult.c('feature', attributes: {'var': _features[i]}).up();
for (int i = 0; i < features.length; i++) {
iqResult.c('feature', attributes: {'var': features[i]}).up();
}

echo!.send(iqResult.nodeTree);
echo!.send(iqResult);
return true;
}

Expand Down Expand Up @@ -265,7 +265,7 @@ class DiscoExtension extends Extension {
}
iqResult.c('item', attributes: attributes).up();
}
echo!.send(iqResult.nodeTree);
echo!.send(iqResult);
return true;
}

Expand Down
1 change: 1 addition & 0 deletions lib/extensions/extensions.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'caps/caps_extension.dart';
export 'disco/disco_extension.dart';
export 'pubsub/pubsub_extension.dart';
export 'v-card/vcard_extension.dart';
Loading