federator/php/federator/dio/posts.php
Yannis Vogel d355b5a7cd
integrate queue for NewContent
- also integrated better support for newContent types
- Integrated saving posts to database (posts gotten via outbox request as well as posts received in the NewContent endpoint)
- proper support for handling comments
- support for likes/dislikes
- support for requesting followers / following endpoints
- better inbox support

database needs changes, don't forget to run migration
2025-05-20 16:34:50 +02:00

230 lines
7.2 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 string $min
* minimum date
* @param string $max
* maximum date
* @return \Federator\Data\ActivityPub\Common\Activity[]
*/
public static function getPostsByUser($dbh, $userid, $connector, $cache, $min, $max)
{
// ask cache
if ($cache !== null) {
$posts = $cache->getRemotePostsByUser($userid, $min, $max);
if ($posts !== false) {
return $posts;
}
}
$posts = self::getPostsFromDb($dbh, $userid, $min, $max);
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) {
$publishedStr = gmdate('Y-m-d H:i:s', $published);
if ($latestPublished === null || $publishedStr > $latestPublished) {
$latestPublished = $publishedStr;
}
}
}
if ($latestPublished !== null) {
$remoteMin = $latestPublished;
}
}
// Always fetch newer posts from connector (if any)
$newPosts = $connector->getRemotePostsByUser($userid, $remoteMin, $max);
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()])) {
$posts[] = $newPost;
}
}
}
// save posts to DB
foreach ($posts as $post) {
if ($post->getID() !== "") {
self::savePost($dbh, $userid, $post);
}
}
if ($cache !== null) {
$cache->saveRemotePostsByUser($userid, $posts);
}
return $posts;
}
/**
* Get posts for a user from the DB (optionally by date)
*
* @param \mysqli $dbh
* @param string $userId
* @param string|null $min
* @param string|null $max
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
*/
public static function getPostsFromDb($dbh, $userId, $min = null, $max = null)
{
$sql = 'SELECT id, user_id, type, object, published FROM posts WHERE user_id = ?';
$params = [$userId];
$types = 's';
if ($min !== null) {
$sql .= ' AND published >= ?';
$params[] = $min;
$types .= 's';
}
if ($max !== null) {
$sql .= ' AND published <= ?';
$params[] = $max;
$types .= 's';
}
$sql .= ' ORDER BY published DESC';
$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 (!empty($row['object'])) {
$objectData = json_decode($row['object'], true);
if (is_array($objectData)) {
// Use the ActivityPub factory to create the APObject
$object = \Federator\Data\ActivityPub\Factory::newActivityFromJson($objectData);
if ($object !== false) {
$posts[] = $object;
}
}
}
}
$stmt->close();
return $posts;
}
/**
* Save a post (insert or update)
*
* @param \mysqli $dbh
* @param string $userId
* @param \Federator\Data\ActivityPub\Common\Activity $post
* @return bool
*/
public static function savePost($dbh, $userId, $post)
{
$sql = 'INSERT INTO posts (
`id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`
) 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`)';
$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;
}
$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(
"sssssssss",
$id,
$url,
$userId,
$actor,
$type,
$objectJson,
$toJson,
$ccJson,
$publishedStr
);
$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;
}
}