Merge pull request 'Bluesky: New option to complete threads' (#1556) from heluecht/friendica-addons:bluesky-complete-threads into develop

Reviewed-on: https://git.friendi.ca/friendica/friendica-addons/pulls/1556
This commit is contained in:
Tobias Diekershoff 2024-10-06 15:07:14 +02:00
commit feb7722f72
6 changed files with 235 additions and 161 deletions

View file

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Name: Bluesky Connector * Name: Bluesky Connector
* Description: Post to Bluesky * Description: Post to Bluesky, import timelines and feeds
* Version: 1.1 * Version: 1.1
* Author: Michael Vogel <https://pirati.ca/profile/heluecht> * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
* *
@ -10,6 +10,8 @@
* - Outgoing mentions * - Outgoing mentions
* *
* At some point in time: * At some point in time:
* - post videos
* - direct messages
* - Sending Quote shares https://atproto.com/lexicons/app-bsky-embed#appbskyembedrecord and https://atproto.com/lexicons/app-bsky-embed#appbskyembedrecordwithmedia * - Sending Quote shares https://atproto.com/lexicons/app-bsky-embed#appbskyembedrecord and https://atproto.com/lexicons/app-bsky-embed#appbskyembedrecordwithmedia
* *
* Possibly not possible: * Possibly not possible:
@ -151,7 +153,7 @@ function bluesky_probe_detect(array &$hookData)
} }
$data = bluesky_xrpc_get($pconfig['uid'], 'app.bsky.actor.getProfile', ['actor' => $did]); $data = bluesky_xrpc_get($pconfig['uid'], 'app.bsky.actor.getProfile', ['actor' => $did]);
if (empty($data)) { if (empty($data) || empty($data->did)) {
return; return;
} }
@ -346,15 +348,16 @@ function bluesky_settings(array &$data)
return; return;
} }
$enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post') ?? false; $enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post') ?? false;
$def_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default') ?? false; $def_enabled = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default') ?? false;
$pds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'pds'); $pds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'pds');
$handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle'); $handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle');
$did = bluesky_get_user_did(DI::userSession()->getLocalUserId()); $did = bluesky_get_user_did(DI::userSession()->getLocalUserId());
$token = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'access_token'); $token = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'access_token');
$import = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import') ?? false; $import = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import') ?? false;
$import_feeds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import_feeds') ?? false; $import_feeds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'import_feeds') ?? false;
$custom_handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'friendica_handle') ?? false; $complete_threads = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'complete_threads') ?? false;
$custom_handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'friendica_handle') ?? false;
if (DI::config()->get('bluesky', 'friendica_handles')) { if (DI::config()->get('bluesky', 'friendica_handles')) {
$self = User::getById(DI::userSession()->getLocalUserId(), ['nickname']); $self = User::getById(DI::userSession()->getLocalUserId(), ['nickname']);
@ -369,16 +372,17 @@ function bluesky_settings(array &$data)
$t = Renderer::getMarkupTemplate('connector_settings.tpl', 'addon/bluesky/'); $t = Renderer::getMarkupTemplate('connector_settings.tpl', 'addon/bluesky/');
$html = Renderer::replaceMacros($t, [ $html = Renderer::replaceMacros($t, [
'$enable' => ['bluesky', DI::l10n()->t('Enable Bluesky Post Addon'), $enabled], '$enable' => ['bluesky', DI::l10n()->t('Enable Bluesky Post Addon'), $enabled],
'$bydefault' => ['bluesky_bydefault', DI::l10n()->t('Post to Bluesky by default'), $def_enabled], '$bydefault' => ['bluesky_bydefault', DI::l10n()->t('Post to Bluesky by default'), $def_enabled],
'$import' => ['bluesky_import', DI::l10n()->t('Import the remote timeline'), $import], '$import' => ['bluesky_import', DI::l10n()->t('Import the remote timeline'), $import],
'$import_feeds' => ['bluesky_import_feeds', DI::l10n()->t('Import the pinned feeds'), $import_feeds, DI::l10n()->t('When activated, Posts will be imported from all the feeds that you pinned in Bluesky.')], '$import_feeds' => ['bluesky_import_feeds', DI::l10n()->t('Import the pinned feeds'), $import_feeds, DI::l10n()->t('When activated, Posts will be imported from all the feeds that you pinned in Bluesky.')],
'$custom_handle' => $friendica_handle, '$complete_threads' => ['bluesky_complete_threads', DI::l10n()->t('Complete the threads'), $complete_threads, DI::l10n()->t('When activated, the system fetches additional replies for the posts in the timeline. This leads to more complete threads.')],
'$pds' => ['bluesky_pds', DI::l10n()->t('Personal Data Server'), $pds, DI::l10n()->t('The personal data server (PDS) is the system that hosts your profile.'), '', 'readonly'], '$custom_handle' => $friendica_handle,
'$handle' => ['bluesky_handle', DI::l10n()->t('Bluesky handle'), $handle, '', '', $custom_handle ? 'readonly' : ''], '$pds' => ['bluesky_pds', DI::l10n()->t('Personal Data Server'), $pds, DI::l10n()->t('The personal data server (PDS) is the system that hosts your profile.'), '', 'readonly'],
'$did' => ['bluesky_did', DI::l10n()->t('Bluesky DID'), $did, DI::l10n()->t('This is the unique identifier. It will be fetched automatically, when the handle is entered.'), '', 'readonly'], '$handle' => ['bluesky_handle', DI::l10n()->t('Bluesky handle'), $handle, '', '', $custom_handle ? 'readonly' : ''],
'$password' => ['bluesky_password', DI::l10n()->t('Bluesky app password'), '', DI::l10n()->t("Please don't add your real password here, but instead create a specific app password in the Bluesky settings.")], '$did' => ['bluesky_did', DI::l10n()->t('Bluesky DID'), $did, DI::l10n()->t('This is the unique identifier. It will be fetched automatically, when the handle is entered.'), '', 'readonly'],
'$status' => bluesky_get_status($handle, $did, $pds, $token), '$password' => ['bluesky_password', DI::l10n()->t('Bluesky app password'), '', DI::l10n()->t("Please don't add your real password here, but instead create a specific app password in the Bluesky settings.")],
'$status' => bluesky_get_status($handle, $did, $pds, $token),
]); ]);
$data = [ $data = [
@ -446,6 +450,7 @@ function bluesky_settings_post(array &$b)
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'handle', $handle); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'handle', $handle);
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'import', intval($_POST['bluesky_import'])); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'import', intval($_POST['bluesky_import']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'import_feeds', intval($_POST['bluesky_import_feeds'])); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'import_feeds', intval($_POST['bluesky_import_feeds']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'complete_threads', intval($_POST['bluesky_complete_threads']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'friendica_handle', intval($_POST['bluesky_friendica_handle'] ?? false)); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'friendica_handle', intval($_POST['bluesky_friendica_handle'] ?? false));
if (!empty($handle)) { if (!empty($handle)) {
@ -534,15 +539,15 @@ function bluesky_cron()
Logger::debug('Refresh the token', ['uid' => $pconfig['uid']]); Logger::debug('Refresh the token', ['uid' => $pconfig['uid']]);
bluesky_get_token($pconfig['uid']); bluesky_get_token($pconfig['uid']);
Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_notifications.php', $pconfig['uid'], $last); Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_notifications.php', $pconfig['uid']);
if (DI::pConfig()->get($pconfig['uid'], 'bluesky', 'import')) { if (DI::pConfig()->get($pconfig['uid'], 'bluesky', 'import')) {
Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_timeline.php', $pconfig['uid'], $last); Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_timeline.php', $pconfig['uid']);
} }
if (DI::pConfig()->get($pconfig['uid'], 'bluesky', 'import_feeds')) { if (DI::pConfig()->get($pconfig['uid'], 'bluesky', 'import_feeds')) {
Logger::debug('Fetch feeds for user', ['uid' => $pconfig['uid']]); Logger::debug('Fetch feeds for user', ['uid' => $pconfig['uid']]);
$feeds = bluesky_get_feeds($pconfig['uid']); $feeds = bluesky_get_feeds($pconfig['uid']);
foreach ($feeds as $feed) { foreach ($feeds as $feed) {
Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_feed.php', $pconfig['uid'], $feed, $last); Worker::add(['priority' => Worker::PRIORITY_MEDIUM, 'force_priority' => true], 'addon/bluesky/bluesky_feed.php', $pconfig['uid'], $feed);
} }
} }
Logger::debug('Polling done for user', ['uid' => $pconfig['uid']]); Logger::debug('Polling done for user', ['uid' => $pconfig['uid']]);
@ -710,7 +715,7 @@ function bluesky_create_activity(array $item, stdClass $parent = null)
} }
$activity = bluesky_xrpc_post($uid, 'com.atproto.repo.createRecord', $post); $activity = bluesky_xrpc_post($uid, 'com.atproto.repo.createRecord', $post);
if (empty($activity)) { if (empty($activity->uri)) {
return; return;
} }
Logger::debug('Activity done', ['return' => $activity]); Logger::debug('Activity done', ['return' => $activity]);
@ -794,7 +799,7 @@ function bluesky_create_post(array $item, stdClass $root = null, stdClass $paren
]; ];
$parent = bluesky_xrpc_post($uid, 'com.atproto.repo.createRecord', $post); $parent = bluesky_xrpc_post($uid, 'com.atproto.repo.createRecord', $post);
if (empty($parent)) { if (empty($parent->uri)) {
if ($part == 0) { if ($part == 0) {
Worker::defer(); Worker::defer();
} }
@ -972,7 +977,7 @@ function bluesky_upload_blob(int $uid, array $photo): ?stdClass
Logger::info('Uploading', ['uid' => $uid, 'retrial' => $retrial, 'height' => $new_height, 'width' => $new_width, 'size' => $new_size, 'orig-height' => $height, 'orig-width' => $width, 'orig-size' => $size]); Logger::info('Uploading', ['uid' => $uid, 'retrial' => $retrial, 'height' => $new_height, 'width' => $new_width, 'size' => $new_size, 'orig-height' => $height, 'orig-width' => $width, 'orig-size' => $size]);
$data = bluesky_post($uid, '/xrpc/com.atproto.repo.uploadBlob', $content, ['Content-type' => $photo['type'], 'Authorization' => ['Bearer ' . bluesky_get_token($uid)]]); $data = bluesky_post($uid, '/xrpc/com.atproto.repo.uploadBlob', $content, ['Content-type' => $photo['type'], 'Authorization' => ['Bearer ' . bluesky_get_token($uid)]]);
if (empty($data)) { if (empty($data) || empty($data->blob)) {
Logger::info('Uploading failed', ['uid' => $uid, 'retrial' => $retrial, 'height' => $new_height, 'width' => $new_width, 'size' => $new_size, 'orig-height' => $height, 'orig-width' => $width, 'orig-size' => $size]); Logger::info('Uploading failed', ['uid' => $uid, 'retrial' => $retrial, 'height' => $new_height, 'width' => $new_width, 'size' => $new_size, 'orig-height' => $height, 'orig-width' => $width, 'orig-size' => $size]);
return null; return null;
} }
@ -993,7 +998,7 @@ function bluesky_delete_post(string $uri, int $uid)
Logger::debug('Deleted', ['parts' => $parts]); Logger::debug('Deleted', ['parts' => $parts]);
} }
function bluesky_fetch_timeline(int $uid, int $last_poll) function bluesky_fetch_timeline(int $uid)
{ {
$data = bluesky_xrpc_get($uid, 'app.bsky.feed.getTimeline'); $data = bluesky_xrpc_get($uid, 'app.bsky.feed.getTimeline');
if (empty($data)) { if (empty($data)) {
@ -1004,15 +1009,44 @@ function bluesky_fetch_timeline(int $uid, int $last_poll)
return; return;
} }
$last_post = DBA::selectFirst('post-thread-user', ['received'], ['network' => Protocol::BLUESKY, 'uid' => $uid], ['order' => ['received' => true]]);
$last_poll = !empty($last_post['received']) ? strtotime($last_post['received']) : 0;
foreach (array_reverse($data->feed) as $entry) { foreach (array_reverse($data->feed) as $entry) {
$causer = bluesky_get_contact($entry->post->author, 0, $uid);
if (!empty($entry->reply)) {
if (!empty($entry->reply->root)) {
bluesky_complete_post($entry->reply->root, $uid, Item::PR_COMMENT, $causer['id'], $last_poll);
}
if (!empty($entry->reply->parent)) {
bluesky_complete_post($entry->reply->parent, $uid, Item::PR_COMMENT, $causer['id'], $last_poll);
}
}
bluesky_process_post($entry->post, $uid, $uid, Item::PR_NONE, 0, 0, $last_poll); bluesky_process_post($entry->post, $uid, $uid, Item::PR_NONE, 0, 0, $last_poll);
if (!empty($entry->reason)) { if (!empty($entry->reason)) {
bluesky_process_reason($entry->reason, bluesky_get_uri($entry->post), $uid); bluesky_process_reason($entry->reason, bluesky_get_uri($entry->post), $uid);
} }
} }
}
// @todo Support paging function bluesky_complete_post(stdClass $post, int $uid, int $post_reason, int $causer, int $last_poll): int
// [cursor] => 1684670516000::bafyreidq3ilwslmlx72jf5vrk367xcc63s6lrhzlyup2bi3zwcvso6w2vi {
$complete = DI::pConfig()->get($uid, 'bluesky', 'complete_threads');
$existing_uri = bluesky_fetch_post(bluesky_get_uri($post), $uid);
if (!empty($existing_uri)) {
$comments = Post::countPosts(['thr-parent' => $existing_uri, 'gravity' => Item::GRAVITY_COMMENT]);
if (($post->replyCount <= $comments) || !$complete) {
return bluesky_fetch_uri_id($existing_uri, $uid);
}
}
if ($complete) {
$uri = bluesky_fetch_missing_post($post->uri, $uid, $uid, $post_reason, $causer, 0, $last_poll, '', true);
$uri_id = bluesky_fetch_uri_id($uri, $uid);
} else {
$uri_id = bluesky_process_post($post, $uid, $uid, $post_reason, $causer, 0, $last_poll);
}
return $uri_id;
} }
function bluesky_process_reason(stdClass $reason, string $uri, int $uid) function bluesky_process_reason(stdClass $reason, string $uri, int $uid)
@ -1056,12 +1090,16 @@ function bluesky_process_reason(stdClass $reason, string $uri, int $uid)
} }
} }
function bluesky_fetch_notifications(int $uid, int $last_poll) function bluesky_fetch_notifications(int $uid)
{ {
$data = bluesky_xrpc_get($uid, 'app.bsky.notification.listNotifications'); $data = bluesky_xrpc_get($uid, 'app.bsky.notification.listNotifications');
if (empty($data->notifications)) { if (empty($data->notifications)) {
return; return;
} }
$last_post = DBA::selectFirst('post-thread-user', ['received'], ['network' => Protocol::BLUESKY, 'uid' => $uid], ['order' => ['received' => true]]);
$last_poll = !empty($last_post['received']) ? strtotime($last_post['received']) : 0;
foreach ($data->notifications as $notification) { foreach ($data->notifications as $notification) {
$uri = bluesky_get_uri($notification); $uri = bluesky_get_uri($notification);
if (Post::exists(['uri' => $uri, 'uid' => $uid]) || Post::exists(['extid' => $uri, 'uid' => $uid])) { if (Post::exists(['uri' => $uri, 'uid' => $uid]) || Post::exists(['extid' => $uri, 'uid' => $uid])) {
@ -1071,7 +1109,7 @@ function bluesky_fetch_notifications(int $uid, int $last_poll)
Logger::debug('Process notification', ['uid' => $uid, 'reason' => $notification->reason, 'uri' => $uri, 'indexedAt' => $notification->indexedAt]); Logger::debug('Process notification', ['uid' => $uid, 'reason' => $notification->reason, 'uri' => $uri, 'indexedAt' => $notification->indexedAt]);
switch ($notification->reason) { switch ($notification->reason) {
case 'like': case 'like':
$item = bluesky_get_header($notification, $uri, $uid, $uid); $item = bluesky_get_header($notification, $uri, $uid, $uid, $last_poll);
$item['gravity'] = Item::GRAVITY_ACTIVITY; $item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['body'] = $item['verb'] = Activity::LIKE; $item['body'] = $item['verb'] = Activity::LIKE;
$item['thr-parent'] = bluesky_get_uri($notification->record->subject); $item['thr-parent'] = bluesky_get_uri($notification->record->subject);
@ -1085,7 +1123,7 @@ function bluesky_fetch_notifications(int $uid, int $last_poll)
break; break;
case 'repost': case 'repost':
$item = bluesky_get_header($notification, $uri, $uid, $uid); $item = bluesky_get_header($notification, $uri, $uid, $uid, $last_poll);
$item['gravity'] = Item::GRAVITY_ACTIVITY; $item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['body'] = $item['verb'] = Activity::ANNOUNCE; $item['body'] = $item['verb'] = Activity::ANNOUNCE;
$item['thr-parent'] = bluesky_get_uri($notification->record->subject); $item['thr-parent'] = bluesky_get_uri($notification->record->subject);
@ -1128,7 +1166,7 @@ function bluesky_fetch_notifications(int $uid, int $last_poll)
} }
} }
function bluesky_fetch_feed(int $uid, string $feed, int $last_poll) function bluesky_fetch_feed(int $uid, string $feed)
{ {
$data = bluesky_xrpc_get($uid, 'app.bsky.feed.getFeed', ['feed' => $feed]); $data = bluesky_xrpc_get($uid, 'app.bsky.feed.getFeed', ['feed' => $feed]);
if (empty($data)) { if (empty($data)) {
@ -1139,8 +1177,11 @@ function bluesky_fetch_feed(int $uid, string $feed, int $last_poll)
return; return;
} }
$last_post = DBA::selectFirst('post-thread-user', ['received'], ['network' => Protocol::BLUESKY, 'uid' => $uid], ['order' => ['received' => true]]);
$last_poll = !empty($last_post['received']) ? strtotime($last_post['received']) : 0;
$feeddata = bluesky_xrpc_get($uid, 'app.bsky.feed.getFeedGenerator', ['feed' => $feed]); $feeddata = bluesky_xrpc_get($uid, 'app.bsky.feed.getFeedGenerator', ['feed' => $feed]);
if (!empty($feeddata)) { if (!empty($feeddata) && !empty($feeddata->view)) {
$feedurl = $feeddata->view->uri; $feedurl = $feeddata->view->uri;
$feedname = $feeddata->view->displayName; $feedname = $feeddata->view->displayName;
} else { } else {
@ -1153,10 +1194,11 @@ function bluesky_fetch_feed(int $uid, string $feed, int $last_poll)
$languages = $entry->post->record->langs ?? []; $languages = $entry->post->record->langs ?? [];
if (!Relay::isWantedLanguage($entry->post->record->text, 0, $contact['id'] ?? 0, $languages)) { if (!Relay::isWantedLanguage($entry->post->record->text, 0, $contact['id'] ?? 0, $languages)) {
Logger::debug('Unwanted language detected', ['text' => $entry->post->record->text]); Logger::debug('Unwanted language detected', ['languages' => $languages, 'text' => $entry->post->record->text]);
continue; continue;
} }
$uri_id = bluesky_process_post($entry->post, $uid, $uid, Item::PR_TAG, 0, 0, $last_poll); $causer = bluesky_get_contact($entry->post->author, 0, $uid);
$uri_id = bluesky_complete_post($entry->post, $uid, Item::PR_TAG, $causer['id'], $last_poll);
if (!empty($uri_id)) { if (!empty($uri_id)) {
$stored = Post\Category::storeFileByURIId($uri_id, $uid, Post\Category::SUBCRIPTION, $feedname, $feedurl); $stored = Post\Category::storeFileByURIId($uri_id, $uid, Post\Category::SUBCRIPTION, $feedname, $feedurl);
Logger::debug('Stored tag subscription for user', ['uri-id' => $uri_id, 'uid' => $uid, 'name' => $feedname, 'url' => $feedurl, 'stored' => $stored]); Logger::debug('Stored tag subscription for user', ['uri-id' => $uri_id, 'uid' => $uid, 'name' => $feedname, 'url' => $feedurl, 'stored' => $stored]);
@ -1184,7 +1226,7 @@ function bluesky_process_post(stdClass $post, int $uid, int $fetch_uid, int $pos
Logger::debug('Importing post', ['uid' => $uid, 'indexedAt' => $post->indexedAt, 'uri' => $post->uri, 'cid' => $post->cid, 'root' => $post->record->reply->root ?? '']); Logger::debug('Importing post', ['uid' => $uid, 'indexedAt' => $post->indexedAt, 'uri' => $post->uri, 'cid' => $post->cid, 'root' => $post->record->reply->root ?? '']);
$item = bluesky_get_header($post, $uri, $uid, $fetch_uid); $item = bluesky_get_header($post, $uri, $uid, $fetch_uid, $last_poll);
$item = bluesky_get_content($item, $post->record, $uri, $uid, $fetch_uid, $level, $last_poll); $item = bluesky_get_content($item, $post->record, $uri, $uid, $fetch_uid, $level, $last_poll);
if (empty($item)) { if (empty($item)) {
return 0; return 0;
@ -1208,7 +1250,7 @@ function bluesky_process_post(stdClass $post, int $uid, int $fetch_uid, int $pos
return bluesky_fetch_uri_id($uri, $uid); return bluesky_fetch_uri_id($uri, $uid);
} }
function bluesky_get_header(stdClass $post, string $uri, int $uid, int $fetch_uid): array function bluesky_get_header(stdClass $post, string $uri, int $uid, int $fetch_uid, int $last_poll = 0): array
{ {
$parts = bluesky_get_uri_parts($uri); $parts = bluesky_get_uri_parts($uri);
if (empty($post->author) || empty($post->cid) || empty($parts->rkey)) { if (empty($post->author) || empty($post->cid) || empty($parts->rkey)) {
@ -1221,6 +1263,7 @@ function bluesky_get_header(stdClass $post, string $uri, int $uid, int $fetch_ui
'wall' => false, 'wall' => false,
'uri' => $uri, 'uri' => $uri,
'guid' => $post->cid, 'guid' => $post->cid,
'received' => DateTimeFormat::utc($post->indexedAt, DateTimeFormat::MYSQL),
'private' => Item::UNLISTED, 'private' => Item::UNLISTED,
'verb' => Activity::POST, 'verb' => Activity::POST,
'contact-id' => $contact['id'], 'contact-id' => $contact['id'],
@ -1231,6 +1274,10 @@ function bluesky_get_header(stdClass $post, string $uri, int $uid, int $fetch_ui
'source' => json_encode($post), 'source' => json_encode($post),
]; ];
if (($last_poll != 0) && strtotime($item['received']) < $last_poll) {
unset($item['received']);
}
$account = Contact::selectFirstAccountUser(['pid'], ['id' => $contact['id']]); $account = Contact::selectFirstAccountUser(['pid'], ['id' => $contact['id']]);
$item['author-id'] = $account['pid']; $item['author-id'] = $account['pid'];
@ -1329,10 +1376,6 @@ function bluesky_get_content(array $item, stdClass $record, string $uri, int $ui
$item['created'] = DateTimeFormat::utc($record->createdAt, DateTimeFormat::MYSQL); $item['created'] = DateTimeFormat::utc($record->createdAt, DateTimeFormat::MYSQL);
$item['transmitted-languages'] = $record->langs ?? []; $item['transmitted-languages'] = $record->langs ?? [];
if (($last_poll != 0) && strtotime($item['created']) > $last_poll) {
$item['received'] = $item['created'];
}
return $item; return $item;
} }
@ -1422,7 +1465,7 @@ function bluesky_add_media(stdClass $embed, array $item, int $fetch_uid, int $le
]; ];
Post\Media::insert($media); Post\Media::insert($media);
break; break;
case 'app.bsky.embed.external#view': case 'app.bsky.embed.external#view':
$media = [ $media = [
'uri-id' => $item['uri-id'], 'uri-id' => $item['uri-id'],
@ -1522,10 +1565,10 @@ function bluesky_get_uri_parts(string $uri): ?stdClass
return $class; return $class;
} }
function bluesky_fetch_missing_post(string $uri, int $uid, int $fetch_uid, int $post_reason, int $causer, int $level, int $last_poll, string $fallback = ''): string function bluesky_fetch_missing_post(string $uri, int $uid, int $fetch_uid, int $post_reason, int $causer, int $level, int $last_poll, string $fallback = '', bool $always_fetch = false): string
{ {
$fetched_uri = bluesky_fetch_post($uri, $uid); $fetched_uri = bluesky_fetch_post($uri, $uid);
if (!empty($fetched_uri)) { if (!$always_fetch && !empty($fetched_uri)) {
return $fetched_uri; return $fetched_uri;
} }
@ -1545,7 +1588,7 @@ function bluesky_fetch_missing_post(string $uri, int $uid, int $fetch_uid, int $
Logger::debug('Fetch missing post', ['level' => $level, 'uid' => $uid, 'uri' => $uri]); Logger::debug('Fetch missing post', ['level' => $level, 'uid' => $uid, 'uri' => $uri]);
$data = bluesky_xrpc_get($fetch_uid, 'app.bsky.feed.getPostThread', ['uri' => $fetch_uri]); $data = bluesky_xrpc_get($fetch_uid, 'app.bsky.feed.getPostThread', ['uri' => $fetch_uri]);
if (empty($data)) { if (empty($data) || empty($data->thread)) {
Logger::info('Thread was not fetched', ['level' => $level, 'uid' => $uid, 'uri' => $uri, 'fallback' => $fallback]); Logger::info('Thread was not fetched', ['level' => $level, 'uid' => $uid, 'uri' => $uri, 'fallback' => $fallback]);
return $fallback; return $fallback;
} }
@ -1556,9 +1599,31 @@ function bluesky_fetch_missing_post(string $uri, int $uid, int $fetch_uid, int $
$causer = Contact::getPublicContactId($causer, $uid); $causer = Contact::getPublicContactId($causer, $uid);
} }
if (!empty($data->thread->parent)) {
$parents = bluesky_fetch_parents($data->thread->parent, $uid);
foreach ($parents as $parent) {
$uri_id = bluesky_process_post($parent, $uid, $fetch_uid, Item::PR_FETCHED, $causer, $level, $last_poll);
Logger::debug('Parent created', ['uri-id' => $uri_id]);
}
}
return bluesky_process_thread($data->thread, $uid, $fetch_uid, $post_reason, $causer, $level, $last_poll); return bluesky_process_thread($data->thread, $uid, $fetch_uid, $post_reason, $causer, $level, $last_poll);
} }
function bluesky_fetch_parents(stdClass $parent, int $uid, array $parents = []): array
{
if (!empty($parent->parent)) {
$parents = bluesky_fetch_parents($parent->parent, $uid, $parents);
}
if (empty(bluesky_fetch_post(bluesky_get_uri($parent->post), $uid))) {
$parents[] = $parent->post;
}
return $parents;
}
function bluesky_fetch_post(string $uri, int $uid): string function bluesky_fetch_post(string $uri, int $uid): string
{ {
if (Post::exists(['uri' => $uri, 'uid' => [$uid, 0]])) { if (Post::exists(['uri' => $uri, 'uid' => [$uid, 0]])) {
@ -1694,7 +1759,7 @@ function bluesky_get_contact_fields(stdClass $author, int $uid, int $fetch_uid,
} }
$data = bluesky_get(BLUESKY_DIRECTORY . '/' . $author->did); $data = bluesky_get(BLUESKY_DIRECTORY . '/' . $author->did);
if (!empty($data)) { if (!empty($data)) {
$fields['baseurl'] = bluesky_get_pds('', $data); $fields['baseurl'] = bluesky_get_pds('', $data);
if (!empty($fields['baseurl'])) { if (!empty($fields['baseurl'])) {
GServer::check($fields['baseurl'], Protocol::BLUESKY); GServer::check($fields['baseurl'], Protocol::BLUESKY);
@ -1877,6 +1942,13 @@ function bluesky_get_did(string $handle, int $uid): string
return $did; return $did;
} }
// The profile page can contain hints to the DID as well
$did = bluesky_get_did_by_profile('https://' . $handle);
if ($did != '') {
Logger::debug('Got DID by profile page', ['handle' => $handle, 'did' => $did]);
return $did;
}
// And finally we use the default PDS from Bluesky. // And finally we use the default PDS from Bluesky.
$data = bluesky_get(BLUESKY_PDS . '/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle)); $data = bluesky_get(BLUESKY_PDS . '/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle));
if (!empty($data) && !empty($data->did)) { if (!empty($data) && !empty($data->did)) {
@ -1996,7 +2068,7 @@ function bluesky_refresh_token(int $uid): string
$token = DI::pConfig()->get($uid, 'bluesky', 'refresh_token'); $token = DI::pConfig()->get($uid, 'bluesky', 'refresh_token');
$data = bluesky_post($uid, '/xrpc/com.atproto.server.refreshSession', '', ['Authorization' => ['Bearer ' . $token]]); $data = bluesky_post($uid, '/xrpc/com.atproto.server.refreshSession', '', ['Authorization' => ['Bearer ' . $token]]);
if (empty($data)) { if (empty($data) || empty($data->accessJwt)) {
return ''; return '';
} }
@ -2015,7 +2087,7 @@ function bluesky_create_token(int $uid, string $password): string
} }
$data = bluesky_post($uid, '/xrpc/com.atproto.server.createSession', json_encode(['identifier' => $did, 'password' => $password]), ['Content-type' => 'application/json']); $data = bluesky_post($uid, '/xrpc/com.atproto.server.createSession', json_encode(['identifier' => $did, 'password' => $password]), ['Content-type' => 'application/json']);
if (empty($data)) { if (empty($data) || empty($data->accessJwt)) {
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_TOKEN_FAIL); DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_TOKEN_FAIL);
return ''; return '';
} }
@ -2052,14 +2124,18 @@ function bluesky_post(int $uid, string $url, string $params, array $headers): ?s
return null; return null;
} }
$data = json_decode($curlResult->getBodyString());
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
Logger::notice('API Error', ['error' => json_decode($curlResult->getBodyString()) ?: $curlResult->getBodyString()]); Logger::notice('API Error', ['url' => $url, 'code' => $curlResult->getReturnCode(), 'error' => $data ?: $curlResult->getBodyString()]);
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_API_FAIL); if (!$data) {
return null; DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_API_FAIL);
return null;
}
$data->code = $curlResult->getReturnCode();
} }
DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_SUCCESS); DI::pConfig()->set($uid, 'bluesky', 'status', BLUEKSY_STATUS_SUCCESS);
return json_decode($curlResult->getBodyString()); return $data;
} }
function bluesky_xrpc_get(int $uid, string $url, array $parameters = []): ?stdClass function bluesky_xrpc_get(int $uid, string $url, array $parameters = []): ?stdClass
@ -2073,7 +2149,14 @@ function bluesky_xrpc_get(int $uid, string $url, array $parameters = []): ?stdCl
return null; return null;
} }
$data = bluesky_get($pds . '/xrpc/' . $url, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]); $headers = ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]];
$languages = User::getWantedLanguages($uid);
if (!empty($languages)) {
$headers['Accept-Language'] = implode(',', $languages);
}
$data = bluesky_get($pds . '/xrpc/' . $url, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => $headers]);
DI::pConfig()->set($uid, 'bluesky', 'status', is_null($data) ? BLUEKSY_STATUS_API_FAIL : BLUEKSY_STATUS_SUCCESS); DI::pConfig()->set($uid, 'bluesky', 'status', is_null($data) ? BLUEKSY_STATUS_API_FAIL : BLUEKSY_STATUS_SUCCESS);
return $data; return $data;
} }
@ -2087,11 +2170,15 @@ function bluesky_get(string $url, string $accept_content = HttpClientAccept::DEF
return null; return null;
} }
$data = json_decode($curlResult->getBodyString());
if (!$curlResult->isSuccess()) { if (!$curlResult->isSuccess()) {
Logger::notice('API Error', ['url' => $url, 'error' => json_decode($curlResult->getBodyString()) ?: $curlResult->getBodyString()]); Logger::notice('API Error', ['url' => $url, 'code' => $curlResult->getReturnCode(), 'error' => $data ?: $curlResult->getBodyString()]);
return null; if (!$data) {
return null;
}
$data->code = $curlResult->getReturnCode();
} }
Item::incrementInbound(Protocol::BLUESKY); Item::incrementInbound(Protocol::BLUESKY);
return json_decode($curlResult->getBodyString()); return $data;
} }

