Browse Source

Implement XEP-0390: Entity Capabilities 2.0

pull/1456/head
Timothée Jaussoin 2 months ago
parent
commit
ffbe771eda
  1. 3
      CHANGELOG.md
  2. 2
      app/Identity.php
  3. 35
      app/Info.php
  4. 1
      app/Post.php
  5. 5
      app/Presence.php
  6. 6
      app/Widgets/Chat/chat.css
  7. 23
      database/migrations/20250813153534_add_lang_name_to_identities_table.php
  8. 17
      doap.xml
  9. 12
      src/Moxl/Stanza/Disco.php
  10. 9
      src/Moxl/Stanza/Presence.php
  11. 75
      src/Moxl/Utils.php
  12. 18
      src/Moxl/Xec/Action/Disco/Request.php
  13. 24
      src/Moxl/Xec/Payload/Caps.php
  14. 3
      src/Moxl/Xec/Payload/DiscoInfo.php

3
CHANGELOG.md

@ -1,7 +1,7 @@
Movim Changelog
================
v0.31.1 (master)
v0.32 (master)
---------------------------
* Add a copy external link button on the Posts
* Improve the Communities discovery flow when visiting a server
@ -14,6 +14,7 @@ v0.31.1 (master)
* Add a "Scroll to Message in History" feature
* Implement full text search of Message bodies using the PostgreSQL tsvector and tssearch features
* Refactor the Public URL/Public Blog flow and texts
* Implement XEP-0390: Entity Capabilities 2.0
v0.31
---------------------------

2
app/Identity.php

@ -22,6 +22,8 @@ class Identity extends Model
'info_id' => $identity->info_id,
'category' => $identity->category,
'type' => $identity->type,
'lang' => $identity->lang,
'name' => $identity->name
];
})->all(), $identities->first()->primaryKey);
}

35
app/Info.php

@ -3,13 +3,15 @@
namespace App;
use Awobaz\Compoships\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Moxl\Utils;
class Info extends Model
{
protected $fillable = ['server', 'node', 'avatarhash'];
protected $with = ['identities'];
private $freshIdentities;
private Collection $freshIdentities;
public function identities()
{
@ -329,6 +331,14 @@ class Info extends Model
$identity->category = (string)$i->attributes()->category;
$identity->type = (string)$i->attributes()->type;
if ($i->attributes()->name) {
$identity->name = (string)$i->attributes()->name;
}
if ($i->attributes()->{'xml-lang'}) {
$identity->lang = (string)$i->attributes()->{'xml-lang'};
}
$this->freshIdentities->push($identity);
$this->name = ($i->attributes()->name)
? (string)$i->attributes()->name
@ -511,14 +521,33 @@ class Info extends Model
return $roles;
}
public function isPubsubService()
public function isPubsubService(): bool
{
return ($this->identities->contains('category', 'pubsub')
&& $this->identities->contains('type', 'service'));
}
public function isMicroblogCommentsNode()
public function isMicroblogCommentsNode(): bool
{
return (substr($this->node, 0, 29) == 'urn:xmpp:microblog:0:comments');
}
public function checkCapabilityHash(): bool
{
preg_match('/urn:xmpp:caps#(.*)\./', $this->node, $matches);
$generatedHash = Utils::getCapabilityHashNode(
Utils::generateCapabilityHash(
$this->freshIdentities,
unserialize($this->attributes['features']),
$matches[1]
)
);
if ($this->node != $generatedHash) {
\logError('XEP-0390: Wrong hash for ' . $this->node . ' != ' . $generatedHash);
}
return $this->node == $generatedHash;
}
}

1
app/Post.php

@ -11,7 +11,6 @@ use Illuminate\Support\Collection;
use Movim\Widget\Wrapper;
use Moxl\Xec\Payload\Packet;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
use SimpleXMLElement;
class Post extends Model

5
app/Presence.php

