Merge pull request 'Bluesky: Support personal data servers' (#1437) from heluecht/friendica-addons:bluesky-pds into 2023.09-rc

Reviewed-on: https://git.friendi.ca/friendica/friendica-addons/pulls/1437
pull/1438/head
Hypolite Petovan 2023-11-20 00:48:07 +01:00
commit 1c91ee200e
3 changed files with 86 additions and 39 deletions

View File

@ -36,6 +36,7 @@ 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\GServer;
use Friendica\Model\Item; use Friendica\Model\Item;
use Friendica\Model\ItemURI; use Friendica\Model\ItemURI;
use Friendica\Model\Photo; use Friendica\Model\Photo;
@ -49,9 +50,15 @@ use Friendica\Util\DateTimeFormat;
use Friendica\Util\Strings; use Friendica\Util\Strings;
const BLUESKY_DEFAULT_POLL_INTERVAL = 10; // given in minutes const BLUESKY_DEFAULT_POLL_INTERVAL = 10; // given in minutes
const BLUESKY_HOST = 'https://bsky.app'; // Hard wired until Bluesky will run on multiple systems
const BLUESKY_IMAGE_SIZE = [1000000, 500000, 100000, 50000]; const BLUESKY_IMAGE_SIZE = [1000000, 500000, 100000, 50000];
/*
* (Currently) hard wired paths for Bluesky services
*/
const BLUESKY_DIRECTORY = 'https://plc.directory'; // Path to the directory server service to fetch the PDS of a given DID
const BLUESKY_PDS = 'https://bsky.social'; // Path to the personal data server service (PDS) to fetch the DID for a given handle
const BLUESKY_WEB = 'https://bsky.app'; // Path to the web interface with the user profile and posts
function bluesky_install() function bluesky_install()
{ {
Hook::register('load_config', __FILE__, 'bluesky_load_config'); Hook::register('load_config', __FILE__, 'bluesky_load_config');
@ -106,8 +113,8 @@ function bluesky_probe_detect(array &$hookData)
if (parse_url($hookData['uri'], PHP_URL_SCHEME) == 'did') { if (parse_url($hookData['uri'], PHP_URL_SCHEME) == 'did') {
$did = $hookData['uri']; $did = $hookData['uri'];
} elseif (preg_match('#^' . BLUESKY_HOST . '/profile/(.+)#', $hookData['uri'], $matches)) { } elseif (preg_match('#^' . BLUESKY_WEB . '/profile/(.+)#', $hookData['uri'], $matches)) {
$did = bluesky_get_did($pconfig['uid'], $matches[1]); $did = bluesky_get_did($matches[1]);
if (empty($did)) { if (empty($did)) {
return; return;
} }
@ -127,6 +134,8 @@ function bluesky_probe_detect(array &$hookData)
$hookData['result'] = bluesky_get_contact_fields($data, 0, false); $hookData['result'] = bluesky_get_contact_fields($data, 0, false);
$hookData['result']['baseurl'] = bluesky_get_pds($did);
// Preparing probe data. This differs slightly from the contact array // Preparing probe data. This differs slightly from the contact array
$hookData['result']['about'] = HTML::toBBCode($data->description ?? ''); $hookData['result']['about'] = HTML::toBBCode($data->description ?? '');
$hookData['result']['photo'] = $data->avatar ?? ''; $hookData['result']['photo'] = $data->avatar ?? '';
@ -152,11 +161,11 @@ function bluesky_item_by_link(array &$hookData)
return; return;
} }
if (!preg_match('#^' . BLUESKY_HOST . '/profile/(.+)/post/(.+)#', $hookData['uri'], $matches)) { if (!preg_match('#^' . BLUESKY_WEB . '/profile/(.+)/post/(.+)#', $hookData['uri'], $matches)) {
return; return;
} }
$did = bluesky_get_did($hookData['uid'], $matches[1]); $did = bluesky_get_did($matches[1]);
if (empty($did)) { if (empty($did)) {
return; return;
} }
@ -306,7 +315,7 @@ function bluesky_settings(array &$data)
$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;
$host = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'host') ?: 'https://bsky.social'; $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 = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did'); $did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
$token = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'access_token'); $token = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'access_token');
@ -321,7 +330,7 @@ function bluesky_settings(array &$data)
'$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.')],
'$host' => ['bluesky_host', DI::l10n()->t('Bluesky host'), $host, '', '', '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'],
'$handle' => ['bluesky_handle', DI::l10n()->t('Bluesky handle'), $handle], '$handle' => ['bluesky_handle', DI::l10n()->t('Bluesky handle'), $handle],
'$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'], '$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'],
'$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.")], '$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.")],
@ -343,26 +352,28 @@ function bluesky_settings_post(array &$b)
return; return;
} }
$old_host = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'host'); $old_pds = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'pds');
$old_handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle'); $old_handle = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle');
$old_did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did'); $old_did = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
$host = $_POST['bluesky_host'];
$handle = $_POST['bluesky_handle']; $handle = $_POST['bluesky_handle'];
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'post', intval($_POST['bluesky'])); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'post', intval($_POST['bluesky']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default', intval($_POST['bluesky_bydefault'])); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'post_by_default', intval($_POST['bluesky_bydefault']));
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'host', $host);
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']));
if (!empty($host) && !empty($handle)) { if (!empty($host) && !empty($handle)) {
if (empty($old_did) || $old_host != $host || $old_handle != $handle) { if (empty($old_did) || $old_handle != $handle) {
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'did', bluesky_get_did(DI::userSession()->getLocalUserId(), DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle'))); DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'did', bluesky_get_did(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'handle')));
}
if (empty($old_pds) || $old_handle != $handle) {
DI::pConfig()->set(DI::userSession()->getLocalUserId(), 'bluesky', 'pds', bluesky_get_pds(DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'bluesky', 'did')));
} }
} else { } else {
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'did'); DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'did');
DI::pConfig()->delete(DI::userSession()->getLocalUserId(), 'bluesky', 'pds');
} }
if (!empty($_POST['bluesky_password'])) { if (!empty($_POST['bluesky_password'])) {
@ -1452,10 +1463,9 @@ function bluesky_get_contact_fields(stdClass $author, int $uid, bool $update): a
'blocked' => false, 'blocked' => false,
'readonly' => false, 'readonly' => false,
'pending' => false, 'pending' => false,
'baseurl' => BLUESKY_HOST,
'url' => $author->did, 'url' => $author->did,
'nurl' => $author->did, 'nurl' => $author->did,
'alias' => BLUESKY_HOST . '/profile/' . $author->handle, 'alias' => BLUESKY_WEB . '/profile/' . $author->handle,
'name' => $author->displayName ?? $author->handle, 'name' => $author->displayName ?? $author->handle,
'nick' => $author->handle, 'nick' => $author->handle,
'addr' => $author->handle, 'addr' => $author->handle,
@ -1466,6 +1476,12 @@ function bluesky_get_contact_fields(stdClass $author, int $uid, bool $update): a
return $fields; return $fields;
} }
$fields['baseurl'] = bluesky_get_pds($author->did);
if (!empty($fields['baseurl'])) {
GServer::check($fields['baseurl'], Protocol::BLUESKY);
$fields['gsid'] = GServer::getID($fields['baseurl'], true);
}
$data = bluesky_xrpc_get($uid, 'app.bsky.actor.getProfile', ['actor' => $author->did]); $data = bluesky_xrpc_get($uid, 'app.bsky.actor.getProfile', ['actor' => $author->did]);
if (empty($data)) { if (empty($data)) {
Logger::debug('Error fetching contact fields', ['uid' => $uid, 'url' => $fields['url']]); Logger::debug('Error fetching contact fields', ['uid' => $uid, 'url' => $fields['url']]);
@ -1524,9 +1540,9 @@ function bluesky_get_preferences(int $uid): stdClass
return $data; return $data;
} }
function bluesky_get_did(int $uid, string $handle): string function bluesky_get_did(string $handle): string
{ {
$data = bluesky_get($uid, '/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle)); $data = bluesky_get(BLUESKY_PDS . '/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle));
if (empty($data)) { if (empty($data)) {
return ''; return '';
} }
@ -1534,6 +1550,33 @@ function bluesky_get_did(int $uid, string $handle): string
return $data->did; return $data->did;
} }
function bluesky_get_user_pds(int $uid): string
{
$pds = DI::pConfig()->get($uid, 'bluesky', 'pds');
if (!empty($pds)) {
return $pds;
}
$pds = bluesky_get_pds(DI::pConfig()->get($uid, 'bluesky', 'did'));
DI::pConfig()->set($uid, 'bluesky', 'pds', $pds);
return $pds;
}
function bluesky_get_pds(string $did): ?string
{
$data = bluesky_get(BLUESKY_DIRECTORY . '/' . $did);
if (empty($data) || empty($data->service)) {
return null;
}
foreach ($data->service as $service) {
if (($service->id == '#atproto_pds') && ($service->type == 'AtprotoPersonalDataServer') && !empty($service->serviceEndpoint)) {
return $service->serviceEndpoint;
}
}
return null;
}
function bluesky_get_token(int $uid): string function bluesky_get_token(int $uid): string
{ {
$token = DI::pConfig()->get($uid, 'bluesky', 'access_token'); $token = DI::pConfig()->get($uid, 'bluesky', 'access_token');
@ -1588,7 +1631,7 @@ function bluesky_xrpc_post(int $uid, string $url, $parameters): ?stdClass
function bluesky_post(int $uid, string $url, string $params, array $headers): ?stdClass function bluesky_post(int $uid, string $url, string $params, array $headers): ?stdClass
{ {
try { try {
$curlResult = DI::httpClient()->post(DI::pConfig()->get($uid, 'bluesky', 'host') . $url, $params, $headers); $curlResult = DI::httpClient()->post(bluesky_get_user_pds($uid) . $url, $params, $headers);
} catch (\Exception $e) { } catch (\Exception $e) {
Logger::notice('Exception on post', ['exception' => $e]); Logger::notice('Exception on post', ['exception' => $e]);
return null; return null;
@ -1608,13 +1651,13 @@ function bluesky_xrpc_get(int $uid, string $url, array $parameters = []): ?stdCl
$url .= '?' . http_build_query($parameters); $url .= '?' . http_build_query($parameters);
} }
return bluesky_get($uid, '/xrpc/' . $url, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]); return bluesky_get(bluesky_get_user_pds($uid) . '/xrpc/' . $url, HttpClientAccept::JSON, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . bluesky_get_token($uid)]]]);
} }
function bluesky_get(int $uid, string $url, string $accept_content = HttpClientAccept::DEFAULT, array $opts = []): ?stdClass function bluesky_get(string $url, string $accept_content = HttpClientAccept::DEFAULT, array $opts = []): ?stdClass
{ {
try { try {
$curlResult = DI::httpClient()->get(DI::pConfig()->get($uid, 'bluesky', 'host') . $url, $accept_content, $opts); $curlResult = DI::httpClient()->get($url, $accept_content, $opts);
} catch (\Exception $e) { } catch (\Exception $e) {
Logger::notice('Exception on get', ['exception' => $e]); Logger::notice('Exception on get', ['exception' => $e]);
return null; return null;

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: 2023-06-05 04:34+0000\n" "POT-Creation-Date: 2023-11-19 18:51+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,70 +17,74 @@ 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:314 #: bluesky.php:325
msgid "" msgid ""
"You are authenticated to Bluesky. For security reasons the password isn't " "You are authenticated to Bluesky. For security reasons the password isn't "
"stored." "stored."
msgstr "" msgstr ""
#: bluesky.php:314 #: bluesky.php:325
msgid "You are not authenticated. Please enter the app password." msgid "You are not authenticated. Please enter the app password."
msgstr "" msgstr ""
#: bluesky.php:318 #: bluesky.php:329
msgid "Enable Bluesky Post Addon" msgid "Enable Bluesky Post Addon"
msgstr "" msgstr ""
#: bluesky.php:319 #: bluesky.php:330
msgid "Post to Bluesky by default" msgid "Post to Bluesky by default"
msgstr "" msgstr ""
#: bluesky.php:320 #: bluesky.php:331
msgid "Import the remote timeline" msgid "Import the remote timeline"
msgstr "" msgstr ""
#: bluesky.php:321 #: bluesky.php:332
msgid "Import the pinned feeds" msgid "Import the pinned feeds"
msgstr "" msgstr ""
#: bluesky.php:321 #: bluesky.php:332
msgid "" msgid ""
"When activated, Posts will be imported from all the feeds that you pinned in " "When activated, Posts will be imported from all the feeds that you pinned in "
"Bluesky." "Bluesky."
msgstr "" msgstr ""
#: bluesky.php:322 #: bluesky.php:333
msgid "Bluesky host" msgid "Personal Data Server"
msgstr "" msgstr ""
#: bluesky.php:323 #: bluesky.php:333
msgid "The personal data server (PDS) is the system that hosts your profile."
msgstr ""
#: bluesky.php:334
msgid "Bluesky handle" msgid "Bluesky handle"
msgstr "" msgstr ""
#: bluesky.php:324 #: bluesky.php:335
msgid "Bluesky DID" msgid "Bluesky DID"
msgstr "" msgstr ""
#: bluesky.php:324 #: bluesky.php:335
msgid "" msgid ""
"This is the unique identifier. It will be fetched automatically, when the " "This is the unique identifier. It will be fetched automatically, when the "
"handle is entered." "handle is entered."
msgstr "" msgstr ""
#: bluesky.php:325 #: bluesky.php:336
msgid "Bluesky app password" msgid "Bluesky app password"
msgstr "" msgstr ""
#: bluesky.php:325 #: bluesky.php:336
msgid "" msgid ""
"Please don't add your real password here, but instead create a specific app " "Please don't add your real password here, but instead create a specific app "
"password in the Bluesky settings." "password in the Bluesky settings."
msgstr "" msgstr ""
#: bluesky.php:331 #: bluesky.php:342
msgid "Bluesky Import/Export" msgid "Bluesky Import/Export"
msgstr "" msgstr ""
#: bluesky.php:382 #: bluesky.php:395
msgid "Post to Bluesky" msgid "Post to Bluesky"
msgstr "" msgstr ""

View File

@ -3,7 +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_input.tpl" field=$host}} {{include file="field_input.tpl" field=$pds}}
{{include file="field_input.tpl" field=$handle}} {{include file="field_input.tpl" field=$handle}}
{{include file="field_input.tpl" field=$did}} {{include file="field_input.tpl" field=$did}}
{{include file="field_input.tpl" field=$password}} {{include file="field_input.tpl" field=$password}}