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 = {
|
const headers = {
|
||||||
...(session ? { "X-Session": session } : {}),
|
...(session ? { "X-Session": session } : {}),
|
||||||
...(profile ? { "X-Profile": profile } : {})
|
...(profile ? { "X-Profile": profile } : {}),
|
||||||
|
"HTTP_HOST": "localhost",
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch("http://localhost/" + targetLink, {
|
fetch("http://localhost/" + targetLink, {
|
||||||
|
|
|
@ -241,6 +241,11 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$atPos = strpos($_recipientId, '@');
|
||||||
|
if ($atPos !== false) {
|
||||||
|
$_recipientId = substr($_recipientId, 0, $atPos);
|
||||||
|
}
|
||||||
|
|
||||||
// get recipient
|
// get recipient
|
||||||
$recipient = \Federator\DIO\User::getUserByName(
|
$recipient = \Federator\DIO\User::getUserByName(
|
||||||
$dbh,
|
$dbh,
|
||||||
|
@ -265,11 +270,8 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
|
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'Follow':
|
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) {
|
if ($success === false) {
|
||||||
error_log("Inbox::postForUser: Failed to add follower for user $user->id");
|
error_log("Inbox::postForUser: Failed to add follower for user $user->id");
|
||||||
}
|
}
|
||||||
|
|
|
@ -346,25 +346,37 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
break;
|
break;
|
||||||
case 'Note':
|
case 'Note':
|
||||||
// Undo Note (remove note)
|
// Undo Note (remove note)
|
||||||
if (method_exists($object, 'getID')) {
|
|
||||||
$noteId = $object->getID();
|
$noteId = $object->getID();
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'Article':
|
case 'Article':
|
||||||
// Undo Article (remove article)
|
// Undo Article (remove article)
|
||||||
if (method_exists($object, 'getID')) {
|
$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();
|
$articleId = $object->getID();
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $articleId);
|
\Federator\DIO\Posts::deletePost($dbh, $articleId);
|
||||||
// also remove latest saved article-update
|
// also remove latest saved article-update
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $articleId . '#update');
|
\Federator\DIO\Posts::deletePost($dbh, $articleId . '#update');
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (is_string($object)) {
|
} else if (is_string($object)) {
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $object);
|
\Federator\DIO\Posts::deletePost($dbh, $object);
|
||||||
} else {
|
} 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;
|
break;
|
||||||
|
|
||||||
|
@ -376,7 +388,7 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
|
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
||||||
} else {
|
} 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;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -387,7 +399,27 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
if (is_object($object)) {
|
if (is_object($object)) {
|
||||||
switch ($object->getType()) {
|
switch ($object->getType()) {
|
||||||
case 'Note':
|
case 'Note':
|
||||||
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
||||||
|
|
||||||
|
break;
|
||||||
case 'Article':
|
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:
|
default:
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -376,6 +376,17 @@ class APObject implements \JsonSerializable
|
||||||
$this->summary = $summary;
|
$this->summary = $summary;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get summary
|
||||||
|
*
|
||||||
|
* @return string summary
|
||||||
|
*/
|
||||||
|
public function getSummary()
|
||||||
|
{
|
||||||
|
return $this->summary;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set type
|
* set type
|
||||||
*
|
*
|
||||||
|
@ -459,6 +470,16 @@ class APObject implements \JsonSerializable
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get name
|
||||||
|
*
|
||||||
|
* @return string name
|
||||||
|
*/
|
||||||
|
public function getName() : string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add Image
|
* 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 \mysqli $dbh database handle
|
||||||
* @param string $followId the follow ID to use (should be an external url)
|
* @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
|
* @param string $targetUserId target user id
|
||||||
* @return boolean true on success, false on failure
|
* @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
|
// Check if we already follow this user
|
||||||
$sql = 'select id from follows where source_user = ? and target_user = ?';
|
$sql = 'select id from follows where source_user = ? and target_user = ?';
|
||||||
|
@ -405,7 +405,7 @@ class Followers
|
||||||
if ($stmt === false) {
|
if ($stmt === false) {
|
||||||
throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare statement");
|
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;
|
$foundId = 0;
|
||||||
$ret = $stmt->bind_result($foundId);
|
$ret = $stmt->bind_result($foundId);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
@ -423,7 +423,7 @@ class Followers
|
||||||
if ($stmt === false) {
|
if ($stmt === false) {
|
||||||
throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare insert statement");
|
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->execute();
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
return true;
|
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
|
// save posts to DB
|
||||||
foreach ($posts as $post) {
|
foreach ($posts as $post) {
|
||||||
if ($post->getID() !== "") {
|
if ($post->getID() !== "") {
|
||||||
self::savePost($dbh, $userid, $post);
|
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) {
|
if ($cache !== null) {
|
||||||
|
|
|
@ -635,9 +635,9 @@ class ContentNation implements Connector
|
||||||
}
|
}
|
||||||
$replyType = $jsonData['object']['inReplyTo']['type'] ?? null;
|
$replyType = $jsonData['object']['inReplyTo']['type'] ?? null;
|
||||||
if ($replyType === "article") {
|
if ($replyType === "article") {
|
||||||
$returnJson['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
|
$returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
|
||||||
} elseif ($replyType === "comment") {
|
} elseif ($replyType === "comment") {
|
||||||
$returnJson['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id'];
|
$returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id'];
|
||||||
} else {
|
} else {
|
||||||
error_log("ContentNation::generateObjectJson for comment - unknown inReplyTo type: {$replyType}");
|
error_log("ContentNation::generateObjectJson for comment - unknown inReplyTo type: {$replyType}");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue