converted from date field to timestamp

fixed reloading logic on paging
This commit is contained in:
Sascha Nitsch 2025-06-11 03:20:35 +02:00
parent 474631dff2
commit 4cc9cfdc8c
7 changed files with 89 additions and 73 deletions

View file

@ -56,34 +56,33 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
// get posts from user // get posts from user
$outbox = new \Federator\Data\ActivityPub\Common\Outbox(); $outbox = new \Federator\Data\ActivityPub\Common\Outbox();
$min = $this->main->extractFromURI("min", ""); $min = intval($this->main->extractFromURI('min', '0'), 10);
$max = $this->main->extractFromURI("max", ""); $max = intval($this->main->extractFromURI('max', '0'), 10);
$page = $this->main->extractFromURI("page", ""); $page = $this->main->extractFromURI("page", "");
if ($page !== "") { if ($page !== "") {
$items = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max); $items = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max, 20);
$outbox->setItems($items); $outbox->setItems($items);
} else { } else {
$tmpitems = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max, 99999);
$outbox->setTotalItems(sizeof($tmpitems));
$items = []; $items = [];
} }
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$id = 'https://' . $domain . '/users/' . $_user . '/outbox'; $id = 'https://' . $domain . '/' . $_user . '/outbox';
$outbox->setPartOf($id); $outbox->setPartOf($id);
$outbox->setID($id); $outbox->setID($id);
if ($page !== '') { if ($page === '') {
$id .= '?page=' . urlencode($page);
} else {
$outbox->setType('OrderedCollection'); $outbox->setType('OrderedCollection');
} }
if ($page === '' || $outbox->count() == 0) { if ($page === '' || $outbox->getCount() == 0) {
$outbox->setFirst($id . '?page=0'); $outbox->setFirst($id . '?page=true');
$outbox->setLast($id . '&min=0');
} }
if (sizeof($items) > 0) { if (sizeof($items) > 0) {
$newestId = $items[0]->getPublished(); $oldestTS = $items[0]->getPublished();
$oldestId = $items[sizeof($items) - 1]->getPublished(); $newestTS = $items[sizeof($items) - 1]->getPublished();
$outbox->setNext($id . '&max=' . $newestId); $outbox->setNext($id . '?page=true&max=' . $newestTS);
$outbox->setPrev($id . '&min=' . $oldestId); $outbox->setPrev($id . '?page=true&min=' . $oldestTS);
} }
$obj = $outbox->toObject(); $obj = $outbox->toObject();
return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);

View file

@ -35,10 +35,13 @@ interface Cache extends \Federator\Connector\Connector
* save remote posts by user * save remote posts by user
* *
* @param string $user user name * @param string $user user name
* @param int $min min timestamp
* @param int $max max timestamp
* @param int $limit limit results
* @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts * @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts
* @return void * @return void
*/ */
public function saveRemotePostsByUser($user, $posts); public function saveRemotePostsByUser($user, $min, $max, $limit, $posts);
/** /**
* save remote stats * save remote stats

View file

@ -35,12 +35,13 @@ interface Connector
* get posts by given user * get posts by given user
* *
* @param string $id user id * @param string $id user id
* @param string $min min date * @param int $min min value
* @param string $max max date * @param int $max max value
* @param int $limit maximum number of results
* @return \Federator\Data\ActivityPub\Common\Activity[]|false * @return \Federator\Data\ActivityPub\Common\Activity[]|false
*/ */
public function getRemotePostsByUser($id, $min, $max); public function getRemotePostsByUser($id, $min, $max, $limit);
/** /**
* get remote user by given name * get remote user by given name

View file

@ -24,22 +24,24 @@ class Posts
* connector to fetch use with * connector to fetch use with
* @param \Federator\Cache\Cache|null $cache * @param \Federator\Cache\Cache|null $cache
* optional caching service * optional caching service
* @param string $min * @param int $min
* minimum date * minimum timestamp
* @param string $max * @param int $max
* maximum date * maximum timestamp
* @param int $limit
* maximum number of results
* @return \Federator\Data\ActivityPub\Common\Activity[] * @return \Federator\Data\ActivityPub\Common\Activity[]
*/ */
public static function getPostsByUser($dbh, $userid, $connector, $cache, $min, $max) public static function getPostsByUser($dbh, $userid, $connector, $cache, $min, $max, $limit)
{ {
// ask cache // ask cache
if ($cache !== null) { if ($cache !== null) {
$posts = $cache->getRemotePostsByUser($userid, $min, $max); $posts = $cache->getRemotePostsByUser($userid, $min, $max, $limit);
if ($posts !== false) { if ($posts !== false) {
return $posts; return $posts;
} }
} }
$posts = self::getPostsFromDb($dbh, $userid, $min, $max); $posts = self::getPostsFromDb($dbh, $userid, $min, $max, $limit);
if ($posts === false) { if ($posts === false) {
$posts = []; $posts = [];
} }
@ -52,9 +54,8 @@ class Posts
foreach ($posts as $post) { foreach ($posts as $post) {
$published = $post->getPublished(); $published = $post->getPublished();
if ($published != null) { if ($published != null) {
$publishedStr = gmdate('Y-m-d H:i:s', $published); if ($latestPublished === null || $published > $latestPublished) {
if ($latestPublished === null || $publishedStr > $latestPublished) { $latestPublished = $published;
$latestPublished = $publishedStr;
} }
} }
} }
@ -63,17 +64,24 @@ class Posts
} }
} }
// Always fetch newer posts from connector (if any) // Fetch newer posts from connector (if any) if max is not set and limit not reached
$newPosts = $connector->getRemotePostsByUser($userid, $remoteMin, $max); if ($max == 0 && sizeof($posts) < $limit) {
if ($newPosts !== false && is_array($newPosts)) { $newPosts = $connector->getRemotePostsByUser($userid, $remoteMin, $max, $limit);
// Merge new posts with DB posts, avoiding duplicates by ID if ($newPosts !== false && is_array($newPosts)) {
$existingIds = []; // Merge new posts with DB posts, avoiding duplicates by ID
foreach ($posts as $post) { $existingIds = [];
$existingIds[$post->getID()] = true; foreach ($posts as $post) {
} $existingIds[$post->getID()] = true;
foreach ($newPosts as $newPost) { }
if (!isset($existingIds[$newPost->getID()])) { foreach ($newPosts as $newPost) {
$posts[] = $newPost; if (!isset($existingIds[$newPost->getID()])) {
if ($newPost->getID() !== "") {
self::savePost($dbh, $userid, $newPost);
}
if (sizeof($posts) < $limit) {
$posts[] = $newPost;
}
}
} }
} }
} }
@ -95,11 +103,8 @@ class Posts
$originUrl = 'localhost'; // Fallback to localhost if no origin is set $originUrl = 'localhost'; // Fallback to localhost if no origin is set
} }
// save posts to DB // optionally convert from article to note
foreach ($posts as $post) { foreach ($posts as $post) {
if ($post->getID() !== "") {
self::savePost($dbh, $userid, $post);
}
switch (strtolower($post->getType())) { switch (strtolower($post->getType())) {
case 'undo': case 'undo':
$object = $post->getObject(); $object = $post->getObject();
@ -134,7 +139,7 @@ class Posts
} }
if ($cache !== null) { if ($cache !== null) {
$cache->saveRemotePostsByUser($userid, $posts); $cache->saveRemotePostsByUser($userid, $min, $max, $limit, $posts);
} }
return $posts; return $posts;
} }
@ -144,26 +149,27 @@ class Posts
* *
* @param \mysqli $dbh * @param \mysqli $dbh
* @param string $userId * @param string $userId
* @param string|null $min * @param int $min min timestamp
* @param string|null $max * @param int $max max timestamp
* @param int $limit
* @return \Federator\Data\ActivityPub\Common\Activity[]|false * @return \Federator\Data\ActivityPub\Common\Activity[]|false
*/ */
public static function getPostsFromDb($dbh, $userId, $min = null, $max = null) public static function getPostsFromDb($dbh, $userId, $min, $max, $limit = 20)
{ {
$sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published` FROM posts WHERE user_id = ?'; $sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, unix_timestamp(`published`) as published FROM posts WHERE user_id = ?';
$params = [$userId]; $params = [$userId];
$types = 's'; $types = 's';
if ($min !== null && $min !== "") { if ($min > 0) {
$sql .= ' AND published >= ?'; $sql .= ' AND published >= from_unixtime(?)';
$params[] = $min; $params[] = $min;
$types .= 's'; $types .= 's';
} }
if ($max !== null && $max !== "") { if ($max > 0) {
$sql .= ' AND published <= ?'; $sql .= ' AND published <= from_unixtime(?)';
$params[] = $max; $params[] = $max;
$types .= 's'; $types .= 's';
} }
$sql .= ' ORDER BY published DESC'; $sql .= ' ORDER BY published DESC LIMIT ' . $limit;
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
@ -193,9 +199,7 @@ class Posts
} }
if (isset($row['published']) && $row['published'] !== null) { 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 it's numeric, keep as int. If it's a string, try to parse as ISO 8601.
if (is_numeric($row['published'])) { if (!is_numeric($row['published'])) {
$row['published'] = intval($row['published'], 10);
} else {
// Try to parse as datetime string // Try to parse as datetime string
$timestamp = strtotime($row['published']); $timestamp = strtotime($row['published']);
$row['published'] = $timestamp !== false ? $timestamp : null; $row['published'] = $timestamp !== false ? $timestamp : null;

View file

@ -109,21 +109,23 @@ class ContentNation implements Connector
* get posts by given user * get posts by given user
* *
* @param string $userId user id * @param string $userId user id
* @param string $min min date * @param int $min min date
* @param string $max max date * @param int $max max date
* @param int $limit limit results
* @unused-param $limit
* @return \Federator\Data\ActivityPub\Common\Activity[]|false * @return \Federator\Data\ActivityPub\Common\Activity[]|false
*/ */
public function getRemotePostsByUser($userId, $min, $max) public function getRemotePostsByUser($userId, $min, $max, $limit)
{ {
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) { if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
$userId = $matches[1]; $userId = $matches[1];
} }
$remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/activities'; $remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/activities';
if ($min !== '') { if ($min > 0) {
$remoteURL .= '&minTS=' . urlencode($min); $remoteURL .= '&minTS=' . intval($min, 10);
} }
if ($max !== '') { if ($max > 0) {
$remoteURL .= '&maxTS=' . urlencode($max); $remoteURL .= '&maxTS=' . intval($max, 10);
} }
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, []); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
@ -148,12 +150,11 @@ class ContentNation implements Connector
case 'Article': case 'Article':
$create = new \Federator\Data\ActivityPub\Common\Create(); $create = new \Federator\Data\ActivityPub\Common\Create();
$create->setAActor($ourUrl . '/' . $userId); $create->setAActor($ourUrl . '/' . $userId);
$create->setID($activity['id']) $create->setPublished($activity['published'] ?? $activity['timestamp'])
->setPublished($activity['published'] ?? $activity['timestamp'])
->addTo($ourUrl . '/' . $userId . '/followers') ->addTo($ourUrl . '/' . $userId . '/followers')
->addCC("https://www.w3.org/ns/activitystreams#Public"); ->addCC("https://www.w3.org/ns/activitystreams#Public");
$create->setURL($ourUrl . '/' . $activity['profilename'] . '/' . $activity['name']); $create->setURL($ourUrl . '/' . $activity['profilename'] . '/' . $activity['name']);
$create->setID($ourUrl . '/' . $activity['profilename'] . '/' . $activity['id']); $create->setID($ourUrl . '/' . $activity['profilename'] . '/' . $activity['name']);
$apArticle = new \Federator\Data\ActivityPub\Common\Article(); $apArticle = new \Federator\Data\ActivityPub\Common\Article();
if (array_key_exists('tags', $activity)) { if (array_key_exists('tags', $activity)) {
foreach ($activity['tags'] as $tag) { foreach ($activity['tags'] as $tag) {

View file

@ -46,11 +46,12 @@ class DummyConnector implements Connector
* get posts by given user * get posts by given user
* *
* @param string $id user id @unused-param * @param string $id user id @unused-param
* @param string $min min date @unused-param * @param int $min min timestamp @unused-param
* @param string $max max date @unused-param * @param int $max max timestamp @unused-param
* @param int $limit limit number of results @unused-param
* @return \Federator\Data\ActivityPub\Common\Activity[]|false * @return \Federator\Data\ActivityPub\Common\Activity[]|false
*/ */
public function getRemotePostsByUser($id, $min, $max) public function getRemotePostsByUser($id, $min, $max, $limit)
{ {
return false; return false;
} }

View file

@ -139,12 +139,13 @@ class RedisCache implements Cache
* get posts by given user * get posts by given user
* *
* @param string $id user id @unused-param * @param string $id user id @unused-param
* @param string $min min date @unused-param * @param int $min min timestamp @unused-param
* @param string $max max date @unused-param * @param int $max max timestamp @unused-param
* @param int $limit limit results @unused-param
* @return \Federator\Data\ActivityPub\Common\Activity[]|false * @return \Federator\Data\ActivityPub\Common\Activity[]|false
*/ */
public function getRemotePostsByUser($id, $min, $max) public function getRemotePostsByUser($id, $min, $max, $limit)
{ {
error_log("rediscache::getRemotePostsByUser not implemented"); error_log("rediscache::getRemotePostsByUser not implemented");
return false; return false;
@ -273,10 +274,13 @@ class RedisCache implements Cache
* save remote posts by user * save remote posts by user
* *
* @param string $user user name @unused-param * @param string $user user name @unused-param
* @param int $min min timestamp @unused-param
* @param int $max max timestamp @unused-param
* @param int $limit limit results @unused-param
* @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts @unused-param * @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts @unused-param
* @return void * @return void
*/ */
public function saveRemotePostsByUser($user, $posts) public function saveRemotePostsByUser($user, $min, $max, $limit, $posts)
{ {
error_log("rediscache::saveRemotePostsByUser not implemented"); error_log("rediscache::saveRemotePostsByUser not implemented");
} }
@ -305,6 +309,9 @@ class RedisCache implements Cache
*/ */
public function saveRemoteUserByName($_name, $user) public function saveRemoteUserByName($_name, $user)
{ {
if (!$this->connected) {
$this->connect();
}
$key = self::createKey('u', $_name); $key = self::createKey('u', $_name);
$serialized = $user->toJson(); $serialized = $user->toJson();
$this->redis->setEx($key, $this->userTTL, $serialized); $this->redis->setEx($key, $this->userTTL, $serialized);