mirror of
https://github.com/friendica/friendica
synced 2024-12-22 19:20:17 +00:00
Merge pull request #14596 from annando/probe-at-proto
Native probe support for AT-Proto
This commit is contained in:
commit
a7160e4a1f
6 changed files with 332 additions and 43 deletions
|
@ -302,7 +302,7 @@ class Protocol
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($protocol, array_merge(self::NATIVE_SUPPORT, [self::ZOT, self::PHANTOM]))) {
|
if (in_array($protocol, array_merge(self::NATIVE_SUPPORT, [self::ZOT, self::BLUESKY, self::PHANTOM]))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,7 @@ abstract class DI
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return AtProtocol\Arguments
|
* @return ATProtocol\Actor
|
||||||
*/
|
*/
|
||||||
public static function atpActor()
|
public static function atpActor()
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,6 +10,7 @@ namespace Friendica\Network;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
use DomXPath;
|
use DomXPath;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Friendica\Content\Text\HTML;
|
||||||
use Friendica\Core\Hook;
|
use Friendica\Core\Hook;
|
||||||
use Friendica\Core\Logger;
|
use Friendica\Core\Logger;
|
||||||
use Friendica\Core\Protocol;
|
use Friendica\Core\Protocol;
|
||||||
|
@ -24,6 +25,7 @@ use Friendica\Network\HTTPClient\Client\HttpClientOptions;
|
||||||
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
|
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
|
||||||
use Friendica\Protocol\ActivityNamespace;
|
use Friendica\Protocol\ActivityNamespace;
|
||||||
use Friendica\Protocol\ActivityPub;
|
use Friendica\Protocol\ActivityPub;
|
||||||
|
use Friendica\Protocol\ATProtocol;
|
||||||
use Friendica\Protocol\Diaspora;
|
use Friendica\Protocol\Diaspora;
|
||||||
use Friendica\Protocol\Email;
|
use Friendica\Protocol\Email;
|
||||||
use Friendica\Protocol\Feed;
|
use Friendica\Protocol\Feed;
|
||||||
|
@ -724,8 +726,8 @@ class Probe
|
||||||
|
|
||||||
$parts = parse_url($uri);
|
$parts = parse_url($uri);
|
||||||
if (empty($parts['scheme']) && empty($parts['host']) && (empty($parts['path']) || strpos($parts['path'], '@') === false)) {
|
if (empty($parts['scheme']) && empty($parts['host']) && (empty($parts['path']) || strpos($parts['path'], '@') === false)) {
|
||||||
Logger::info('URI was not detectable', ['uri' => $uri]);
|
Logger::info('URI was not detectable, probe for AT Protocol now', ['uri' => $uri]);
|
||||||
return [];
|
return self::atProtocol($uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the URI starts with "mailto:" then jump directly to the mail detection
|
// If the URI starts with "mailto:" then jump directly to the mail detection
|
||||||
|
@ -749,6 +751,10 @@ class Probe
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($data)) {
|
if (empty($data)) {
|
||||||
|
$data = self::atProtocol($uri);
|
||||||
|
if (!empty($data)) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
if (!empty($parts['scheme'])) {
|
if (!empty($parts['scheme'])) {
|
||||||
return self::feed($uri);
|
return self::feed($uri);
|
||||||
} elseif (!empty($uid)) {
|
} elseif (!empty($uid)) {
|
||||||
|
@ -1677,6 +1683,75 @@ class Probe
|
||||||
return (string)Uri::fromParts((array)(array)$baseParts);
|
return (string)Uri::fromParts((array)(array)$baseParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for AT Protocol (Bluesky)
|
||||||
|
*
|
||||||
|
* @param string $uri Profile link
|
||||||
|
* @return array Profile data or empty array
|
||||||
|
*/
|
||||||
|
private static function atProtocol(string $uri): array
|
||||||
|
{
|
||||||
|
if (parse_url($uri, PHP_URL_SCHEME) == 'did') {
|
||||||
|
$did = $uri;
|
||||||
|
} elseif (parse_url($uri, PHP_URL_PATH) == $uri && strpos($uri, '@') === false) {
|
||||||
|
$did = DI::atProtocol()->getDid($uri);
|
||||||
|
if (empty($did)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} elseif (Network::isValidHttpUrl($uri)) {
|
||||||
|
$did = DI::atProtocol()->getDidByProfile($uri);
|
||||||
|
if (empty($did)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = DI::atProtocol()->XRPCGet('app.bsky.actor.getProfile', ['actor' => $did]);
|
||||||
|
if (empty($profile) || empty($profile->did)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$nick = $profile->handle ?? $profile->did;
|
||||||
|
$name = $profile->displayName ?? $nick;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'network' => Protocol::BLUESKY,
|
||||||
|
'url' => $profile->did,
|
||||||
|
'alias' => ATProtocol::WEB . '/profile/' . $nick,
|
||||||
|
'name' => $name ?: $nick,
|
||||||
|
'nick' => $nick,
|
||||||
|
'addr' => $nick,
|
||||||
|
'poll' => ATProtocol::WEB . '/profile/' . $profile->did . '/rss',
|
||||||
|
'photo' => $profile->avatar ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($profile->description)) {
|
||||||
|
$data['about'] = HTML::toBBCode($profile->description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($profile->banner)) {
|
||||||
|
$data['header'] = $profile->banner;
|
||||||
|
}
|
||||||
|
|
||||||
|
$directory = DI::atProtocol()->get(ATProtocol::DIRECTORY . '/' . $profile->did);
|
||||||
|
if (!empty($directory)) {
|
||||||
|
foreach ($directory->service as $service) {
|
||||||
|
if (($service->id == '#atproto_pds') && ($service->type == 'AtprotoPersonalDataServer') && !empty($service->serviceEndpoint)) {
|
||||||
|
$data['baseurl'] = $service->serviceEndpoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($directory->verificationMethod as $method) {
|
||||||
|
if (!empty($method->publicKeyMultibase)) {
|
||||||
|
$data['pubkey'] = $method->publicKeyMultibase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for feed contact
|
* Check for feed contact
|
||||||
*
|
*
|
||||||
|
|
|
@ -66,6 +66,11 @@ final class ATProtocol
|
||||||
$this->httpClient = $httpClient;
|
$this->httpClient = $httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of user ids who want to import the Bluesky timeline
|
||||||
|
*
|
||||||
|
* @return array user ids
|
||||||
|
*/
|
||||||
public function getUids(): array
|
public function getUids(): array
|
||||||
{
|
{
|
||||||
$uids = [];
|
$uids = [];
|
||||||
|
@ -92,6 +97,15 @@ final class ATProtocol
|
||||||
return $uids;
|
return $uids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches XRPC data
|
||||||
|
* @see https://atproto.com/specs/xrpc#lexicon-http-endpoints
|
||||||
|
*
|
||||||
|
* @param string $url for example "app.bsky.feed.getTimeline"
|
||||||
|
* @param array $parameters Array with parameters
|
||||||
|
* @param integer $uid User ID
|
||||||
|
* @return stdClass|null Fetched data
|
||||||
|
*/
|
||||||
public function XRPCGet(string $url, array $parameters = [], int $uid = 0): ?stdClass
|
public function XRPCGet(string $url, array $parameters = [], int $uid = 0): ?stdClass
|
||||||
{
|
{
|
||||||
if (!empty($parameters)) {
|
if (!empty($parameters)) {
|
||||||
|
@ -119,6 +133,13 @@ final class ATProtocol
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data from the given URL via GET and return it as a JSON class
|
||||||
|
*
|
||||||
|
* @param string $url HTTP URL
|
||||||
|
* @param array $opts HTTP options
|
||||||
|
* @return stdClass|null Fetched data
|
||||||
|
*/
|
||||||
public function get(string $url, array $opts = []): ?stdClass
|
public function get(string $url, array $opts = []): ?stdClass
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
@ -141,12 +162,30 @@ final class ATProtocol
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform an XRPC post for a given user
|
||||||
|
* @see https://atproto.com/specs/xrpc#lexicon-http-endpoints
|
||||||
|
*
|
||||||
|
* @param integer $uid User ID
|
||||||
|
* @param string $url Endpoints like "com.atproto.repo.createRecord"
|
||||||
|
* @param [type] $parameters array or StdClass with parameters
|
||||||
|
* @return stdClass|null
|
||||||
|
*/
|
||||||
public function XRPCPost(int $uid, string $url, $parameters): ?stdClass
|
public function XRPCPost(int $uid, string $url, $parameters): ?stdClass
|
||||||
{
|
{
|
||||||
$data = $this->post($uid, '/xrpc/' . $url, json_encode($parameters), ['Content-type' => 'application/json', 'Authorization' => ['Bearer ' . $this->getUserToken($uid)]]);
|
$data = $this->post($uid, '/xrpc/' . $url, json_encode($parameters), ['Content-type' => 'application/json', 'Authorization' => ['Bearer ' . $this->getUserToken($uid)]]);
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post data to the user PDS
|
||||||
|
*
|
||||||
|
* @param integer $uid User ID
|
||||||
|
* @param string $url HTTP URL without the hostname
|
||||||
|
* @param string $params Parameter string
|
||||||
|
* @param array $headers HTTP header information
|
||||||
|
* @return stdClass|null
|
||||||
|
*/
|
||||||
public function post(int $uid, string $url, string $params, array $headers): ?stdClass
|
public function post(int $uid, string $url, string $params, array $headers): ?stdClass
|
||||||
{
|
{
|
||||||
$pds = $this->getUserPds($uid);
|
$pds = $this->getUserPds($uid);
|
||||||
|
@ -180,6 +219,13 @@ final class ATProtocol
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the PDS for a given user
|
||||||
|
* @see https://atproto.com/guides/glossary#pds-personal-data-server
|
||||||
|
*
|
||||||
|
* @param integer $uid User ID or 0
|
||||||
|
* @return string|null PDS or null if the user has got no PDS assigned. If UID set to 0, the public api URL is used
|
||||||
|
*/
|
||||||
private function getUserPds(int $uid): ?string
|
private function getUserPds(int $uid): ?string
|
||||||
{
|
{
|
||||||
if ($uid == 0) {
|
if ($uid == 0) {
|
||||||
|
@ -205,6 +251,14 @@ final class ATProtocol
|
||||||
return $pds;
|
return $pds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the DID for a given user
|
||||||
|
* @see https://atproto.com/guides/glossary#did-decentralized-id
|
||||||
|
*
|
||||||
|
* @param integer $uid User ID
|
||||||
|
* @param boolean $refresh Default "false". If set to true, the DID is detected from the handle again.
|
||||||
|
* @return string|null DID or null if no DID has been found.
|
||||||
|
*/
|
||||||
public function getUserDid(int $uid, bool $refresh = false): ?string
|
public function getUserDid(int $uid, bool $refresh = false): ?string
|
||||||
{
|
{
|
||||||
if (!$this->pConfig->get($uid, 'bluesky', 'post')) {
|
if (!$this->pConfig->get($uid, 'bluesky', 'post')) {
|
||||||
|
@ -233,6 +287,12 @@ final class ATProtocol
|
||||||
return $did;
|
return $did;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the DID for a given handle
|
||||||
|
*
|
||||||
|
* @param string $handle The user handle
|
||||||
|
* @return string DID (did:plc:...)
|
||||||
|
*/
|
||||||
public function getDid(string $handle): string
|
public function getDid(string $handle): string
|
||||||
{
|
{
|
||||||
if ($handle == '') {
|
if ($handle == '') {
|
||||||
|
@ -268,6 +328,12 @@ final class ATProtocol
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a DID for a given profile URL
|
||||||
|
*
|
||||||
|
* @param string $url HTTP path to the profile in the format https://bsky.app/profile/username
|
||||||
|
* @return string DID (did:plc:...)
|
||||||
|
*/
|
||||||
public function getDidByProfile(string $url): string
|
public function getDidByProfile(string $url): string
|
||||||
{
|
{
|
||||||
if (preg_match('#^' . self::WEB . '/profile/(.+)#', $url, $matches)) {
|
if (preg_match('#^' . self::WEB . '/profile/(.+)#', $url, $matches)) {
|
||||||
|
@ -317,6 +383,13 @@ final class ATProtocol
|
||||||
return $ids['bsky_did'];
|
return $ids['bsky_did'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the DID of a given handle via a HTTP request to the .well-known URL.
|
||||||
|
* This is one of the ways, custom handles can be authorized.
|
||||||
|
*
|
||||||
|
* @param string $handle The user handle
|
||||||
|
* @return string DID (did:plc:...)
|
||||||
|
*/
|
||||||
private function getDidByWellknown(string $handle): string
|
private function getDidByWellknown(string $handle): string
|
||||||
{
|
{
|
||||||
$curlResult = $this->httpClient->get('http://' . $handle . '/.well-known/atproto-did');
|
$curlResult = $this->httpClient->get('http://' . $handle . '/.well-known/atproto-did');
|
||||||
|
@ -331,6 +404,13 @@ final class ATProtocol
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the DID of a given handle via a DND request.
|
||||||
|
* This is one of the ways, custom handles can be authorized.
|
||||||
|
*
|
||||||
|
* @param string $handle The user handle
|
||||||
|
* @return string DID (did:plc:...)
|
||||||
|
*/
|
||||||
private function getDidByDns(string $handle): string
|
private function getDidByDns(string $handle): string
|
||||||
{
|
{
|
||||||
$records = @dns_get_record('_atproto.' . $handle . '.', DNS_TXT);
|
$records = @dns_get_record('_atproto.' . $handle . '.', DNS_TXT);
|
||||||
|
@ -350,7 +430,13 @@ final class ATProtocol
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getPdsOfDid(string $did): ?string
|
/**
|
||||||
|
* Fetch the PDS of a given DID
|
||||||
|
*
|
||||||
|
* @param string $did DID (did:plc:...)
|
||||||
|
* @return string|null URL of the PDS, e.g. https://enoki.us-east.host.bsky.network
|
||||||
|
*/
|
||||||
|
public function getPdsOfDid(string $did): ?string
|
||||||
{
|
{
|
||||||
$data = $this->get(self::DIRECTORY . '/' . $did);
|
$data = $this->get(self::DIRECTORY . '/' . $did);
|
||||||
if (empty($data) || empty($data->service)) {
|
if (empty($data) || empty($data->service)) {
|
||||||
|
@ -366,6 +452,13 @@ final class ATProtocol
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the provided DID matches the handle
|
||||||
|
*
|
||||||
|
* @param string $did DID (did:plc:...)
|
||||||
|
* @param string $handle The user handle
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
private function isValidDid(string $did, string $handle): bool
|
private function isValidDid(string $did, string $handle): bool
|
||||||
{
|
{
|
||||||
$data = $this->get(self::DIRECTORY . '/' . $did);
|
$data = $this->get(self::DIRECTORY . '/' . $did);
|
||||||
|
@ -376,6 +469,12 @@ final class ATProtocol
|
||||||
return in_array('at://' . $handle, $data->alsoKnownAs);
|
return in_array('at://' . $handle, $data->alsoKnownAs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the user token for a given user
|
||||||
|
*
|
||||||
|
* @param integer $uid User ID
|
||||||
|
* @return string user token
|
||||||
|
*/
|
||||||
public function getUserToken(int $uid): string
|
public function getUserToken(int $uid): string
|
||||||
{
|
{
|
||||||
$token = $this->pConfig->get($uid, 'bluesky', 'access_token');
|
$token = $this->pConfig->get($uid, 'bluesky', 'access_token');
|
||||||
|
@ -390,6 +489,12 @@ final class ATProtocol
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh and returns the user token for a given user.
|
||||||
|
*
|
||||||
|
* @param integer $uid User ID
|
||||||
|
* @return string user token
|
||||||
|
*/
|
||||||
private function refreshUserToken(int $uid): string
|
private function refreshUserToken(int $uid): string
|
||||||
{
|
{
|
||||||
$token = $this->pConfig->get($uid, 'bluesky', 'refresh_token');
|
$token = $this->pConfig->get($uid, 'bluesky', 'refresh_token');
|
||||||
|
@ -412,6 +517,13 @@ final class ATProtocol
|
||||||
return $data->accessJwt;
|
return $data->accessJwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a user token for the given user
|
||||||
|
*
|
||||||
|
* @param integer $uid User ID
|
||||||
|
* @param string $password Application password
|
||||||
|
* @return string user token
|
||||||
|
*/
|
||||||
public function createUserToken(int $uid, string $password): string
|
public function createUserToken(int $uid, string $password): string
|
||||||
{
|
{
|
||||||
$did = $this->getUserDid($uid);
|
$did = $this->getUserDid($uid);
|
||||||
|
|
|
@ -35,7 +35,13 @@ class Actor
|
||||||
$this->atprotocol = $atprotocol;
|
$this->atprotocol = $atprotocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function syncContacts(int $uid)
|
/**
|
||||||
|
* Syncronize the contacts (followers, sharers) for the given user
|
||||||
|
*
|
||||||
|
* @param integer $uid User ID
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function syncContacts(int $uid): void
|
||||||
{
|
{
|
||||||
$this->logger->info('Sync contacts for user - start', ['uid' => $uid]);
|
$this->logger->info('Sync contacts for user - start', ['uid' => $uid]);
|
||||||
$contacts = Contact::selectToArray(['id', 'url', 'rel'], ['uid' => $uid, 'network' => Protocol::BLUESKY, 'rel' => [Contact::FRIEND, Contact::SHARING, Contact::FOLLOWER]]);
|
$contacts = Contact::selectToArray(['id', 'url', 'rel'], ['uid' => $uid, 'network' => Protocol::BLUESKY, 'rel' => [Contact::FRIEND, Contact::SHARING, Contact::FOLLOWER]]);
|
||||||
|
@ -93,9 +99,16 @@ class Actor
|
||||||
$this->logger->info('Sync contacts for user - done', ['uid' => $uid]);
|
$this->logger->info('Sync contacts for user - done', ['uid' => $uid]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateContactByDID(string $did)
|
/**
|
||||||
|
* Update a contact for a given DID and user id
|
||||||
|
*
|
||||||
|
* @param string $did DID (did:plc:...)
|
||||||
|
* @param integer $contact_uid User id of the contact to be updated
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function updateContactByDID(string $did, int $contact_uid): void
|
||||||
{
|
{
|
||||||
$profile = $this->atprotocol->XRPCGet('app.bsky.actor.getProfile', ['actor' => $did]);
|
$profile = $this->atprotocol->XRPCGet('app.bsky.actor.getProfile', ['actor' => $did], $contact_uid);
|
||||||
if (empty($profile) || empty($profile->did)) {
|
if (empty($profile) || empty($profile->did)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -139,20 +152,7 @@ class Actor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
Contact::update($fields, ['nurl' => $profile->did, 'network' => Protocol::BLUESKY]);
|
||||||
@todo Add this part when the function will be callable with a uid
|
|
||||||
if (!empty($profile->viewer)) {
|
|
||||||
if (!empty($profile->viewer->following) && !empty($profile->viewer->followedBy)) {
|
|
||||||
$fields['rel'] = Contact::FRIEND;
|
|
||||||
} elseif (!empty($profile->viewer->following) && empty($profile->viewer->followedBy)) {
|
|
||||||
$fields['rel'] = Contact::SHARING;
|
|
||||||
} elseif (empty($profile->viewer->following) && !empty($profile->viewer->followedBy)) {
|
|
||||||
$fields['rel'] = Contact::FOLLOWER;
|
|
||||||
} else {
|
|
||||||
$fields['rel'] = Contact::NOTHING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!empty($profile->avatar)) {
|
if (!empty($profile->avatar)) {
|
||||||
$contact = Contact::selectFirst(['id', 'avatar'], ['network' => Protocol::BLUESKY, 'nurl' => $did, 'uid' => 0]);
|
$contact = Contact::selectFirst(['id', 'avatar'], ['network' => Protocol::BLUESKY, 'nurl' => $did, 'uid' => 0]);
|
||||||
|
@ -161,16 +161,37 @@ class Actor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->notice('Update profile', ['did' => $profile->did, 'fields' => $fields]);
|
$this->logger->notice('Update global profile', ['did' => $profile->did, 'fields' => $fields]);
|
||||||
|
|
||||||
Contact::update($fields, ['nurl' => $profile->did, 'network' => Protocol::BLUESKY]);
|
if (!empty($profile->viewer) && ($contact_uid != 0)) {
|
||||||
|
if (!empty($profile->viewer->following) && !empty($profile->viewer->followedBy)) {
|
||||||
|
$user_fields = ['rel' => Contact::FRIEND];
|
||||||
|
} elseif (!empty($profile->viewer->following) && empty($profile->viewer->followedBy)) {
|
||||||
|
$user_fields = ['rel' => Contact::SHARING];
|
||||||
|
} elseif (empty($profile->viewer->following) && !empty($profile->viewer->followedBy)) {
|
||||||
|
$user_fields = ['rel' => Contact::FOLLOWER];
|
||||||
|
} else {
|
||||||
|
$user_fields = ['rel' => Contact::NOTHING];
|
||||||
|
}
|
||||||
|
Contact::update($user_fields, ['nurl' => $profile->did, 'network' => Protocol::BLUESKY, 'uid' => $contact_uid]);
|
||||||
|
$this->logger->notice('Update user profile', ['uid' => $contact_uid, 'did' => $profile->did, 'fields' => $user_fields]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getContactByDID(string $did, int $uid, int $contact_uid): array
|
/**
|
||||||
|
* Fetch and possibly create a contact array for a given DID
|
||||||
|
*
|
||||||
|
* @param string $did The contact DID
|
||||||
|
* @param integer $uid "0" when either the public contact or the user contact is desired
|
||||||
|
* @param integer $contact_uid If not found, the contact will be created for this user id
|
||||||
|
* @param boolean $auto_update Default "false". If activated, the contact will be updated every 24 hours
|
||||||
|
* @return array Contact array
|
||||||
|
*/
|
||||||
|
public function getContactByDID(string $did, int $uid, int $contact_uid, bool $auto_update = false): array
|
||||||
{
|
{
|
||||||
$contact = Contact::selectFirst([], ['network' => Protocol::BLUESKY, 'nurl' => $did, 'uid' => [$contact_uid, $uid]], ['order' => ['uid' => true]]);
|
$contact = Contact::selectFirst([], ['network' => Protocol::BLUESKY, 'nurl' => $did, 'uid' => [$contact_uid, $uid]], ['order' => ['uid' => true]]);
|
||||||
|
|
||||||
if (!empty($contact)) {
|
if (!empty($contact) && (!$auto_update || ($contact['updated'] > DateTimeFormat::utc('now -24 hours')))) {
|
||||||
return $contact;
|
return $contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +217,7 @@ class Actor
|
||||||
$cid = $contact['id'];
|
$cid = $contact['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->updateContactByDID($did);
|
$this->updateContactByDID($did, $contact_uid);
|
||||||
|
|
||||||
return Contact::getById($cid);
|
return Contact::getById($cid);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,6 @@ class Jetstream
|
||||||
private $uids = [];
|
private $uids = [];
|
||||||
private $self = [];
|
private $self = [];
|
||||||
private $capped = false;
|
private $capped = false;
|
||||||
private $next_stat = 0;
|
|
||||||
|
|
||||||
/** @var LoggerInterface */
|
/** @var LoggerInterface */
|
||||||
private $logger;
|
private $logger;
|
||||||
|
@ -74,10 +73,12 @@ class Jetstream
|
||||||
$this->processor = $processor;
|
$this->processor = $processor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// *****************************************
|
/**
|
||||||
// * Listener
|
* Listen to incoming webstream messages from Jetstream
|
||||||
// *****************************************
|
*
|
||||||
public function listen()
|
* @return void
|
||||||
|
*/
|
||||||
|
public function listen(): void
|
||||||
{
|
{
|
||||||
$timeout = 300;
|
$timeout = 300;
|
||||||
$timeout_limit = 10;
|
$timeout_limit = 10;
|
||||||
|
@ -137,7 +138,12 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function incrementMessages()
|
/**
|
||||||
|
* Increment the message counter for the statistics page
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function incrementMessages(): void
|
||||||
{
|
{
|
||||||
$packets = (int)($this->keyValue->get('jetstream_messages') ?? 0);
|
$packets = (int)($this->keyValue->get('jetstream_messages') ?? 0);
|
||||||
if ($packets >= PHP_INT_MAX) {
|
if ($packets >= PHP_INT_MAX) {
|
||||||
|
@ -146,6 +152,11 @@ class Jetstream
|
||||||
$this->keyValue->set('jetstream_messages', $packets + 1);
|
$this->keyValue->set('jetstream_messages', $packets + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize contacts for all active users
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
private function syncContacts()
|
private function syncContacts()
|
||||||
{
|
{
|
||||||
$active_uids = $this->atprotocol->getUids();
|
$active_uids = $this->atprotocol->getUids();
|
||||||
|
@ -158,6 +169,11 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set options like the followed DIDs
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
private function setOptions()
|
private function setOptions()
|
||||||
{
|
{
|
||||||
$active_uids = $this->atprotocol->getUids();
|
$active_uids = $this->atprotocol->getUids();
|
||||||
|
@ -219,6 +235,15 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of DIDs provided by an array of contacts
|
||||||
|
*
|
||||||
|
* @param array $contacts Array of contact records
|
||||||
|
* @param array $uids Array with the user ids with enabled bluesky timeline import
|
||||||
|
* @param integer $did_limit Maximum limit of entries
|
||||||
|
* @param array $dids Array of DIDs that are added to the output list
|
||||||
|
* @return array DIDs
|
||||||
|
*/
|
||||||
private function addDids(array $contacts, array $uids, int $did_limit, array $dids): array
|
private function addDids(array $contacts, array $uids, int $did_limit, array $dids): array
|
||||||
{
|
{
|
||||||
foreach ($contacts as $contact) {
|
foreach ($contacts as $contact) {
|
||||||
|
@ -233,7 +258,13 @@ class Jetstream
|
||||||
return $dids;
|
return $dids;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function route(stdClass $data)
|
/**
|
||||||
|
* Route incoming messages
|
||||||
|
*
|
||||||
|
* @param stdClass $data message object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function route(stdClass $data): void
|
||||||
{
|
{
|
||||||
Item::incrementInbound(Protocol::BLUESKY);
|
Item::incrementInbound(Protocol::BLUESKY);
|
||||||
|
|
||||||
|
@ -254,7 +285,13 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function routeCommits(stdClass $data)
|
/**
|
||||||
|
* Route incoming commit messages
|
||||||
|
*
|
||||||
|
* @param stdClass $data message object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function routeCommits(stdClass $data): void
|
||||||
{
|
{
|
||||||
$drift = $this->getDrift($data);
|
$drift = $this->getDrift($data);
|
||||||
$this->logger->notice('Received commit', ['time' => date(DateTimeFormat::ATOM, $data->time_us / 1000000), 'drift' => $drift, 'capped' => $this->capped, 'did' => $data->did, 'operation' => $data->commit->operation, 'collection' => $data->commit->collection, 'timestamp' => $data->time_us]);
|
$this->logger->notice('Received commit', ['time' => date(DateTimeFormat::ATOM, $data->time_us / 1000000), 'drift' => $drift, 'capped' => $this->capped, 'did' => $data->did, 'operation' => $data->commit->operation, 'collection' => $data->commit->collection, 'timestamp' => $data->time_us]);
|
||||||
|
@ -304,6 +341,12 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the drift between the server timestamp and the current time.
|
||||||
|
*
|
||||||
|
* @param stdClass $data message object
|
||||||
|
* @return integer The calculated drift
|
||||||
|
*/
|
||||||
private function getDrift(stdClass $data): int
|
private function getDrift(stdClass $data): int
|
||||||
{
|
{
|
||||||
$drift = max(0, round(time() - $data->time_us / 1000000));
|
$drift = max(0, round(time() - $data->time_us / 1000000));
|
||||||
|
@ -321,7 +364,14 @@ class Jetstream
|
||||||
return $drift;
|
return $drift;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function routePost(stdClass $data, int $drift)
|
/**
|
||||||
|
* Route app.bsky.feed.post commits
|
||||||
|
*
|
||||||
|
* @param stdClass $data message object
|
||||||
|
* @param integer $drift
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function routePost(stdClass $data, int $drift): void
|
||||||
{
|
{
|
||||||
switch ($data->commit->operation) {
|
switch ($data->commit->operation) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
@ -338,7 +388,14 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function routeRepost(stdClass $data, int $drift)
|
/**
|
||||||
|
* Route app.bsky.feed.repost commits
|
||||||
|
*
|
||||||
|
* @param stdClass $data message object
|
||||||
|
* @param integer $drift
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function routeRepost(stdClass $data, int $drift): void
|
||||||
{
|
{
|
||||||
switch ($data->commit->operation) {
|
switch ($data->commit->operation) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
@ -355,7 +412,13 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function routeLike(stdClass $data)
|
/**
|
||||||
|
* Route app.bsky.feed.like commits
|
||||||
|
*
|
||||||
|
* @param stdClass $data message object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function routeLike(stdClass $data): void
|
||||||
{
|
{
|
||||||
switch ($data->commit->operation) {
|
switch ($data->commit->operation) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
@ -372,7 +435,13 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function routeProfile(stdClass $data)
|
/**
|
||||||
|
* Route app.bsky.actor.profile commits
|
||||||
|
*
|
||||||
|
* @param stdClass $data message object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function routeProfile(stdClass $data): void
|
||||||
{
|
{
|
||||||
switch ($data->commit->operation) {
|
switch ($data->commit->operation) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
@ -380,11 +449,11 @@ class Jetstream
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'create':
|
case 'create':
|
||||||
$this->actor->updateContactByDID($data->did);
|
$this->actor->updateContactByDID($data->did, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'update':
|
case 'update':
|
||||||
$this->actor->updateContactByDID($data->did);
|
$this->actor->updateContactByDID($data->did, 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -393,7 +462,13 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function routeFollow(stdClass $data)
|
/**
|
||||||
|
* Route app.bsky.graph.follow commits
|
||||||
|
*
|
||||||
|
* @param stdClass $data message object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function routeFollow(stdClass $data): void
|
||||||
{
|
{
|
||||||
switch ($data->commit->operation) {
|
switch ($data->commit->operation) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
@ -416,7 +491,13 @@ class Jetstream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function storeCommitMessage(stdClass $data)
|
/**
|
||||||
|
* Store commit messages for debugging purposes
|
||||||
|
*
|
||||||
|
* @param stdClass $data message object
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function storeCommitMessage(stdClass $data): void
|
||||||
{
|
{
|
||||||
if ($this->config->get('debug', 'jetstream_log')) {
|
if ($this->config->get('debug', 'jetstream_log')) {
|
||||||
$tempfile = tempnam(System::getTempPath(), 'at-proto.commit.' . $data->commit->collection . '.' . $data->commit->operation . '-');
|
$tempfile = tempnam(System::getTempPath(), 'at-proto.commit.' . $data->commit->collection . '.' . $data->commit->operation . '-');
|
||||||
|
|
Loading…
Reference in a new issue