View file

@ -6,11 +6,11 @@ function bluesky_feed_run($argv, $argc)
{ {
require_once 'addon/bluesky/bluesky.php'; require_once 'addon/bluesky/bluesky.php';
if ($argc != 4) { if ($argc < 3) {
return; return;
} }
Logger::debug('Importing feed - start', ['user' => $argv[1], 'feed' => $argv[2], 'last_poll' => $argv[3]]); Logger::debug('Importing feed - start', ['user' => $argv[1], 'feed' => $argv[2]]);
bluesky_fetch_feed($argv[1], $argv[2], $argv[3]); bluesky_fetch_feed($argv[1], $argv[2]);
Logger::debug('Importing feed - done', ['user' => $argv[1], 'feed' => $argv[2], 'last_poll' => $argv[3]]); Logger::debug('Importing feed - done', ['user' => $argv[1], 'feed' => $argv[2]]);
} }

View file

@ -6,11 +6,11 @@ function bluesky_notifications_run($argv, $argc)
{ {
require_once 'addon/bluesky/bluesky.php'; require_once 'addon/bluesky/bluesky.php';
if ($argc != 3) { if ($argc < 2) {
return; return;
} }
Logger::notice('importing notifications - start', ['user' => $argv[1], 'last_poll' => $argv[2]]); Logger::notice('importing notifications - start', ['user' => $argv[1]]);
bluesky_fetch_notifications($argv[1], $argv[2]); bluesky_fetch_notifications($argv[1]);
Logger::notice('importing notifications - done', ['user' => $argv[1], 'last_poll' => $argv[2]]); Logger::notice('importing notifications - done', ['user' => $argv[1]]);
} }

