forked from grumpydevelop/federator
342 lines
12 KiB
PHP
342 lines
12 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\DIO;
|
|
|
|
/**
|
|
* IO functions related to users
|
|
*/
|
|
class Posts
|
|
{
|
|
/**
|
|
* get posts by user
|
|
*
|
|
* @param \mysqli $dbh @unused-param
|
|
* database handle
|
|
* @param string $userid
|
|
* user id
|
|
* @param \Federator\Connector\Connector $connector
|
|
* connector to fetch use with
|
|
* @param \Federator\Cache\Cache|null $cache
|
|
* optional caching service
|
|
* @param int $min
|
|
* minimum timestamp
|
|
* @param int $max
|
|
* maximum timestamp
|
|
* @param int $limit
|
|
* maximum number of results
|
|
* @return \Federator\Data\ActivityPub\Common\Activity[]
|
|
*/
|
|
public static function getPostsByUser($dbh, $userid, $connector, $cache, $min, $max, $limit)
|
|
{
|
|
// ask cache
|
|
if ($cache !== null) {
|
|
$posts = $cache->getRemotePostsByUser($userid, $min, $max, $limit);
|
|
if ($posts !== false) {
|
|
return $posts;
|
|
}
|
|
}
|
|
$posts = self::getPostsFromDb($dbh, $userid, $min, $max, $limit);
|
|
if ($posts === false) {
|
|
$posts = [];
|
|
}
|
|
|
|
// Only override $min if we found posts in our DB
|
|
$remoteMin = $min;
|
|
if (!empty($posts)) {
|
|
// Find the latest published date in the DB posts
|
|
$latestPublished = null;
|
|
foreach ($posts as $post) {
|
|
$published = $post->getPublished();
|
|
if ($published != null) {
|
|
if ($latestPublished === null || $published > $latestPublished) {
|
|
$latestPublished = $published;
|
|
}
|
|
}
|
|
}
|
|
if ($latestPublished !== null) {
|
|
$remoteMin = $latestPublished;
|
|
}
|
|
}
|
|
|
|
// Fetch newer posts from connector (if any) if max is not set and limit not reached
|
|
if ($max == 0 && sizeof($posts) < $limit) {
|
|
$newPosts = $connector->getRemotePostsByUser($userid, $remoteMin, $max, $limit);
|
|
if ($newPosts !== false && is_array($newPosts)) {
|
|
// Merge new posts with DB posts, avoiding duplicates by ID
|
|
$existingIds = [];
|
|
foreach ($posts as $post) {
|
|
$existingIds[$post->getID()] = true;
|
|
}
|
|
foreach ($newPosts as $newPost) {
|
|
if (!isset($existingIds[$newPost->getID()])) {
|
|
if ($newPost->getID() !== "") {
|
|
self::savePost($dbh, $userid, $newPost);
|
|
}
|
|
if (sizeof($posts) < $limit) {
|
|
$posts[] = $newPost;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$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
|
|
}
|
|
|
|
// optionally convert from article to note
|
|
foreach ($posts as $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) {
|
|
$cache->saveRemotePostsByUser($userid, $min, $max, $limit, $posts);
|
|
}
|
|
return $posts;
|
|
}
|
|
|
|
/**
|
|
* Get posts for a user from the DB (optionally by date)
|
|
*
|
|
* @param \mysqli $dbh
|
|
* @param string $userId
|
|
* @param int $min min timestamp
|
|
* @param int $max max timestamp
|
|
* @param int $limit
|
|
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
|
*/
|
|
public static function getPostsFromDb($dbh, $userId, $min, $max, $limit = 20)
|
|
{
|
|
$sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, unix_timestamp(`published`) as published FROM posts WHERE user_id = ?';
|
|
$params = [$userId];
|
|
$types = 's';
|
|
if ($min > 0) {
|
|
$sql .= ' AND published >= from_unixtime(?)';
|
|
$params[] = $min;
|
|
$types .= 's';
|
|
}
|
|
if ($max > 0) {
|
|
$sql .= ' AND published <= from_unixtime(?)';
|
|
$params[] = $max;
|
|
$types .= 's';
|
|
}
|
|
$sql .= ' ORDER BY published DESC LIMIT ' . $limit;
|
|
|
|
$stmt = $dbh->prepare($sql);
|
|
if ($stmt === false) {
|
|
throw new \Federator\Exceptions\ServerError();
|
|
}
|
|
$stmt->bind_param($types, ...$params);
|
|
$stmt->execute();
|
|
$result = $stmt->get_result();
|
|
if (!($result instanceof \mysqli_result)) {
|
|
$stmt->close();
|
|
return false;
|
|
}
|
|
$posts = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
if (isset($row['to']) && $row['to'] !== null) {
|
|
$row['to'] = json_decode($row['to'], true);
|
|
}
|
|
if (isset($row['cc']) && $row['cc'] !== null) {
|
|
$row['cc'] = json_decode($row['cc'], true);
|
|
}
|
|
if (isset($row['object']) && $row['object'] !== null) {
|
|
$decoded = json_decode($row['object'], true);
|
|
// Only use decoded value if it's an array/object
|
|
if (is_array($decoded)) {
|
|
$row['object'] = $decoded;
|
|
}
|
|
}
|
|
if (isset($row['published']) && $row['published'] !== null) {
|
|
// If it's numeric, keep as int. If it's a string, try to parse as ISO 8601.
|
|
if (!is_numeric($row['published'])) {
|
|
// Try to parse as datetime string
|
|
$timestamp = strtotime($row['published']);
|
|
$row['published'] = $timestamp !== false ? $timestamp : null;
|
|
}
|
|
}
|
|
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($row);
|
|
if ($activity !== false) {
|
|
$posts[] = $activity;
|
|
}
|
|
}
|
|
$stmt->close();
|
|
return $posts;
|
|
}
|
|
|
|
/**
|
|
* Save a post (insert or update)
|
|
*
|
|
* @param \mysqli $dbh
|
|
* @param string $userId
|
|
* @param \Federator\Data\ActivityPub\Common\Activity $post
|
|
* @param string|null $articleId the original id of the article
|
|
* (used to identify the source article in the remote system)
|
|
* @return bool
|
|
*/
|
|
public static function savePost($dbh, $userId, $post, $articleId = null)
|
|
{
|
|
$sql = 'INSERT INTO posts (
|
|
`id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`, `article_id`
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
`url` = VALUES(`url`),
|
|
`user_id` = VALUES(`user_id`),
|
|
`actor` = VALUES(`actor`),
|
|
`type` = VALUES(`type`),
|
|
`object` = VALUES(`object`),
|
|
`to` = VALUES(`to`),
|
|
`cc` = VALUES(`cc`),
|
|
`published` = VALUES(`published`),
|
|
`article_id` = VALUES(`article_id`)';
|
|
$stmt = $dbh->prepare($sql);
|
|
if ($stmt === false) {
|
|
throw new \Federator\Exceptions\ServerError();
|
|
}
|
|
|
|
$id = $post->getID();
|
|
$url = $post->getUrl();
|
|
$actor = $post->getAActor();
|
|
$type = $post->getType();
|
|
$object = $post->getObject();
|
|
$objectJson = ($object instanceof \Federator\Data\ActivityPub\Common\APObject)
|
|
? json_encode($object)
|
|
: $object;
|
|
if ($objectJson === false) {
|
|
$objectJson = null;
|
|
}
|
|
if (is_object($object)) {
|
|
$id = $object->getID();
|
|
}
|
|
$to = $post->getTo();
|
|
$cc = $post->getCC();
|
|
$toJson = is_array($to) ? json_encode($to) : (is_string($to) ? json_encode([$to]) : null);
|
|
$ccJson = is_array($cc) ? json_encode($cc) : (is_string($cc) ? json_encode([$cc]) : null);
|
|
$published = $post->getPublished();
|
|
$publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
|
|
|
|
$stmt->bind_param(
|
|
"ssssssssss",
|
|
$id,
|
|
$url,
|
|
$userId,
|
|
$actor,
|
|
$type,
|
|
$objectJson,
|
|
$toJson,
|
|
$ccJson,
|
|
$publishedStr,
|
|
$articleId,
|
|
);
|
|
$result = $stmt->execute();
|
|
$stmt->close();
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Delete a post
|
|
*
|
|
* @param \mysqli $dbh
|
|
* @param string $id The post ID
|
|
* @return bool
|
|
*/
|
|
public static function deletePost($dbh, $id)
|
|
{
|
|
$sql = 'delete from posts where id = ?';
|
|
$stmt = $dbh->prepare($sql);
|
|
if ($stmt === false) {
|
|
throw new \Federator\Exceptions\ServerError();
|
|
}
|
|
$stmt->bind_param("s", $id);
|
|
$stmt->execute();
|
|
$affectedRows = $stmt->affected_rows;
|
|
$stmt->close();
|
|
return $affectedRows > 0;
|
|
}
|
|
|
|
/** retrieve original article id of post
|
|
*
|
|
* @param \mysqli $dbh
|
|
* @param \Federator\Data\ActivityPub\Common\Activity $post
|
|
* @return string|null
|
|
*/
|
|
public static function getOriginalArticleId($dbh, $post)
|
|
{
|
|
$sql = 'SELECT `article_id` FROM posts WHERE id = ?';
|
|
$stmt = $dbh->prepare($sql);
|
|
if ($stmt === false) {
|
|
throw new \Federator\Exceptions\ServerError();
|
|
}
|
|
$id = $post->getID();
|
|
$object = $post->getObject();
|
|
if (is_object($object)) {
|
|
$inReplyTo = $object->getInReplyTo();
|
|
if ($inReplyTo !== "") {
|
|
$id = $inReplyTo; // Use inReplyTo as ID if it's a string
|
|
} else {
|
|
$id = $object->getObject();
|
|
}
|
|
} elseif (is_string($object)) {
|
|
$id = $object; // If object is a string, use it directly
|
|
}
|
|
$stmt->bind_param("s", $id);
|
|
$articleId = null;
|
|
$ret = $stmt->bind_result($articleId);
|
|
$stmt->execute();
|
|
if ($ret) {
|
|
$stmt->fetch();
|
|
}
|
|
$stmt->close();
|
|
return $articleId;
|
|
}
|
|
}
|