@ -112,8 +112,9 @@ class Presence extends Model
}
if ($stanza->c) {
$this->node = (string)$stanza->c->attributes()->node .
'#' . (string)$stanza->c->attributes()->ver;
$this->node = (string)$stanza->c->attributes()->xmlns == 'urn:xmpp:caps'
? 'urn:xmpp:caps#' . (string)$stanza->c->hash->attributes()->algo . '.' . (string)$stanza->c->hash
: (string)$stanza->c->attributes()->node . '#' . (string)$stanza->c->attributes()->ver;
}
$this->priority = ($stanza->priority) ? (int)$stanza->priority : 0;

6
app/Widgets/Chat/chat.css

@ -192,13 +192,15 @@ main:not(.enabled) #chat_widget {
#chat_widget .chat_box form textarea[data-encryptedstate="disabled"]~span.control.icon.encrypted_disabled,
#chat_widget .chat_box form textarea[data-encryptedstate="build"]~span.control.icon.encrypted_loading {
display: inline-block;
font-size: 3rem;
font-size: 2.75rem;
line-height: 6rem;
height: 6rem;
position: absolute;
right: 0;
top: 0;
padding: 0 1rem;
padding: 0 1.5rem;
z-index: 1;
color: rgba(var(--movim-font), 0.68);
}
#chat_widget .chat_box li.main > span.primary,

23
database/migrations/20250813153534_add_lang_name_to_identities_table.php

@ -0,0 +1,23 @@
<?php
use Movim\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddLangNameToIdentitiesTable extends Migration
{
public function up()
{
$this->schema->table('identities', function (Blueprint $table) {
$table->string('lang')->nullable();
$table->string('name')->nullable();
});
}
public function down()
{
$this->schema->table('identities', function (Blueprint $table) {
$table->dropColumn('lang');
$table->dropColumn('name');
});
}
}

17
doap.xml

@ -392,6 +392,15 @@
<xmpp:note>only used for Carbons</xmpp:note>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0300.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.0.0</xmpp:version>
<xmpp:note>for XEP-0390</xmpp:note>
<xmpp:since>0.32</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/>
@ -535,6 +544,14 @@
<xmpp:since>0.24</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0390.html"/>
<xmpp:status>partial</xmpp:status>
<xmpp:version>0.3.2</xmpp:version>
<xmpp:since>0.32</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0392.html"/>

12
src/Moxl/Stanza/Disco.php

@ -6,16 +6,18 @@ use Moxl\Utils;
class Disco
{
public static function answer($to, $id)
public static function answer(string $to, string $id, string $node)
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$query = $dom->createElementNS('http://jabber.org/protocol/disco#info', 'query');
$query->setAttribute('node', 'https://movim.eu/#'.Utils::generateCaps());
$query->setAttribute('node', $node);
$identityData = Utils::getIdentity();
$identity = $dom->createElement('identity');
$identity->setAttribute('category', 'client');
$identity->setAttribute('type', 'web');
$identity->setAttribute('name', 'Movim');
$identity->setAttribute('category', $identityData->category);
$identity->setAttribute('type', $identityData->type);
$identity->setAttribute('name', $identityData->name);
$query->appendChild($identity);

9
src/Moxl/Stanza/Presence.php

@ -89,6 +89,15 @@ class Presence
$root->appendChild($x);
}
$c = $dom->createElementNS('urn:xmpp:caps', 'c');
$hash = $dom->createElement('hash', \Moxl\Utils::getOwnCapabilityHash());
$hash->setAttribute('xmlns', 'urn:xmpp:hashes:2');
$hash->setAttribute('algo', \Moxl\Utils::CAPABILITY_HASH_ALGORITHM);
$c->appendChild($hash);
$root->appendChild($c);
$c = $dom->createElementNS('http://jabber.org/protocol/caps', 'c');
$c->setAttribute('hash', 'sha-1');
$c->setAttribute('node', 'https://movim.eu/');

75
src/Moxl/Utils.php

