将 Joomla 网站转变为 ActivityPub 服务器并成为 Fediverse 的独立参与者 - 开始
本文分为两部分。在第一部分中,我总结了 Fediverse 和 ActivityPub 这两个术语的含义。在第二部分中,我简要描述了我为了使 Joomla 网站支持 ActivityPub 协议并允许 Mastodon 用户关注 Joomla 网站用户而实施的内容。
TLDR 和常见问题解答
- 为什么网站需要 Fediverse 或 IndieWeb?
网站所有者希望他们的网站能够与其他网站联网。 - 为什么有人希望他的网站这样做呢?
他希望他的内容能被他人轻松找到。他希望他的内容能与其他网站上的内容链接,而无需直接在这些网站上发布。这意味着他仍然完全控制自己网站上的内容,并且可以与他人分享,无论平台如何。Mastodon 用户能否关注并喜欢 Joomla 网站上的博客文章不是很好吗? - 我仍然不清楚这为什么重要。
如果主要社交网络发生变化,以至于你不再想成为其中的一员,或者它关闭了,你将失去你的粉丝和内容。经验表明,在这样的事件之后,许多用户会转而使用其他平台。这意味着许多用户需要付出大量工作,并且在过程中通常会有所损失。这不必是这样。使用联邦网络并自行控制你的内容。
Fediverse 和 ActivityPub
我不记得我第一次发现有关 Fediverse 的内容是什么时候。我觉得这很有趣。但我并没有陷入其中。当2022年春季关于 Elon Musk 和 Twitter 的巨大兴奋开始时,我惊讶地发现,Joomla 社区中很多人从未听说过 Fediverse、ActivityPub 或 IndieWeb 这些术语。毕竟,这些人大多是技术高手。对于集中式和去中心化服务的区别,很少有人能清楚。我经常听到这样的说法:“Mastodon 是新的 Twitter”。我承认我以前听说过所有这些术语,但我不能精确地定义它们。我想改变这一点,也许这会对某个 Joomlaner 有所帮助。此外,我现在在 Fediverse 中感到非常舒适,并想为它做一点宣传。
联邦宇宙
联邦宇宙(“federation”和“universe”的组合)是一系列联邦服务器(即相互连接的服务器),用于网页发布(即社交网络、微博、博客或网站)和文件托管,但这些服务器虽然独立托管,但仍可以相互通信。在不同的服务器(技术上称为实例)上,用户可以创建所谓的身份。这些身份能够跨越实例的边界进行通信,因为运行在服务器上的软件支持一个或多个遵循开放标准的通信协议。作为联邦宇宙中的一个身份,用户可以发布文本和其他媒体,或关注其他身份的帖子。- wikipedia.org
听起来很复杂。但并不复杂。最重要的是
- 联邦服务器
- 独立托管
- 可以相互通信。
因此,基本上,人们可以运行自己的服务器。用户可以创建账户,并且他们可以与任何其他服务器进行通信,因为他们已经同意了规则或标准。
这意味着联邦宇宙不是集中组织的。它更像是电子邮件服务器,你可以在Joomla服务器上创建一个@community.joomla.org
,在Google Gmail服务器上创建一个@gmail.com
,或者创建你自己的账户,如@astrid-guenther.de
。
这有优势
- 你可以选择一个具有你喜欢的严格或宽松规则的社区。
- 如果你找不到满足你需求的社区,你可以自己创建它。你可以托管自己的服务器,从而控制你的数据和规则。
联邦宇宙有许多服务。
最受欢迎的联邦宇宙服务是Mastodon。Mastodon在外观和感觉上与Twitter类似,但提供不同的实例。因此,你可以在以下任何地方拥有账户
- social.joomla.org,
- fimidi.com,
- mastodon.social
- 或其它地方。
无论你使用哪种服务,你都可以与所有同意规则和标准的人进行通信。
不仅仅只有Mastodon
一般来说,联邦可以用于
在“许多其它”中可能包括CMS。例如,WordPress和Drupal都为它做好了准备。关于Joomla的更多内容将在后面介绍。
关键是每个系统都可以使用一个身份,你可以和你使用的服务器选择与谁交流,与谁忽视。如果你自己托管服务器,你说了算。具体来说,这意味着以下内容
- 联邦宇宙中的服务器具有抗审查性。
- 联邦宇宙提供给你加入一个不允许你不喜欢的东西的社区。如果你找不到满足你需求的社区,你可以自己创建它。
- 联邦宇宙中的服务让你能够控制你想要控制的数据。
抗审查和有规则的社区?乍一看,这似乎是矛盾的。这里的重要点是,一切都是复数。它不是一个社区。它是许多独立的社区。这些社区相互沟通。但只有当它们愿意的时候。如果你认为一个社区的规则是审查,那么你有选择加入规则更宽松的社区的权利。
如果一个实例是讨论你和你社区不关心的事情的人的聚集地,你不会加入。你可以在你的服务器上发布最棒的Joomla新闻。一个园艺社区会与你分享很少的内容。在Fediverse中,确实存在像现实生活中和在中心化的社交网络中一样的恶意参与者。不受欢迎的恶意人士可以在他们自己的实例上交换内容,而不受审查。这是许多人的一个问题点,但比替代方案要好得多。
听起来不错。但它是如何工作的呢?
我已经提到,你需要规则和标准来使这一切工作。这里我们来到了ActivityPub。
ActivityPub
进入ActivityPub!ActivityPub是一种基于ActivityStreams 2.0数据格式的去中心化社交网络协议。ActivityPub是由W3C社交网络工作组发布的官方W3C推荐标准。它提供了一个客户端到服务器的API,用于创建、更新和删除内容,以及一个联邦服务器到服务器的API,用于发送通知和订阅内容。听起来很令人兴奋?深入探索!- activitypub.rocks
听起来很令人兴奋?不!但它很有用!这就是我们为什么要深入研究。
基本上,这是一份关于事物之间如何相互关联的承诺。所以服务和服务网站之间通信的协议或规则。
一个来自日常生活的例子可以使这一点更容易理解。当人们相互交流时,他们会交换信息。联邦是语言。语法规则和词汇可以与ActivityPub协议相提并论。
ActivityPub和联邦必须是一起吗?不!联邦是指连接和交互多个独立的系统或网络,以便它们作为一个单一、更大的系统有效地协同工作。ActivityPub不是必须的。有其他协议和技术可以用于去中心化网络中的通信和交互。然而,ActivityPub已经确立为标准,并得到Fediverse中许多平台的支持。
在我的关于联邦和ActivityPub的研究中,我不断遇到术语IndieWeb。IndieWeb和Fediverse基于相同的概念。但它们并不相同。
Joomla作为ActivityPub服务器的步骤
在过去几周里,我一直把我Joomla测试博客连接到Fediverse。也许分享这个项目的经验会激发其他Joomla开发者的灵感。让我们来看看实现细节。
Webfinger
如果你在一个Mastodon客户端的搜索栏中输入一个处理程序,比如@
会发生什么?理想情况下,当然,有人已经搜索了这个处理程序,Mastodon服务器已经知道它,并且已经将其全部内容缓存。如果没有这种情况,就会触发一些请求。
首先,使用Webfinger协议。服务器调用以下地址
https://ug-mayen.de/.well-known/webfinger?resource=acct:@This email address is being protected from spambots. You need JavaScript enabled to view it.
应该用如下JSON字符串回答
{
"subject":"acct:This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"aliases":[
"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json"
],
"links":[
{
"rel":"self",
"type":"application\/activity+json",
"href":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json"
}
]
}
什么是JSON?JSON,或JavaScript对象表示法,是一种最小、可读的数据结构格式。它主要用于在服务器和Web应用程序之间作为XML的替代品来传输数据。
这就是我需要实现的第一部分。返回JSON。所以我将组件 com_activitypubs
与JSON视图 components/com_activitypubs/src/View/Webfinger/JsonView.php
集成到我的Joomla安装中,并添加了一个系统插件 plugins/system/activitypub/activitypub.php
用于重定向URL https://example.org/.well-known/webfinger
。
你可能之前听说过“webfinger”这个术语。《Name/Finger》是互联网最早的协议之一。它始于1971年,旨在获取其他计算机上用户的信息。当时,当 *互联网* 用户数量很少时,这被认为是一个好主意。我对“webfinger”这个名称很好奇,所以我查了一下:术语“finger”的定义是“告密”或“识别”。至少我从 维基百科文章 和 Mastodon文档 中得到了这样的理解。
下面的代码片段展示了我在实现JSON视图中的第一步。
// components/com_activitypubs/src/View/Webfinger/JsonView.php
namespace ActivitypubNamespace\Component\Activitypubs\Site\View\Webfinger;
...
class JsonView extends AbstractView
{
...
public function display($tpl = null): void
{
$app = Factory::getApplication();
$params = $app->getParams();
$this->handle = $params->get('handle');
$this->data = $this->getWebfingerData();
$this->response = json_encode($this->data);
header('Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"');
echo json_encode($this->data);
Factory::getApplication()->close();
}
protected function getWebfingerData(): array
{
$uri = Uri::base();
return [
'subject' => 'acct:' . $this->handle . '@' . Uri::getInstance()->toString(['host']),
'aliases' => [
$uri . 'index.php?option=com_activitypubs&view=Profil&format=json'
],
'links' => [
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json'
]
]
];
}
}
据我所知,无法直接在地址
https://example.org/.well-known/webfinger
下通过Joomla扩展实现视图。我首先想到的是通过.htaccess
重定向这个地址。然后我决定使用系统插件。一方面,对于安装不需要了解Web服务器的配置和.htaccess
文件的语法。此外,这个解决方案更加灵活和可定制。
为了简单起见,因为我的博客只有一个用户,我在这个原型设计中通过参数设置了句柄。我计划稍后用匹配的Joomla作者的姓名替换参数。在WordPress Activitypub插件的GitHub仓库 讨论 中显示,这一步更复杂。
为什么我没有使用Joomla API?视图类
Joomla\CMS\MVC\View\JsonApiView
是用于生成符合 json:api 规范的输出的。对于我的 自定义JSON,我认为我需要使用Joomla\CMS\MVC\View\JsonView
类。
下一步是将URL https://ug-mayen.de/.well-known/webfinger?resource=acct:@[email protected]
转发到刚刚实现的视图 https://ug-mayen.de/index.php/component/activitypubs/?view=Webfinger&format=json
。下面的代码片段展示了我在系统插件中使用的方法。
// plugins/system/activitypub/activitypub.php
...
class PlgSystemActivitypub extends CMSPlugin
{
...
public function onAfterInitialise()
{
$uriI = Uri::getInstance();
$host = $uriI->toString(['host']);
$path = $uriI->toString(['path']);
$query = $uriI->toString(['query']);
if ($this->app->isClient('site')
&& str_contains(Uri::getInstance()->toString(['path']), '.well-known/webfinger')
){
Log::add($host . '-' . $path . '-' . $query, Log::DEBUG, 'plg_system_activitypubs');
if (str_starts_with($query, '?')) {
$url = 'index.php/component/activitypubs' . $query . '&view=Webfinger&format=json';
} else {
$url = 'index.php/component/activitypubs/?view=Webfinger&format=json';
}
Factory::getApplication()->redirect($url);
}
}
}
现在我很好奇。我试图通过我的家庭Mastodon服务器fimidi.com的搜索GUI找到 @
。不幸的是,一开始没有成功。这也是可以理解的,Mastodon对 @
一无所知。所有信息,如显示名称、头像、个人资料描述等,都与WebFinger无关。WebFinger只确保这个账户存在。所有这些其他信息都在不同的文件中。这个文件可以在WebFinger响应的“链接”部分找到。它是到 'self'
或用户资料的链接。在我的情况下,用户资料的链接是 $uri . 'index.php?option=com_activitypubs&view=Profil&format=json'
。这个链接的响应看起来像这样
{
"@context":[
"https:\/\/www.w3.org\/ns\/activitystreams",
"https:\/\/w3id.org\/security\/v1"
],
"id":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json",
"type":"Person",
"preferredUsername":"joomla_test_blog",
"name":"joomla_test_blog",
"manuallyApprovesFollowers":false,
"discoverable":true,
"inbox":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Inbox&format=json",
"outbox":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Outbox&format=json",
"followers":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Followers&format=json",
"following":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Following&format=json",
"publicKey":{
"id":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json#main",
"owner":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json",
"publicKeyPem":"-----BEGIN PUBLIC KEY-----\r\nMIIB...QAB\r\n-----END PUBLIC KEY-----"
},
"summary":"<p>A blog of Joomla! users who live in the Mayen region. Here we do not only inform. We also like to test out new things on this website.<\/p>\r\n<p>Ein Blog von Joomla!-Benutzern, die in der Region Mayen leben. Hier informieren wir nicht nur. Wir testen auch gerne neue Dinge auf dieser Website aus.<\/p>",
"url":"https:\/\/ug-mayen.de\/",
"publishedDate":"2023-01-12T00:00:00Z",
"icon":{
"type":"Image",
"url":"https:\/\/ug-mayen.de\/images\/maennchen.png#joomlaImage:\/\/local-images\/maennchen.png?width=141&height=130"
},
}
提示:在Mastodon服务器上搜索用户时,尽量使用配置文件URL
$uri . 'index.php?option=com_activitypubs&view=Profil&format=json'
,而不是使用昵称@
。Mastodon使用缓存来搜索昵称。如果使用配置文件URL,它将被直接调用,配置文件将加载最新版本。此电子邮件地址正在防止垃圾邮件软件。您需要启用JavaScript才能查看。
因此,接下来我根据ActivityPub日志创建了一个用户配置文件的路径。同样,我直接以JSON视图输出数据。
namespace ActivitypubNamespace\Component\Activitypubs\Site\View\Profil;
...
class JsonView extends AbstractView
{
...
public function display($tpl = null): void
{
$app = Factory::getApplication();
$params = $app->getParams();
$this->handle = $params->get('handle');
$this->public_key = $params->get('public');
$this->summary = $params->get('summary');
$this->icon = $params->get('icon');
$this->data = $this->getProfilData();
Log::add(Text::_('COM_ACTIVITYPUBS_PROFIL') . ': ' . json_encode($this->data), Log::DEBUG, 'com_activitypubs');
$this->response = json_encode($this->data);
header('Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"');
echo json_encode($this->data);
Factory::getApplication()->close();
}
protected function getProfilData(): array
{
$uri = Uri::base();
return [
'@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
'id' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json',
'type' => 'Person',
'preferredUsername' => $this->handle,
'name' => $this->handle,
'manuallyApprovesFollowers' => false,
'discoverable' => true,
'inbox' => $uri . 'index.php?option=com_activitypubs&view=Inbox&format=json',
'outbox' => $uri . 'index.php?option=com_activitypubs&view=Outbox&format=json',
'followers' => $uri . 'index.php?option=com_activitypubs&view=Followers&format=json',
'following' => $uri . 'index.php?option=com_activitypubs&view=Following&format=json',
'publicKey' => [
'id' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json' . '#main',
'owner' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json',
'publicKeyPem' => $this->public_key
],
'summary' => $this->summary,
'url' => $uri,
'publishedDate' => '2023-01-12T00:00:00Z',
'icon' => [
'type' => 'Image',
'mediaType' => 'image/png',
'url' => $uri . $this->icon
],
];
}
}
证明:它工作了,我能在fimidi.com的Mastodon搜索中找到我的账户,甚至可以点击它。但当我点击关注
时,还没有发生任何事。
为了完整性:配置文件中还有其他重要数据:除了我们将在下一节查看的
收件箱
和公钥
外,还有一些其他链接,例如关注者
和被关注者
,它们各自返回一个关注者和被关注者的列表。此外,还有'发件箱',它理想情况下包含用户触发的一切活动。
插曲:CURL
可以使用CURL测试输出。什么是CURL?CURL用于命令行或脚本中传输数据。
$ curl -L 'https://ug-mayen.de/.well-known/webfinger?resource=acct:@This email address is being protected from spambots. You need JavaScript enabled to view it. '
{"subject":"acct:This email address is being protected from spambots. You need JavaScript enabled to view it. ","aliases":["https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json"],"links":[{"rel":"self","type":"application\/activity+json","href":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json"}]}
或者
$ curl -L 'https://ug-mayen.de/index.php/component/activitypubs/?view=Webfinger&format=json'
{"subject":"acct:This email address is being protected from spambots. You need JavaScript enabled to view it. ","aliases":["https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json"],"links":[{"rel":"self","type":"application\/activity+json","href":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json"}]}
什么是jq?jq是一种使JSON输出更易于使用的工具。Ubuntu用户请参阅wiki.ubuntuusers.de/jq。
$ curl -L 'https://ug-mayen.de/index.php/component/activitypubs/?view=Webfinger&format=json' | jq .
{
"subject": "acct:This email address is being protected from spambots. You need JavaScript enabled to view it. ",
"aliases": [
"https://ug-mayen.de/index.php?option=com_activitypubs&view=Profil&format=json"
],
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "https://ug-mayen.de/index.php?option=com_activitypubs&view=Profil&format=json"
}
]
}
关注流程
关注
在这个例子中,行为者https://fimidi.com/users/astrid
想关注用户配置文件为https://ug-mayen.de/index.php?option=com_activitypubs&view=Profil&format=json
的用户。
当行为者https://fimidi.com/users/astrid
在他的Mastodon GUI中点击“关注”时,服务器fimidi.com/
会将一个类型为“关注”的活动发送到你想要关注的账户的服务器。在我们的例子中是到ug-mayen.de/
。
{
"@context":"https://www.w3.org/ns/activitystreams",
"id":"https://fimidi.com/63a59186-c186-4190-995c-0adbcb4984cb",
"type":"Follow",
"actor":"https://fimidi.com/users/astrid",
"object":"https://ug-mayen.de/index.php?option=com_activitypubs&view=Profil&format=json"
}
但服务器确切地发送了这个活动到哪儿?为了找出答案,让我们再次查看我们在上一步中提到的用户配置文件。除了名称外,还有一些URL也被存储在那里。这些链接对于ActivityPub通信非常重要。
我们现在需要的是“收件箱”。那里指定的URL应该是一个通过POST接受匹配JSON有效负载的端点。由于Joomla可以轻松处理通过POST的JSON有效负载,我将以下代码插入我的显示控制器。使用$this->data = (array) json_decode($this->input->json->getRaw(), true);
将POST数据存储在变量$this->data
中。
namespace ActivitypubNamespace\Component\Activitypubs\Site\Controller;
...
class DisplayController extends BaseController
{
...
public function display($cachable = false, $urlparams = [])
{
$view = $this->input->getString('view');
...
if ($view == 'Inbox') {
$this->data = (array) json_decode($this->input->json->getRaw(), true);
switch ($this->data['type']) {
case 'Follow':
$element = new \stdClass();
$element->name = $this->data['type'];
$element->wert = $this->data['actor'];
$element->zuordnung = $this->data['object'];
$element->debug = '';
$db->insertObject('#__activitypubs_details', $element);
$this->sendFollowAccept();
break;
...
$this->response = '{"success": true}';
}
header('Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"');
echo $this->response;
Factory::getApplication()->close();
...
}
现在,我必须通过发送一个通过POST返回的“接受”活动来接受“关注”。这变得有些复杂,因为所有请求都必须签名并保存。
让我们一步一步来。我在函数$this->sendFollowAccept();
中这样做。首先,我创建了这个活动的JSON代码。
{
"@context":"https:\/\/www.w3.org\/ns\/activitystreams",
"id":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json&id=63e37628f3341",
"type":"Accept",
"actor":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json",
"object":{
"@context":"https:\/\/www.w3.org\/ns\/activitystreams",
"id":"https:\/\/fimidi.com\/63a59186-c186-4190-995c-0adbcb4984cb",
"type":"Follow",
"actor":"https:\/\/fimidi.com\/users\/astrid",
"object":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json"
}
}
然后,我签名并发送它通过CURL到想要关注我的用户的服务器。我在函数sendFollowAccept()
中这样做。这是我的代码
...
protected function sendFollowAccept(): void
{
$uri = Uri::base();
$data = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json&id=' . uniqid(),
'type' => 'Accept',
'actor' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json',
'object' => $this->data
];
$data = json_encode($data);
$date = gmdate('D, d M Y H:i:s T', time());
$digest = HttpSignatureHelper::digest($data);
$actor = ActorHelper::fromActorString($this->data['actor']);
$signature = HttpSignatureHelper::sign(
$this->private_key,
$actor->inbox,
$actor->host,
$date,
$digest
);
$signatureHeader = sprintf(
'keyId="%s",headers="(request-target) host date digest",signature="%s"',
$uri . 'index.php?option=com_activitypubs&view=Profil&format=json' . '#main',
base64_encode($signature)
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, sprintf('https://%s%s', $actor->host, $actor->inbox));
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$headers = [
'Content-Type: application/activity+json',
'Date: ' . $date,
'Signature: ' . $signatureHeader,
'Digest: ' . $digest
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_exec($ch);
curl_close($ch);
}
...
释义:什么是摘要?摘要是对较大数据块(如消息或文件)的一种短固定长度表示。创建摘要的过程称为“散列”。散列函数接受输入(或“消息”)并返回一个固定长度的字节数字串,这通常是输入数据的唯一表示。这个固定长度的输出称为“散列”或“摘要”。摘要被广泛应用于计算机科学的许多领域,包括数字签名、数据完整性和数据库中数据的索引。它们还用于识别大型数据集中的重复项,并快速比较大型文件以确定它们是否相同。在密码学中,摘要用于确保消息或文件的完整性,通过检测对原始数据的任何更改。例如,可以创建消息的摘要并将其与消息一起传输。然后,收件人可以计算接收消息的摘要并将其与传输的摘要进行比较。如果两个摘要匹配,收件人可以确信消息在传输过程中未被更改。
进一步解释
我在创建签名时遇到了一些困难。因此,以下解释可能对感兴趣的 Joomla 开发者有所帮助并节省时间。基本上,你创建一个看起来像这样的文本字符串
(request-target): post /users/astrid/inbox
host: fimidi.com
date: Thu, 09 Feb 2023 09:53:16 GMT
digest: SHA-256=/qqi8j+GFlSGSFzLIOVlsLgegS6+3CwN9tkBULflgLM=
然后可以使用 openssl_sign
进行签名。我在 HttpSignatureHelper
辅助类中创建了 $signature
。
然后我将 $signature
作为 base64 字符串添加到签名头中。提醒一下,我在 sendFollowAccept()
函数中创建了签名头,代码如下
...
$signatureHeader = sprintf(
'keyId="%s",headers="(request-target) host date digest",signature="%s"',
$uri . 'index.php?option=com_activitypubs&view=Profil&format=json' . '#main',
base64_encode($signature)
...
此命令创建了一个与以下代码片段模式相同的头。
keyId="https://ug-mayen.de/index.php?option=com_activitypubs&view=Profil&format=json#main",headers="(request-target) host date digest",signature="NP6EymeuvQw/jgXeLVPvKb5O5Bfd7u0wCiCxjXKhDo51oJ82nZKRIe3L8gdvNF6IVcJqTI6LEvc7hR4naMcZE01LnZDEtbXM2Ci8ociSdwiwjduAunbBptU3Bc0H5rBDs+ZvCJF4zTIqPYCdHTMhU9uAcdeF5Znk6ZNO5GkcTUgszhNXjHOIyoWgjhLkkQtSuVXEUggOAfcyIgMm+xSKQjZnQVas88gXE0l6CGAln12oVjLaa0HE8WwuIDNe6IYO3T3YMoSGKqOaFRTw21Dbm27ymEFAJB0o4XSnP95cneqpSpMkc/3j2xdJmaLZkw9D3/RQJxShhy/linx+rPikXQ=="
重要的是,签名头中链接的公钥也必须存在,并且目标服务器可以访问它。在我的例子中,它在上面的用户配置文件的 public key
字段中。
一旦你准备好了所有组件,即 WebFinger 和用户配置文件响应,以及可以处理并响应收件箱请求的东西,你就可以开始了。你有一个简单的 Joomla Fediverse 订阅者可以关注了!
撤销关注请求
如果操作者 https://fimidi.com/users/astrid
不想再关注配置文件 https://ug-mayen.de/index.php?option=com_activitypubs&view=Profil&format=json
,撤销关注请求的活动看起来如下代码片段。
{
"@context":"https://www.w3.org/ns/activitystreams",
"id":"https://fimidi.com/users/astrid#follows/7646/undo",
"type":"Undo",
"actor":"https://fimidi.com/users/astrid",
"object":{
"id":"https://fimidi.com/6c3f6346-c623-4d6c-9975-a3e63bb0ceb0",
"type":"Follow",
"actor":"https://fimidi.com/users/astrid",
"object":"https://ug-mayen.de/index.php?option=com_activitypubs&view=Profil&format=json"
}
}
这突出了一个需要从安全角度考虑的点。如果我不支持服务器上的“撤销”活动,那么用户无法撤销他的关注请求。他总是会看到我新发布的帖子在他的收件箱中。为了防止这种情况,他必须阻止我。
释义:HTTP 签名、ActivityPub 和 Mastodon
为了确保关注请求确实来自发送它的人,你必须验证它。一种可能的方法是:密码学!更确切地说,是非对称加密。
ActivityPub 规范没有要求在这里使用适当的签名,但建议使用。Mastodon 使用Signature-HTTP 头。
该头包含一个指向公钥的链接,一个用于创建签名的 HTTP 头的列表,当然还有签名本身,作为一个 base64 字符串。
除了签名外,Mastodon 还要求在请求中包含一个“摘要”头。这个头包含上面解释的负载的散列,即 JSON 对象。这个 digest
也是签名的一部分,因此不仅确保请求来自正确的人,而且还确保内容没有被更改。
如何创建密钥对?你可以使用 openssl。
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
像大象一样在你的 Joomla 贡献中展示 Fediverse
现在让我们继续创建在发布新博客文章时创建的toot!这里也有一些小小的陷阱。
在Fediverse中,你发送的活动看起来像什么?
我将通过一个具体的例子来展示:当我保存博客文章 joomla-test时,以下JSON被发送到了关注者的收件箱
{
"@context":"https:\/\/www.w3.org\/ns\/activitystreams",
"id":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json&id=63e4cf993bd90",
"type":"Create",
"actor":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json",
"to":[
"https:\/\/www.w3.org\/ns\/activitystreams#Public"
],
"cc":[
"https:\/\/fimidi.com\/users\/astrid",
"..."
],
"object":{
"@context":{
"@language":"de"
},
"id":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json&id=189",
"type":"Note",
...
"summary":"Joomla! 5 - Was erwartet uns?",
"published":"2023-02-08T01:23:19+00:00",
"updated":"2023-02-09T10:48:57+00:00",
"attributedTo":"https:\/\/ug-mayen.de\/index.php?option=com_activitypubs&view=Profil&format=json",
"to":[
"https:\/\/www.w3.org\/ns\/activitystreams#Public"
],
"cc":[
"https:\/\/fimidi.com\/users\/astrid",
"..."
],
"content":"<h1>Joomla! 5<\/h1>\r\n<p style=\"text-align: justify;\">Gerade bin ...<p>"
}
}
你现在在想什么?为每个关注者发送请求?这有效率吗?最终并不是那么糟糕,ActivityPub也有共享收件箱的概念,Mastodon也是这样实现的。你只需向一个"to"服务器发送一次,并为每个关注者使用"cc"。
现在还缺一步:当然,我不仅要在我的Joomla服务器上为发送的所有请求生成签名。我们之前在发送Foller-Accept时就已经有了这个。当然,我还需要验证所有落入其他收件箱的内容的签名。验证签名的工作方式与创建签名非常相似。你根据给定的模式创建明文,只是现在你使用的是请求的头部。
插曲:签名和验证:签名是指创建一个数字签名的过程,可以用来验证消息或文档的来源和完整性。数字签名是通过将数学算法应用于消息或文档的内容和只有签名者知道的私钥来创建的。产生的签名与消息或文档一起发送,以便接收者可以验证消息或文档是否被篡改,并来自指定的发送者。验证,另一方面,是指检查数字签名消息或文档的真实性和完整性的过程。这涉及到使用接收者的公钥应用相同的数学算法到签名和消息或文档的内容。如果验证过程的结果符合预期,接收者可以确信消息或文档没有被篡改,并来自指定的发送者。
为了将新文章发送到其他收件箱,我编写了内容插件plugins/content/activitypub/activitypub.php
。
// plugins/content/activitypub/activitypub.php
...
class PlgContentActivitypub extends CMSPlugin
{
...
public function onContentAfterSave($context, $article, $isNew)
{
switch ($article->state) {
case 1:
$this->safeRequest($activitypubs_params, $article, 'Create');
break;
...
default:
break;
}
return true;
}
public function onContentAfterDelete($context, $article)
{
...
}
public function onContentChangeState($context, $pks, $value)
{
...
}
return true;
}
public function safeRequest($activitypubs_params, $article, $action)
{
// Todo: Create Model
$db = Factory::getDbo();
$followers = [];
$query = $db->getQuery(true);
$query->select($db->quoteName('wert'))
->from($db->quoteName('#__activitypubs_details'))
->where(
[
$db->quoteName('name') . ' = "Follow"',
]
);
$followers = $db->setQuery($query)->loadAssocList();
// Todo: Create Helper
$groupedByHost = [];
foreach ($followers as $follower) {
$actor = ActorHelper::fromActorString(trim($follower['wert']));
$host = $actor->host;
if (empty($groupedByHost[$host])) {
$groupedByHost[$host] = [
'host' => $host,
'inbox' => $actor->sharedInbox,
'followers' => []
];
}
$groupedByHost[$host]['followers'][] = $actor->actor;
}
$content_to_send = $article->introtext;
...
foreach ($groupedByHost as $host) {
$data = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json&id=' . uniqid(),
'type' => $action ,
'actor' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json',
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
'cc' => $host['followers'],
'object' => [
'@context' => $context,
'id' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json&id=' . $article->id,
'type' => 'Note',
'url' => $perma_url,
'edited' => $edited,
'summary' => $article->title,
'published' => $published,
'updated' => $updated,
...
'attributedTo' => $uri . 'index.php?option=com_activitypubs&view=Profil&format=json',
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
'cc' => $host['followers'],
'content' => $content_to_send
],
];
$data = json_encode($data);
$date = gmdate('D, d M Y H:i:s T', time());
$digest = HttpSignatureHelper::digest($data);
$signature = HttpSignatureHelper::sign(
$private_key,
$host['inbox'],
$host['host'],
$date,
$digest
);
$signatureHeader = sprintf(
'keyId="%s",headers="(request-target) host date digest",signature="%s"',
$uri . 'index.php?option=com_activitypubs&view=Profil&format=json' . '#main',
base64_encode($signature)
);
$ch = curl_init();
$serverinbox = sprintf('https://%s%s/', $host['host'], $host['inbox']);
curl_setopt($ch, CURLOPT_URL, $serverinbox);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
$headers = [
'Content-Type: application/activity+json',
'Date: ' . $date,
'Signature: ' . $signatureHeader,
'Digest: ' . $digest
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_exec($ch);
curl_close($ch);
}
}
}
最后思考
你现在可以通过用户名https://ug-mayen.de/joomla-blog-de
关注博客@
,新帖子将出现在每个关注者的收件箱中。这就是我的目标。
但还有很多功能缺失来支持所有的ActivityPub活动。此外,我仅测试了一个Mastodon用户。到目前为止,我还没有包括其他Fediverse服务。
我还不确定是否会继续开发这些扩展。到目前为止,这对我的体验来说已经很有趣了。
在Joomla社区杂志上发表的一些文章代表了作者对特定主题的个人观点或经验,可能并不与Joomla项目的官方立场一致
通过接受,您将访问由https://magazine.joomla.net.cn/之外的第三方提供的服务
评论 5
实际上,这比那要复杂得多。你没有考虑到,没有任何Fediverse服务器会在你联邦(推送)内容到它们之前看到你网站上的任何东西,你没有谈论多个订阅者之间的共享收件箱或跟踪订阅者,你没有谈论验证跟随请求的加密密钥以确保请求是合法的,你没有提供你自己的加密密钥……这就像你只是快速浏览了一下ActivityPub,然后提出了最基本的东西来显示你演员个人资料页面上类似内容的ActibityPub服务器的渲染。我给你打1/10分,以示努力。
这就是它实际上是如何完成的:[url=https://github.com/nikosdion/fediverse/tree/feature/activitypub]https://github.com/nikosdion/fediverse/tree/feature/activitypub[/url]。
你好
为了走向一个更开放、更直接的互联网
我使用几个对等应用程序,PeerTube(我很熟悉framasoft),Mastodon,并且我希望Joomla成为这个动态的一部分。
这篇文章对我来说仍然有点复杂。
我希望它将导致扩展Fediverse,Indieweb,以便更容易地集成
你好
您收到了吗?会发布吗?如果不发布,为什么?
你好
我长期支持免费软件;我经常尝试分享Joomla内容,不通过Facebook、Twitter。这非常困难。
我没有完全理解,因为您的例子是技术性的。我鼓励您将这个建议提交给Joomla开发者
如果开发者创建了便于采用Fediverse、ActivityPub或Indieweb的扩展,那就太好了。这将间接提高Joomla内容的可见性。
祝好
[感谢删除针对管理员的先前评论。
我为两篇文章各发送了2条评论;根据先前的理解,我没有收到它们]
@herve 虽然晚了,但我还是想简要回应。我刚刚看到您的评论,并且没有像通常那样收到评论通知。