forked from grumpydevelop/federator
		
	conditionally convert article to note
- fix bug in which inReplyTo isn't correctly set from contentnation-comments - added dio/article which has functions to convert article to note based on new file - added formatsupport.json to manage special cases (f.e. includes which servers can handle articles)
This commit is contained in:
		
							parent
							
								
									30c577c82f
								
							
						
					
					
						commit
						10dec5ebd3
					
				
					 9 changed files with 203 additions and 23 deletions
				
			
		
							
								
								
									
										8
									
								
								formatsupport.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								formatsupport.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
{
 | 
			
		||||
    "activitypub": {
 | 
			
		||||
        "article": [
 | 
			
		||||
            "localhost",
 | 
			
		||||
            "writefreely.org"
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +62,8 @@
 | 
			
		|||
 | 
			
		||||
            const headers = {
 | 
			
		||||
                ...(session ? { "X-Session": session } : {}),
 | 
			
		||||
                ...(profile ? { "X-Profile": profile } : {})
 | 
			
		||||
                ...(profile ? { "X-Profile": profile } : {}),
 | 
			
		||||
                "HTTP_HOST": "localhost",
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            fetch("http://localhost/" + targetLink, {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -241,6 +241,11 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $atPos = strpos($_recipientId, '@');
 | 
			
		||||
        if ($atPos !== false) {
 | 
			
		||||
            $_recipientId = substr($_recipientId, 0, $atPos);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // get recipient
 | 
			
		||||
        $recipient = \Federator\DIO\User::getUserByName(
 | 
			
		||||
            $dbh,
 | 
			
		||||
| 
						 | 
				
			
			@ -265,11 +270,8 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
 | 
			
		||||
        switch ($type) {
 | 
			
		||||
            case 'Follow':
 | 
			
		||||
                $success = false;
 | 
			
		||||
                $actor = $inboxActivity->getAActor();
 | 
			
		||||
                if ($actor !== '') {
 | 
			
		||||
                    $success = \Federator\DIO\Followers::addExternalFollow($dbh, $inboxActivity->getID(), $user->id, $recipient->id);
 | 
			
		||||
                }
 | 
			
		||||
                $success = \Federator\DIO\Followers::addExternalFollow($dbh, $inboxActivity->getID(), $user->id, $recipient->id);
 | 
			
		||||
 | 
			
		||||
                if ($success === false) {
 | 
			
		||||
                    error_log("Inbox::postForUser: Failed to add follower for user $user->id");
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -346,25 +346,37 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                            break;
 | 
			
		||||
                        case 'Note':
 | 
			
		||||
                            // Undo Note (remove note)
 | 
			
		||||
                            if (method_exists($object, 'getID')) {
 | 
			
		||||
                                $noteId = $object->getID();
 | 
			
		||||
                                \Federator\DIO\Posts::deletePost($dbh, $noteId);
 | 
			
		||||
                            }
 | 
			
		||||
                            $noteId = $object->getID();
 | 
			
		||||
                            \Federator\DIO\Posts::deletePost($dbh, $noteId);
 | 
			
		||||
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Article':
 | 
			
		||||
                            // Undo Article (remove article)
 | 
			
		||||
                            if (method_exists($object, 'getID')) {
 | 
			
		||||
                                $articleId = $object->getID();
 | 
			
		||||
                                \Federator\DIO\Posts::deletePost($dbh, $articleId);
 | 
			
		||||
                                // also remove latest saved article-update
 | 
			
		||||
                                \Federator\DIO\Posts::deletePost($dbh, $articleId . '#update');
 | 
			
		||||
                            $idPart = strrchr($recipient->id, '@');
 | 
			
		||||
                            if ($idPart === false) {
 | 
			
		||||
                                error_log("NewContent::postForUser Error in Undo Article. $recipient->id, recipient ID is not valid");
 | 
			
		||||
                                return false;
 | 
			
		||||
                            } else {
 | 
			
		||||
                                $targetUrl = ltrim($idPart, '@');
 | 
			
		||||
 | 
			
		||||
                                if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
 | 
			
		||||
                                    $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
 | 
			
		||||
                                    $newActivity->setObject($object);
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    error_log("NewContent::postForUser Error in Undo Article for recipient $recipient->id, object is not an Article");
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            $articleId = $object->getID();
 | 
			
		||||
                            \Federator\DIO\Posts::deletePost($dbh, $articleId);
 | 
			
		||||
                            // also remove latest saved article-update
 | 
			
		||||
                            \Federator\DIO\Posts::deletePost($dbh, $articleId . '#update');
 | 
			
		||||
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (is_string($object)) {
 | 
			
		||||
                    \Federator\DIO\Posts::deletePost($dbh, $object);
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("NewContent::postForUser Error in Undo for user $user->id, object is not a string or object");
 | 
			
		||||
                    error_log("NewContent::postForUser Error in Undo for recipient $recipient->id, object is not a string or object");
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -376,7 +388,7 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                    // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
 | 
			
		||||
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("NewContent::postForUser Error in Add Like/Dislike for user $user->id, targetId is not a string");
 | 
			
		||||
                    error_log("NewContent::postForUser Error in Add Like/Dislike for recipient $recipient->id, targetId is not a string");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			@ -387,7 +399,27 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                if (is_object($object)) {
 | 
			
		||||
                    switch ($object->getType()) {
 | 
			
		||||
                        case 'Note':
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Article':
 | 
			
		||||
                            $idPart = strrchr($recipient->id, '@');
 | 
			
		||||
                            if ($idPart === false) {
 | 
			
		||||
                                error_log("NewContent::postForUser Error in Create/Update Article. $recipient->id, recipient ID is not valid");
 | 
			
		||||
                                return false;
 | 
			
		||||
                            } else {
 | 
			
		||||
                                $targetUrl = ltrim($idPart, '@');
 | 
			
		||||
 | 
			
		||||
                                if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
 | 
			
		||||
                                    $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
 | 
			
		||||
                                    $newActivity->setObject($object);
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    error_log("NewContent::postForUser Error in Create/Update Article for recipient $recipient->id, object is not an Article");
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
 | 
			
		||||
                            break;
 | 
			
		||||
                        default:
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
                            break;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -376,6 +376,17 @@ class APObject implements \JsonSerializable
 | 
			
		|||
        $this->summary = $summary;
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get summary
 | 
			
		||||
     *
 | 
			
		||||
     * @return string summary
 | 
			
		||||
     */
 | 
			
		||||
    public function getSummary()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->summary;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * set type
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			@ -459,6 +470,16 @@ class APObject implements \JsonSerializable
 | 
			
		|||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get name
 | 
			
		||||
     *
 | 
			
		||||
     * @return string name
 | 
			
		||||
     */
 | 
			
		||||
    public function getName() : string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * add Image
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										68
									
								
								php/federator/dio/article.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								php/federator/dio/article.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Yannis Vogel (vogeldevelopment)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\DIO;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * IO functions related to articles
 | 
			
		||||
 */
 | 
			
		||||
class Article
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert an Article to a Note
 | 
			
		||||
     *
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Article $article
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Note
 | 
			
		||||
     *      The generated note
 | 
			
		||||
     */
 | 
			
		||||
    public static function convertToNote($article)
 | 
			
		||||
    {
 | 
			
		||||
        $note = new \Federator\Data\ActivityPub\Common\Note();
 | 
			
		||||
        $note->setId($article->getId())
 | 
			
		||||
            ->setURL($article->getURL());
 | 
			
		||||
        $note->setContent($article->getContent());
 | 
			
		||||
        $note->setSummary($article->getSummary());
 | 
			
		||||
        $note->setPublished($article->getPublished());
 | 
			
		||||
        $note->setName($article->getName());
 | 
			
		||||
        $note->setAttributedTo($article->getAttributedTo());
 | 
			
		||||
        foreach ($article->getTo() as $to) {
 | 
			
		||||
            $note->addTo($to);
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($article->getCc() as $cc) {
 | 
			
		||||
            $note->addCc($cc);
 | 
			
		||||
        }
 | 
			
		||||
        return $note;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Conditionally convert article to a note
 | 
			
		||||
     *
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Article $article
 | 
			
		||||
     * @param string $targetUrl
 | 
			
		||||
     *      The target URL for the activity (e.g. mastodon.social)
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Note|\Federator\Data\ActivityPub\Common\Article
 | 
			
		||||
     *      The generated note on success, false on failure
 | 
			
		||||
     */
 | 
			
		||||
    public static function conditionalConvertToNote($article, $targetUrl)
 | 
			
		||||
    {
 | 
			
		||||
        $supportFile = file_get_contents(PROJECT_ROOT . '/formatsupport.json');
 | 
			
		||||
        if ($supportFile === false) {
 | 
			
		||||
            error_log("Article::conditionalConvertToNote Failed to read support file for article conversion.");
 | 
			
		||||
            return $article; // Fallback to original article if file read fails
 | 
			
		||||
        }
 | 
			
		||||
        $supportlist = json_decode($supportFile, true);
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            !isset($supportlist['activitypub']['article']) ||
 | 
			
		||||
            !is_array($supportlist['activitypub']['article']) ||
 | 
			
		||||
            !in_array($targetUrl, $supportlist['activitypub']['article'], true)
 | 
			
		||||
        ) {
 | 
			
		||||
            return self::convertToNote($article); // Articles are not supported for this target
 | 
			
		||||
        }
 | 
			
		||||
        return $article; // Articles are supported, return as is
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -393,11 +393,11 @@ class Followers
 | 
			
		|||
     *
 | 
			
		||||
     * @param \mysqli $dbh database handle
 | 
			
		||||
     * @param string $followId the follow ID to use (should be an external url)
 | 
			
		||||
     * @param string $sourceUser source user id
 | 
			
		||||
     * @param string $sourceUserId source user id
 | 
			
		||||
     * @param string $targetUserId target user id
 | 
			
		||||
     * @return boolean true on success, false on failure
 | 
			
		||||
     */
 | 
			
		||||
    public static function addExternalFollow($dbh, $followId, $sourceUser, $targetUserId)
 | 
			
		||||
    public static function addExternalFollow($dbh, $followId, $sourceUserId, $targetUserId)
 | 
			
		||||
    {
 | 
			
		||||
        // Check if we already follow this user
 | 
			
		||||
        $sql = 'select id from follows where source_user = ? and target_user = ?';
 | 
			
		||||
| 
						 | 
				
			
			@ -405,7 +405,7 @@ class Followers
 | 
			
		|||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare statement");
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("ss", $sourceUser, $targetUserId);
 | 
			
		||||
        $stmt->bind_param("ss", $sourceUserId, $targetUserId);
 | 
			
		||||
        $foundId = 0;
 | 
			
		||||
        $ret = $stmt->bind_result($foundId);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
| 
						 | 
				
			
			@ -423,7 +423,7 @@ class Followers
 | 
			
		|||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare insert statement");
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("sss", $followId, $sourceUser, $targetUserId);
 | 
			
		||||
        $stmt->bind_param("sss", $followId, $sourceUserId, $targetUserId);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        return true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,11 +78,59 @@ class Posts
 | 
			
		|||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $originUrl = 'localhost';
 | 
			
		||||
        if (isset($_SERVER['HTTP_HOST'])) {
 | 
			
		||||
            $originUrl = $_SERVER['HTTP_HOST']; // origin of our request - e.g. mastodon
 | 
			
		||||
        } elseif (isset($_SERVER['HTTP_ORIGIN'])) {
 | 
			
		||||
            $origin = $_SERVER['HTTP_ORIGIN'];
 | 
			
		||||
            $parsed = parse_url($origin);
 | 
			
		||||
            if (isset($parsed) && isset($parsed['host'])) {
 | 
			
		||||
                $parsedHost = $parsed['host'];
 | 
			
		||||
                if (is_string($parsedHost) && $parsedHost !== "") {
 | 
			
		||||
                    $originUrl = $parsedHost;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!isset($originUrl) || $originUrl === "") {
 | 
			
		||||
            $originUrl = 'localhost'; // Fallback to localhost if no origin is set
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // save posts to DB
 | 
			
		||||
        foreach ($posts as $post) {
 | 
			
		||||
            if ($post->getID() !== "") {
 | 
			
		||||
                self::savePost($dbh, $userid, $post);
 | 
			
		||||
            }
 | 
			
		||||
            switch (strtolower($post->getType())) {
 | 
			
		||||
                case 'undo':
 | 
			
		||||
                    $object = $post->getObject();
 | 
			
		||||
                    if (is_object($object)) {
 | 
			
		||||
                        if (strtolower($object->getType()) === 'article') {
 | 
			
		||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
 | 
			
		||||
                                $object = \Federator\DIO\Article::conditionalConvertToNote($object, $originUrl);
 | 
			
		||||
                                $post->setObject($object);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case 'create':
 | 
			
		||||
                case 'update':
 | 
			
		||||
                    $object = $post->getObject();
 | 
			
		||||
                    if (is_object($object)) {
 | 
			
		||||
                        if (strtolower($object->getType()) === 'article') {
 | 
			
		||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
 | 
			
		||||
                                $object = \Federator\DIO\Article::conditionalConvertToNote($object, $originUrl);
 | 
			
		||||
                                $post->setObject($object);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                default:
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($cache !== null) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -635,9 +635,9 @@ class ContentNation implements Connector
 | 
			
		|||
            }
 | 
			
		||||
            $replyType = $jsonData['object']['inReplyTo']['type'] ?? null;
 | 
			
		||||
            if ($replyType === "article") {
 | 
			
		||||
                $returnJson['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
 | 
			
		||||
                $returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
 | 
			
		||||
            } elseif ($replyType === "comment") {
 | 
			
		||||
                $returnJson['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id'];
 | 
			
		||||
                $returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id'];
 | 
			
		||||
            } else {
 | 
			
		||||
                error_log("ContentNation::generateObjectJson for comment - unknown inReplyTo type: {$replyType}");
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue