federator/php/federator/dio/posts.php
Sascha Nitsch 4cc9cfdc8c converted from date field to timestamp
fixed reloading logic on paging
2025-06-11 03:21:19 +02:00

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