@ -2,13 +2,82 @@
namespace Moxl;
use App\Identity;
use App\Post;
use Illuminate\Support\Collection;
class Utils
{
public const CAPABILITY_HASH_ALGORITHM = 'sha-256';
/**
* https://xmpp.org/extensions/xep-0390.html#algorithm-hashnodes
*/
public static function getCapabilityHashNode(string $capabilityHash, ?string $hash = Utils::CAPABILITY_HASH_ALGORITHM): string
{
return 'urn:xmpp:caps#' . $hash . '.' . $capabilityHash;
}
public static function getOwnCapabilityHash(?string $hash = Utils::CAPABILITY_HASH_ALGORITHM): string
{
return Utils::generateCapabilityHash(collect([Utils::getIdentity()]), Utils::getSupportedServices(), $hash);
}
/**
* https://xmpp.org/extensions/xep-0390.html#algorithm-example
*/
public static function generateCapabilityHash(Collection $identities, array $features, ?string $hash = Utils::CAPABILITY_HASH_ALGORITHM)
{
$data = '';
asort($features);
foreach ($features as $feature) {
$data .= $feature . chr(31); // 0x1f (ASCII Unit Separator)
}
$data .= chr(28); // 0x1c (ASCII File Separator)
$identitiesData = [];
foreach ($identities as $identity) {
array_push(
$identitiesData,
$identity->category . chr(31) .
$identity->type . chr(31) .
$identity->lang ?? '' . chr(31) .
$identity->name ?? '' . chr(31) .
chr(30) // 0x1e (ASCII Record Separator)
);
}
asort($identitiesData);
foreach ($identitiesData as $identityData) {
$data .= $identityData;
}
$data .= chr(28);
$data .= chr(28);
return base64_encode(hash(IANAHashToPhp()[$hash], $data, true));
}
public static function getIdentity(): Identity
{
$identity = new Identity;
$identity->category = 'client';
$identity->type = 'web';
$identity->lang = null;
$identity->name = 'Movim';
return $identity;
}
public static function getSupportedServices()
{
return [
$features = [
'urn:xmpp:microblog:0',
Post::MICROBLOG_NODE . '+notify',
Post::STORIES_NODE . '+notify',
@ -81,6 +150,10 @@ class Utils
//'http://jabber.org/protocol/tune',
//'http://jabber.org/protocol/tune+notify';
];
asort($features);
return $features;
}
public static function injectConfigInX(\DOMNode $x, array $inputs)

18
src/Moxl/Xec/Action/Disco/Request.php

@ -33,9 +33,17 @@ class Request extends Action
$info = new \App\Info;
$info->set($stanza, $this->_node, $this->_parent);
/**
* https://xmpp.org/extensions/xep-0390.html#rules-processing-caching
*/
if (
str_starts_with($info->node, 'urn:xmpp:caps')
&& !$info->checkCapabilityHash()
) return;
$found = \App\Info::where('server', $info->server)
->where('node', $info->node)
->first();
->where('node', $info->node)
->first();
if ($found) {
$found->set(
@ -52,8 +60,10 @@ class Request extends Action
$info->save();
}
if (!$info->identities->contains('category', 'account')
&& !$info->identities->contains('category', 'client')) {
if (
!$info->identities->contains('category', 'account')
&& !$info->identities->contains('category', 'client')
) {
$this->deliver();
}
}

24
src/Moxl/Xec/Payload/Caps.php

@ -1,24 +0,0 @@
<?php
namespace Moxl\Xec\Payload;
use Moxl\Xec\Action\Disco\Request;
use App\Info;
class Caps extends Payload
{
public function handle(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null)
{
$node = $stanza->attributes()->node.'#'.$stanza->attributes()->ver;
$to = (string)$parent->attributes()->from;
$info = Info::where('node', $node)->first();
if ((!$info || $info->isEmptyFeatures())
&& $parent->getName() != 'streamfeatures') {
$d = new Request;
$d->setTo($to)
->setNode($node)
->request();
}
}
}

3
src/Moxl/Xec/Payload/DiscoInfo.php

@ -3,6 +3,7 @@
namespace Moxl\Xec\Payload;
use Moxl\Stanza\Disco;
use Moxl\Utils;
class DiscoInfo extends Payload
{
@ -12,7 +13,7 @@ class DiscoInfo extends Payload
$jid = (string)$parent->attributes()->from;
$id = (string)$parent->attributes()->id;
Disco::answer($jid, $id);
Disco::answer($jid, $id, (string)$stanza->attributes()->node);
}
}
}
Loading…
Cancel
Save