+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_xrpc_get($pconfig['uid'], 'app.bsky.actor.getProfile', ['actor' => $did]);
+ if (empty($data)) {
+ return;
+ }
+
+ $hookData['result'] = bluesky_get_contact_fields($data, 0, false);
+
+ // Preparing probe data. This differs slightly from the contact array
+ $hookData['result']['about'] = HTML::toBBCode($data->description ?? '');
+ $hookData['result']['photo'] = $data->avatar ?? '';
+ $hookData['result']['header'] = $data->banner ?? '';
+ $hookData['result']['batch'] = '';
+ $hookData['result']['notify'] = '';
+ $hookData['result']['poll'] = '';
+ $hookData['result']['poco'] = '';
+ $hookData['result']['pubkey'] = '';
+ $hookData['result']['priority'] = 0;
+ $hookData['result']['guid'] = '';
+}
+
+function bluesky_item_by_link(array &$hookData)
+{
+ // Don't overwrite an existing result
+ if (isset($hookData['item_id'])) {
+ return;
+ }
+
+ $token = bluesky_get_token($hookData['uid']);
+ if (empty($token)) {
+ return;
+ }
+
+ if (!preg_match('#^' . BLUESKY_HOST . '/profile/(.+)/post/(.+)#', $hookData['uri'], $matches)) {
+ return;
+ }
+
+ $did = bluesky_get_did($hookData['uid'], $matches[1]);
+ if (empty($did)) {
+ return;
+ }
+
+ Logger::debug('Found bluesky post', ['url' => $hookData['uri'], 'handle' => $matches[1], 'did' => $did, 'cid' => $matches[2]]);
+
+ $uri = 'at://' . $did . '/app.bsky.feed.post/' . $matches[2];
+
+ $uri = bluesky_fetch_missing_post($uri, $hookData['uid'], 0, 0);
+ Logger::debug('Got post', ['profile' => $matches[1], 'cid' => $matches[2], 'result' => $uri]);
+ if (!empty($uri)) {
+ $item = Post::selectFirst(['id'], ['uri' => $uri, 'uid' => $hookData['uid']]);
+ if (!empty($item['id'])) {
+ $hookData['item_id'] = $item['id'];
+ }
+ }
+}
+
+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_xrpc_post($hook_data['uid'], 'com.atproto.repo.createRecord', $post);
+ 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_xrpc_get($hook_data['uid'], 'app.bsky.actor.getProfile', ['actor' => $hook_data['contact']['url']]);
+ 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_xrpc_post($hook_data['uid'], 'com.atproto.repo.createRecord', $post);
+ 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_xrpc_get($hook_data['uid'], 'app.bsky.actor.getProfile', ['actor' => $hook_data['contact']['url']]);
+ if (empty($data->viewer) || empty($data->viewer->blocking)) {
+ return;
+ }
+
+ bluesky_delete_post($data->viewer->blocking, $hook_data['uid']);
+
+ $hook_data['result'] = true;
+}
+