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:
Yannis Vogel 2025-05-26 16:17:23 +02:00
parent 30c577c82f
commit 10dec5ebd3
No known key found for this signature in database
9 changed files with 203 additions and 23 deletions

8
formatsupport.json Normal file
View file

@ -0,0 +1,8 @@
{
"activitypub": {
"article": [
"localhost",
"writefreely.org"
]
}
}

View file

@ -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, {

View file

@ -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");
}

View file

@ -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;

View file

@ -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
*

View 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
}
}

View file

@ -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;

View file

@ -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) {

View file

@ -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}");
}