View file

@ -6,11 +6,11 @@ function bluesky_timeline_run($argv, $argc)
{ {
require_once 'addon/bluesky/bluesky.php'; require_once 'addon/bluesky/bluesky.php';
if ($argc != 3) { if ($argc < 2) {
return; return;
} }
Logger::notice('importing timeline - start', ['user' => $argv[1], 'last_poll' => $argv[2]]); Logger::notice('importing timeline - start', ['user' => $argv[1]]);
bluesky_fetch_timeline($argv[1], $argv[2]); bluesky_fetch_timeline($argv[1]);
Logger::notice('importing timeline - done', ['user' => $argv[1], 'last_poll' => $argv[2]]); Logger::notice('importing timeline - done', ['user' => $argv[1]]);
} }

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-03-22 05:31+0000\n" "POT-Creation-Date: 2024-09-29 18:16+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,131 +17,117 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: bluesky.php:325 #: bluesky.php:335
msgid "Save Settings" msgid "Save Settings"
msgstr "" msgstr ""
#: bluesky.php:326 #: bluesky.php:336
msgid "Allow your users to use your hostname for their Bluesky handles" msgid "Allow your users to use your hostname for their Bluesky handles"
msgstr "" msgstr ""
#: bluesky.php:326 #: bluesky.php:336
#, php-format #, php-format
msgid "" msgid "Before enabling this option, you have to setup a wildcard domain configuration and you have to enable wildcard requests in your webserver configuration. On Apache this is done by adding \"ServerAlias *.%s\" to your HTTP configuration. You don't need to change the HTTPS configuration."
"Before enabling this option, you have to setup a wildcard domain "
"configuration and you have to enable wildcard requests in your webserver "
"configuration. On Apache this is done by adding \"ServerAlias *.%s\" to your "
"HTTP configuration. You don't need to change the HTTPS configuration."
msgstr "" msgstr ""
#: bluesky.php:354 #: bluesky.php:365
#, php-format #, php-format
msgid "Allow to use %s as your Bluesky handle." msgid "Allow to use %s as your Bluesky handle."
msgstr "" msgstr ""
#: bluesky.php:354 #: bluesky.php:365
#, php-format #, php-format
msgid "" msgid "When enabled, you can use %s as your Bluesky handle. After you enabled this option, please go to https://bsky.app/settings and select to change your handle. Select that you have got your own domain. Then enter %s and select \"No DNS Panel\". Then select \"Verify Text File\"."
"When enabled, you can use %s as your Bluesky handle. After you enabled this "
"option, please go to https://bsky.app/settings and select to change your "
"handle. Select that you have got your own domain. Then enter %s and select "
"\"No DNS Panel\". Then select \"Verify Text File\"."
msgstr ""
#: bluesky.php:361
msgid "Enable Bluesky Post Addon"
msgstr ""
#: bluesky.php:362
msgid "Post to Bluesky by default"
msgstr ""
#: bluesky.php:363
msgid "Import the remote timeline"
msgstr ""
#: bluesky.php:364
msgid "Import the pinned feeds"
msgstr ""
#: bluesky.php:364
msgid ""
"When activated, Posts will be imported from all the feeds that you pinned in "
"Bluesky."
msgstr ""
#: bluesky.php:366
msgid "Personal Data Server"
msgstr ""
#: bluesky.php:366
msgid "The personal data server (PDS) is the system that hosts your profile."
msgstr ""
#: bluesky.php:367
msgid "Bluesky handle"
msgstr ""
#: bluesky.php:368
msgid "Bluesky DID"
msgstr ""
#: bluesky.php:368
msgid ""
"This is the unique identifier. It will be fetched automatically, when the "
"handle is entered."
msgstr ""
#: bluesky.php:369
msgid "Bluesky app password"
msgstr ""
#: bluesky.php:369
msgid ""
"Please don't add your real password here, but instead create a specific app "
"password in the Bluesky settings."
msgstr "" msgstr ""
#: bluesky.php:375 #: bluesky.php:375
msgid "Enable Bluesky Post Addon"
msgstr ""
#: bluesky.php:376
msgid "Post to Bluesky by default"
msgstr ""
#: bluesky.php:377
msgid "Import the remote timeline"
msgstr ""
#: bluesky.php:378
msgid "Import the pinned feeds"
msgstr ""
#: bluesky.php:378
msgid "When activated, Posts will be imported from all the feeds that you pinned in Bluesky."
msgstr ""
#: bluesky.php:379
msgid "Complete the threads"
msgstr ""
#: bluesky.php:379
msgid "When activated, the system fetches additional replies for the posts in the timeline. This leads to more complete threads."
msgstr ""
#: bluesky.php:381
msgid "Personal Data Server"
msgstr ""
#: bluesky.php:381
msgid "The personal data server (PDS) is the system that hosts your profile."
msgstr ""
#: bluesky.php:382
msgid "Bluesky handle"
msgstr ""
#: bluesky.php:383
msgid "Bluesky DID"
msgstr ""
#: bluesky.php:383
msgid "This is the unique identifier. It will be fetched automatically, when the handle is entered."
msgstr ""
#: bluesky.php:384
msgid "Bluesky app password"
msgstr ""
#: bluesky.php:384
msgid "Please don't add your real password here, but instead create a specific app password in the Bluesky settings."
msgstr ""
#: bluesky.php:390
msgid "Bluesky Import/Export" msgid "Bluesky Import/Export"
msgstr "" msgstr ""
#: bluesky.php:385 #: bluesky.php:400
msgid "" msgid "You are not authenticated. Please enter your handle and the app password."
"You are not authenticated. Please enter your handle and the app password."
msgstr "" msgstr ""
#: bluesky.php:405 #: bluesky.php:420
msgid "" msgid "You are authenticated to Bluesky. For security reasons the password isn't stored."
"You are authenticated to Bluesky. For security reasons the password isn't "
"stored."
msgstr "" msgstr ""
#: bluesky.php:407 #: bluesky.php:422
msgid "" msgid "The communication with the personal data server service (PDS) is established."
"The communication with the personal data server service (PDS) is established."
msgstr "" msgstr ""
#: bluesky.php:409 #: bluesky.php:424
msgid "Communication issues with the personal data server service (PDS)." msgid "Communication issues with the personal data server service (PDS)."
msgstr "" msgstr ""
#: bluesky.php:411 #: bluesky.php:426
msgid "" msgid "The DID for the provided handle could not be detected. Please check if you entered the correct handle."
"The DID for the provided handle could not be detected. Please check if you "
"entered the correct handle."
msgstr "" msgstr ""
#: bluesky.php:413 #: bluesky.php:428
msgid "The personal data server service (PDS) could not be detected." msgid "The personal data server service (PDS) could not be detected."
msgstr "" msgstr ""
#: bluesky.php:415 #: bluesky.php:430
msgid "" msgid "The authentication with the provided handle and password failed. Please check if you entered the correct password."
"The authentication with the provided handle and password failed. Please "
"check if you entered the correct password."
msgstr "" msgstr ""
#: bluesky.php:484 #: bluesky.php:492
msgid "Post to Bluesky" msgid "Post to Bluesky"
msgstr "" msgstr ""

View file

@ -3,6 +3,7 @@
{{include file="field_checkbox.tpl" field=$bydefault}} {{include file="field_checkbox.tpl" field=$bydefault}}
{{include file="field_checkbox.tpl" field=$import}} {{include file="field_checkbox.tpl" field=$import}}
{{include file="field_checkbox.tpl" field=$import_feeds}} {{include file="field_checkbox.tpl" field=$import_feeds}}
{{include file="field_checkbox.tpl" field=$complete_threads}}
{{if $custom_handle}} {{if $custom_handle}}
{{include file="field_checkbox.tpl" field=$custom_handle}} {{include file="field_checkbox.tpl" field=$custom_handle}}
{{/if}} {{/if}}