Skip to content

Commit

Permalink
Merge pull request #20 from vsevex/caps
Browse files Browse the repository at this point in the history
`caps` final
  • Loading branch information
vsevex authored Jul 31, 2023
2 parents 978a9df + 63354ec commit 9076000
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 70 deletions.
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

0 comments on commit 9076000

Please sign in to comment.