Bluesky: Import is mosty feature complete #1395

Merged
MrPetovan merged 1 commit from heluecht/friendica-addons:bluesky into develop 2023-06-04 02:56:59 +02:00
2 changed files with 493 additions and 196 deletions

View file

@ -6,21 +6,15 @@
* Author: Michael Vogel <https://pirati.ca/profile/heluecht> * Author: Michael Vogel <https://pirati.ca/profile/heluecht>
* *
* @todo * @todo
* Nice to have: * - Links in outgoing comments
* - Probing for contacts * - Outgoing mentions
* *
* Need more information: * Possibly not possible:
* - only fetch new posts * - only fetch new posts
* - detect contact relations
* - receive likes
* - follow contacts
* - unfollow contacts
* *
* Possible but less important: * Currently not possible, due to limitations in Friendica
* - Block contacts * - mute contacts https://atproto.com/lexicons/app-bsky-graph#appbskygraphmuteactor
* - unblock contacts * - unmute contacts https://atproto.com/lexicons/app-bsky-graph#appbskygraphunmuteactor
* - mute contacts
* - unmute contacts
*/ */
use Friendica\Content\Text\BBCode; use Friendica\Content\Text\BBCode;
@ -31,6 +25,7 @@ use Friendica\Core\Hook;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\Core\Worker;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact; use Friendica\Model\Contact;
@ -57,14 +52,14 @@ function bluesky_install()
Hook::register('connector_settings', __FILE__, 'bluesky_settings'); Hook::register('connector_settings', __FILE__, 'bluesky_settings');
Hook::register('connector_settings_post', __FILE__, 'bluesky_settings_post'); Hook::register('connector_settings_post', __FILE__, 'bluesky_settings_post');
Hook::register('cron', __FILE__, 'bluesky_cron'); Hook::register('cron', __FILE__, 'bluesky_cron');
// Hook::register('support_follow', __FILE__, 'bluesky_support_follow'); Hook::register('support_follow', __FILE__, 'bluesky_support_follow');
// Hook::register('support_probe', __FILE__, 'bluesky_support_probe'); Hook::register('support_probe', __FILE__, 'bluesky_support_probe');
// Hook::register('follow', __FILE__, 'bluesky_follow'); Hook::register('follow', __FILE__, 'bluesky_follow');
// Hook::register('unfollow', __FILE__, 'bluesky_unfollow'); Hook::register('unfollow', __FILE__, 'bluesky_unfollow');
// Hook::register('block', __FILE__, 'bluesky_block'); Hook::register('block', __FILE__, 'bluesky_block');
// Hook::register('unblock', __FILE__, 'bluesky_unblock'); Hook::register('unblock', __FILE__, 'bluesky_unblock');
Hook::register('check_item_notification', __FILE__, 'bluesky_check_item_notification'); Hook::register('check_item_notification', __FILE__, 'bluesky_check_item_notification');
// Hook::register('probe_detect', __FILE__, 'bluesky_probe_detect'); Hook::register('probe_detect', __FILE__, 'bluesky_probe_detect');
Hook::register('item_by_link', __FILE__, 'bluesky_item_by_link'); Hook::register('item_by_link', __FILE__, 'bluesky_item_by_link');
} }
@ -75,15 +70,59 @@ function bluesky_load_config(ConfigFileManager $loader)
function bluesky_check_item_notification(array &$notification_data) function bluesky_check_item_notification(array &$notification_data)
{ {
$handle = DI::pConfig()->get($notification_data['uid'], 'bluesky', 'handle'); $did = DI::pConfig()->get($notification_data['uid'], 'bluesky', 'did');
$did = DI::pConfig()->get($notification_data['uid'], 'bluesky', 'did');
if (!empty($handle) && !empty($did)) { if (!empty($did)) {
$notification_data['profiles'][] = $handle;
$notification_data['profiles'][] = $did; $notification_data['profiles'][] = $did;
} }
} }
function bluesky_probe_detect(array &$hookData)
{
// Don't overwrite an existing result
if (isset($hookData['result'])) {
return;
}
// Avoid a lookup for the wrong network
if (!in_array($hookData['network'], ['', Protocol::BLUESKY])) {
return;
}
$pconfig = DBA::selectFirst('pconfig', ['uid'], ["`cat` = ? AND `k` = ? AND `v` != ?", 'bluesky', 'access_token', '']);
if (empty($pconfig['uid'])) {
return;
}
if (parse_url($hookData['uri'], PHP_URL_SCHEME) == 'did') {
$did = $hookData['uri'];
} elseif (preg_match('#^' . BLUESKY_HOST . '/profile/(.+)#', $hookData['uri'], $matches)) {
$did = bluesky_get_did($pconfig['uid'], $matches[1]);
if (empty($did)) {
return;
}
} else {
return;
}
$token = bluesky_get_token($pconfig['uid']);
if (empty($token)) {
return;
}
$data = bluesky_get($pconfig['uid'], '/xrpc/app.bsky.actor.getProfile?actor=' . $did, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . $token]]]);
if (empty($data)) {
return;
}
$hookData['result'] = bluesky_get_contact($data, 0, $pconfig['uid']);
// Authoritative probe should set the result even if the probe was unsuccessful
if ($hookData['network'] == Protocol::BLUESKY && empty($hookData['result'])) {
$hookData['result'] = [];
}
}
function bluesky_item_by_link(array &$hookData) function bluesky_item_by_link(array &$hookData)
{ {
// Don't overwrite an existing result // Don't overwrite an existing result
@ -109,7 +148,7 @@ function bluesky_item_by_link(array &$hookData)
$uri = 'at://' . $did . '/app.bsky.feed.post/' . $matches[2]; $uri = 'at://' . $did . '/app.bsky.feed.post/' . $matches[2];
$uri = bluesky_fetch_missing_post($uri, $hookData['uid'], 0, true); $uri = bluesky_fetch_missing_post($uri, $hookData['uid'], 0);
Logger::debug('Got post', ['profile' => $matches[1], 'cid' => $matches[2], 'result' => $uri]); Logger::debug('Got post', ['profile' => $matches[1], 'cid' => $matches[2], 'result' => $uri]);
if (!empty($uri)) { if (!empty($uri)) {
$item = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $hookData['uid']]); $item = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $hookData['uid']]);
@ -119,6 +158,129 @@ function bluesky_item_by_link(array &$hookData)
} }
} }
function bluesky_support_follow(array &$data)
{
if ($data['protocol'] == Protocol::BLUESKY) {
$data['result'] = true;
}
}
function bluesky_support_probe(array &$data)
{
if ($data['protocol'] == Protocol::BLUESKY) {
$data['result'] = true;
}
}
function bluesky_follow(array &$hook_data)
{
$token = bluesky_get_token($hook_data['uid']);
if (empty($token)) {
return;
}
Logger::debug('Check if contact is bluesky', ['data' => $hook_data]);
$contact = DBA::selectFirst('contact', [], ['network' => Protocol::BLUESKY, 'url' => $hook_data['url'], 'uid' => [0, $hook_data['uid']]]);
if (empty($contact)) {
return;
}
$record = [
'subject' => $contact['url'],
'createdAt' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
'$type' => 'app.bsky.graph.follow'
];
$post = [
'collection' => 'app.bsky.graph.follow',
'repo' => DI::pConfig()->get($hook_data['uid'], 'bluesky', 'did'),
'record' => $record
];
$activity = bluesky_post($hook_data['uid'], '/xrpc/com.atproto.repo.createRecord', json_encode($post), ['Content-type' => 'application/json', 'Authorization' => ['Bearer ' . $token]]);
if (!empty($activity->uri)) {
$hook_data['contact'] = $contact;
Logger::debug('Successfully start following', ['url' => $contact['url'], 'uri' => $activity->uri]);
}
}
function bluesky_unfollow(array &$hook_data)
{
$token = bluesky_get_token($hook_data['uid']);
if (empty($token)) {
return;
}
if ($hook_data['contact']['network'] != Protocol::BLUESKY) {
return;
}
$data = bluesky_get($hook_data['uid'], '/xrpc/app.bsky.actor.getProfile?actor=' . $hook_data['contact']['url'], HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . $token]]]);
if (empty($data->viewer) || empty($data->viewer->following)) {
return;
}
bluesky_delete_post($data->viewer->following, $hook_data['uid']);
$hook_data['result'] = true;
}
function bluesky_block(array &$hook_data)
{
$token = bluesky_get_token($hook_data['uid']);
if (empty($token)) {
return;
}
Logger::debug('Check if contact is bluesky', ['data' => $hook_data]);
$contact = DBA::selectFirst('contact', [], ['network' => Protocol::BLUESKY, 'url' => $hook_data['url'], 'uid' => [0, $hook_data['uid']]]);
if (empty($contact)) {
return;
}
$record = [
'subject' => $contact['url'],
'createdAt' => DateTimeFormat::utcNow(DateTimeFormat::ATOM),
'$type' => 'app.bsky.graph.block'
];
$post = [
'collection' => 'app.bsky.graph.block',
'repo' => DI::pConfig()->get($hook_data['uid'], 'bluesky', 'did'),
'record' => $record
];
$activity = bluesky_post($hook_data['uid'], '/xrpc/com.atproto.repo.createRecord', json_encode($post), ['Content-type' => 'application/json', 'Authorization' => ['Bearer ' . $token]]);
if (!empty($activity->uri)) {
$cdata = Contact::getPublicAndUserContactID($hook_data['contact']['id'], $hook_data['uid']);
if (!empty($cdata['user'])) {
Contact::remove($cdata['user']);
}
Logger::debug('Successfully blocked contact', ['url' => $hook_data['contact']['url'], 'uri' => $activity->uri]);
}
}
function bluesky_unblock(array &$hook_data)
{
$token = bluesky_get_token($hook_data['uid']);
if (empty($token)) {
return;
}
if ($hook_data['contact']['network'] != Protocol::BLUESKY) {
return;
}
$data = bluesky_get($hook_data['uid'], '/xrpc/app.bsky.actor.getProfile?actor=' . $hook_data['contact']['url'], HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . $token]]]);
if (empty($data->viewer) || empty($data->viewer->blocking)) {
return;
}
bluesky_delete_post($data->viewer->blocking, $hook_data['uid']);
$hook_data['result'] = true;
}
function bluesky_settings(array &$data) function bluesky_settings(array &$data)
{ {
if (!DI::userSession()->getLocalUserId()) { if (!DI::userSession()->getLocalUserId()) {
@ -243,6 +405,21 @@ function bluesky_cron()
Logger::notice('importing timeline - start', ['user' => $pconfig['uid']]); Logger::notice('importing timeline - start', ['user' => $pconfig['uid']]);
bluesky_fetch_timeline($pconfig['uid']); bluesky_fetch_timeline($pconfig['uid']);
Logger::notice('importing timeline - done', ['user' => $pconfig['uid']]); Logger::notice('importing timeline - done', ['user' => $pconfig['uid']]);
Logger::notice('importing notifications - start', ['user' => $pconfig['uid']]);
bluesky_fetch_notifications($pconfig['uid']);
Logger::notice('importing notifications - done', ['user' => $pconfig['uid']]);
}
$last_clean = DI::keyValue()->get('bluesky_last_clean');
if (empty($last_clean) || ($last_clean + 86400 < time())) {
Logger::notice('Start contact cleanup');
$contacts = DBA::select('account-user-view', ['id', 'pid'], ["`network` = ? AND `uid` != ? AND `rel` = ?", Protocol::BLUESKY, 0, Contact::NOTHING]);
while ($contact = DBA::fetch($contacts)) {
Worker::add(Worker::PRIORITY_LOW, 'MergeContact', $contact['pid'], $contact['id'], 0);
}
DBA::close($contacts);
DI::keyValue()->set('bluesky_last_clean', time());
Logger::notice('Contact cleanup done');
} }
Logger::notice('cron_end'); Logger::notice('cron_end');
@ -521,7 +698,7 @@ function bluesky_add_embed(int $uid, array $msg, array $record): array
$photo = Photo::selectFirst([], ["`resource-id` = ? AND `scale` > ?", $photo['resource-id'], 0], ['order' => ['scale']]); $photo = Photo::selectFirst([], ["`resource-id` = ? AND `scale` > ?", $photo['resource-id'], 0], ['order' => ['scale']]);
$blob = bluesky_upload_blob($uid, $photo); $blob = bluesky_upload_blob($uid, $photo);
if (!empty($blob) && count($images) < 4) { if (!empty($blob) && count($images) < 4) {
$images[] = ['alt' => $image['description'], 'image' => $blob]; $images[] = ['alt' => $image['description'] ?? '', 'image' => $blob];
} }
} }
if (!empty($images)) { if (!empty($images)) {
@ -600,7 +777,7 @@ function bluesky_process_reason(stdClass $reason, string $uri, int $uid)
return; return;
} }
$contact = bluesky_get_contact($reason->by, $uid); $contact = bluesky_get_contact($reason->by, $uid, $uid);
$item = [ $item = [
'network' => Protocol::BLUESKY, 'network' => Protocol::BLUESKY,
@ -633,6 +810,65 @@ function bluesky_process_reason(stdClass $reason, string $uri, int $uid)
} }
} }
function bluesky_fetch_notifications(int $uid)
{
$result = bluesky_get($uid, '/xrpc/app.bsky.notification.listNotifications', HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]);
if (empty($result->notifications)) {
return;
}
foreach ($result->notifications as $notification) {
$uri = bluesky_get_uri($notification);
if (Post::exists(['uri' => $uri, 'uid' => $uid]) || Post::exists(['extid' => $uri, 'uid' => $uid])) {
Logger::debug('Notification already processed', ['uid' => $uid, 'reason' => $notification->reason, 'uri' => $uri, 'indexedAt' => $notification->indexedAt]);
continue;
}
Logger::debug('Process notification', ['uid' => $uid, 'reason' => $notification->reason, 'uri' => $uri, 'indexedAt' => $notification->indexedAt]);
switch ($notification->reason) {
case 'like':
$item = bluesky_get_header($notification, $uri, $uid, $uid);
$item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['body'] = $item['verb'] = Activity::LIKE;
$item['thr-parent'] = bluesky_get_uri($notification->record->subject);
$result = Item::insert($item);
Logger::debug('Got like', ['uid' => $uid, 'result' => $result]);
break;
case 'repost':
$item = bluesky_get_header($notification, $uri, $uid, $uid);
$item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['body'] = $item['verb'] = Activity::ANNOUNCE;
$item['thr-parent'] = bluesky_get_uri($notification->record->subject);
$result = Item::insert($item);
Logger::debug('Got repost', ['uid' => $uid, 'result' => $result]);
break;
case 'follow':
$contact = bluesky_get_contact($notification->author, $uid, $uid);
Logger::debug('New follower', ['uid' => $uid, 'nick' => $contact['nick']]);
break;
case 'mention':
$result = bluesky_process_post($notification, $uid);
Logger::debug('Got mention', ['uid' => $uid, 'result' => $result]);
break;
case 'reply':
$result = bluesky_process_post($notification, $uid);
Logger::debug('Got reply', ['uid' => $uid, 'result' => $result]);
break;
case 'quote':
$result = bluesky_process_post($notification, $uid);
Logger::debug('Got quote', ['uid' => $uid, 'result' => $result]);
break;
default:
Logger::notice('Unhandled reason', ['reason' => $notification->reason]);
break;
}
}
}
function bluesky_process_post(stdClass $post, int $uid): int function bluesky_process_post(stdClass $post, int $uid): int
{ {
$uri = bluesky_get_uri($post); $uri = bluesky_get_uri($post);
@ -643,23 +879,23 @@ function bluesky_process_post(stdClass $post, int $uid): int
Logger::debug('Importing post', ['uid' => $uid, 'indexedAt' => $post->indexedAt, 'uri' => $post->uri, 'cid' => $post->cid]); Logger::debug('Importing post', ['uid' => $uid, 'indexedAt' => $post->indexedAt, 'uri' => $post->uri, 'cid' => $post->cid]);
$item = bluesky_get_header($post, $uri, $uid); $item = bluesky_get_header($post, $uri, $uid, $uid);
$item = bluesky_get_content($item, $post->record, $uid); $item = bluesky_get_content($item, $post->record, $uid);
if (!empty($post->embed)) { if (!empty($post->embed)) {
$item = bluesky_add_media($post->embed, $item); $item = bluesky_add_media($post->embed, $item, $uid);
} }
return item::insert($item); return item::insert($item);
} }
function bluesky_get_header(stdClass $post, string $uri, int $uid): array function bluesky_get_header(stdClass $post, string $uri, int $uid, int $fetch_uid): array
{ {
$parts = bluesky_get_uri_parts($uri); $parts = bluesky_get_uri_parts($uri);
if (empty($post->author)) { if (empty($post->author)) {
return []; return [];
} }
$contact = bluesky_get_contact($post->author, $uid); $contact = bluesky_get_contact($post->author, $uid, $fetch_uid);
$item = [ $item = [
'network' => Protocol::BLUESKY, 'network' => Protocol::BLUESKY,
'uid' => $uid, 'uid' => $uid,
@ -716,21 +952,29 @@ function bluesky_get_text(stdClass $record, int $uid): string
$linktext = substr($text, $facet->index->byteStart, $facet->index->byteEnd - $facet->index->byteStart); $linktext = substr($text, $facet->index->byteStart, $facet->index->byteEnd - $facet->index->byteStart);
$suffix = substr($text, $facet->index->byteEnd); $suffix = substr($text, $facet->index->byteEnd);
$url = ''; $url = '';
$type = '$type';
foreach ($facet->features as $feature) { foreach ($facet->features as $feature) {
if (!empty($feature->uri)) {
$url = $feature->uri; switch ($feature->$type) {
} case 'app.bsky.richtext.facet#link':
if (!empty($feature->did)) { $url = $feature->uri;
$contact = Contact::selectFirst(['id'], ['nurl' => $feature->did, 'uid' => [0, $uid]]); break;
if (!empty($contact['id'])) {
$url = DI::baseUrl() . '/contact/' . $contact['id']; case 'app.bsky.richtext.facet#mention':
if (substr($linktext, 0, 1) == '@') { $contact = Contact::selectFirst(['id'], ['nurl' => $feature->did, 'uid' => [0, $uid]]);
$prefix .= '@'; if (!empty($contact['id'])) {
$linktext = substr($linktext, 1); $url = DI::baseUrl() . '/contact/' . $contact['id'];
if (substr($linktext, 0, 1) == '@') {
$prefix .= '@';
$linktext = substr($linktext, 1);
}
} }
} break;
default:
Logger::notice('Unhandled feature type', ['type' => $feature->$type, 'record' => $record]);
break;
} }
} }
if (!empty($url)) { if (!empty($url)) {
@ -740,50 +984,86 @@ function bluesky_get_text(stdClass $record, int $uid): string
return $text; return $text;
} }
function bluesky_add_media(stdClass $embed, array $item): array function bluesky_add_media(stdClass $embed, array $item, int $fetch_uid): array
{ {
if (!empty($embed->images)) { $type = '$type';
foreach ($embed->images as $image) { switch ($embed->$type) {
case 'app.bsky.embed.images#view':
foreach ($embed->images as $image) {
$media = [
'uri-id' => $item['uri-id'],
'type' => Post\Media::IMAGE,
'url' => $image->fullsize,
'preview' => $image->thumb,
'description' => $image->alt,
];
Post\Media::insert($media);
}
break;
case 'app.bsky.embed.external#view':
$media = [ $media = [
'uri-id' => $item['uri-id'], 'uri-id' => $item['uri-id'],
'type' => Post\Media::IMAGE, 'type' => Post\Media::HTML,
'url' => $image->fullsize, 'url' => $embed->external->uri,
'preview' => $image->thumb, 'name' => $embed->external->title,
'description' => $image->alt, 'description' => $embed->external->description,
]; ];
Post\Media::insert($media); Post\Media::insert($media);
} break;
} elseif (!empty($embed->external)) {
$media = [
'uri-id' => $item['uri-id'],
'type' => Post\Media::HTML,
'url' => $embed->external->uri,
'name' => $embed->external->title,
'description' => $embed->external->description,
];
Post\Media::insert($media);
} elseif (!empty($embed->record)) {
$uri = bluesky_get_uri($embed->record);
$shared = Post::selectFirst(['uri-id'], ['uri' => $uri, 'uid' => $item['uid']]);
if (empty($shared)) {
$shared = bluesky_get_header($embed->record, $uri, 0);
if (!empty($shared)) {
$shared = bluesky_get_content($shared, $embed->record->value, $item['uid']);
if (!empty($embed->record->embeds)) { case 'app.bsky.embed.record#view':
foreach ($embed->record->embeds as $single) { $uri = bluesky_get_uri($embed->record);
$shared = bluesky_add_media($single, $shared); $shared = Post::selectFirst(['uri-id'], ['uri' => $uri, 'uid' => $item['uid']]);
if (empty($shared)) {
$shared = bluesky_get_header($embed->record, $uri, 0, $fetch_uid);
if (!empty($shared)) {
$shared = bluesky_get_content($shared, $embed->record->value, $item['uid']);
if (!empty($embed->record->embeds)) {
foreach ($embed->record->embeds as $single) {
$shared = bluesky_add_media($single, $shared, $fetch_uid);
}
} }
$id = Item::insert($shared);
$shared = Post::selectFirst(['uri-id'], ['id' => $id]);
} }
$id = Item::insert($shared);
$shared = Post::selectFirst(['uri-id'], ['id' => $id]);
} }
} if (!empty($shared)) {
if (!empty($shared)) { $item['quote-uri-id'] = $shared['uri-id'];
$item['quote-uri-id'] = $shared['uri-id']; }
} break;
} else {
Logger::debug('Unsupported embed', ['embed' => $embed, 'item' => $item]); case 'app.bsky.embed.recordWithMedia#view':
$uri = bluesky_get_uri($embed->record->record);
$shared = Post::selectFirst(['uri-id'], ['uri' => $uri, 'uid' => $item['uid']]);
if (empty($shared)) {
$shared = bluesky_get_header($embed->record->record, $uri, 0, $fetch_uid);
if (!empty($shared)) {
$shared = bluesky_get_content($shared, $embed->record->record->value, $item['uid']);
if (!empty($embed->record->embeds)) {
foreach ($embed->record->record->embeds as $single) {
$shared = bluesky_add_media($single, $shared, $fetch_uid);
}
}
if (!empty($embed->media)) {
bluesky_add_media($embed->media, $item, $fetch_uid);
}
$id = Item::insert($shared);
$shared = Post::selectFirst(['uri-id'], ['id' => $id]);
}
}
if (!empty($shared)) {
$item['quote-uri-id'] = $shared['uri-id'];
}
break;
default:
Logger::notice('Unhandled embed type', ['type' => $embed->$type, 'embed' => $embed]);
break;
} }
return $item; return $item;
} }
@ -810,6 +1090,11 @@ function bluesky_get_uri_class(string $uri): ?stdClass
$class->cid = array_pop($elements); $class->cid = array_pop($elements);
$class->uri = implode(':', $elements); $class->uri = implode(':', $elements);
if ((substr_count($class->uri, '/') == 2) && (substr_count($class->cid, '/') == 2)) {
$class->uri .= ':' . $class->cid;
$class->cid = '';
}
return $class; return $class;
} }
@ -831,7 +1116,7 @@ function bluesky_get_uri_parts(string $uri): ?stdClass
return $class; return $class;
} }
function bluesky_fetch_missing_post(string $uri, int $uid, int $causer, bool $original = false): string function bluesky_fetch_missing_post(string $uri, int $uid, int $causer): string
{ {
if (Post::exists(['uri' => $uri, 'uid' => [$uid, 0]])) { if (Post::exists(['uri' => $uri, 'uid' => [$uid, 0]])) {
Logger::debug('Post exists', ['uri' => $uri]); Logger::debug('Post exists', ['uri' => $uri]);
@ -844,12 +1129,8 @@ function bluesky_fetch_missing_post(string $uri, int $uid, int $causer, bool $or
} }
Logger::debug('Fetch missing post', ['uri' => $uri]); Logger::debug('Fetch missing post', ['uri' => $uri]);
if (!$original) { $class = bluesky_get_uri_class($uri);
$class = bluesky_get_uri_class($uri); $fetch_uri = $class->uri;
$fetch_uri = $class->uri;
} else {
$fetch_uri = $uri;
}
$data = bluesky_get($uid, '/xrpc/app.bsky.feed.getPosts?uris=' . urlencode($fetch_uri), HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]); $data = bluesky_get($uid, '/xrpc/app.bsky.feed.getPosts?uris=' . urlencode($fetch_uri), HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]);
if (empty($data)) { if (empty($data)) {
@ -862,7 +1143,7 @@ function bluesky_fetch_missing_post(string $uri, int $uid, int $causer, bool $or
foreach ($data->posts as $post) { foreach ($data->posts as $post) {
$uri = bluesky_get_uri($post); $uri = bluesky_get_uri($post);
$item = bluesky_get_header($post, $uri, $uid); $item = bluesky_get_header($post, $uri, $uid, $uid);
$item = bluesky_get_content($item, $post->record, $uid); $item = bluesky_get_content($item, $post->record, $uid);
$item['post-reason'] = Item::PR_FETCHED; $item['post-reason'] = Item::PR_FETCHED;
@ -872,7 +1153,7 @@ function bluesky_fetch_missing_post(string $uri, int $uid, int $causer, bool $or
} }
if (!empty($post->embed)) { if (!empty($post->embed)) {
$item = bluesky_add_media($post->embed, $item); $item = bluesky_add_media($post->embed, $item, $uid);
} }
$id = Item::insert($item); $id = Item::insert($item);
Logger::debug('Stored item', ['id' => $id, 'uri' => $uri]); Logger::debug('Stored item', ['id' => $id, 'uri' => $uri]);
@ -881,52 +1162,55 @@ function bluesky_fetch_missing_post(string $uri, int $uid, int $causer, bool $or
return $uri; return $uri;
} }
function bluesky_get_contact(stdClass $author, int $uid): array function bluesky_get_contact(stdClass $author, int $uid, int $fetch_uid): array
{ {
$condition = ['network' => Protocol::BLUESKY, 'uid' => $uid, 'url' => $author->did]; $condition = ['network' => Protocol::BLUESKY, 'uid' => 0, 'url' => $author->did];
$contact = Contact::selectFirst(['id', 'updated'], $condition);
$fields = [ $update = empty($contact) || $contact['updated'] < DateTimeFormat::utc('now -24 hours');
'alias' => BLUESKY_HOST . '/profile/' . $author->handle,
'name' => $author->displayName,
'nick' => $author->handle,
'addr' => $author->handle,
];
$contact = Contact::selectFirst([], $condition); $public_fields = $fields = bluesky_get_contact_fields($author, $fetch_uid, $update);
$public_fields['uid'] = 0;
$public_fields['rel'] = Contact::NOTHING;
if (empty($contact)) { if (empty($contact)) {
$cid = bluesky_insert_contact($author, $uid); $cid = Contact::insert($public_fields);
} else { } else {
$cid = $contact['id']; $cid = $contact['id'];
if ($fields['alias'] != $contact['alias'] || $fields['name'] != $contact['name'] || $fields['nick'] != $contact['nick'] || $fields['addr'] != $contact['addr']) { Contact::update($public_fields, ['id' => $cid], true);
Contact::update($fields, ['id' => $cid]); }
if ($uid != 0) {
$condition = ['network' => Protocol::BLUESKY, 'uid' => $uid, 'url' => $author->did];
$contact = Contact::selectFirst(['id', 'rel', 'uid'], $condition);
if (!isset($fields['rel']) && isset($contact['rel'])) {
$fields['rel'] = $contact['rel'];
} elseif (!isset($fields['rel'])) {
$fields['rel'] = Contact::NOTHING;
} }
} }
$condition['uid'] = 0; if (($uid != 0) && ($fields['rel'] != Contact::NOTHING)) {
if (empty($contact)) {
$contact = Contact::selectFirst([], $condition); $cid = Contact::insert($fields);
if (empty($contact)) { } else {
$pcid = bluesky_insert_contact($author, 0); $cid = $contact['id'];
Contact::update($fields, ['id' => $cid], true);
}
Logger::debug('Get user contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
} else { } else {
$pcid = $contact['id']; Logger::debug('Get public contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
if ($fields['alias'] != $contact['alias'] || $fields['name'] != $contact['name'] || $fields['nick'] != $contact['nick'] || $fields['addr'] != $contact['addr']) {
Contact::update($fields, ['id' => $pcid]);
}
} }
if (!empty($author->avatar)) { if (!empty($author->avatar)) {
Contact::updateAvatar($cid, $author->avatar); Contact::updateAvatar($cid, $author->avatar);
} }
if (empty($contact) || $contact['updated'] < DateTimeFormat::utc('now -24 hours')) {
bluesky_update_contact($author, $uid, $cid, $pcid);
}
return Contact::getById($cid); return Contact::getById($cid);
} }
function bluesky_insert_contact(stdClass $author, int $uid) function bluesky_get_contact_fields(stdClass $author, int $uid, bool $update): array
{ {
$fields = [ $fields = [
'uid' => $uid, 'uid' => $uid,
@ -943,23 +1227,19 @@ function bluesky_insert_contact(stdClass $author, int $uid)
'nick' => $author->handle, 'nick' => $author->handle,
'addr' => $author->handle, 'addr' => $author->handle,
]; ];
return Contact::insert($fields);
}
function bluesky_update_contact(stdClass $author, int $uid, int $cid, int $pcid) if (!$update) {
{ Logger::debug('Got contact fields', ['uid' => $uid, 'url' => $fields['url']]);
$data = bluesky_get($uid, '/xrpc/app.bsky.actor.getProfile?actor=' . $author->did, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]); return $fields;
if (empty($data)) {
return;
} }
$fields = [ $data = bluesky_get($uid, '/xrpc/app.bsky.actor.getProfile?actor=' . $author->did, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]);
'alias' => BLUESKY_HOST . '/profile/' . $data->handle, if (empty($data)) {
'name' => $data->displayName, Logger::debug('Error fetching contact fields', ['uid' => $uid, 'url' => $fields['url']]);
'nick' => $data->handle, return $fields;
'addr' => $data->handle, }
'updated' => DateTimeFormat::utcNow(DateTimeFormat::MYSQL),
]; $fields['updated'] = DateTimeFormat::utcNow(DateTimeFormat::MYSQL);
if (!empty($data->description)) { if (!empty($data->description)) {
$fields['about'] = HTML::toBBCode($data->description); $fields['about'] = HTML::toBBCode($data->description);
@ -969,8 +1249,20 @@ function bluesky_update_contact(stdClass $author, int $uid, int $cid, int $pcid)
$fields['header'] = $data->banner; $fields['header'] = $data->banner;
} }
Contact::update($fields, ['id' => $cid]); if (!empty($data->viewer)) {
Contact::update($fields, ['id' => $pcid]); if (!empty($data->viewer->following) && !empty($data->viewer->followedBy)) {
$fields['rel'] = Contact::FRIEND;
} elseif (!empty($data->viewer->following) && empty($data->viewer->followedBy)) {
$fields['rel'] = Contact::SHARING;
} elseif (empty($data->viewer->following) && !empty($data->viewer->followedBy)) {
$fields['rel'] = Contact::FOLLOWER;
} else {
$fields['rel'] = Contact::NOTHING;
}
}
Logger::debug('Got updated contact fields', ['uid' => $uid, 'url' => $fields['url']]);
return $fields;
} }
function bluesky_get_did(int $uid, string $handle): string function bluesky_get_did(int $uid, string $handle): string

View file

@ -18,11 +18,11 @@ use Friendica\Core\Logger;
use Friendica\Core\Protocol; use Friendica\Core\Protocol;
use Friendica\Core\Renderer; use Friendica\Core\Renderer;
use Friendica\Core\System; use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\DBA; use Friendica\Database\DBA;
use Friendica\DI; use Friendica\DI;
use Friendica\Model\Contact; use Friendica\Model\Contact;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\ItemURI;
use Friendica\Model\Photo; use Friendica\Model\Photo;
use Friendica\Model\Post; use Friendica\Model\Post;
use Friendica\Model\Tag; use Friendica\Model\Tag;
@ -79,7 +79,7 @@ function tumblr_check_item_notification(array &$notification_data)
return; return;
} }
$own_user = Contact::selectFirst(['url', 'alias'], ['uid' => $notification_data['uid'], 'poll' => 'tumblr::' . $page]); $own_user = Contact::selectFirst(['url', 'alias'], ['network' => Protocol::TUMBLR, 'uid' => [0, $notification_data['uid']], 'poll' => 'tumblr::' . $page]);
if ($own_user) { if ($own_user) {
$notification_data['profiles'][] = $own_user['url']; $notification_data['profiles'][] = $own_user['url'];
$notification_data['profiles'][] = $own_user['alias']; $notification_data['profiles'][] = $own_user['alias'];
@ -444,6 +444,18 @@ function tumblr_cron()
Logger::notice('importing timeline - done', ['user' => $pconfig['uid']]); Logger::notice('importing timeline - done', ['user' => $pconfig['uid']]);
} }
$last_clean = DI::keyValue()->get('tumblr_last_clean');
if (empty($last_clean) || ($last_clean + 86400 < time())) {
Logger::notice('Start contact cleanup');
$contacts = DBA::select('account-user-view', ['id', 'pid'], ["`network` = ? AND `uid` != ? AND `rel` = ?", Protocol::TUMBLR, 0, Contact::NOTHING]);
while ($contact = DBA::fetch($contacts)) {
Worker::add(Worker::PRIORITY_LOW, 'MergeContact', $contact['pid'], $contact['id'], 0);
}
DBA::close($contacts);
DI::keyValue()->set('tumblr_last_clean', time());
Logger::notice('Contact cleanup done');
}
Logger::notice('cron_end'); Logger::notice('cron_end');
DI::keyValue()->set('tumblr_last_poll', time()); DI::keyValue()->set('tumblr_last_poll', time());
@ -1042,39 +1054,58 @@ function tumblr_get_type_replacement(array $data, string $plink): string
*/ */
function tumblr_get_contact(stdClass $blog, int $uid): array function tumblr_get_contact(stdClass $blog, int $uid): array
{ {
$condition = ['network' => Protocol::TUMBLR, 'uid' => $uid, 'poll' => 'tumblr::' . $blog->uuid]; $condition = ['network' => Protocol::TUMBLR, 'uid' => 0, 'poll' => 'tumblr::' . $blog->uuid];
$contact = Contact::selectFirst([], $condition); $contact = Contact::selectFirst(['id', 'updated'], $condition);
if (!empty($contact) && (strtotime($contact['updated']) >= $blog->updated)) {
return $contact; $update = empty($contact) || $contact['updated'] < DateTimeFormat::utc('now -24 hours');
}
$public_fields = $fields = tumblr_get_contact_fields($blog, $uid, $update);
$avatar = $fields['avatar'] ?? '';
unset($fields['avatar']);
unset($public_fields['avatar']);
$public_fields['uid'] = 0;
$public_fields['rel'] = Contact::NOTHING;
if (empty($contact)) { if (empty($contact)) {
$cid = tumblr_insert_contact($blog, $uid); $cid = Contact::insert($public_fields);
} else { } else {
$cid = $contact['id']; $cid = $contact['id'];
Contact::update($public_fields, ['id' => $cid], true);
} }
$condition['uid'] = 0; if ($uid != 0) {
$condition = ['network' => Protocol::TUMBLR, 'uid' => $uid, 'poll' => 'tumblr::' . $blog->uuid];
$contact = Contact::selectFirst([], $condition); $contact = Contact::selectFirst(['id', 'rel', 'uid'], $condition);
if (empty($contact)) { if (!isset($fields['rel']) && isset($contact['rel'])) {
$pcid = tumblr_insert_contact($blog, 0); $fields['rel'] = $contact['rel'];
} elseif (!isset($fields['rel'])) {
$fields['rel'] = Contact::NOTHING;
}
}
if (($uid != 0) && ($fields['rel'] != Contact::NOTHING)) {
if (empty($contact)) {
$cid = Contact::insert($fields);
} else {
$cid = $contact['id'];
Contact::update($fields, ['id' => $cid], true);
}
Logger::debug('Get user contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
} else { } else {
$pcid = $contact['id']; Logger::debug('Get public contact', ['id' => $cid, 'uid' => $uid, 'update' => $update]);
} }
tumblr_update_contact($blog, $uid, $cid, $pcid); if (!empty($avatar)) {
Contact::updateAvatar($cid, $avatar);
}
return Contact::getById($cid); return Contact::getById($cid);
} }
/** function tumblr_get_contact_fields(stdClass $blog, int $uid, bool $update): array
* Create a new contact
*
* @param stdClass $blog
* @param integer $uid
* @return void
*/
function tumblr_insert_contact(stdClass $blog, int $uid)
{ {
$baseurl = 'https://tumblr.com'; $baseurl = 'https://tumblr.com';
$url = $baseurl . '/' . $blog->name; $url = $baseurl . '/' . $blog->name;
@ -1098,63 +1129,37 @@ function tumblr_insert_contact(stdClass $blog, int $uid)
'about' => HTML::toBBCode($blog->description), 'about' => HTML::toBBCode($blog->description),
'updated' => date(DateTimeFormat::MYSQL, $blog->updated) 'updated' => date(DateTimeFormat::MYSQL, $blog->updated)
]; ];
return Contact::insert($fields);
}
/** if (!$update) {
* Updates the given contact for the given user and proviced contact ids Logger::debug('Got contact fields', ['uid' => $uid, 'url' => $fields['url']]);
* return $fields;
* @param stdClass $blog }
* @param integer $uid
* @param integer $cid
* @param integer $pcid
* @return void
*/
function tumblr_update_contact(stdClass $blog, int $uid, int $cid, int $pcid)
{
$info = tumblr_get($uid, 'blog/' . $blog->uuid . '/info'); $info = tumblr_get($uid, 'blog/' . $blog->uuid . '/info');
if ($info->meta->status > 399) { if ($info->meta->status > 399) {
Logger::notice('Error fetching dashboard', ['meta' => $info->meta, 'response' => $info->response, 'errors' => $info->errors]); Logger::notice('Error fetching blog info', ['meta' => $info->meta, 'response' => $info->response, 'errors' => $info->errors]);
return; return $fields;
} }
$avatar = $info->response->blog->avatar; $avatar = $info->response->blog->avatar;
if (!empty($avatar)) { if (!empty($avatar)) {
Contact::updateAvatar($cid, $avatar[0]->url); $fields['avatar'] = $avatar[0]->url;
} }
$baseurl = 'https://tumblr.com';
$url = $baseurl . '/' . $info->response->blog->name;
if ($info->response->blog->followed && $info->response->blog->subscribed) { if ($info->response->blog->followed && $info->response->blog->subscribed) {
$rel = Contact::FRIEND; $fields['rel'] = Contact::FRIEND;
} elseif ($info->response->blog->followed && !$info->response->blog->subscribed) { } elseif ($info->response->blog->followed && !$info->response->blog->subscribed) {
$rel = Contact::SHARING; $fields['rel'] = Contact::SHARING;
} elseif (!$info->response->blog->followed && $info->response->blog->subscribed) { } elseif (!$info->response->blog->followed && $info->response->blog->subscribed) {
$rel = Contact::FOLLOWER; $fields['rel'] = Contact::FOLLOWER;
} else { } else {
$rel = Contact::NOTHING; $fields['rel'] = Contact::NOTHING;
} }
$uri_id = ItemURI::getIdByURI($url); $fields['header'] = $info->response->blog->theme->header_image_focused;
$fields = [
'url' => $url,
'nurl' => Strings::normaliseLink($url),
'uri-id' => $uri_id,
'alias' => $info->response->blog->url,
'name' => $info->response->blog->title ?: $info->response->blog->name,
'nick' => $info->response->blog->name,
'addr' => $info->response->blog->name . '@tumblr.com',
'about' => HTML::toBBCode($info->response->blog->description),
'updated' => date(DateTimeFormat::MYSQL, $info->response->blog->updated),
'header' => $info->response->blog->theme->header_image_focused,
'rel' => $rel,
];
Contact::update($fields, ['id' => $cid]); Logger::debug('Got updated contact fields', ['uid' => $uid, 'url' => $fields['url']]);
return $fields;
$fields['rel'] = Contact::NOTHING;
Contact::update($fields, ['id' => $pcid]);
} }
/** /**