1122 lines
		
	
	
	
		
			51 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			1122 lines
		
	
	
	
		
			51 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/**
 | 
						|
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
						|
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
						|
 *
 | 
						|
 * @author Sascha Nitsch (grumpydeveloper)
 | 
						|
 **/
 | 
						|
 | 
						|
namespace Federator\Connector;
 | 
						|
 | 
						|
/**
 | 
						|
 * Connector to ContentNation.net
 | 
						|
 */
 | 
						|
class ContentNation implements Connector
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * config parameter
 | 
						|
     *
 | 
						|
     * @var array<string, mixed> $config
 | 
						|
     */
 | 
						|
    private $config;
 | 
						|
 | 
						|
    /**
 | 
						|
     * main instance
 | 
						|
     *
 | 
						|
     * @var \Federator\Main $main
 | 
						|
     */
 | 
						|
    private $main;
 | 
						|
 | 
						|
    /**
 | 
						|
     * service-URL
 | 
						|
     *
 | 
						|
     * @var string $service
 | 
						|
     */
 | 
						|
    private $service;
 | 
						|
 | 
						|
    /**
 | 
						|
     * constructor
 | 
						|
     *
 | 
						|
     * @param \Federator\Main $main
 | 
						|
     */
 | 
						|
    public function __construct($main)
 | 
						|
    {
 | 
						|
        $config = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '../contentnation.ini', true);
 | 
						|
        if ($config !== false) {
 | 
						|
            $this->config = $config;
 | 
						|
        }
 | 
						|
        $this->service = $config['contentnation']['service-uri'];
 | 
						|
        $this->main = $main;
 | 
						|
        $this->main->setHost($this->service);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * get followers of given user
 | 
						|
     *
 | 
						|
     * @param string $userId user id
 | 
						|
     * @return \Federator\Data\FedUser[]|false
 | 
						|
     */
 | 
						|
    public function getRemoteFollowersOfUser($userId)
 | 
						|
    {
 | 
						|
        // todo implement queue for this
 | 
						|
        if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
 | 
						|
            $userId = $matches[1];
 | 
						|
        }
 | 
						|
        $remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/followers';
 | 
						|
 | 
						|
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
 | 
						|
        if ($info['http_code'] != 200) {
 | 
						|
            error_log("ContentNation::getRemoteFollowersOfUser error retrieving followers for userId: $userId . Error: "
 | 
						|
                . json_encode($info));
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $r = json_decode($response, true);
 | 
						|
        if ($r === false || $r === null || !is_array($r)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $followers = [];
 | 
						|
        return $followers;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * get following of given user
 | 
						|
     *
 | 
						|
     * @param string $userId user id
 | 
						|
 | 
						|
     * @return \Federator\Data\FedUser[]|false
 | 
						|
     */
 | 
						|
    public function getRemoteFollowingForUser($userId)
 | 
						|
    {
 | 
						|
        // todo implement queue for this
 | 
						|
        if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
 | 
						|
            $userId = $matches[1];
 | 
						|
        }
 | 
						|
        $remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/following';
 | 
						|
 | 
						|
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
 | 
						|
        if ($info['http_code'] != 200) {
 | 
						|
            error_log('ContentNation::getRemoteFollowingForUser error retrieving following for userId: ' . $userId
 | 
						|
                . '. Error: ' . json_encode($info));
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $r = json_decode($response, true);
 | 
						|
        if ($r === false || $r === null || !is_array($r)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $followers = [];
 | 
						|
        return $followers;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * get posts by given user
 | 
						|
     *
 | 
						|
     * @param string $userId user id
 | 
						|
     * @param int $min min date
 | 
						|
     * @param int $max max date
 | 
						|
     * @param int $limit limit results
 | 
						|
     * @unused-param $limit
 | 
						|
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
						|
     */
 | 
						|
    public function getRemotePostsByUser($userId, $min, $max, $limit)
 | 
						|
    {
 | 
						|
        if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
 | 
						|
            $userId = $matches[1];
 | 
						|
        }
 | 
						|
        $remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/activities';
 | 
						|
        if ($min > 0) {
 | 
						|
            $remoteURL .= '&minTS=' . intval($min, 10);
 | 
						|
        }
 | 
						|
        if ($max > 0) {
 | 
						|
            $remoteURL .= '&maxTS=' . intval($max, 10);
 | 
						|
        }
 | 
						|
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
 | 
						|
        if ($info['http_code'] != 200) {
 | 
						|
            error_log('ContentNation::getRemotePostsByUser error retrieving activities for userId: ' . $userId
 | 
						|
                . '. Error: ' . json_encode($info));
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $r = json_decode($response, true);
 | 
						|
        if ($r === false || $r === null || !is_array($r)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $posts = [];
 | 
						|
        if (array_key_exists('activities', $r)) {
 | 
						|
            $activities = $r['activities'];
 | 
						|
            $config = $this->main->getConfig();
 | 
						|
            $domain = $config['generic']['externaldomain'];
 | 
						|
            $ourUrl = 'https://' . $domain;
 | 
						|
 | 
						|
            $imgpath = $this->config['userdata']['path'];
 | 
						|
            $userdata = $this->config['userdata']['url'];
 | 
						|
            foreach ($activities as $activity) {
 | 
						|
                switch ($activity['type']) {
 | 
						|
                    case 'Article':
 | 
						|
                        $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
						|
                        $create->setAActor($ourUrl . '/' . $userId);
 | 
						|
                        $create->setPublished($activity['published'] ?? $activity['timestamp'])
 | 
						|
                            ->addTo($ourUrl . '/' . $userId . '/followers')
 | 
						|
                            ->addCC("https://www.w3.org/ns/activitystreams#Public");
 | 
						|
                        $create->setURL($ourUrl . '/' . $activity['profilename'] . '/' . $activity['name']);
 | 
						|
                        $create->setID($ourUrl . '/' . $activity['profilename'] . '/' . $activity['name']);
 | 
						|
                        $apArticle = new \Federator\Data\ActivityPub\Common\Article();
 | 
						|
                        if (array_key_exists('tags', $activity)) {
 | 
						|
                            foreach ($activity['tags'] as $tag) {
 | 
						|
                                $href = $ourUrl . '/search.htm?tagsearch=' . urlencode($tag);
 | 
						|
                                $tagObj = new \Federator\Data\ActivityPub\Common\Tag();
 | 
						|
                                $tagObj->setHref($href)
 | 
						|
                                    ->setName('#' . urlencode(str_replace(' ', '', $tag)))
 | 
						|
                                    ->setType('Hashtag');
 | 
						|
                                $apArticle->addTag($tagObj);
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                        $apArticle->setPublished($activity['published'])
 | 
						|
                            ->setName($activity['title'])
 | 
						|
                            ->setAttributedTo($ourUrl . '/' . $activity['profilename'])
 | 
						|
                            ->setContent(
 | 
						|
                                $activity['teaser'] ??
 | 
						|
                                $this->main->translate(
 | 
						|
                                    $activity['language'],
 | 
						|
                                    'article',
 | 
						|
                                    'newarticle'
 | 
						|
                                )
 | 
						|
                            )
 | 
						|
                            ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
						|
                            ->addCC($ourUrl . '/' . $userId . '/followers.json');
 | 
						|
                        $articleimage = $activity['imagealt'] ??
 | 
						|
                            $this->main->translate($activity['language'], 'article', 'image');
 | 
						|
                        $idurl = $ourUrl . '/' . $userId . '/' . $activity['name'];
 | 
						|
                        $apArticle->setID($idurl)
 | 
						|
                            ->setURL($idurl);
 | 
						|
                        $image = $activity['image'] ?? $activity['profileimg'];
 | 
						|
                        $path = $imgpath . $activity['profile'] . '/' . $image;
 | 
						|
 | 
						|
                        $type = file_exists($path) ? mime_content_type($path) : false;
 | 
						|
                        $mediaType = ($type !== false && !str_starts_with($type, 'text/'))
 | 
						|
                            ? $type
 | 
						|
                            : 'image/jpeg';
 | 
						|
 | 
						|
                        $img = new \Federator\Data\ActivityPub\Common\Image();
 | 
						|
                        $img->setMediaType($mediaType)
 | 
						|
                            ->setName($articleimage)
 | 
						|
                            ->setURL($userdata . '/' . $activity['profile'] . $image);
 | 
						|
                        $apArticle->addImage($img);
 | 
						|
                        $create->setObject($apArticle);
 | 
						|
                        $posts[] = $create;
 | 
						|
                        break; // Article
 | 
						|
 | 
						|
                    case 'Comment':
 | 
						|
                        $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
						|
                        $create->setAActor($ourUrl . '/' . $userId);
 | 
						|
                        $create->setID($activity['id'])
 | 
						|
                            ->setPublished($activity['published'] ?? $activity['timestamp'])
 | 
						|
                            ->addTo($ourUrl . '/' . $userId . '/followers')
 | 
						|
                            ->addCC("https://www.w3.org/ns/activitystreams#Public");
 | 
						|
                        $commentJson = $activity;
 | 
						|
                        $commentJson['type'] = 'Note';
 | 
						|
                        $commentJson['summary'] = $activity['subject'];
 | 
						|
                        $commentJson['id'] = $ourUrl . '/' . $activity['articleOwnerName'] . '/'
 | 
						|
                            . $activity['articleName'] . '#' . $activity['id'];
 | 
						|
                        $note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, "");
 | 
						|
                        if ($note === null) {
 | 
						|
                            error_log("ContentNation::getRemotePostsByUser couldn't create comment");
 | 
						|
                            $note = new \Federator\Data\ActivityPub\Common\Activity('Comment');
 | 
						|
                            $create->setObject($note);
 | 
						|
                            break;
 | 
						|
                        }
 | 
						|
                        $note->setID($commentJson['id']);
 | 
						|
                        if (!isset($commentJson['parent']) || $commentJson['parent'] === null) {
 | 
						|
                            $note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/'
 | 
						|
                                . $activity['articleName']);
 | 
						|
                        } else {
 | 
						|
                            $note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/'
 | 
						|
                                . $activity['articleName'] . "#" . $commentJson['parent']);
 | 
						|
                        }
 | 
						|
                        $url = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']
 | 
						|
                            . '#' . $activity['id'];
 | 
						|
                        $create->setURL($url);
 | 
						|
                        $create->setID($url);
 | 
						|
                        $create->setObject($note);
 | 
						|
                        $posts[] = $create;
 | 
						|
                        break; // Comment
 | 
						|
 | 
						|
                    case 'Vote':
 | 
						|
                        // Build Like/Dislike as top-level activity
 | 
						|
                        $likeType = $activity['upvote'] === true ? 'Like' : 'Dislike';
 | 
						|
                        $like = new \Federator\Data\ActivityPub\Common\Activity($likeType);
 | 
						|
                        $like->setAActor($ourUrl . '/' . $userId);
 | 
						|
                        $like->setID($activity['id'])
 | 
						|
                            ->setPublished($activity['published'] ?? $activity['timestamp']);
 | 
						|
                        // $like->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
						|
                        // ->addCC('https://' . $domain . '/' . $userId . '/followers');
 | 
						|
                        $like->setSummary(
 | 
						|
                            $this->main->translate(
 | 
						|
                                $activity['articlelang'],
 | 
						|
                                'vote',
 | 
						|
                                $likeType === 'Like' ? 'like' : 'dislike',
 | 
						|
                                [$activity['username']]
 | 
						|
                            )
 | 
						|
                        );
 | 
						|
                        $objectUrl = $ourUrl . '/' . $userId . '/' . $activity['articlename'];
 | 
						|
                        $like->setURL($objectUrl . '#' . $activity['id']);
 | 
						|
                        $like->setID($objectUrl . '#' . $activity['id']);
 | 
						|
                        $like->setObject($objectUrl);
 | 
						|
                        $posts[] = $like;
 | 
						|
                        break; // Vote
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return $posts;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * get statistics from remote system
 | 
						|
     *
 | 
						|
     * @return \Federator\Data\Stats|false
 | 
						|
     */
 | 
						|
    public function getRemoteStats()
 | 
						|
    {
 | 
						|
        $remoteURL = $this->service . '/api/stats';
 | 
						|
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
 | 
						|
        if ($info['http_code'] != 200) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $r = json_decode($response, true);
 | 
						|
        if ($r === false || $r === null || !is_array($r)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $stats = new \Federator\Data\Stats();
 | 
						|
        $stats->userCount = array_key_exists('userCount', $r) ? $r['userCount'] : 0;
 | 
						|
        $stats->postCount = array_key_exists('pageCount', $r) ? $r['pageCount'] : 0;
 | 
						|
        $stats->commentCount = array_key_exists('commentCount', $r) ? $r['commentCount'] : 0;
 | 
						|
        return $stats;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * get remote user by given name
 | 
						|
     *
 | 
						|
     * @param string $_name user/profile name
 | 
						|
     * @return \Federator\Data\User | false
 | 
						|
     */
 | 
						|
    public function getRemoteUserByName(string $_name)
 | 
						|
    {
 | 
						|
        // validate name
 | 
						|
        if (preg_match("/^[a-zA-Z0-9\._\-]+$/", $_name) != 1) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $remoteURL = $this->service . '/api/users/info?user=' . urlencode($_name);
 | 
						|
        $headers = ['Accept: application/json'];
 | 
						|
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
						|
        if ($info['http_code'] != 200) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $r = json_decode($response, true);
 | 
						|
        if ($r === false || $r === null || !is_array($r)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $user = new \Federator\Data\User();
 | 
						|
        $user->externalid = $_name;
 | 
						|
        $user->id = $_name;
 | 
						|
        $user->iconMediaType = $r['iconMediaType'];
 | 
						|
        $user->iconURL = $r['iconURL'];
 | 
						|
        $user->imageMediaType = $r['imageMediaType'];
 | 
						|
        $user->imageURL = $r['imageURL'];
 | 
						|
        $user->name = $r['name'];
 | 
						|
        $user->summary = $r['summary'];
 | 
						|
        $user->type = $r['type'];
 | 
						|
        $user->registered = intval($r['registered'], 10);
 | 
						|
        return $user;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * get remote user by given session
 | 
						|
     *
 | 
						|
     * @param string $_session session id
 | 
						|
     * @param string $_user user or profile name
 | 
						|
     * @return \Federator\Data\User | false
 | 
						|
     */
 | 
						|
    public function getRemoteUserBySession(string $_session, string $_user)
 | 
						|
    {
 | 
						|
        // validate $_session and $user
 | 
						|
        if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_user) != 1) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user);
 | 
						|
        $headers = ['Cookie: session=' . $_session, 'Accept: application/json'];
 | 
						|
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
						|
 | 
						|
        if ($info['http_code'] != 200) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $r = json_decode($response, true);
 | 
						|
        if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        $user = $this->getRemoteUserByName($_user);
 | 
						|
        if ($user === false) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
        // extend with permissions
 | 
						|
        $user->permissions = [];
 | 
						|
        $user->session = $_session;
 | 
						|
        foreach ($r[$_user] as $p) {
 | 
						|
            $user->permissions[] = $p;
 | 
						|
        }
 | 
						|
        return $user;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Convert jsonData to Activity format
 | 
						|
     *
 | 
						|
     * @param array<string, mixed> $jsonData the json data from our platfrom
 | 
						|
     * @param string $articleId the original id of the article (if applicable)
 | 
						|
     *                           (used to identify the article in the remote system)
 | 
						|
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
						|
     */
 | 
						|
    public function jsonToActivity($jsonData, &$articleId)
 | 
						|
    {
 | 
						|
        $returnActivity = false;
 | 
						|
        // Common fields for all activity types
 | 
						|
        $ap = [
 | 
						|
            '@context' => 'https://www.w3.org/ns/activitystreams',
 | 
						|
            'type' => 'Create', // Default to 'Create'
 | 
						|
            'id' => $jsonData['id'] ?? null,
 | 
						|
            'actor' => $jsonData['actor'] ?? null,
 | 
						|
        ];
 | 
						|
 | 
						|
        $config = $this->main->getConfig();
 | 
						|
        $domain = $config['generic']['externaldomain'];
 | 
						|
 | 
						|
        $ourUrl = 'https://' . $domain;
 | 
						|
 | 
						|
        // Extract actorName as the last segment of the actor URL (after the last '/')
 | 
						|
        $actorData = $jsonData['actor'] ?? null;
 | 
						|
        $actorName = $actorData['name'] ?? null;
 | 
						|
 | 
						|
        $ap['actor'] = $ourUrl . '/' . $actorName;
 | 
						|
 | 
						|
        if (isset($jsonData['type'])) {
 | 
						|
            switch ($jsonData['type']) {
 | 
						|
                case 'undo':
 | 
						|
                    $ap['type'] = 'Undo';
 | 
						|
                    $ap['actor'] = $ourUrl . '/' . $actorName;
 | 
						|
                    $objectType = $jsonData['object']['type'] ?? null;
 | 
						|
                    if ($objectType === "article") {
 | 
						|
                        $articleName = $jsonData['object']['name'] ?? null;
 | 
						|
                        $ownerName = $jsonData['object']['ownerName'] ?? null;
 | 
						|
                        $ap['id'] = $ourUrl . '/' . $ownerName . '/' . $articleName . '/undo';
 | 
						|
                        $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
 | 
						|
                    } elseif ($objectType === "comment") {
 | 
						|
                        $articleName = $jsonData['object']['articleName'] ?? null;
 | 
						|
                        $ownerName = $jsonData['object']['articleOwnerName'] ?? null;
 | 
						|
                        $commentId = $jsonData['object']['id'] ?? null;
 | 
						|
                        $ap['id'] = $ourUrl . '/' . $ownerName . '/' . $articleName . '#' . $commentId . '/undo';
 | 
						|
                        $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
 | 
						|
                    } elseif ($objectType === "vote") {
 | 
						|
                        $id = $jsonData['object']['id'] ?? null;
 | 
						|
                        $articleName = $jsonData['object']['articleName'] ?? null;
 | 
						|
                        $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
 | 
						|
                        $ap['id'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $id . '/undo';
 | 
						|
                        $ap['published'] = $jsonData['object']['published'] ?? null;
 | 
						|
                        $ap['actor'] = $ourUrl . '/' . $actorName;
 | 
						|
                        $ap['object']['id'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $id;
 | 
						|
                        $ap['object']['url'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $id;
 | 
						|
                        $ap['object']['actor'] = $ourUrl . '/' . $actorName;
 | 
						|
                        if ($jsonData['object']['vote']['value'] == 1) {
 | 
						|
                            $ap['object']['type'] = 'Like';
 | 
						|
                        } elseif ($jsonData['object']['vote']['value'] == 0) {
 | 
						|
                            $ap['object']['type'] = 'Dislike';
 | 
						|
                        } else {
 | 
						|
                            error_log('ContentNation::jsonToActivity unknown vote value: '
 | 
						|
                                . $jsonData['object']['vote']['value']);
 | 
						|
                            break;
 | 
						|
                        }
 | 
						|
                        $ap['object']['object'] = self::generateObjectJson($ourUrl, $jsonData);
 | 
						|
                    } else {
 | 
						|
                        error_log("ContentNation::jsonToActivity unknown undo type: {$objectType}");
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
                    $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
 | 
						|
                    if ($returnActivity === false) {
 | 
						|
                        error_log('ContentNation::jsonToActivity couldn\'t create undo');
 | 
						|
                        $returnActivity = new \Federator\Data\ActivityPub\Common\Undo();
 | 
						|
                    } else {
 | 
						|
                        $returnActivity->setID($ap['id']);
 | 
						|
                        $returnActivity->setURL($ap['id']);
 | 
						|
                    }
 | 
						|
                    break;
 | 
						|
                default:
 | 
						|
                    // Handle unsupported types or fallback to default behavior
 | 
						|
                    throw new \InvalidArgumentException('ContentNation::jsonToActivity Unsupported type: '
 | 
						|
                        . $jsonData['type']);
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            // Handle specific fields based on the type
 | 
						|
            switch ($jsonData['object']['type']) {
 | 
						|
                case 'article':
 | 
						|
                    $articleName = $jsonData['object']['name'] ?? null;
 | 
						|
                    $articleOwnerName = $jsonData['object']['ownerName'] ?? null;
 | 
						|
                    // Set Create-level fields
 | 
						|
                    $updatedOn = $jsonData['object']['modified'] ?? null;
 | 
						|
                    $originalPublished = $jsonData['object']['published'] ?? null;
 | 
						|
                    $update = $updatedOn !== $originalPublished;
 | 
						|
                    $ap['published'] = $updatedOn ?? $originalPublished;
 | 
						|
                    $ap['id'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
 | 
						|
                    $ap['url'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
 | 
						|
                    $ap['type'] = $update ? 'Update' : 'Create';
 | 
						|
                    $ap['actor'] = $ourUrl . '/' . $actorName;
 | 
						|
                    if ($update) {
 | 
						|
                        $ap['id'] .= '#update';
 | 
						|
                        $ap['url'] .= '#update';
 | 
						|
                    }
 | 
						|
                    $ap['cc'] = ['https://www.w3.org/ns/activitystreams#Public'];
 | 
						|
 | 
						|
                    if (isset($jsonData['options'])) {
 | 
						|
                        if (isset($jsonData['options']['informFollowers'])) {
 | 
						|
                            if ($jsonData['options']['informFollowers'] === true) {
 | 
						|
                                $ap['to'][] = $ourUrl . '/' . $actorName . '/followers';
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
 | 
						|
                    $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
 | 
						|
                    if ($returnActivity === false) {
 | 
						|
                        error_log('ContentNation::jsonToActivity couldn\'t create article');
 | 
						|
                        $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create');
 | 
						|
                    } else {
 | 
						|
                        $returnActivity->setID($ap['id']);
 | 
						|
                        $returnActivity->setURL($ap['url']);
 | 
						|
                    }
 | 
						|
                    $articleId = $jsonData['object']['id']; // Set the article ID for the activity
 | 
						|
                    break;
 | 
						|
 | 
						|
                case 'comment':
 | 
						|
                    $commentId = $jsonData['object']['id'] ?? null;
 | 
						|
                    $articleName = $jsonData['object']['articleName'] ?? null;
 | 
						|
                    $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
 | 
						|
                    // Set Create-level fields
 | 
						|
                    $ap['published'] = $jsonData['object']['published'] ?? null;
 | 
						|
                    $ap['actor'] = $ourUrl . '/' . $actorName;
 | 
						|
                    $ap['id'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId;
 | 
						|
                    $ap['url'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId;
 | 
						|
                    $ap['type'] = 'Create';
 | 
						|
                    $ap['cc'] = ['https://www.w3.org/ns/activitystreams#Public'];
 | 
						|
                    if (isset($jsonData['options'])) {
 | 
						|
                        if (isset($jsonData['options']['informFollowers'])) {
 | 
						|
                            if ($jsonData['options']['informFollowers'] === true) {
 | 
						|
                                if ($actorName !== $articleOwnerName) {
 | 
						|
                                    $ap['to'][] = $ourUrl . '/' . $articleOwnerName;
 | 
						|
                                }
 | 
						|
                                $ap['to'][] = $ourUrl . '/' . $actorName . '/followers';
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
 | 
						|
                    $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
 | 
						|
                    if ($returnActivity === false) {
 | 
						|
                        error_log('ContentNation::jsonToActivity couldn\'t create comment');
 | 
						|
                        $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create');
 | 
						|
                    } else {
 | 
						|
                        $returnActivity->setID($ap['id']);
 | 
						|
                        $returnActivity->setURL($ap['url']);
 | 
						|
                    }
 | 
						|
                    $articleId = $jsonData['object']['articleId']; // Set the article ID for the activity
 | 
						|
                    break;
 | 
						|
 | 
						|
                case 'vote':
 | 
						|
                    $articleName = $jsonData['object']['articleName'] ?? null;
 | 
						|
                    $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
 | 
						|
                    $voteId = $jsonData['object']['id'] ?? null;
 | 
						|
                    $ap['published'] = $jsonData['object']['published'] ?? null;
 | 
						|
                    $ap['actor'] = $ourUrl . '/' . $actorName;
 | 
						|
                    $ap['id'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $voteId;
 | 
						|
                    $ap['url'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $voteId;
 | 
						|
                    if ($jsonData['object']['vote']['value'] == 1) {
 | 
						|
                        $ap['type'] = 'Like';
 | 
						|
                    } elseif ($jsonData['object']['vote']['value'] == 0) {
 | 
						|
                        $ap['type'] = 'Dislike';
 | 
						|
                    } else {
 | 
						|
                        error_log('ContentNation::jsonToActivity unknown vote value: '
 | 
						|
                            . $jsonData['object']['vote']['value']);
 | 
						|
                        break;
 | 
						|
                    }
 | 
						|
 | 
						|
                    $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
 | 
						|
                    $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
 | 
						|
                    if ($returnActivity === false) {
 | 
						|
                        error_log('ContentNation::jsonToActivity couldn\'t create vote');
 | 
						|
                        if ($ap['type'] === 'Like') {
 | 
						|
                            $returnActivity = new \Federator\Data\ActivityPub\Common\Like();
 | 
						|
                        } elseif ($ap['type'] === 'Dislike') {
 | 
						|
                            $returnActivity = new \Federator\Data\ActivityPub\Common\Dislike();
 | 
						|
                        } else {
 | 
						|
                            $returnActivity = new \Federator\Data\ActivityPub\Common\Undo();
 | 
						|
                        }
 | 
						|
                    } else {
 | 
						|
                        $returnActivity->setID($ap['id']);
 | 
						|
                        $returnActivity->setURL($ap['url']);
 | 
						|
                    }
 | 
						|
                    $articleId = $jsonData['object']['articleId']; // Set the article ID for the activity
 | 
						|
                    break;
 | 
						|
 | 
						|
                default:
 | 
						|
                    // Handle unsupported types or fallback to default behavior
 | 
						|
                    throw new \InvalidArgumentException('ContentNation::jsonToActivity Unsupported object type: '
 | 
						|
                        . $jsonData['type']);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $returnActivity;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Convert jsonData to Activity format
 | 
						|
     *
 | 
						|
     * @param string $ourUrl the url of our instance
 | 
						|
     * @param array<string, mixed> $jsonData the json data from our platfrom
 | 
						|
     * @return array|string|false the json object data or false
 | 
						|
     */
 | 
						|
    private static function generateObjectJson($ourUrl, $jsonData)
 | 
						|
    {
 | 
						|
        $objectType = $jsonData['object']['type'] ?? null;
 | 
						|
        $actorData = $jsonData['actor'] ?? null;
 | 
						|
        $actorName = $actorData['name'] ?? null;
 | 
						|
 | 
						|
        $actorUrl = $ourUrl . '/' . $actorName;
 | 
						|
 | 
						|
        if ($objectType === 'article') {
 | 
						|
            $articleName = $jsonData['object']['name'] ?? null;
 | 
						|
            $articleOwnerName = $jsonData['object']['ownerName'] ?? null;
 | 
						|
            $updatedOn = $jsonData['object']['modified'] ?? null;
 | 
						|
            $originalPublished = $jsonData['object']['published'] ?? null;
 | 
						|
            $update = $updatedOn !== $originalPublished;
 | 
						|
            $returnJson = [
 | 
						|
                'type' => 'Article',
 | 
						|
                'id' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName,
 | 
						|
                'name' => $jsonData['object']['title'] ?? null,
 | 
						|
                'published' => $originalPublished,
 | 
						|
                'summary' => $jsonData['object']['summary'] ?? null,
 | 
						|
                'content' => $jsonData['object']['content'] ?? null,
 | 
						|
                'attributedTo' => $actorUrl,
 | 
						|
                'url' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName,
 | 
						|
                'cc' => ['https://www.w3.org/ns/activitystreams#Public'],
 | 
						|
            ];
 | 
						|
            if ($update) {
 | 
						|
                $returnJson['updated'] = $updatedOn;
 | 
						|
            }
 | 
						|
            if (isset($jsonData['object']['tags'])) {
 | 
						|
                if (is_array($jsonData['object']['tags'])) {
 | 
						|
                    foreach ($jsonData['object']['tags'] as $tag) {
 | 
						|
                        $returnJson['tags'][] = $tag;
 | 
						|
                    }
 | 
						|
                } elseif (is_string($jsonData['object']['tags']) && $jsonData['object']['tags'] !== '') {
 | 
						|
                    // If it's a single tag as a string, add it as a one-element array
 | 
						|
                    $returnJson['tags'][] = $jsonData['object']['tags'];
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (isset($jsonData['options'])) {
 | 
						|
                if (isset($jsonData['options']['informFollowers'])) {
 | 
						|
                    if ($jsonData['options']['informFollowers'] === true) {
 | 
						|
                        $returnJson['to'][] = $ourUrl . '/' . $actorName . '/followers';
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        } elseif ($objectType === 'comment') {
 | 
						|
            $commentId = $jsonData['object']['id'] ?? null;
 | 
						|
            $articleName = $jsonData['object']['articleName'] ?? null;
 | 
						|
            $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
 | 
						|
            $returnJson = [
 | 
						|
                'type' => 'Note',
 | 
						|
                'id' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $commentId,
 | 
						|
                'url' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $commentId,
 | 
						|
                'attributedTo' => $actorUrl,
 | 
						|
                'content' => $jsonData['object']['content'] ?? null,
 | 
						|
                'summary' => $jsonData['object']['summary'] ?? null,
 | 
						|
                'published' => $jsonData['object']['published'] ?? null,
 | 
						|
                'cc' => ['https://www.w3.org/ns/activitystreams#Public'],
 | 
						|
            ];
 | 
						|
            if (isset($jsonData['options'])) {
 | 
						|
                if (isset($jsonData['options']['informFollowers'])) {
 | 
						|
                    if ($jsonData['options']['informFollowers'] === true) {
 | 
						|
                        $returnJson['to'][] = $ourUrl . '/' . $actorName . '/followers';
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
            $replyType = $jsonData['object']['inReplyTo']['type'] ?? null;
 | 
						|
            if ($replyType === "article") {
 | 
						|
                $returnJson['inReplyTo'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName;
 | 
						|
            } elseif ($replyType === "comment") {
 | 
						|
                $returnJson['inReplyTo'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName
 | 
						|
                    . '#' . $jsonData['object']['inReplyTo']['id'];
 | 
						|
            } else {
 | 
						|
                error_log('ContentNation::generateObjectJson for comment - unknown inReplyTo type: '
 | 
						|
                    . $replyType);
 | 
						|
            }
 | 
						|
        } elseif ($objectType === 'vote') {
 | 
						|
            $votedOn = $jsonData['object']['type'] ?? null;
 | 
						|
            $articleName = $jsonData['object']['articleName'] ?? null;
 | 
						|
            $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
 | 
						|
            $objectId = $ourUrl . '/' . $articleOwnerName . '/' . $articleName;
 | 
						|
            if ($votedOn === 'comment') {
 | 
						|
                $objectId .= '#' . $jsonData['object']['commentId'];
 | 
						|
            }
 | 
						|
 | 
						|
            $returnJson = $objectId;
 | 
						|
        } else {
 | 
						|
            error_log('ContentNation::generateObjectJson unknown object type: ' . $objectType);
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        return $returnJson;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * send CN-friendly json from ActivityPub activity
 | 
						|
     *
 | 
						|
     * @param \Federator\Data\FedUser $sender the user of the sender
 | 
						|
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
 | 
						|
     * @return boolean did we successfully send the activity?
 | 
						|
     */
 | 
						|
    public function sendActivity($sender, $activity)
 | 
						|
    {
 | 
						|
        $targetUrl = $this->service;
 | 
						|
        $targetRequestType = 'post'; // Default request type
 | 
						|
        // Convert ActivityPub activity to ContentNation JSON format and retrieve target url
 | 
						|
        $jsonData = self::activityToJson(
 | 
						|
            $this->main->getDatabase(),
 | 
						|
            $this->service,
 | 
						|
            $activity,
 | 
						|
            $targetUrl,
 | 
						|
            $targetRequestType
 | 
						|
        );
 | 
						|
 | 
						|
        if ($jsonData === false) {
 | 
						|
            error_log('ContentNation::sendActivity failed to convert activity to JSON');
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        $json = json_encode($jsonData, JSON_UNESCAPED_SLASHES);
 | 
						|
 | 
						|
        if ($json === false) {
 | 
						|
            throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
 | 
						|
        }
 | 
						|
        $digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
 | 
						|
        $date = gmdate('D, d M Y H:i:s') . ' GMT';
 | 
						|
        $parsed = parse_url($targetUrl);
 | 
						|
        if ($parsed === false) {
 | 
						|
            throw new \Exception('Failed to parse URL: ' . $targetUrl);
 | 
						|
        }
 | 
						|
 | 
						|
        if (!isset($parsed['host']) || !isset($parsed['path'])) {
 | 
						|
            throw new \Exception('Invalid target URL: missing host or path');
 | 
						|
        }
 | 
						|
        $extHost = $parsed['host'];
 | 
						|
        $path = $parsed['path'];
 | 
						|
 | 
						|
        // Build the signature string
 | 
						|
        $signatureString = "(request-target): $targetRequestType {$path}\n" .
 | 
						|
            "host: {$extHost}\n" .
 | 
						|
            "date: {$date}\n" .
 | 
						|
            "digest: {$digest}";
 | 
						|
 | 
						|
        $pKeyPath = PROJECT_ROOT . '/' . $this->main->getConfig()['keys']['federatorPrivateKeyPath'];
 | 
						|
        $privateKeyPem = file_get_contents($pKeyPath);
 | 
						|
        if ($privateKeyPem === false) {
 | 
						|
            http_response_code(500);
 | 
						|
            throw new \Federator\Exceptions\PermissionDenied("Private key couldn't be determined");
 | 
						|
        }
 | 
						|
 | 
						|
        $pkeyId = openssl_pkey_get_private($privateKeyPem);
 | 
						|
 | 
						|
        if ($pkeyId === false) {
 | 
						|
            throw new \Exception('Invalid private key');
 | 
						|
        }
 | 
						|
 | 
						|
        openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
 | 
						|
        $signature_b64 = base64_encode($signature);
 | 
						|
 | 
						|
        $signatureHeader = 'algorithm="rsa-sha256",headers="(request-target) host date digest",signature="'
 | 
						|
            . $signature_b64 . '"';
 | 
						|
 | 
						|
        $ch = curl_init($targetUrl);
 | 
						|
        if ($ch === false) {
 | 
						|
            throw new \Exception('Failed to initialize cURL');
 | 
						|
        }
 | 
						|
        $headers = [
 | 
						|
            'Host: ' . $extHost,
 | 
						|
            'Date: ' . $date,
 | 
						|
            'Digest: ' . $digest,
 | 
						|
            'Content-Type: application/json',
 | 
						|
            'Signature: ' . $signatureHeader,
 | 
						|
            'Accept: application/json',
 | 
						|
            'Username: ' . 'ap:' . $sender->id,
 | 
						|
        ];
 | 
						|
 | 
						|
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 | 
						|
        switch ($targetRequestType) {
 | 
						|
            case 'post':
 | 
						|
                curl_setopt($ch, CURLOPT_POST, true);
 | 
						|
                break;
 | 
						|
            case 'delete':
 | 
						|
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
 | 
						|
                break;
 | 
						|
            default:
 | 
						|
                throw new \Exception('ContentNation::sendActivity Unsupported target request type: '
 | 
						|
                    . $targetRequestType);
 | 
						|
        }
 | 
						|
        curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
 | 
						|
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
 | 
						|
        $response = curl_exec($ch);
 | 
						|
        curl_close($ch);
 | 
						|
 | 
						|
        if ($response === false) {
 | 
						|
            throw new \Exception('Failed to send activity: ' . curl_error($ch));
 | 
						|
        } else {
 | 
						|
            $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
 | 
						|
            if ($httpcode != 200 && $httpcode != 202) {
 | 
						|
                throw new \Exception('Unexpected HTTP code ' . $httpcode .':' . $response);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Convert ActivityPub activity to ContentNation JSON format
 | 
						|
     *
 | 
						|
     * @param \mysqli $dbh database handle
 | 
						|
     * @param string $serviceUrl the service URL
 | 
						|
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
 | 
						|
     * @param string $targetUrl the target URL for the activity
 | 
						|
     * @param string $targetRequestType the target request type (e.g., 'post', 'delete', etc.)
 | 
						|
     * @return array<string, mixed>|false the json data or false on failure
 | 
						|
     */
 | 
						|
    private function activityToJson($dbh, $serviceUrl, $activity, &$targetUrl, &$targetRequestType)
 | 
						|
    {
 | 
						|
        $type = strtolower($activity->getType());
 | 
						|
        $targetRequestType = 'post'; // Default request type
 | 
						|
        switch ($type) {
 | 
						|
            case 'create':
 | 
						|
            case 'update':
 | 
						|
                $object = $activity->getObject();
 | 
						|
                if (is_object($object)) {
 | 
						|
                    $objType = strtolower($object->getType());
 | 
						|
                    $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
 | 
						|
                    if ($articleId === null) {
 | 
						|
                        error_log('ContentNation::activityToJson Failed to get original article ID'
 | 
						|
                            .' for create/update activity');
 | 
						|
                    }
 | 
						|
                    switch ($objType) {
 | 
						|
                        case 'article':
 | 
						|
                            // We don't support article create/update at this point in time
 | 
						|
                            error_log('ContentNation::activityToJson Unsupported create/update object type: '
 | 
						|
                                .$objType);
 | 
						|
                            break;
 | 
						|
                        case 'note':
 | 
						|
                            $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/comment';
 | 
						|
                            $type = 'comment';
 | 
						|
                            $inReplyTo = $object->getInReplyTo();
 | 
						|
                            if ($inReplyTo !== '') {
 | 
						|
                                $target = $inReplyTo;
 | 
						|
                            } else {
 | 
						|
                                $target = $object->getObject();
 | 
						|
                            }
 | 
						|
                            $comment = null;
 | 
						|
                            if (is_string($target)) {
 | 
						|
                                if (strpos($target, '#') !== false) {
 | 
						|
                                    $parts = explode('#', $target);
 | 
						|
                                    if (count($parts) > 0) {
 | 
						|
                                        $comment = $parts[count($parts) - 1];
 | 
						|
                                    }
 | 
						|
                                }
 | 
						|
                            } else {
 | 
						|
                                error_log('ContentNation::activityToJson Unsupported target type for comment with id: '
 | 
						|
                                    . $activity->getID() . ' Type: ' . gettype($target));
 | 
						|
                                return false;
 | 
						|
                            }
 | 
						|
                            return [
 | 
						|
                                'type' => $type,
 | 
						|
                                'id' => $activity->getID(),
 | 
						|
                                'parent' => $comment,
 | 
						|
                                'subject' => $object->getSummary(),
 | 
						|
                                'comment' => $object->getContent(),
 | 
						|
                            ];
 | 
						|
                        default:
 | 
						|
                            error_log('ContentNation::activityToJson Unsupported create/update object type: '
 | 
						|
                                . $objType);
 | 
						|
                            return false;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                break;
 | 
						|
 | 
						|
            case 'follow':
 | 
						|
                $profileUrl = $activity->getObject();
 | 
						|
                if (!is_string($profileUrl)) {
 | 
						|
                    error_log('ContentNation::activityToJson Invalid profile URL: ' . json_encode($profileUrl));
 | 
						|
                    return false;
 | 
						|
                }
 | 
						|
                $receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? ''));
 | 
						|
                $ourDomain = parse_url($profileUrl, PHP_URL_HOST);
 | 
						|
                if ($receiverName === '' || $ourDomain === '') {
 | 
						|
                    error_log('ContentNation::activityToJson no profileName or domain found for object url: '
 | 
						|
                        . $profileUrl);
 | 
						|
                    return false;
 | 
						|
                }
 | 
						|
                $receiver = $receiverName;
 | 
						|
                try {
 | 
						|
                    $localUser = \Federator\DIO\User::getUserByName(
 | 
						|
                        $dbh,
 | 
						|
                        $receiver,
 | 
						|
                        $this,
 | 
						|
                        null
 | 
						|
                    );
 | 
						|
                } catch (\Throwable $e) {
 | 
						|
                    error_log('ContentNation::activityToJson get user by name: ' . $receiver . '. Exception: '
 | 
						|
                        . $e->getMessage());
 | 
						|
                    return false;
 | 
						|
                }
 | 
						|
                if ($localUser === null || $localUser->id === null) {
 | 
						|
                    error_log('ContentNation::activityToJson couldn\'t find user: ' . $receiver);
 | 
						|
                    return false;
 | 
						|
                }
 | 
						|
                $targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow';
 | 
						|
                $type = 'follow';
 | 
						|
                $actor = $activity->getAActor();
 | 
						|
                $fedUser = \Federator\DIO\FedUser::getUserByName(
 | 
						|
                    $dbh,
 | 
						|
                    $actor,
 | 
						|
                    null
 | 
						|
                );
 | 
						|
                $from = $fedUser->id;
 | 
						|
                return [
 | 
						|
                    'type' => $type,
 | 
						|
                    'id' => $activity->getID(),
 | 
						|
                    'from' => $from,
 | 
						|
                    'to' => $localUser->id,
 | 
						|
                ];
 | 
						|
 | 
						|
            case 'like':
 | 
						|
            case 'dislike':
 | 
						|
                $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
 | 
						|
                if ($articleId === null) {
 | 
						|
                    error_log('ContentNation::activityToJson Failed to get original article ID for vote activity');
 | 
						|
                }
 | 
						|
                $voteValue = $type === 'like' ? true : false;
 | 
						|
                $activityType = 'vote';
 | 
						|
                $inReplyTo = $activity->getInReplyTo();
 | 
						|
                if ($inReplyTo !== '') {
 | 
						|
                    $target = $inReplyTo;
 | 
						|
                } else {
 | 
						|
                    $target = $activity->getObject();
 | 
						|
                }
 | 
						|
                $comment = null;
 | 
						|
                if (is_string($target)) {
 | 
						|
                    if (strpos($target, '#') !== false) {
 | 
						|
                        $parts = explode('#', $target);
 | 
						|
                        if (count($parts) > 0) {
 | 
						|
                            $comment = $parts[count($parts) - 1];
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                } else {
 | 
						|
                    error_log('ContentNation::activityToJson Unsupported target type for vote with id: '
 | 
						|
                        . $activity->getID() . ' Type: ' . gettype($target));
 | 
						|
                    return false;
 | 
						|
                }
 | 
						|
                $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
 | 
						|
                return [
 | 
						|
                    'vote' => $voteValue,
 | 
						|
                    'type' => $activityType,
 | 
						|
                    'id' => $activity->getID(),
 | 
						|
                    'comment' => $comment,
 | 
						|
                ];
 | 
						|
 | 
						|
            case 'undo':
 | 
						|
                $object = $activity->getObject();
 | 
						|
                if (is_object($object)) {
 | 
						|
                    $objType = strtolower($object->getType());
 | 
						|
                    switch ($objType) {
 | 
						|
                        case 'follow':
 | 
						|
                            $profileUrl = $object->getObject();
 | 
						|
                            if (!is_string($profileUrl)) {
 | 
						|
                                error_log('ContentNation::activityToJson Invalid profile URL: '
 | 
						|
                                    . json_encode($profileUrl));
 | 
						|
                                return false;
 | 
						|
                            }
 | 
						|
                            $receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? ''));
 | 
						|
                            $ourDomain = parse_url($profileUrl, PHP_URL_HOST);
 | 
						|
                            if ($receiverName === '' || $ourDomain === '') {
 | 
						|
                                error_log('ContentNation::activityToJson no profileName or domain found for object'
 | 
						|
                                    . ' url: ' . $profileUrl);
 | 
						|
                                return false;
 | 
						|
                            }
 | 
						|
                            $receiver = $receiverName;
 | 
						|
                            try {
 | 
						|
                                $localUser = \Federator\DIO\User::getUserByName(
 | 
						|
                                    $dbh,
 | 
						|
                                    $receiver,
 | 
						|
                                    $this,
 | 
						|
                                    null
 | 
						|
                                );
 | 
						|
                            } catch (\Throwable $e) {
 | 
						|
                                error_log('ContentNation::activityToJson get user by name: ' . $receiver
 | 
						|
                                    . '. Exception: ' . $e->getMessage());
 | 
						|
                                return false;
 | 
						|
                            }
 | 
						|
                            if ($localUser === null || $localUser->id === null) {
 | 
						|
                                error_log('ContentNation::activityToJson couldn\'t find user: ' . $receiver);
 | 
						|
                                return false;
 | 
						|
                            }
 | 
						|
                            $targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow';
 | 
						|
                            $type = 'follow';
 | 
						|
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
 | 
						|
                                $actor = $object->getAActor();
 | 
						|
                                if ($actor !== '') {
 | 
						|
                                    $fedUser = \Federator\DIO\FedUser::getUserByName(
 | 
						|
                                        $dbh,
 | 
						|
                                        $actor,
 | 
						|
                                        null
 | 
						|
                                    );
 | 
						|
                                    $from = $fedUser->id;
 | 
						|
                                    $targetRequestType = 'delete';
 | 
						|
                                    return [
 | 
						|
                                        'type' => $type,
 | 
						|
                                        'id' => $object->getID(),
 | 
						|
                                        'from' => $from,
 | 
						|
                                        'to' => $localUser->id,
 | 
						|
                                    ];
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                            return false;
 | 
						|
 | 
						|
                        case 'like':
 | 
						|
                        case 'dislike':
 | 
						|
                            $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
 | 
						|
                            if ($articleId === null) {
 | 
						|
                                error_log('ContentNation::activityToJson Failed to get original article ID '
 | 
						|
                                    . 'for undo vote activity');
 | 
						|
                            }
 | 
						|
                            $activityType = 'vote';
 | 
						|
                            $inReplyTo = $object->getInReplyTo();
 | 
						|
                            if ($inReplyTo !== '') {
 | 
						|
                                $target = $inReplyTo;
 | 
						|
                            } else {
 | 
						|
                                $target = $object->getObject();
 | 
						|
                            }
 | 
						|
                            $comment = null;
 | 
						|
                            if (is_string($target)) {
 | 
						|
                                if (strpos($target, '#') !== false) {
 | 
						|
                                    $parts = explode('#', $target);
 | 
						|
                                    if (count($parts) > 0) {
 | 
						|
                                        $comment = $parts[count($parts) - 1];
 | 
						|
                                    }
 | 
						|
                                }
 | 
						|
                            } else {
 | 
						|
                                error_log('ContentNation::activityToJson Unsupported target type for undo '
 | 
						|
                                    . 'vote with id: ' . $activity->getID() . " Type: " . gettype($target));
 | 
						|
                                return false;
 | 
						|
                            }
 | 
						|
                            $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
 | 
						|
                            return [
 | 
						|
                                'vote' => null,
 | 
						|
                                'type' => $activityType,
 | 
						|
                                'id' => $object->getID(),
 | 
						|
                                'comment' => $comment,
 | 
						|
                            ];
 | 
						|
                        case 'note':
 | 
						|
                            // We don't support comment deletions at this point in time
 | 
						|
                            error_log('ContentNation::activityToJson Unsupported undo object type: ' . $objType);
 | 
						|
                            break;
 | 
						|
                        default:
 | 
						|
                            error_log('ContentNation::activityToJson Unsupported create/update object type: '
 | 
						|
                                . $objType);
 | 
						|
                            return false;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                break;
 | 
						|
            default:
 | 
						|
                error_log('ContentNation::activityToJson Unsupported activity type: ' . $type);
 | 
						|
                return false;
 | 
						|
        }
 | 
						|
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * check if the headers include a valid signature
 | 
						|
     *
 | 
						|
     * @param string[] $headers the headers
 | 
						|
     * @throws \Federator\Exceptions\PermissionDenied
 | 
						|
     * @return string|\Federator\Exceptions\PermissionDenied
 | 
						|
     */
 | 
						|
    public function checkSignature($headers)
 | 
						|
    {
 | 
						|
        $signatureHeader = $headers['Signature'] ?? null;
 | 
						|
 | 
						|
        if (!isset($signatureHeader)) {
 | 
						|
            throw new \Federator\Exceptions\PermissionDenied('Missing Signature header');
 | 
						|
        }
 | 
						|
 | 
						|
        if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName']) {
 | 
						|
            throw new \Federator\Exceptions\PermissionDenied('Invalid sender name');
 | 
						|
        }
 | 
						|
 | 
						|
        // Parse Signature header
 | 
						|
        preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches);
 | 
						|
        $signatureParts = array_combine($matches[1], $matches[2]);
 | 
						|
 | 
						|
        $signature = base64_decode($signatureParts['signature']);
 | 
						|
        $signedHeaders = explode(' ', $signatureParts['headers']);
 | 
						|
 | 
						|
        $pKeyPath = PROJECT_ROOT . '/' . $this->config['keys']['publicKeyPath'];
 | 
						|
        $publicKeyPem = file_get_contents($pKeyPath);
 | 
						|
        if ($publicKeyPem === false) {
 | 
						|
            http_response_code(500);
 | 
						|
            throw new \Federator\Exceptions\PermissionDenied('Public key couldn\'t be determined');
 | 
						|
        }
 | 
						|
 | 
						|
        // Reconstruct the signed string
 | 
						|
        $signedString = '';
 | 
						|
        foreach ($signedHeaders as $header) {
 | 
						|
            if ($header === '(request-target)') {
 | 
						|
                $method = strtolower($_SERVER['REQUEST_METHOD']);
 | 
						|
                $path = $_SERVER['REQUEST_URI'];
 | 
						|
                $headerValue = "$method $path";
 | 
						|
            } else {
 | 
						|
                $headerValue = $headers[ucwords($header, '-')] ?? '';
 | 
						|
            }
 | 
						|
 | 
						|
            $signedString .= strtolower($header) . ': ' . $headerValue . "\n";
 | 
						|
        }
 | 
						|
 | 
						|
        $signedString = rtrim($signedString);
 | 
						|
 | 
						|
        // Verify the signature
 | 
						|
        $pubkeyRes = openssl_pkey_get_public($publicKeyPem);
 | 
						|
        $verified = false;
 | 
						|
        if ($pubkeyRes instanceof \OpenSSLAsymmetricKey && is_string($signature)) {
 | 
						|
            $verified = openssl_verify($signedString, $signature, $pubkeyRes, OPENSSL_ALGO_SHA256);
 | 
						|
        }
 | 
						|
        if ($verified != 1) {
 | 
						|
            http_response_code(500);
 | 
						|
            throw new \Federator\Exceptions\PermissionDenied('Signature verification failed');
 | 
						|
        }
 | 
						|
        return 'Signature verified.';
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
namespace Federator;
 | 
						|
 | 
						|
/**
 | 
						|
 * Function to initialize plugin
 | 
						|
 *
 | 
						|
 * @param  \Federator\Main $main main instance
 | 
						|
 * @return void
 | 
						|
 */
 | 
						|
function contentnation_load($main)
 | 
						|
{
 | 
						|
    $cn = new Connector\ContentNation($main);
 | 
						|
    $main->setConnector($cn);
 | 
						|
}
 |