aboutsummaryrefslogtreecommitdiff
path: root/bridges/MastodonBridge.php
diff options
context:
space:
mode:
authorGravatar Dag <me@dvikan.no> 2022-07-01 15:10:30 +0200
committerGravatar GitHub <noreply@github.com> 2022-07-01 15:10:30 +0200
commit4f75591060d95208a301bc6bf460d875631b29cc (patch)
tree4e37d86840e8d990a563ba75d3de6f84a53cc2de /bridges/MastodonBridge.php
parent66568e3a39c61546c09a47a5688914a0bdf3c60c (diff)
downloadrss-bridge-4f75591060d95208a301bc6bf460d875631b29cc.tar.gz
rss-bridge-4f75591060d95208a301bc6bf460d875631b29cc.tar.zst
rss-bridge-4f75591060d95208a301bc6bf460d875631b29cc.zip
Reformat codebase v4 (#2872)
Reformat code base to PSR12 Co-authored-by: rssbridge <noreply@github.com>
Diffstat (limited to 'bridges/MastodonBridge.php')
-rw-r--r--bridges/MastodonBridge.php364
1 files changed, 187 insertions, 177 deletions
diff --git a/bridges/MastodonBridge.php b/bridges/MastodonBridge.php
index bbbc5587..04c92ba5 100644
--- a/bridges/MastodonBridge.php
+++ b/bridges/MastodonBridge.php
@@ -1,196 +1,206 @@
<?php
-class MastodonBridge extends BridgeAbstract {
- // This script attempts to imitiate the behaviour of a read-only ActivityPub server
- // to read the outbox.
+class MastodonBridge extends BridgeAbstract
+{
+ // This script attempts to imitiate the behaviour of a read-only ActivityPub server
+ // to read the outbox.
- // Note: Most PixelFed instances have ActivityPub outbox disabled,
- // so use the official feed: https://pixelfed.instance/users/username.atom (Posts only)
+ // Note: Most PixelFed instances have ActivityPub outbox disabled,
+ // so use the official feed: https://pixelfed.instance/users/username.atom (Posts only)
- const MAINTAINER = 'Austin Huang';
- const NAME = 'ActivityPub Bridge';
- const CACHE_TIMEOUT = 900; // 15mn
- const DESCRIPTION = 'Returns recent statuses. Supports Mastodon, Pleroma and Misskey, among others. Access to
+ const MAINTAINER = 'Austin Huang';
+ const NAME = 'ActivityPub Bridge';
+ const CACHE_TIMEOUT = 900; // 15mn
+ const DESCRIPTION = 'Returns recent statuses. Supports Mastodon, Pleroma and Misskey, among others. Access to
instances that have Authorized Fetch enabled requires
<a href="https://rss-bridge.github.io/rss-bridge/Bridge_Specific/ActivityPub_(Mastodon).html">configuration</a>.';
- const URI = 'https://mastodon.social';
+ const URI = 'https://mastodon.social';
- // Some Mastodon instances use Secure Mode which requires all requests to be signed.
- // You do not need this for most instances, but if you want to support every known
- // instance, then you should configure them.
- // See also https://docs.joinmastodon.org/spec/security/#http
- const CONFIGURATION = array(
- 'private_key' => array(
- 'required' => false,
- ),
- 'key_id' => array(
- 'required' => false,
- ),
- );
+ // Some Mastodon instances use Secure Mode which requires all requests to be signed.
+ // You do not need this for most instances, but if you want to support every known
+ // instance, then you should configure them.
+ // See also https://docs.joinmastodon.org/spec/security/#http
+ const CONFIGURATION = [
+ 'private_key' => [
+ 'required' => false,
+ ],
+ 'key_id' => [
+ 'required' => false,
+ ],
+ ];
- const PARAMETERS = array(array(
- 'canusername' => array(
- 'name' => 'Canonical username',
- 'exampleValue' => '@sebsauvage@framapiaf.org',
- 'required' => true,
- ),
- 'norep' => array(
- 'name' => 'Without replies',
- 'type' => 'checkbox',
- 'title' => 'Only return statuses that are not replies, as determined by relations (not mentions).'
- ),
- 'noboost' => array(
- 'name' => 'Without boosts',
- 'required' => false,
- 'type' => 'checkbox',
- 'title' => 'Hide boosts. Note that RSS-Bridge will fetch the original status from other federated instances.'
- )
- ));
+ const PARAMETERS = [[
+ 'canusername' => [
+ 'name' => 'Canonical username',
+ 'exampleValue' => '@sebsauvage@framapiaf.org',
+ 'required' => true,
+ ],
+ 'norep' => [
+ 'name' => 'Without replies',
+ 'type' => 'checkbox',
+ 'title' => 'Only return statuses that are not replies, as determined by relations (not mentions).'
+ ],
+ 'noboost' => [
+ 'name' => 'Without boosts',
+ 'required' => false,
+ 'type' => 'checkbox',
+ 'title' => 'Hide boosts. Note that RSS-Bridge will fetch the original status from other federated instances.'
+ ]
+ ]];
- public function getName() {
- if($this->getInput('canusername')) {
- return $this->getInput('canusername');
- }
- return parent::getName();
- }
+ public function getName()
+ {
+ if ($this->getInput('canusername')) {
+ return $this->getInput('canusername');
+ }
+ return parent::getName();
+ }
- private function getInstance() {
- preg_match('/^@[a-zA-Z0-9_]+@(.+)/', $this->getInput('canusername'), $matches);
- return $matches[1];
- }
+ private function getInstance()
+ {
+ preg_match('/^@[a-zA-Z0-9_]+@(.+)/', $this->getInput('canusername'), $matches);
+ return $matches[1];
+ }
- private function getUsername() {
- preg_match('/^@([a-zA-Z_0-9_]+)@.+/', $this->getInput('canusername'), $matches);
- return $matches[1];
- }
+ private function getUsername()
+ {
+ preg_match('/^@([a-zA-Z_0-9_]+)@.+/', $this->getInput('canusername'), $matches);
+ return $matches[1];
+ }
- public function getURI(){
- if($this->getInput('canusername')) {
- // We parse webfinger to make sure the URL is correct. This is mostly because
- // MissKey uses user ID instead of the username in the endpoint, domain delegations,
- // and also to be compatible with future ActivityPub implementations.
- $resource = 'acct:' . $this->getUsername() . '@' . $this->getInstance();
- $webfingerUrl = 'https://' . $this->getInstance() . '/.well-known/webfinger?resource=' . $resource;
- $webfingerHeader = array(
- 'Content-Type: application/jrd+json'
- );
- $webfinger = json_decode(getContents($webfingerUrl, $webfingerHeader), true);
- foreach ($webfinger['links'] as $link) {
- if ($link['type'] === 'application/activity+json') {
- return $link['href'];
- }
- }
- }
+ public function getURI()
+ {
+ if ($this->getInput('canusername')) {
+ // We parse webfinger to make sure the URL is correct. This is mostly because
+ // MissKey uses user ID instead of the username in the endpoint, domain delegations,
+ // and also to be compatible with future ActivityPub implementations.
+ $resource = 'acct:' . $this->getUsername() . '@' . $this->getInstance();
+ $webfingerUrl = 'https://' . $this->getInstance() . '/.well-known/webfinger?resource=' . $resource;
+ $webfingerHeader = [
+ 'Content-Type: application/jrd+json'
+ ];
+ $webfinger = json_decode(getContents($webfingerUrl, $webfingerHeader), true);
+ foreach ($webfinger['links'] as $link) {
+ if ($link['type'] === 'application/activity+json') {
+ return $link['href'];
+ }
+ }
+ }
- return parent::getURI();
- }
+ return parent::getURI();
+ }
- public function collectData() {
- $url = $this->getURI() . '/outbox?page=true';
- $content = $this->fetchAP($url);
- if ($content['id'] === $url) {
- foreach ($content['orderedItems'] as $status) {
- $this->items[] = $this->parseItem($status);
- }
- } else {
- throw new \Exception('Unexpected response from server.');
- }
- }
+ public function collectData()
+ {
+ $url = $this->getURI() . '/outbox?page=true';
+ $content = $this->fetchAP($url);
+ if ($content['id'] === $url) {
+ foreach ($content['orderedItems'] as $status) {
+ $this->items[] = $this->parseItem($status);
+ }
+ } else {
+ throw new \Exception('Unexpected response from server.');
+ }
+ }
- protected function parseItem($content) {
- $item = array();
- switch ($content['type']) {
- case 'Announce': // boost
- if ($this->getInput('noboost')) {
- return null;
- }
- // We fetch the boosted content.
- try {
- $rtContent = $this->fetchAP($content['object']);
- $rtUser = $this->loadCacheValue($rtContent['attributedTo'], 86400);
- if (!isset($rtUser)) {
- // We fetch the author, since we cannot always assume the format of the URL.
- $user = $this->fetchAP($rtContent['attributedTo']);
- preg_match('/https?:\/\/([a-z0-9-\.]{0,})\//', $rtContent['attributedTo'], $matches);
- // We assume that the server name as indicated by the path is the actual server name,
- // since using webfinger to delegate domains is not officially supported, and it only
- // seems to work in one way.
- $rtUser = '@' . $user['preferredUsername'] . '@' . $matches[1];
- $this->saveCacheValue($rtContent['attributedTo'], $rtUser);
- }
- $item['author'] = $rtUser;
- $item['title'] = 'Shared a status by ' . $rtUser . ': ';
- $item = $this->parseObject($rtContent, $item);
- } catch (UnexpectedResponseException $th) {
- $item['title'] = 'Shared an unreachable status: ' . $content['object'];
- $item['content'] = $content['object'];
- $item['uri'] = $content['object'];
- }
- break;
- case 'Create': // posts
- if ($this->getInput('norep') && isset($content['object']['inReplyTo'])) {
- return null;
- }
- $item['author'] = $this->getInput('canusername');
- $item['title'] = '';
- $item = $this->parseObject($content['object'], $item);
- }
- $item['timestamp'] = $content['published'];
- $item['uid'] = $content['id'];
- return $item;
- }
+ protected function parseItem($content)
+ {
+ $item = [];
+ switch ($content['type']) {
+ case 'Announce': // boost
+ if ($this->getInput('noboost')) {
+ return null;
+ }
+ // We fetch the boosted content.
+ try {
+ $rtContent = $this->fetchAP($content['object']);
+ $rtUser = $this->loadCacheValue($rtContent['attributedTo'], 86400);
+ if (!isset($rtUser)) {
+ // We fetch the author, since we cannot always assume the format of the URL.
+ $user = $this->fetchAP($rtContent['attributedTo']);
+ preg_match('/https?:\/\/([a-z0-9-\.]{0,})\//', $rtContent['attributedTo'], $matches);
+ // We assume that the server name as indicated by the path is the actual server name,
+ // since using webfinger to delegate domains is not officially supported, and it only
+ // seems to work in one way.
+ $rtUser = '@' . $user['preferredUsername'] . '@' . $matches[1];
+ $this->saveCacheValue($rtContent['attributedTo'], $rtUser);
+ }
+ $item['author'] = $rtUser;
+ $item['title'] = 'Shared a status by ' . $rtUser . ': ';
+ $item = $this->parseObject($rtContent, $item);
+ } catch (UnexpectedResponseException $th) {
+ $item['title'] = 'Shared an unreachable status: ' . $content['object'];
+ $item['content'] = $content['object'];
+ $item['uri'] = $content['object'];
+ }
+ break;
+ case 'Create': // posts
+ if ($this->getInput('norep') && isset($content['object']['inReplyTo'])) {
+ return null;
+ }
+ $item['author'] = $this->getInput('canusername');
+ $item['title'] = '';
+ $item = $this->parseObject($content['object'], $item);
+ }
+ $item['timestamp'] = $content['published'];
+ $item['uid'] = $content['id'];
+ return $item;
+ }
- protected function parseObject($object, $item) {
- $item['content'] = $object['content'];
- $strippedContent = strip_tags(str_replace('<br>', ' ', $object['content']));
+ protected function parseObject($object, $item)
+ {
+ $item['content'] = $object['content'];
+ $strippedContent = strip_tags(str_replace('<br>', ' ', $object['content']));
- if (mb_strlen($strippedContent) > 75) {
- $contentSubstring = mb_substr($strippedContent, 0, mb_strpos(wordwrap($strippedContent, 75), "\n"));
- $item['title'] .= $contentSubstring . '...';
- } else {
- $item['title'] .= $strippedContent;
- }
- $item['uri'] = $object['id'];
- foreach ($object['attachment'] as $attachment) {
- // Only process REMOTE pictures (prevent xss)
- if ($attachment['mediaType']
- && preg_match('/^image\//', $attachment['mediaType'], $match)
- && preg_match('/^http(s|):\/\//', $attachment['url'], $match)
- ) {
- $item['content'] = $item['content'] . '<br /><img ';
- if ($attachment['name']) {
- $item['content'] .= sprintf('alt="%s" ', $attachment['name']);
- }
- $item['content'] .= sprintf('src="%s" />', $attachment['url']);
- }
- }
- return $item;
- }
+ if (mb_strlen($strippedContent) > 75) {
+ $contentSubstring = mb_substr($strippedContent, 0, mb_strpos(wordwrap($strippedContent, 75), "\n"));
+ $item['title'] .= $contentSubstring . '...';
+ } else {
+ $item['title'] .= $strippedContent;
+ }
+ $item['uri'] = $object['id'];
+ foreach ($object['attachment'] as $attachment) {
+ // Only process REMOTE pictures (prevent xss)
+ if (
+ $attachment['mediaType']
+ && preg_match('/^image\//', $attachment['mediaType'], $match)
+ && preg_match('/^http(s|):\/\//', $attachment['url'], $match)
+ ) {
+ $item['content'] = $item['content'] . '<br /><img ';
+ if ($attachment['name']) {
+ $item['content'] .= sprintf('alt="%s" ', $attachment['name']);
+ }
+ $item['content'] .= sprintf('src="%s" />', $attachment['url']);
+ }
+ }
+ return $item;
+ }
- protected function fetchAP($url) {
- $d = new DateTime();
- $d->setTimezone(new DateTimeZone('GMT'));
- $date = $d->format('D, d M Y H:i:s e');
- preg_match('/https?:\/\/([a-z0-9-\.]{0,})(\/[^?#]+)/', $url, $matches);
- $headers = array(
- 'Accept: application/activity+json',
- 'Host: ' . $matches[1],
- 'Date: ' . $date
- );
- $privateKey = $this->getOption('private_key');
- $keyId = $this->getOption('key_id');
- if ($privateKey && $keyId) {
- $pkey = openssl_pkey_get_private('file://' . $privateKey);
- $toSign = '(request-target): get ' . $matches[2] . "\nhost: " . $matches[1] . "\ndate: " . $date;
- $result = openssl_sign($toSign, $signature, $pkey, 'RSA-SHA256');
- if ($result) {
- Debug::log($toSign);
- $sig = 'Signature: keyId="' . $keyId . '",headers="(request-target) host date",signature="' .
- base64_encode($signature) . '"';
- Debug::log($sig);
- array_push($headers, $sig);
- }
- }
- return json_decode(getContents($url, $headers), true);
- }
+ protected function fetchAP($url)
+ {
+ $d = new DateTime();
+ $d->setTimezone(new DateTimeZone('GMT'));
+ $date = $d->format('D, d M Y H:i:s e');
+ preg_match('/https?:\/\/([a-z0-9-\.]{0,})(\/[^?#]+)/', $url, $matches);
+ $headers = [
+ 'Accept: application/activity+json',
+ 'Host: ' . $matches[1],
+ 'Date: ' . $date
+ ];
+ $privateKey = $this->getOption('private_key');
+ $keyId = $this->getOption('key_id');
+ if ($privateKey && $keyId) {
+ $pkey = openssl_pkey_get_private('file://' . $privateKey);
+ $toSign = '(request-target): get ' . $matches[2] . "\nhost: " . $matches[1] . "\ndate: " . $date;
+ $result = openssl_sign($toSign, $signature, $pkey, 'RSA-SHA256');
+ if ($result) {
+ Debug::log($toSign);
+ $sig = 'Signature: keyId="' . $keyId . '",headers="(request-target) host date",signature="' .
+ base64_encode($signature) . '"';
+ Debug::log($sig);
+ array_push($headers, $sig);
+ }
+ }
+ return json_decode(getContents($url, $headers), true);
+ }
}