diff options
author | 2022-07-01 15:10:30 +0200 | |
---|---|---|
committer | 2022-07-01 15:10:30 +0200 | |
commit | 4f75591060d95208a301bc6bf460d875631b29cc (patch) | |
tree | 4e37d86840e8d990a563ba75d3de6f84a53cc2de /bridges/MastodonBridge.php | |
parent | 66568e3a39c61546c09a47a5688914a0bdf3c60c (diff) | |
download | rss-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.php | 364 |
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); + } } |