2023-05-21 18:54:02 +00:00
< ? php
/**
* Name : Bluesky Connector
* Description : Post to Bluesky
2023-05-24 05:49:26 +00:00
* Version : 1.1
2023-05-21 18:54:02 +00:00
* Author : Michael Vogel < https :// pirati . ca / profile / heluecht >
2023-05-24 05:49:26 +00:00
*
2023-05-23 05:23:13 +00:00
* @ todo
2023-06-06 20:33:10 +00:00
* Currently technical issues in the core :
2023-06-03 23:06:31 +00:00
* - Outgoing mentions
2023-05-24 05:49:26 +00:00
*
2023-06-11 19:24:44 +00:00
* At some point in time :
* - Sending Quote shares https :// atproto . com / lexicons / app - bsky - embed #appbskyembedrecord and https://atproto.com/lexicons/app-bsky-embed#appbskyembedrecordwithmedia
2023-06-12 22:06:31 +00:00
*
2023-06-03 23:06:31 +00:00
* Possibly not possible :
2023-05-23 05:23:13 +00:00
* - only fetch new posts
2023-05-26 20:54:00 +00:00
*
2023-06-03 23:06:31 +00:00
* Currently not possible , due to limitations in Friendica
* - mute contacts https :// atproto . com / lexicons / app - bsky - graph #appbskygraphmuteactor
* - unmute contacts https :// atproto . com / lexicons / app - bsky - graph #appbskygraphunmuteactor
2023-06-05 04:36:50 +00:00
*
* Possibly interesting :
* - https :// atproto . com / lexicons / com - atproto - label #comatprotolabelsubscribelabels
2023-05-21 18:54:02 +00:00
*/
use Friendica\Content\Text\BBCode ;
2023-05-23 05:23:13 +00:00
use Friendica\Content\Text\HTML ;
2023-05-21 18:54:02 +00:00
use Friendica\Content\Text\Plaintext ;
2023-06-05 04:36:50 +00:00
use Friendica\Core\Cache\Enum\Duration ;
2023-05-21 18:54:02 +00:00
use Friendica\Core\Config\Util\ConfigFileManager ;
use Friendica\Core\Hook ;
use Friendica\Core\Logger ;
2023-05-23 05:23:13 +00:00
use Friendica\Core\Protocol ;
2023-05-21 18:54:02 +00:00
use Friendica\Core\Renderer ;
2023-06-03 23:06:31 +00:00
use Friendica\Core\Worker ;
2023-05-23 05:23:13 +00:00
use Friendica\Database\DBA ;
2023-05-21 18:54:02 +00:00
use Friendica\DI ;
2023-05-23 05:23:13 +00:00
use Friendica\Model\Contact ;
2023-11-19 18:55:05 +00:00
use Friendica\Model\GServer ;
2023-05-21 18:54:02 +00:00
use Friendica\Model\Item ;
2023-05-23 05:23:13 +00:00
use Friendica\Model\ItemURI ;
2023-05-21 18:54:02 +00:00
use Friendica\Model\Photo ;
2023-05-23 05:23:13 +00:00
use Friendica\Model\Post ;
2023-11-25 22:00:45 +00:00
use Friendica\Model\Tag ;
2024-02-28 03:05:57 +00:00
use Friendica\Model\User ;
2023-05-21 18:54:02 +00:00
use Friendica\Network\HTTPClient\Client\HttpClientAccept ;
use Friendica\Network\HTTPClient\Client\HttpClientOptions ;
2024-09-03 11:28:49 +00:00
use Friendica\Network\HTTPClient\Client\HttpClientRequest ;
2023-08-15 20:25:17 +00:00
use Friendica\Object\Image ;
2023-05-23 05:23:13 +00:00
use Friendica\Protocol\Activity ;
2023-06-05 04:36:50 +00:00
use Friendica\Protocol\Relay ;
2023-05-21 18:54:02 +00:00
use Friendica\Util\DateTimeFormat ;
2024-09-03 11:28:49 +00:00
use Friendica\Util\Network ;
2023-05-26 20:54:00 +00:00
use Friendica\Util\Strings ;
2023-05-21 18:54:02 +00:00
2023-05-26 20:54:00 +00:00
const BLUESKY_DEFAULT_POLL_INTERVAL = 10 ; // given in minutes
2023-08-15 20:25:17 +00:00
const BLUESKY_IMAGE_SIZE = [ 1000000 , 500000 , 100000 , 50000 ];
2023-05-23 05:23:13 +00:00
2023-12-06 06:31:52 +00:00
const BLUEKSY_STATUS_UNKNOWN = 0 ;
const BLUEKSY_STATUS_TOKEN_OK = 1 ;
const BLUEKSY_STATUS_SUCCESS = 2 ;
const BLUEKSY_STATUS_API_FAIL = 10 ;
const BLUEKSY_STATUS_DID_FAIL = 11 ;
const BLUEKSY_STATUS_PDS_FAIL = 12 ;
const BLUEKSY_STATUS_TOKEN_FAIL = 13 ;
2023-11-19 18:55:05 +00:00
/*
* ( 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
2024-06-13 04:32:00 +00:00
const BLUESKY_HOSTNAME = 'bsky.social' ; // Host name to be added to the handle if incomplete
2023-11-19 18:55:05 +00:00
2023-05-21 18:54:02 +00:00
function bluesky_install ()
{
Hook :: register ( 'load_config' , __FILE__ , 'bluesky_load_config' );
Hook :: register ( 'hook_fork' , __FILE__ , 'bluesky_hook_fork' );
Hook :: register ( 'post_local' , __FILE__ , 'bluesky_post_local' );
Hook :: register ( 'notifier_normal' , __FILE__ , 'bluesky_send' );
Hook :: register ( 'jot_networks' , __FILE__ , 'bluesky_jot_nets' );
Hook :: register ( 'connector_settings' , __FILE__ , 'bluesky_settings' );
Hook :: register ( 'connector_settings_post' , __FILE__ , 'bluesky_settings_post' );
2023-05-23 05:23:13 +00:00
Hook :: register ( 'cron' , __FILE__ , 'bluesky_cron' );
2023-06-03 23:06:31 +00:00
Hook :: register ( 'support_follow' , __FILE__ , 'bluesky_support_follow' );
Hook :: register ( 'support_probe' , __FILE__ , 'bluesky_support_probe' );
Hook :: register ( 'follow' , __FILE__ , 'bluesky_follow' );
Hook :: register ( 'unfollow' , __FILE__ , 'bluesky_unfollow' );
Hook :: register ( 'block' , __FILE__ , 'bluesky_block' );
Hook :: register ( 'unblock' , __FILE__ , 'bluesky_unblock' );
2023-05-24 05:49:26 +00:00
Hook :: register ( 'check_item_notification' , __FILE__ , 'bluesky_check_item_notification' );
2023-06-03 23:06:31 +00:00
Hook :: register ( 'probe_detect' , __FILE__ , 'bluesky_probe_detect' );
2023-05-26 20:54:00 +00:00
Hook :: register ( 'item_by_link' , __FILE__ , 'bluesky_item_by_link' );
2023-05-21 18:54:02 +00:00
}
function bluesky_load_config ( ConfigFileManager $loader )
{
DI :: app () -> getConfigCache () -> load ( $loader -> loadAddonConfig ( 'bluesky' ), \Friendica\Core\Config\ValueObject\Cache :: SOURCE_STATIC );
}
2023-05-24 05:49:26 +00:00
function bluesky_check_item_notification ( array & $notification_data )
{
2024-05-03 02:58:45 +00:00
if ( empty ( $notification_data [ 'uid' ])) {
return ;
}
2024-04-28 10:36:47 +00:00
$did = bluesky_get_user_did ( $notification_data [ 'uid' ]);
2024-05-12 00:53:46 +00:00
if ( empty ( $did )) {
return ;
2023-05-24 05:49:26 +00:00
}
2024-05-12 00:53:46 +00:00
$notification_data [ 'profiles' ][] = $did ;
2023-05-24 05:49:26 +00:00
}
2023-06-03 23:06:31 +00:00
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' ];
2024-09-03 11:28:49 +00:00
} elseif ( parse_url ( $hookData [ 'uri' ], PHP_URL_PATH ) == $hookData [ 'uri' ] && strpos ( $hookData [ 'uri' ], '@' ) === false ) {
$did = bluesky_get_did ( $hookData [ 'uri' ], $pconfig [ 'uid' ]);
if ( empty ( $did )) {
return ;
}
} elseif ( Network :: isValidHttpUrl ( $hookData [ 'uri' ])) {
$did = bluesky_get_did_by_profile ( $hookData [ 'uri' ]);
2023-06-03 23:06:31 +00:00
if ( empty ( $did )) {
return ;
}
} else {
return ;
}
$token = bluesky_get_token ( $pconfig [ 'uid' ]);
if ( empty ( $token )) {
return ;
}
2023-06-13 20:43:51 +00:00
$data = bluesky_xrpc_get ( $pconfig [ 'uid' ], 'app.bsky.actor.getProfile' , [ 'actor' => $did ]);
2023-06-03 23:06:31 +00:00
if ( empty ( $data )) {
return ;
}
2024-09-03 11:28:49 +00:00
$hookData [ 'result' ] = bluesky_get_contact_fields ( $data , 0 , $pconfig [ 'uid' ], true );
2023-11-19 18:55:05 +00:00
2023-06-05 04:36:50 +00:00
// Preparing probe data. This differs slightly from the contact array
$hookData [ 'result' ][ 'photo' ] = $data -> avatar ? ? '' ;
$hookData [ 'result' ][ 'batch' ] = '' ;
$hookData [ 'result' ][ 'notify' ] = '' ;
$hookData [ 'result' ][ 'poll' ] = '' ;
$hookData [ 'result' ][ 'poco' ] = '' ;
$hookData [ 'result' ][ 'priority' ] = 0 ;
$hookData [ 'result' ][ 'guid' ] = '' ;
2023-06-03 23:06:31 +00:00
}
2023-05-26 20:54:00 +00:00
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 ;
}
2024-09-03 11:28:49 +00:00
$did = bluesky_get_did_by_profile ( $hookData [ 'uri' ]);
if ( empty ( $did )) {
2023-05-26 20:54:00 +00:00
return ;
}
2024-09-03 11:28:49 +00:00
if ( ! preg_match ( '#/profile/.+/post/(.+)#' , $hookData [ 'uri' ], $matches )) {
2023-05-26 20:54:00 +00:00
return ;
}
2024-09-03 11:28:49 +00:00
Logger :: debug ( 'Found bluesky post' , [ 'url' => $hookData [ 'uri' ], 'did' => $did , 'cid' => $matches [ 1 ]]);
2023-05-26 20:54:00 +00:00
2024-09-03 11:28:49 +00:00
$uri = 'at://' . $did . '/app.bsky.feed.post/' . $matches [ 1 ];
2023-05-26 20:54:00 +00:00
2024-02-24 06:48:19 +00:00
$uri = bluesky_fetch_missing_post ( $uri , $hookData [ 'uid' ], $hookData [ 'uid' ], Item :: PR_FETCHED , 0 , 0 , 0 );
2024-09-03 11:28:49 +00:00
Logger :: debug ( 'Got post' , [ 'did' => $did , 'cid' => $matches [ 1 ], 'result' => $uri ]);
2023-05-26 20:54:00 +00:00
if ( ! empty ( $uri )) {
$item = Post :: selectFirst ([ 'id' ], [ 'uri' => $uri , 'uid' => $hookData [ 'uid' ]]);
if ( ! empty ( $item [ 'id' ])) {
$hookData [ 'item_id' ] = $item [ 'id' ];
}
}
}
2023-06-03 23:06:31 +00:00
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'
];
2023-06-05 04:36:50 +00:00
2023-06-03 23:06:31 +00:00
$post = [
'collection' => 'app.bsky.graph.follow' ,
2024-05-12 00:53:46 +00:00
'repo' => bluesky_get_user_did ( $hook_data [ 'uid' ]),
2023-06-03 23:06:31 +00:00
'record' => $record
];
2023-06-05 04:36:50 +00:00
2023-06-13 20:43:51 +00:00
$activity = bluesky_xrpc_post ( $hook_data [ 'uid' ], 'com.atproto.repo.createRecord' , $post );
2023-06-03 23:06:31 +00:00
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 ;
}
2023-06-13 20:43:51 +00:00
$data = bluesky_xrpc_get ( $hook_data [ 'uid' ], 'app.bsky.actor.getProfile' , [ 'actor' => $hook_data [ 'contact' ][ 'url' ]]);
2023-06-03 23:06:31 +00:00
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'
];
2023-06-05 04:36:50 +00:00
2023-06-03 23:06:31 +00:00
$post = [
'collection' => 'app.bsky.graph.block' ,
2024-05-12 00:53:46 +00:00
'repo' => bluesky_get_user_did ( $hook_data [ 'uid' ]),
2023-06-03 23:06:31 +00:00
'record' => $record
];
2023-06-05 04:36:50 +00:00
2023-06-13 20:43:51 +00:00
$activity = bluesky_xrpc_post ( $hook_data [ 'uid' ], 'com.atproto.repo.createRecord' , $post );
2023-06-03 23:06:31 +00:00
if ( ! empty ( $activity -> uri )) {
2024-08-12 20:20:32 +00:00
$ucid = Contact :: getUserContactId ( $hook_data [ 'contact' ][ 'id' ], $hook_data [ 'uid' ]);
if ( $ucid ) {
Contact :: remove ( $ucid );
2023-06-03 23:06:31 +00:00
}
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 ;
}
2023-06-13 20:43:51 +00:00
$data = bluesky_xrpc_get ( $hook_data [ 'uid' ], 'app.bsky.actor.getProfile' , [ 'actor' => $hook_data [ 'contact' ][ 'url' ]]);
2023-06-03 23:06:31 +00:00
if ( empty ( $data -> viewer ) || empty ( $data -> viewer -> blocking )) {
return ;
}
bluesky_delete_post ( $data -> viewer -> blocking , $hook_data [ 'uid' ]);
$hook_data [ 'result' ] = true ;
}
2024-02-28 03:05:57 +00:00
function bluesky_addon_admin ( string & $o )
{
$t = Renderer :: getMarkupTemplate ( 'admin.tpl' , 'addon/bluesky/' );
$o = Renderer :: replaceMacros ( $t , [
'$submit' => DI :: l10n () -> t ( 'Save Settings' ),
2024-03-21 07:40:46 +00:00
'$friendica_handles' => [ 'friendica_handles' , DI :: l10n () -> t ( 'Allow your users to use your hostname for their Bluesky handles' ), DI :: config () -> get ( 'bluesky' , 'friendica_handles' ), DI :: l10n () -> t ( 'Before enabling this option, you have to setup a wildcard domain configuration and you have to enable wildcard requests in your webserver configuration. On Apache this is done by adding "ServerAlias *.%s" to your HTTP configuration. You don\'t need to change the HTTPS configuration.' , DI :: baseUrl () -> getHost ())],
2024-02-28 03:05:57 +00:00
]);
}
function bluesky_addon_admin_post ()
{
DI :: config () -> set ( 'bluesky' , 'friendica_handles' , ( bool ) $_POST [ 'friendica_handles' ]);
}
2023-05-21 18:54:02 +00:00
function bluesky_settings ( array & $data )
{
if ( ! DI :: userSession () -> getLocalUserId ()) {
return ;
}
2024-02-28 03:05:57 +00:00
$enabled = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'post' ) ? ? false ;
$def_enabled = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'post_by_default' ) ? ? false ;
$pds = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'pds' );
$handle = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'handle' );
2024-04-28 10:36:47 +00:00
$did = bluesky_get_user_did ( DI :: userSession () -> getLocalUserId ());
2024-02-28 03:05:57 +00:00
$token = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'access_token' );
$import = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'import' ) ? ? false ;
$import_feeds = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'import_feeds' ) ? ? false ;
$custom_handle = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'friendica_handle' ) ? ? false ;
if ( DI :: config () -> get ( 'bluesky' , 'friendica_handles' )) {
$self = User :: getById ( DI :: userSession () -> getLocalUserId (), [ 'nickname' ]);
2024-06-09 20:32:23 +00:00
$host_handle = $self [ 'nickname' ] . '.' . DI :: baseUrl () -> getHost ();
$friendica_handle = [ 'bluesky_friendica_handle' , DI :: l10n () -> t ( 'Allow to use %s as your Bluesky handle.' , $host_handle ), $custom_handle , DI :: l10n () -> t ( 'When enabled, you can use %s as your Bluesky handle. After you enabled this option, please go to https://bsky.app/settings and select to change your handle. Select that you have got your own domain. Then enter %s and select "No DNS Panel". Then select "Verify Text File".' , $host_handle , $host_handle )];
if ( $custom_handle ) {
$handle = $host_handle ;
}
2024-02-28 03:05:57 +00:00
} else {
$friendica_handle = [];
}
2023-05-21 19:25:57 +00:00
2023-05-21 18:54:02 +00:00
$t = Renderer :: getMarkupTemplate ( 'connector_settings.tpl' , 'addon/bluesky/' );
$html = Renderer :: replaceMacros ( $t , [
2023-06-05 04:36:50 +00:00
'$enable' => [ 'bluesky' , DI :: l10n () -> t ( 'Enable Bluesky Post Addon' ), $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_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.' )],
2024-02-28 03:05:57 +00:00
'$custom_handle' => $friendica_handle ,
2023-11-19 18:55:05 +00:00
'$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' ],
2024-06-09 20:32:23 +00:00
'$handle' => [ 'bluesky_handle' , DI :: l10n () -> t ( 'Bluesky handle' ), $handle , '' , '' , $custom_handle ? 'readonly' : '' ],
2023-06-05 04:36:50 +00:00
'$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. " )],
2023-12-06 06:31:52 +00:00
'$status' => bluesky_get_status ( $handle , $did , $pds , $token ),
2023-05-21 18:54:02 +00:00
]);
$data = [
'connector' => 'bluesky' ,
2023-05-24 05:49:26 +00:00
'title' => DI :: l10n () -> t ( 'Bluesky Import/Export' ),
2023-05-21 18:54:02 +00:00
'image' => 'images/bluesky.jpg' ,
'enabled' => $enabled ,
'html' => $html ,
];
}
2023-12-06 06:31:52 +00:00
function bluesky_get_status ( string $handle = null , string $did = null , string $pds = null , string $token = null ) : string
{
if ( empty ( $handle )) {
return DI :: l10n () -> t ( 'You are not authenticated. Please enter your handle and the app password.' );
}
$status = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'status' ) ? ? BLUEKSY_STATUS_UNKNOWN ;
// Fallback mechanism for connection that had been established before the introduction of the status
if ( $status == BLUEKSY_STATUS_UNKNOWN ) {
if ( empty ( $did )) {
$status = BLUEKSY_STATUS_DID_FAIL ;
} elseif ( empty ( $pds )) {
$status = BLUEKSY_STATUS_PDS_FAIL ;
} elseif ( ! empty ( $token )) {
$status = BLUEKSY_STATUS_TOKEN_OK ;
} else {
$status = BLUEKSY_STATUS_TOKEN_FAIL ;
}
}
switch ( $status ) {
case BLUEKSY_STATUS_TOKEN_OK :
return DI :: l10n () -> t ( " You are authenticated to Bluesky. For security reasons the password isn't stored. " );
case BLUEKSY_STATUS_SUCCESS :
return DI :: l10n () -> t ( 'The communication with the personal data server service (PDS) is established.' );
case BLUEKSY_STATUS_API_FAIL ;
return DI :: l10n () -> t ( 'Communication issues with the personal data server service (PDS).' );
case BLUEKSY_STATUS_DID_FAIL :
return DI :: l10n () -> t ( 'The DID for the provided handle could not be detected. Please check if you entered the correct handle.' );
case BLUEKSY_STATUS_PDS_FAIL :
return DI :: l10n () -> t ( 'The personal data server service (PDS) could not be detected.' );
case BLUEKSY_STATUS_TOKEN_FAIL :
return DI :: l10n () -> t ( 'The authentication with the provided handle and password failed. Please check if you entered the correct password.' );
default :
return '' ;
}
}
2023-05-21 18:54:02 +00:00
function bluesky_settings_post ( array & $b )
{
if ( empty ( $_POST [ 'bluesky-submit' ])) {
return ;
}
2023-12-04 20:29:31 +00:00
2023-11-19 18:55:05 +00:00
$old_pds = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'pds' );
2023-05-21 18:54:02 +00:00
$old_handle = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'handle' );
$old_did = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'did' );
2023-12-07 12:03:53 +00:00
$handle = trim ( $_POST [ 'bluesky_handle' ], ' @' );
2023-05-21 18:54:02 +00:00
2024-02-28 03:05:57 +00:00
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' , 'handle' , $handle );
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' ]));
2024-06-09 20:32:23 +00:00
DI :: pConfig () -> set ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'friendica_handle' , intval ( $_POST [ 'bluesky_friendica_handle' ] ? ? false ));
2023-05-21 18:54:02 +00:00
2023-12-04 20:29:31 +00:00
if ( ! empty ( $handle )) {
2024-04-28 10:36:47 +00:00
$did = bluesky_get_user_did ( DI :: userSession () -> getLocalUserId (), empty ( $old_did ) || $old_handle != $handle );
2023-12-06 06:31:52 +00:00
if ( ! empty ( $did ) && ( empty ( $old_pds ) || $old_handle != $handle )) {
$pds = bluesky_get_pds ( $did );
if ( empty ( $pds )) {
DI :: pConfig () -> set ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'status' , BLUEKSY_STATUS_PDS_FAIL );
}
DI :: pConfig () -> set ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'pds' , $pds );
} else {
$pds = DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'pds' );
2023-05-21 18:54:02 +00:00
}
} else {
DI :: pConfig () -> delete ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'did' );
2023-11-19 18:55:05 +00:00
DI :: pConfig () -> delete ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'pds' );
2023-12-06 06:31:52 +00:00
DI :: pConfig () -> delete ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'access_token' );
DI :: pConfig () -> delete ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'refresh_token' );
DI :: pConfig () -> delete ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'token_created' );
DI :: pConfig () -> delete ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'status' );
2023-05-21 18:54:02 +00:00
}
2023-05-21 19:25:57 +00:00
2023-12-06 06:31:52 +00:00
if ( ! empty ( $did ) && ! empty ( $pds ) && ! empty ( $_POST [ 'bluesky_password' ])) {
2023-05-21 19:25:57 +00:00
bluesky_create_token ( DI :: userSession () -> getLocalUserId (), $_POST [ 'bluesky_password' ]);
}
2023-05-21 18:54:02 +00:00
}
function bluesky_jot_nets ( array & $jotnets_fields )
{
if ( ! DI :: userSession () -> getLocalUserId ()) {
return ;
}
if ( DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'post' )) {
$jotnets_fields [] = [
'type' => 'checkbox' ,
'field' => [
'bluesky_enable' ,
DI :: l10n () -> t ( 'Post to Bluesky' ),
DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'post_by_default' )
]
];
}
}
2023-05-23 05:23:13 +00:00
function bluesky_cron ()
{
2023-12-21 05:23:38 +00:00
$last = ( int ) DI :: keyValue () -> get ( 'bluesky_last_poll' );
2023-05-23 05:23:13 +00:00
$poll_interval = intval ( DI :: config () -> get ( 'bluesky' , 'poll_interval' ));
if ( ! $poll_interval ) {
$poll_interval = BLUESKY_DEFAULT_POLL_INTERVAL ;
}
if ( $last ) {
$next = $last + ( $poll_interval * 60 );
if ( $next > time ()) {
Logger :: notice ( 'poll interval not reached' );
return ;
}
}
Logger :: notice ( 'cron_start' );
$abandon_days = intval ( DI :: config () -> get ( 'system' , 'account_abandon_days' ));
if ( $abandon_days < 1 ) {
$abandon_days = 0 ;
}
$abandon_limit = date ( DateTimeFormat :: MYSQL , time () - $abandon_days * 86400 );
2024-06-09 20:32:23 +00:00
$pconfigs = DBA :: selectToArray ( 'pconfig' , [], [ " `cat` = ? AND `k` IN (?, ?) AND `v` " , 'bluesky' , 'import' , 'import_feeds' ]);
2023-05-23 05:23:13 +00:00
foreach ( $pconfigs as $pconfig ) {
2024-05-12 00:53:46 +00:00
if ( empty ( bluesky_get_user_did ( $pconfig [ 'uid' ]))) {
2024-06-10 05:39:58 +00:00
Logger :: debug ( 'User has got no valid DID' , [ 'uid' => $pconfig [ 'uid' ]]);
2024-05-12 00:53:46 +00:00
continue ;
}
2023-05-23 05:23:13 +00:00
if ( $abandon_days != 0 ) {
if ( ! DBA :: exists ( 'user' , [ " `uid` = ? AND `login_date` >= ? " , $pconfig [ 'uid' ], $abandon_limit ])) {
Logger :: notice ( 'abandoned account: timeline from user will not be imported' , [ 'user' => $pconfig [ 'uid' ]]);
continue ;
}
}
2023-06-11 19:24:44 +00:00
// Refresh the token now, so that it doesn't need to be refreshed in parallel by the following workers
2024-06-10 05:39:58 +00:00
Logger :: debug ( 'Refresh the token' , [ 'uid' => $pconfig [ 'uid' ]]);
2023-06-11 19:24:44 +00:00
bluesky_get_token ( $pconfig [ 'uid' ]);
2023-06-05 04:36:50 +00:00
2023-12-20 20:46:24 +00:00
Worker :: add ([ 'priority' => Worker :: PRIORITY_MEDIUM , 'force_priority' => true ], 'addon/bluesky/bluesky_notifications.php' , $pconfig [ 'uid' ], $last );
2024-06-09 20:32:23 +00:00
if ( DI :: pConfig () -> get ( $pconfig [ 'uid' ], 'bluesky' , 'import' )) {
Worker :: add ([ 'priority' => Worker :: PRIORITY_MEDIUM , 'force_priority' => true ], 'addon/bluesky/bluesky_timeline.php' , $pconfig [ 'uid' ], $last );
}
2023-06-05 04:36:50 +00:00
if ( DI :: pConfig () -> get ( $pconfig [ 'uid' ], 'bluesky' , 'import_feeds' )) {
2024-06-10 05:39:58 +00:00
Logger :: debug ( 'Fetch feeds for user' , [ 'uid' => $pconfig [ 'uid' ]]);
2023-06-05 04:36:50 +00:00
$feeds = bluesky_get_feeds ( $pconfig [ 'uid' ]);
foreach ( $feeds as $feed ) {
2023-12-20 20:46:24 +00:00
Worker :: add ([ 'priority' => Worker :: PRIORITY_MEDIUM , 'force_priority' => true ], 'addon/bluesky/bluesky_feed.php' , $pconfig [ 'uid' ], $feed , $last );
2023-06-05 04:36:50 +00:00
}
}
2024-06-10 05:39:58 +00:00
Logger :: debug ( 'Polling done for user' , [ 'uid' => $pconfig [ 'uid' ]]);
2023-06-03 23:06:31 +00:00
}
2024-06-10 05:39:58 +00:00
Logger :: notice ( 'Polling done for all users' );
DI :: keyValue () -> set ( 'bluesky_last_poll' , time ());
2023-06-03 23:06:31 +00:00
$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' );
2023-05-23 05:23:13 +00:00
}
Logger :: notice ( 'cron_end' );
}
2023-05-21 18:54:02 +00:00
function bluesky_hook_fork ( array & $b )
{
if ( $b [ 'name' ] != 'notifier_normal' ) {
return ;
}
$post = $b [ 'data' ];
if (( $post [ 'created' ] !== $post [ 'edited' ]) && ! $post [ 'deleted' ]) {
DI :: logger () -> info ( 'Editing is not supported by the addon' );
$b [ 'execute' ] = false ;
return ;
}
2023-05-24 05:49:26 +00:00
if ( DI :: pConfig () -> get ( $post [ 'uid' ], 'bluesky' , 'import' )) {
// Don't post if it isn't a reply to a bluesky post
if (( $post [ 'parent' ] != $post [ 'id' ]) && ! Post :: exists ([ 'id' => $post [ 'parent' ], 'network' => Protocol :: BLUESKY ])) {
Logger :: notice ( 'No bluesky parent found' , [ 'item' => $post [ 'id' ]]);
$b [ 'execute' ] = false ;
return ;
}
} elseif ( ! strstr ( $post [ 'postopts' ] ? ? '' , 'bluesky' ) || ( $post [ 'parent' ] != $post [ 'id' ]) || $post [ 'private' ]) {
DI :: logger () -> info ( 'Activities are never exported when we don\'t import the bluesky timeline' , [ 'uid' => $post [ 'uid' ]]);
2023-05-21 18:54:02 +00:00
$b [ 'execute' ] = false ;
return ;
}
}
function bluesky_post_local ( array & $b )
{
if ( $b [ 'edit' ]) {
return ;
}
if ( ! DI :: userSession () -> getLocalUserId () || ( DI :: userSession () -> getLocalUserId () != $b [ 'uid' ])) {
return ;
}
if ( $b [ 'private' ] || $b [ 'parent' ]) {
return ;
}
$bluesky_post = intval ( DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'post' ));
$bluesky_enable = (( $bluesky_post && ! empty ( $_REQUEST [ 'bluesky_enable' ])) ? intval ( $_REQUEST [ 'bluesky_enable' ]) : 0 );
// if API is used, default to the chosen settings
if ( $b [ 'api_source' ] && intval ( DI :: pConfig () -> get ( DI :: userSession () -> getLocalUserId (), 'bluesky' , 'post_by_default' ))) {
$bluesky_enable = 1 ;
}
if ( ! $bluesky_enable ) {
return ;
}
if ( strlen ( $b [ 'postopts' ])) {
$b [ 'postopts' ] .= ',' ;
}
$b [ 'postopts' ] .= 'bluesky' ;
}
function bluesky_send ( array & $b )
{
if (( $b [ 'created' ] !== $b [ 'edited' ]) && ! $b [ 'deleted' ]) {
return ;
}
if ( $b [ 'gravity' ] != Item :: GRAVITY_PARENT ) {
2023-05-24 05:49:26 +00:00
Logger :: debug ( 'Got comment' , [ 'item' => $b ]);
if ( $b [ 'deleted' ]) {
$uri = bluesky_get_uri_class ( $b [ 'uri' ]);
if ( empty ( $uri )) {
Logger :: debug ( 'Not a bluesky post' , [ 'uri' => $b [ 'uri' ]]);
return ;
}
bluesky_delete_post ( $b [ 'uri' ], $b [ 'uid' ]);
return ;
}
$root = bluesky_get_uri_class ( $b [ 'parent-uri' ]);
$parent = bluesky_get_uri_class ( $b [ 'thr-parent' ]);
if ( empty ( $root ) || empty ( $parent )) {
Logger :: debug ( 'No bluesky post' , [ 'parent' => $b [ 'parent' ], 'thr-parent' => $b [ 'thr-parent' ]]);
return ;
}
if ( $b [ 'gravity' ] == Item :: GRAVITY_COMMENT ) {
Logger :: debug ( 'Posting comment' , [ 'root' => $root , 'parent' => $parent ]);
bluesky_create_post ( $b , $root , $parent );
return ;
} elseif ( in_array ( $b [ 'verb' ], [ Activity :: LIKE , Activity :: ANNOUNCE ])) {
bluesky_create_activity ( $b , $parent );
}
2023-05-21 18:54:02 +00:00
return ;
} elseif ( $b [ 'private' ] || ! strstr ( $b [ 'postopts' ], 'bluesky' )) {
return ;
}
bluesky_create_post ( $b );
}
2023-05-24 05:49:26 +00:00
function bluesky_create_activity ( array $item , stdClass $parent = null )
{
$uid = $item [ 'uid' ];
$token = bluesky_get_token ( $uid );
if ( empty ( $token )) {
return ;
}
2024-04-28 10:36:47 +00:00
$did = bluesky_get_user_did ( $uid );
2024-05-12 00:53:46 +00:00
if ( empty ( $did )) {
return ;
}
2023-05-24 05:49:26 +00:00
if ( $item [ 'verb' ] == Activity :: LIKE ) {
$record = [
'subject' => $parent ,
'createdAt' => DateTimeFormat :: utcNow ( DateTimeFormat :: ATOM ),
'$type' => 'app.bsky.feed.like'
];
2023-05-26 20:54:00 +00:00
2023-05-24 05:49:26 +00:00
$post = [
'collection' => 'app.bsky.feed.like' ,
'repo' => $did ,
'record' => $record
];
} elseif ( $item [ 'verb' ] == Activity :: ANNOUNCE ) {
$record = [
'subject' => $parent ,
'createdAt' => DateTimeFormat :: utcNow ( DateTimeFormat :: ATOM ),
'$type' => 'app.bsky.feed.repost'
];
$post = [
'collection' => 'app.bsky.feed.repost' ,
'repo' => $did ,
'record' => $record
];
}
2023-06-13 20:43:51 +00:00
$activity = bluesky_xrpc_post ( $uid , 'com.atproto.repo.createRecord' , $post );
2023-05-24 05:49:26 +00:00
if ( empty ( $activity )) {
return ;
}
Logger :: debug ( 'Activity done' , [ 'return' => $activity ]);
$uri = bluesky_get_uri ( $activity );
Item :: update ([ 'extid' => $uri ], [ 'id' => $item [ 'id' ]]);
Logger :: debug ( 'Set extid' , [ 'id' => $item [ 'id' ], 'extid' => $activity ]);
}
function bluesky_create_post ( array $item , stdClass $root = null , stdClass $parent = null )
2023-05-21 18:54:02 +00:00
{
$uid = $item [ 'uid' ];
$token = bluesky_get_token ( $uid );
if ( empty ( $token )) {
return ;
}
2023-11-11 05:30:07 +00:00
// Try to fetch the language from the post itself
if ( ! empty ( $item [ 'language' ])) {
$language = array_key_first ( json_decode ( $item [ 'language' ], true ));
} else {
$language = '' ;
}
2024-03-02 13:32:11 +00:00
$item [ 'body' ] = Post\Media :: removeFromBody ( $item [ 'body' ]);
foreach ( Post\Media :: getByURIId ( $item [ 'uri-id' ], [ Post\Media :: AUDIO , Post\Media :: VIDEO , Post\Media :: ACTIVITY ]) as $media ) {
if ( strpos ( $item [ 'body' ], $media [ 'url' ]) === false ) {
$item [ 'body' ] .= " \n [url] " . $media [ 'url' ] . " [/url] \n " ;
}
}
2024-04-28 10:36:47 +00:00
2024-03-02 13:32:11 +00:00
if ( ! empty ( $item [ 'quote-uri-id' ])) {
$quote = Post :: selectFirstPost ([ 'uri' , 'plink' ], [ 'uri-id' => $item [ 'quote-uri-id' ]]);
if ( ! empty ( $quote )) {
if (( strpos ( $item [ 'body' ], $quote [ 'plink' ] ? : $quote [ 'uri' ]) === false ) && ( strpos ( $item [ 'body' ], $quote [ 'uri' ]) === false )) {
$item [ 'body' ] .= " \n [url] " . ( $quote [ 'plink' ] ? : $quote [ 'uri' ]) . " [/url] \n " ;
}
}
}
2024-04-28 10:36:47 +00:00
2024-03-02 13:32:11 +00:00
$urls = bluesky_get_urls ( $item [ 'body' ]);
2023-06-06 20:33:10 +00:00
$item [ 'body' ] = $urls [ 'body' ];
2023-05-21 18:54:02 +00:00
2023-06-05 04:36:50 +00:00
$msg = Plaintext :: getPost ( $item , 300 , false , BBCode :: BLUESKY );
2023-05-21 18:54:02 +00:00
foreach ( $msg [ 'parts' ] as $key => $part ) {
2023-05-26 20:54:00 +00:00
2023-06-06 20:33:10 +00:00
$facets = bluesky_get_facets ( $part , $urls [ 'urls' ]);
2023-05-26 20:54:00 +00:00
2023-05-21 18:54:02 +00:00
$record = [
2023-05-26 20:54:00 +00:00
'text' => $facets [ 'body' ],
2023-11-11 05:30:07 +00:00
'$type' => 'app.bsky.feed.post' ,
2023-05-21 18:54:02 +00:00
'createdAt' => DateTimeFormat :: utcNow ( DateTimeFormat :: ATOM ),
];
2023-11-11 05:30:07 +00:00
if ( ! empty ( $language )) {
$record [ 'langs' ] = [ $language ];
}
2023-05-26 20:54:00 +00:00
if ( ! empty ( $facets [ 'facets' ])) {
$record [ 'facets' ] = $facets [ 'facets' ];
}
2023-05-21 18:54:02 +00:00
if ( ! empty ( $root )) {
$record [ 'reply' ] = [ 'root' => $root , 'parent' => $parent ];
}
if ( $key == count ( $msg [ 'parts' ]) - 1 ) {
$record = bluesky_add_embed ( $uid , $msg , $record );
2023-08-15 20:25:17 +00:00
if ( empty ( $record )) {
if ( Worker :: getRetrial () < 3 ) {
Worker :: defer ();
}
return ;
}
2023-05-21 18:54:02 +00:00
}
$post = [
'collection' => 'app.bsky.feed.post' ,
2024-05-12 00:53:46 +00:00
'repo' => bluesky_get_user_did ( $uid ),
2023-05-21 18:54:02 +00:00
'record' => $record
];
2023-06-13 20:43:51 +00:00
$parent = bluesky_xrpc_post ( $uid , 'com.atproto.repo.createRecord' , $post );
2023-05-21 18:54:02 +00:00
if ( empty ( $parent )) {
2023-08-15 20:25:17 +00:00
if ( $part == 0 ) {
Worker :: defer ();
}
2023-05-21 18:54:02 +00:00
return ;
}
Logger :: debug ( 'Posting done' , [ 'return' => $parent ]);
if ( empty ( $root )) {
$root = $parent ;
}
2023-05-24 05:49:26 +00:00
if (( $key == 0 ) && ( $item [ 'gravity' ] != Item :: GRAVITY_PARENT )) {
$uri = bluesky_get_uri ( $parent );
Item :: update ([ 'extid' => $uri ], [ 'id' => $item [ 'id' ]]);
Logger :: debug ( 'Set extid' , [ 'id' => $item [ 'id' ], 'extid' => $uri ]);
}
2023-05-21 18:54:02 +00:00
}
}
2023-05-26 20:54:00 +00:00
function bluesky_get_urls ( string $body ) : array
{
2023-07-08 14:54:56 +00:00
// Remove all hashtag and mention links
2023-11-20 21:07:09 +00:00
$body = preg_replace ( " /([@!]) \ [url \ =(.*?) \ ](.*?) \ [ \ /url \ ]/ism " , '$1$3' , $body );
2023-05-26 20:54:00 +00:00
2023-08-15 20:25:17 +00:00
$body = BBCode :: expandVideoLinks ( $body );
2023-05-26 20:54:00 +00:00
$urls = [];
2023-11-20 21:07:09 +00:00
// Search for hash tags
if ( preg_match_all ( " /# \ [url \ =(https?:.*?) \ ](.*?) \ [ \ /url \ ]/ism " , $body , $matches , PREG_SET_ORDER )) {
foreach ( $matches as $match ) {
$text = '#' . $match [ 2 ];
2024-03-01 05:53:42 +00:00
$urls [ strpos ( $body , $match [ 0 ])] = [ 'tag' => $match [ 2 ], 'text' => $text , 'hash' => $text ];
2023-11-20 21:07:09 +00:00
$body = str_replace ( $match [ 0 ], $text , $body );
}
}
2023-05-26 20:54:00 +00:00
// Search for pure links
2023-06-06 20:33:10 +00:00
if ( preg_match_all ( " / \ [url \ ](https?:.*?) \ [ \ /url \ ]/ism " , $body , $matches , PREG_SET_ORDER )) {
foreach ( $matches as $match ) {
$text = Strings :: getStyledURL ( $match [ 1 ]);
$hash = bluesky_get_hash_for_url ( $match [ 0 ], mb_strlen ( $text ));
2024-03-01 05:53:42 +00:00
$urls [ strpos ( $body , $match [ 0 ])] = [ 'url' => $match [ 1 ], 'text' => $text , 'hash' => $hash ];
2023-06-06 20:33:10 +00:00
$body = str_replace ( $match [ 0 ], $hash , $body );
2023-05-26 20:54:00 +00:00
}
}
// Search for links with descriptions
2023-06-06 20:33:10 +00:00
if ( preg_match_all ( " / \ [url \ =(https?:.*?) \ ](.*?) \ [ \ /url \ ]/ism " , $body , $matches , PREG_SET_ORDER )) {
foreach ( $matches as $match ) {
if ( $match [ 1 ] == $match [ 2 ]) {
$text = Strings :: getStyledURL ( $match [ 1 ]);
} else {
$text = $match [ 2 ];
}
if ( mb_strlen ( $text ) < 100 ) {
$hash = bluesky_get_hash_for_url ( $match [ 0 ], mb_strlen ( $text ));
2024-03-01 05:53:42 +00:00
$urls [ strpos ( $body , $match [ 0 ])] = [ 'url' => $match [ 1 ], 'text' => $text , 'hash' => $hash ];
2023-06-06 20:33:10 +00:00
$body = str_replace ( $match [ 0 ], $hash , $body );
} else {
$text = Strings :: getStyledURL ( $match [ 1 ]);
$hash = bluesky_get_hash_for_url ( $match [ 0 ], mb_strlen ( $text ));
2024-03-01 05:53:42 +00:00
$urls [ strpos ( $body , $match [ 0 ])] = [ 'url' => $match [ 1 ], 'text' => $text , 'hash' => $hash ];
2023-06-06 20:33:10 +00:00
$body = str_replace ( $match [ 0 ], $text . ' ' . $hash , $body );
}
2023-05-26 20:54:00 +00:00
}
}
2023-06-06 20:33:10 +00:00
2024-03-01 05:53:42 +00:00
asort ( $urls );
2023-06-06 20:33:10 +00:00
return [ 'body' => $body , 'urls' => $urls ];
}
function bluesky_get_hash_for_url ( string $text , int $linklength ) : string
{
if ( $linklength <= 10 ) {
return '|' . hash ( 'crc32' , $text ) . '|' ;
}
return substr ( '|' . hash ( 'crc32' , $text ) . base64_encode ( $text ), 0 , $linklength - 2 ) . '|' ;
2023-05-26 20:54:00 +00:00
}
function bluesky_get_facets ( string $body , array $urls ) : array
{
$facets = [];
foreach ( $urls as $url ) {
2023-06-06 20:33:10 +00:00
$pos = strpos ( $body , $url [ 'hash' ]);
2023-05-26 20:54:00 +00:00
if ( $pos === false ) {
continue ;
}
if ( $pos > 0 ) {
$prefix = substr ( $body , 0 , $pos );
} else {
$prefix = '' ;
}
2023-06-06 20:33:10 +00:00
$body = $prefix . $url [ 'text' ] . substr ( $body , $pos + strlen ( $url [ 'hash' ]));
2023-05-26 20:54:00 +00:00
$facet = new stdClass ;
$facet -> index = new stdClass ;
2023-06-06 20:33:10 +00:00
$facet -> index -> byteEnd = $pos + strlen ( $url [ 'text' ]);
2023-05-26 20:54:00 +00:00
$facet -> index -> byteStart = $pos ;
$feature = new stdClass ;
2023-11-20 21:07:09 +00:00
2023-05-26 20:54:00 +00:00
$type = '$type' ;
2023-11-20 21:07:09 +00:00
if ( ! empty ( $url [ 'tag' ])) {
$feature -> tag = $url [ 'tag' ];
$feature -> $type = 'app.bsky.richtext.facet#tag' ;
} elseif ( ! empty ( $url [ 'url' ])) {
$feature -> uri = $url [ 'url' ];
$feature -> $type = 'app.bsky.richtext.facet#link' ;
} else {
continue ;
}
2023-05-26 20:54:00 +00:00
$facet -> features = [ $feature ];
$facets [] = $facet ;
}
return [ 'facets' => $facets , 'body' => $body ];
}
2023-05-21 18:54:02 +00:00
function bluesky_add_embed ( int $uid , array $msg , array $record ) : array
{
if (( $msg [ 'type' ] != 'link' ) && ! empty ( $msg [ 'images' ])) {
$images = [];
foreach ( $msg [ 'images' ] as $image ) {
2023-08-15 20:25:17 +00:00
if ( count ( $images ) == 4 ) {
continue ;
}
$photo = Photo :: selectFirst ([], [ 'id' => $image [ 'id' ]]);
2023-05-21 18:54:02 +00:00
$blob = bluesky_upload_blob ( $uid , $photo );
2023-08-15 20:25:17 +00:00
if ( empty ( $blob )) {
return [];
2023-05-21 18:54:02 +00:00
}
2023-08-15 20:25:17 +00:00
$images [] = [ 'alt' => $image [ 'description' ] ? ? '' , 'image' => $blob ];
2023-05-21 18:54:02 +00:00
}
if ( ! empty ( $images )) {
$record [ 'embed' ] = [ '$type' => 'app.bsky.embed.images' , 'images' => $images ];
}
} elseif ( $msg [ 'type' ] == 'link' ) {
$record [ 'embed' ] = [
'$type' => 'app.bsky.embed.external' ,
'external' => [
'uri' => $msg [ 'url' ],
2023-07-22 13:38:13 +00:00
'title' => $msg [ 'title' ] ? ? '' ,
'description' => $msg [ 'description' ] ? ? '' ,
2023-05-21 18:54:02 +00:00
]
];
if ( ! empty ( $msg [ 'image' ])) {
$photo = Photo :: createPhotoForExternalResource ( $msg [ 'image' ]);
$blob = bluesky_upload_blob ( $uid , $photo );
if ( ! empty ( $blob )) {
$record [ 'embed' ][ 'external' ][ 'thumb' ] = $blob ;
}
}
}
return $record ;
}
function bluesky_upload_blob ( int $uid , array $photo ) : ? stdClass
{
2023-08-15 20:25:17 +00:00
$retrial = Worker :: getRetrial ();
2023-05-21 18:54:02 +00:00
$content = Photo :: getImageForPhoto ( $photo );
2023-08-15 20:25:17 +00:00
2024-02-16 02:29:12 +00:00
$picture = new Image ( $content , $photo [ 'type' ], $photo [ 'filename' ]);
2023-08-15 20:25:17 +00:00
$height = $picture -> getHeight ();
2023-08-22 13:01:12 +00:00
$width = $picture -> getWidth ();
2023-08-15 20:25:17 +00:00
$size = strlen ( $content );
$picture = Photo :: resizeToFileSize ( $picture , BLUESKY_IMAGE_SIZE [ $retrial ]);
$new_height = $picture -> getHeight ();
2023-08-22 13:01:12 +00:00
$new_width = $picture -> getWidth ();
2023-08-15 20:25:17 +00:00
$content = $picture -> asString ();
$new_size = strlen ( $content );
Logger :: info ( 'Uploading' , [ 'uid' => $uid , 'retrial' => $retrial , 'height' => $new_height , 'width' => $new_width , 'size' => $new_size , 'orig-height' => $height , 'orig-width' => $width , 'orig-size' => $size ]);
2023-05-21 18:54:02 +00:00
$data = bluesky_post ( $uid , '/xrpc/com.atproto.repo.uploadBlob' , $content , [ 'Content-type' => $photo [ 'type' ], 'Authorization' => [ 'Bearer ' . bluesky_get_token ( $uid )]]);
if ( empty ( $data )) {
2023-08-15 20:25:17 +00:00
Logger :: info ( 'Uploading failed' , [ 'uid' => $uid , 'retrial' => $retrial , 'height' => $new_height , 'width' => $new_width , 'size' => $new_size , 'orig-height' => $height , 'orig-width' => $width , 'orig-size' => $size ]);
2023-05-21 18:54:02 +00:00
return null ;
}
2024-08-12 20:20:32 +00:00
Item :: incrementOutbound ( Protocol :: BLUESKY );
2023-08-15 20:25:17 +00:00
Logger :: debug ( 'Uploaded blob' , [ 'return' => $data , 'uid' => $uid , 'retrial' => $retrial , 'height' => $new_height , 'width' => $new_width , 'size' => $new_size , 'orig-height' => $height , 'orig-width' => $width , 'orig-size' => $size ]);
2023-05-21 18:54:02 +00:00
return $data -> blob ;
}
2023-05-24 05:49:26 +00:00
function bluesky_delete_post ( string $uri , int $uid )
{
$parts = bluesky_get_uri_parts ( $uri );
if ( empty ( $parts )) {
Logger :: debug ( 'No uri delected' , [ 'uri' => $uri ]);
return ;
}
2023-06-13 20:43:51 +00:00
bluesky_xrpc_post ( $uid , 'com.atproto.repo.deleteRecord' , $parts );
2023-05-24 05:49:26 +00:00
Logger :: debug ( 'Deleted' , [ 'parts' => $parts ]);
}
2023-12-20 20:46:24 +00:00
function bluesky_fetch_timeline ( int $uid , int $last_poll )
2023-05-21 18:54:02 +00:00
{
2023-06-13 20:43:51 +00:00
$data = bluesky_xrpc_get ( $uid , 'app.bsky.feed.getTimeline' );
2023-05-21 18:54:02 +00:00
if ( empty ( $data )) {
return ;
}
2023-05-21 20:14:20 +00:00
if ( empty ( $data -> feed )) {
return ;
}
2023-05-23 05:23:13 +00:00
foreach ( array_reverse ( $data -> feed ) as $entry ) {
2024-02-23 13:32:24 +00:00
bluesky_process_post ( $entry -> post , $uid , $uid , Item :: PR_NONE , 0 , 0 , $last_poll );
2023-05-26 20:54:00 +00:00
if ( ! empty ( $entry -> reason )) {
bluesky_process_reason ( $entry -> reason , bluesky_get_uri ( $entry -> post ), $uid );
}
2023-05-21 20:14:20 +00:00
}
2023-05-23 05:23:13 +00:00
// @todo Support paging
// [cursor] => 1684670516000::bafyreidq3ilwslmlx72jf5vrk367xcc63s6lrhzlyup2bi3zwcvso6w2vi
}
2023-05-26 20:54:00 +00:00
function bluesky_process_reason ( stdClass $reason , string $uri , int $uid )
{
$type = '$type' ;
if ( $reason -> $type != 'app.bsky.feed.defs#reasonRepost' ) {
return ;
}
2023-06-03 23:06:31 +00:00
$contact = bluesky_get_contact ( $reason -> by , $uid , $uid );
2023-05-26 20:54:00 +00:00
$item = [
'network' => Protocol :: BLUESKY ,
'uid' => $uid ,
'wall' => false ,
'uri' => $reason -> by -> did . '/app.bsky.feed.repost/' . $reason -> indexedAt ,
'private' => Item :: UNLISTED ,
'verb' => Activity :: POST ,
'contact-id' => $contact [ 'id' ],
'author-name' => $contact [ 'name' ],
'author-link' => $contact [ 'url' ],
'author-avatar' => $contact [ 'avatar' ],
'verb' => Activity :: ANNOUNCE ,
'body' => Activity :: ANNOUNCE ,
'gravity' => Item :: GRAVITY_ACTIVITY ,
'object-type' => Activity\ObjectType :: NOTE ,
'thr-parent' => $uri ,
];
if ( Post :: exists ([ 'uri' => $item [ 'uri' ], 'uid' => $uid ])) {
return ;
}
2023-06-11 19:24:44 +00:00
$item [ 'guid' ] = Item :: guidFromUri ( $item [ 'uri' ], $contact [ 'alias' ]);
2023-05-26 20:54:00 +00:00
$item [ 'owner-name' ] = $item [ 'author-name' ];
$item [ 'owner-link' ] = $item [ 'author-link' ];
$item [ 'owner-avatar' ] = $item [ 'author-avatar' ];
if ( Item :: insert ( $item )) {
2024-08-12 20:20:32 +00:00
$pcid = Contact :: getPublicContactId ( $contact [ 'id' ], $uid );
Item :: update ([ 'post-reason' => Item :: PR_ANNOUNCEMENT , 'causer-id' => $pcid ], [ 'uri' => $uri , 'uid' => $uid ]);
2023-05-26 20:54:00 +00:00
}
}
2023-12-20 20:46:24 +00:00
function bluesky_fetch_notifications ( int $uid , int $last_poll )
2023-06-03 23:06:31 +00:00
{
2023-06-13 20:43:51 +00:00
$data = bluesky_xrpc_get ( $uid , 'app.bsky.notification.listNotifications' );
if ( empty ( $data -> notifications )) {
2023-06-03 23:06:31 +00:00
return ;
}
2023-06-13 20:43:51 +00:00
foreach ( $data -> notifications as $notification ) {
2023-06-03 23:06:31 +00:00
$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 );
2024-02-24 06:48:19 +00:00
$item [ 'thr-parent' ] = bluesky_fetch_missing_post ( $item [ 'thr-parent' ], $uid , $uid , Item :: PR_FETCHED , $item [ 'contact-id' ], 0 , $last_poll );
2023-06-11 19:24:44 +00:00
if ( ! empty ( $item [ 'thr-parent' ])) {
2023-06-13 20:43:51 +00:00
$data = Item :: insert ( $item );
Logger :: debug ( 'Got like' , [ 'uid' => $uid , 'result' => $data , 'uri' => $uri ]);
2023-06-11 19:24:44 +00:00
} else {
2023-08-22 13:01:12 +00:00
Logger :: info ( 'Thread parent not found' , [ 'uid' => $uid , 'parent' => $item [ 'thr-parent' ], 'uri' => $uri ]);
2023-06-11 19:24:44 +00:00
}
2024-02-23 14:04:06 +00:00
break ;
2023-06-03 23:06:31 +00:00
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 );
2024-02-24 06:48:19 +00:00
$item [ 'thr-parent' ] = bluesky_fetch_missing_post ( $item [ 'thr-parent' ], $uid , $uid , Item :: PR_FETCHED , $item [ 'contact-id' ], 0 , $last_poll );
2023-06-11 19:24:44 +00:00
if ( ! empty ( $item [ 'thr-parent' ])) {
2023-06-13 20:43:51 +00:00
$data = Item :: insert ( $item );
Logger :: debug ( 'Got repost' , [ 'uid' => $uid , 'result' => $data , 'uri' => $uri ]);
2023-06-11 19:24:44 +00:00
} else {
2023-08-28 04:50:15 +00:00
Logger :: info ( 'Thread parent not found' , [ 'uid' => $uid , 'parent' => $item [ 'thr-parent' ], 'uri' => $uri ]);
2023-06-11 19:24:44 +00:00
}
2024-02-23 14:04:06 +00:00
break ;
2023-06-03 23:06:31 +00:00
case 'follow' :
$contact = bluesky_get_contact ( $notification -> author , $uid , $uid );
2023-06-06 20:33:10 +00:00
Logger :: debug ( 'New follower' , [ 'uid' => $uid , 'nick' => $contact [ 'nick' ], 'uri' => $uri ]);
2023-06-03 23:06:31 +00:00
break ;
case 'mention' :
2024-02-23 13:32:24 +00:00
$contact = bluesky_get_contact ( $notification -> author , 0 , $uid );
2024-02-24 06:48:19 +00:00
$result = bluesky_fetch_missing_post ( $uri , $uid , $uid , Item :: PR_TO , $contact [ 'id' ], 0 , $last_poll );
2024-02-23 13:32:24 +00:00
Logger :: debug ( 'Got mention' , [ 'uid' => $uid , 'nick' => $contact [ 'nick' ], 'result' => $result , 'uri' => $uri ]);
2023-06-03 23:06:31 +00:00
break ;
case 'reply' :
2024-02-23 13:32:24 +00:00
$contact = bluesky_get_contact ( $notification -> author , 0 , $uid );
2024-02-24 06:48:19 +00:00
$result = bluesky_fetch_missing_post ( $uri , $uid , $uid , Item :: PR_COMMENT , $contact [ 'id' ], 0 , $last_poll );
2024-02-23 13:32:24 +00:00
Logger :: debug ( 'Got reply' , [ 'uid' => $uid , 'nick' => $contact [ 'nick' ], 'result' => $result , 'uri' => $uri ]);
2023-06-03 23:06:31 +00:00
break ;
case 'quote' :
2024-02-23 13:32:24 +00:00
$contact = bluesky_get_contact ( $notification -> author , 0 , $uid );
2024-02-24 06:48:19 +00:00
$result = bluesky_fetch_missing_post ( $uri , $uid , $uid , Item :: PR_PUSHED , $contact [ 'id' ], 0 , $last_poll );
2024-02-23 13:32:24 +00:00
Logger :: debug ( 'Got quote' , [ 'uid' => $uid , 'nick' => $contact [ 'nick' ], 'result' => $result , 'uri' => $uri ]);
2023-06-03 23:06:31 +00:00
break ;
default :
2023-06-06 20:33:10 +00:00
Logger :: notice ( 'Unhandled reason' , [ 'reason' => $notification -> reason , 'uri' => $uri ]);
2023-06-03 23:06:31 +00:00
break ;
}
}
}
2023-12-20 20:46:24 +00:00
function bluesky_fetch_feed ( int $uid , string $feed , int $last_poll )
2023-06-05 04:36:50 +00:00
{
2023-06-13 20:43:51 +00:00
$data = bluesky_xrpc_get ( $uid , 'app.bsky.feed.getFeed' , [ 'feed' => $feed ]);
2023-06-05 04:36:50 +00:00
if ( empty ( $data )) {
return ;
}
if ( empty ( $data -> feed )) {
return ;
}
2023-08-28 04:50:15 +00:00
$feeddata = bluesky_xrpc_get ( $uid , 'app.bsky.feed.getFeedGenerator' , [ 'feed' => $feed ]);
if ( ! empty ( $feeddata )) {
$feedurl = $feeddata -> view -> uri ;
$feedname = $feeddata -> view -> displayName ;
} else {
$feedurl = $feed ;
$feedname = $feed ;
}
2023-06-05 04:36:50 +00:00
foreach ( array_reverse ( $data -> feed ) as $entry ) {
2023-11-11 05:30:07 +00:00
$contact = bluesky_get_contact ( $entry -> post -> author , 0 , $uid );
$languages = $entry -> post -> record -> langs ? ? [];
if ( ! Relay :: isWantedLanguage ( $entry -> post -> record -> text , 0 , $contact [ 'id' ] ? ? 0 , $languages )) {
2023-06-05 04:36:50 +00:00
Logger :: debug ( 'Unwanted language detected' , [ 'text' => $entry -> post -> record -> text ]);
continue ;
}
2024-02-23 13:32:24 +00:00
$uri_id = bluesky_process_post ( $entry -> post , $uid , $uid , Item :: PR_TAG , 0 , 0 , $last_poll );
if ( ! empty ( $uri_id )) {
$stored = Post\Category :: storeFileByURIId ( $uri_id , $uid , Post\Category :: SUBCRIPTION , $feedname , $feedurl );
Logger :: debug ( 'Stored tag subscription for user' , [ 'uri-id' => $uri_id , 'uid' => $uid , 'name' => $feedname , 'url' => $feedurl , 'stored' => $stored ]);
} else {
Logger :: notice ( 'Post not found' , [ 'entry' => $entry ]);
2023-08-28 04:50:15 +00:00
}
2023-06-05 04:36:50 +00:00
if ( ! empty ( $entry -> reason )) {
bluesky_process_reason ( $entry -> reason , bluesky_get_uri ( $entry -> post ), $uid );
}
}
}
2024-02-23 13:32:24 +00:00
function bluesky_process_post ( stdClass $post , int $uid , int $fetch_uid , int $post_reason , int $causer , int $level , int $last_poll ) : int
2023-05-23 05:23:13 +00:00
{
$uri = bluesky_get_uri ( $post );
2024-02-23 13:32:24 +00:00
if ( $uri_id = bluesky_fetch_uri_id ( $uri , $uid )) {
return $uri_id ;
2023-10-01 04:37:11 +00:00
}
2024-02-23 13:32:24 +00:00
if ( empty ( $post -> record )) {
Logger :: debug ( 'Invalid post' , [ 'uri' => $uri ]);
return 0 ;
2023-05-23 05:23:13 +00:00
}
2023-06-11 19:24:44 +00:00
Logger :: debug ( 'Importing post' , [ 'uid' => $uid , 'indexedAt' => $post -> indexedAt , 'uri' => $post -> uri , 'cid' => $post -> cid , 'root' => $post -> record -> reply -> root ? ? '' ]);
2023-05-24 05:49:26 +00:00
2024-02-23 13:32:24 +00:00
$item = bluesky_get_header ( $post , $uri , $uid , $fetch_uid );
$item = bluesky_get_content ( $item , $post -> record , $uri , $uid , $fetch_uid , $level , $last_poll );
2023-06-11 19:24:44 +00:00
if ( empty ( $item )) {
return 0 ;
}
2023-05-23 05:23:13 +00:00
if ( ! empty ( $post -> embed )) {
2023-12-20 20:46:24 +00:00
$item = bluesky_add_media ( $post -> embed , $item , $uid , $level , $last_poll );
2023-05-23 05:23:13 +00:00
}
2023-06-05 04:36:50 +00:00
2024-03-21 07:40:46 +00:00
$item [ 'restrictions' ] = bluesky_get_restrictions_for_user ( $post , $item , $post_reason );
2023-06-05 04:36:50 +00:00
if ( empty ( $item [ 'post-reason' ])) {
$item [ 'post-reason' ] = $post_reason ;
}
2024-02-23 13:32:24 +00:00
if ( $causer != 0 ) {
$item [ 'causer-id' ] = $causer ;
}
Item :: insert ( $item );
return bluesky_fetch_uri_id ( $uri , $uid );
2023-05-23 05:23:13 +00:00
}
2023-06-03 23:06:31 +00:00
function bluesky_get_header ( stdClass $post , string $uri , int $uid , int $fetch_uid ) : array
2023-05-23 05:23:13 +00:00
{
2023-05-26 20:54:00 +00:00
$parts = bluesky_get_uri_parts ( $uri );
2024-02-23 13:32:24 +00:00
if ( empty ( $post -> author ) || empty ( $post -> cid ) || empty ( $parts -> rkey )) {
2023-05-26 20:54:00 +00:00
return [];
}
2023-06-03 23:06:31 +00:00
$contact = bluesky_get_contact ( $post -> author , $uid , $fetch_uid );
2023-05-23 05:23:13 +00:00
$item = [
'network' => Protocol :: BLUESKY ,
'uid' => $uid ,
'wall' => false ,
'uri' => $uri ,
'guid' => $post -> cid ,
'private' => Item :: UNLISTED ,
'verb' => Activity :: POST ,
'contact-id' => $contact [ 'id' ],
'author-name' => $contact [ 'name' ],
'author-link' => $contact [ 'url' ],
'author-avatar' => $contact [ 'avatar' ],
2023-05-26 20:54:00 +00:00
'plink' => $contact [ 'alias' ] . '/post/' . $parts -> rkey ,
2023-08-28 04:50:15 +00:00
'source' => json_encode ( $post ),
2023-05-23 05:23:13 +00:00
];
2024-06-05 03:37:43 +00:00
$account = Contact :: selectFirstAccountUser ([ 'pid' ], [ 'id' => $contact [ 'id' ]]);
$item [ 'author-id' ] = $account [ 'pid' ];
2023-05-23 05:23:13 +00:00
$item [ 'uri-id' ] = ItemURI :: getIdByURI ( $uri );
2024-06-05 03:37:43 +00:00
$item [ 'owner-id' ] = $item [ 'author-id' ];
2023-05-23 05:23:13 +00:00
$item [ 'owner-name' ] = $item [ 'author-name' ];
$item [ 'owner-link' ] = $item [ 'author-link' ];
$item [ 'owner-avatar' ] = $item [ 'author-avatar' ];
2023-06-05 04:36:50 +00:00
if ( in_array ( $contact [ 'rel' ], [ Contact :: SHARING , Contact :: FRIEND ])) {
$item [ 'post-reason' ] = Item :: PR_FOLLOWER ;
}
2024-03-23 06:59:13 +00:00
if ( ! empty ( $post -> labels )) {
foreach ( $post -> labels as $label ) {
// Only flag posts as sensitive based on labels that had been provided by the author.
// When "ver" is set to "1" it was flagged by some automated process.
if ( empty ( $label -> ver )) {
$item [ 'sensitive' ] = true ;
Logger :: debug ( 'Sensitive content' , [ 'uri-id' => $item [ 'uri-id' ], 'label' => $label ]);
}
}
}
2023-05-23 05:23:13 +00:00
return $item ;
}
2024-03-21 07:40:46 +00:00
function bluesky_get_restrictions_for_user ( stdClass $post , array $item , int $post_reason ) : ? int
{
if ( ! empty ( $post -> viewer -> replyDisabled )) {
return Item :: CANT_REPLY ;
}
2024-04-28 10:36:47 +00:00
if ( empty ( $post -> threadgate )) {
2024-03-21 07:40:46 +00:00
return null ;
}
if ( ! isset ( $post -> threadgate -> record -> allow )) {
return null ;
}
if ( $item [ 'uid' ] == 0 ) {
return Item :: CANT_REPLY ;
}
$restrict = true ;
$type = '$type' ;
foreach ( $post -> threadgate -> record -> allow as $allow ) {
switch ( $allow -> $type ) {
case 'app.bsky.feed.threadgate#followingRule' :
// Only followers can reply.
if ( Contact :: isFollower ( $item [ 'author-id' ], $item [ 'uid' ])) {
$restrict = false ;
}
break ;
case 'app.bsky.feed.threadgate#mentionRule' :
// Only mentioned accounts can reply.
if ( $post_reason == Item :: PR_TO ) {
$restrict = false ;
}
break ;
case 'app.bsky.feed.threadgate#listRule' ;
// Only accounts in the provided list can reply. We don't support this at the moment.
break ;
}
}
return $restrict ? Item :: CANT_REPLY : null ;
}
2023-12-20 20:46:24 +00:00
function bluesky_get_content ( array $item , stdClass $record , string $uri , int $uid , int $fetch_uid , int $level , int $last_poll ) : array
2023-05-23 05:23:13 +00:00
{
2023-06-11 19:24:44 +00:00
if ( empty ( $item )) {
return [];
}
2023-05-23 05:23:13 +00:00
if ( ! empty ( $record -> reply )) {
2023-05-24 05:49:26 +00:00
$item [ 'parent-uri' ] = bluesky_get_uri ( $record -> reply -> root );
2023-06-11 19:24:44 +00:00
if ( $item [ 'parent-uri' ] != $uri ) {
2024-02-24 08:26:06 +00:00
$item [ 'parent-uri' ] = bluesky_fetch_missing_post ( $item [ 'parent-uri' ], $uid , $fetch_uid , Item :: PR_FETCHED , $item [ 'contact-id' ], $level , $last_poll );
2023-06-11 19:24:44 +00:00
if ( empty ( $item [ 'parent-uri' ])) {
return [];
}
}
2023-05-23 05:23:13 +00:00
$item [ 'thr-parent' ] = bluesky_get_uri ( $record -> reply -> parent );
2023-06-11 19:24:44 +00:00
if ( ! in_array ( $item [ 'thr-parent' ], [ $uri , $item [ 'parent-uri' ]])) {
2024-02-24 08:26:06 +00:00
$item [ 'thr-parent' ] = bluesky_fetch_missing_post ( $item [ 'thr-parent' ], $uid , $fetch_uid , Item :: PR_FETCHED , $item [ 'contact-id' ], $level , $last_poll , $item [ 'parent-uri' ]);
2023-06-11 19:24:44 +00:00
if ( empty ( $item [ 'thr-parent' ])) {
return [];
}
}
2023-05-23 05:23:13 +00:00
}
2023-11-25 22:00:45 +00:00
$item [ 'body' ] = bluesky_get_text ( $record , $item [ 'uri-id' ]);
2023-05-26 20:54:00 +00:00
$item [ 'created' ] = DateTimeFormat :: utc ( $record -> createdAt , DateTimeFormat :: MYSQL );
2023-11-11 05:30:07 +00:00
$item [ 'transmitted-languages' ] = $record -> langs ? ? [];
2023-12-20 20:46:24 +00:00
if (( $last_poll != 0 ) && strtotime ( $item [ 'created' ]) > $last_poll ) {
$item [ 'received' ] = $item [ 'created' ];
}
2023-05-26 20:54:00 +00:00
return $item ;
}
2023-05-23 05:23:13 +00:00
2023-11-25 22:00:45 +00:00
function bluesky_get_text ( stdClass $record , int $uri_id ) : string
2023-05-26 20:54:00 +00:00
{
2023-10-01 04:37:11 +00:00
$text = $record -> text ? ? '' ;
2023-05-26 20:54:00 +00:00
if ( empty ( $record -> facets )) {
return $text ;
2023-05-23 05:23:13 +00:00
}
2023-05-26 20:54:00 +00:00
$facets = [];
foreach ( $record -> facets as $facet ) {
$facets [ $facet -> index -> byteStart ] = $facet ;
}
krsort ( $facets );
foreach ( $facets as $facet ) {
$prefix = substr ( $text , 0 , $facet -> index -> byteStart );
$linktext = substr ( $text , $facet -> index -> byteStart , $facet -> index -> byteEnd - $facet -> index -> byteStart );
$suffix = substr ( $text , $facet -> index -> byteEnd );
2023-06-03 23:06:31 +00:00
$url = '' ;
$type = '$type' ;
2023-05-26 20:54:00 +00:00
foreach ( $facet -> features as $feature ) {
2023-06-03 23:06:31 +00:00
switch ( $feature -> $type ) {
case 'app.bsky.richtext.facet#link' :
$url = $feature -> uri ;
break ;
case 'app.bsky.richtext.facet#mention' :
2023-06-06 20:33:10 +00:00
$contact = Contact :: getByURL ( $feature -> did , null , [ 'id' ]);
2023-06-03 23:06:31 +00:00
if ( ! empty ( $contact [ 'id' ])) {
$url = DI :: baseUrl () . '/contact/' . $contact [ 'id' ];
if ( substr ( $linktext , 0 , 1 ) == '@' ) {
$prefix .= '@' ;
$linktext = substr ( $linktext , 1 );
}
}
break ;
2023-11-20 21:07:09 +00:00
case 'app.bsky.richtext.facet#tag' ;
2023-11-25 22:00:45 +00:00
Tag :: store ( $uri_id , Tag :: HASHTAG , $feature -> tag );
2023-11-20 21:07:09 +00:00
$url = DI :: baseUrl () . '/search?tag=' . urlencode ( $feature -> tag );
$linktext = '#' . $feature -> tag ;
break ;
2024-01-12 06:16:01 +00:00
2023-06-03 23:06:31 +00:00
default :
2023-11-20 21:07:09 +00:00
Logger :: notice ( 'Unhandled feature type' , [ 'type' => $feature -> $type , 'feature' => $feature , 'record' => $record ]);
2023-06-03 23:06:31 +00:00
break ;
2023-05-26 20:54:00 +00:00
}
}
if ( ! empty ( $url )) {
$text = $prefix . '[url=' . $url . ']' . $linktext . '[/url]' . $suffix ;
}
}
return $text ;
2023-05-23 05:23:13 +00:00
}
2023-12-20 20:46:24 +00:00
function bluesky_add_media ( stdClass $embed , array $item , int $fetch_uid , int $level , int $last_poll ) : array
2023-05-23 05:23:13 +00:00
{
2023-06-03 23:06:31 +00:00
$type = '$type' ;
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' :
2023-05-23 05:23:13 +00:00
$media = [
2023-06-03 23:06:31 +00:00
'uri-id' => $item [ 'uri-id' ],
'type' => Post\Media :: HTML ,
'url' => $embed -> external -> uri ,
'name' => $embed -> external -> title ,
'description' => $embed -> external -> description ,
2023-05-23 05:23:13 +00:00
];
Post\Media :: insert ( $media );
2023-06-03 23:06:31 +00:00
break ;
case 'app.bsky.embed.record#view' :
2024-02-23 13:32:24 +00:00
$original_uri = $uri = bluesky_get_uri ( $embed -> record );
2024-02-24 06:48:19 +00:00
$uri = bluesky_fetch_missing_post ( $uri , $item [ 'uid' ], $fetch_uid , Item :: PR_FETCHED , $item [ 'contact-id' ], $level , $last_poll );
2024-02-23 13:32:24 +00:00
if ( $uri ) {
$shared = Post :: selectFirst ([ 'uri-id' ], [ 'uri' => $uri , 'uid' => [ $item [ 'uid' ], 0 ]]);
$uri_id = $shared [ 'uri-id' ] ? ? 0 ;
2023-06-03 23:06:31 +00:00
}
2024-02-23 13:32:24 +00:00
if ( ! empty ( $uri_id )) {
$item [ 'quote-uri-id' ] = $uri_id ;
} else {
Logger :: debug ( 'Quoted post could not be fetched' , [ 'original-uri' => $original_uri , 'uri' => $uri ]);
2023-06-03 23:06:31 +00:00
}
break ;
case 'app.bsky.embed.recordWithMedia#view' :
2024-03-10 06:16:37 +00:00
bluesky_add_media ( $embed -> media , $item , $fetch_uid , $level , $last_poll );
2024-02-23 13:32:24 +00:00
$original_uri = $uri = bluesky_get_uri ( $embed -> record -> record );
2024-02-24 06:48:19 +00:00
$uri = bluesky_fetch_missing_post ( $uri , $item [ 'uid' ], $fetch_uid , Item :: PR_FETCHED , $item [ 'contact-id' ], $level , $last_poll );
2024-02-23 13:32:24 +00:00
if ( $uri ) {
$shared = Post :: selectFirst ([ 'uri-id' ], [ 'uri' => $uri , 'uid' => [ $item [ 'uid' ], 0 ]]);
$uri_id = $shared [ 'uri-id' ] ? ? 0 ;
2023-06-03 23:06:31 +00:00
}
2024-02-23 13:32:24 +00:00
if ( ! empty ( $uri_id )) {
$item [ 'quote-uri-id' ] = $uri_id ;
} else {
Logger :: debug ( 'Quoted post could not be fetched' , [ 'original-uri' => $original_uri , 'uri' => $uri ]);
2023-07-09 13:38:06 +00:00
}
2023-06-03 23:06:31 +00:00
break ;
default :
2024-02-23 13:32:24 +00:00
Logger :: notice ( 'Unhandled embed type' , [ 'uri-id' => $item [ 'uri-id' ], 'type' => $embed -> $type , 'embed' => $embed ]);
2023-06-03 23:06:31 +00:00
break ;
2023-05-23 05:23:13 +00:00
}
return $item ;
}
function bluesky_get_uri ( stdClass $post ) : string
{
2023-07-22 13:38:13 +00:00
if ( empty ( $post -> cid )) {
2023-10-18 20:16:59 +00:00
Logger :: info ( 'Invalid URI' , [ 'post' => $post ]);
2023-07-22 13:38:13 +00:00
return '' ;
}
2023-05-23 05:23:13 +00:00
return $post -> uri . ':' . $post -> cid ;
2023-05-24 05:49:26 +00:00
}
function bluesky_get_uri_class ( string $uri ) : ? stdClass
{
if ( empty ( $uri )) {
return null ;
}
2023-05-23 05:23:13 +00:00
2023-05-24 05:49:26 +00:00
$elements = explode ( ':' , $uri );
if ( empty ( $elements ) || ( $elements [ 0 ] != 'at' )) {
$post = Post :: selectFirstPost ([ 'extid' ], [ 'uri' => $uri ]);
return bluesky_get_uri_class ( $post [ 'extid' ] ? ? '' );
}
$class = new stdClass ;
$class -> cid = array_pop ( $elements );
$class -> uri = implode ( ':' , $elements );
2023-06-03 23:06:31 +00:00
if (( substr_count ( $class -> uri , '/' ) == 2 ) && ( substr_count ( $class -> cid , '/' ) == 2 )) {
$class -> uri .= ':' . $class -> cid ;
$class -> cid = '' ;
}
2023-05-24 05:49:26 +00:00
return $class ;
2023-05-23 05:23:13 +00:00
}
2023-05-24 05:49:26 +00:00
function bluesky_get_uri_parts ( string $uri ) : ? stdClass
{
$class = bluesky_get_uri_class ( $uri );
if ( empty ( $class )) {
return null ;
}
$parts = explode ( '/' , substr ( $class -> uri , 5 ));
$class = new stdClass ;
$class -> repo = $parts [ 0 ];
$class -> collection = $parts [ 1 ];
$class -> rkey = $parts [ 2 ];
return $class ;
}
2024-02-24 06:48:19 +00:00
function bluesky_fetch_missing_post ( string $uri , int $uid , int $fetch_uid , int $post_reason , int $causer , int $level , int $last_poll , string $fallback = '' ) : string
2023-05-24 05:49:26 +00:00
{
2023-06-06 20:33:10 +00:00
$fetched_uri = bluesky_fetch_post ( $uri , $uid );
if ( ! empty ( $fetched_uri )) {
return $fetched_uri ;
2023-05-24 05:49:26 +00:00
}
2023-06-11 19:24:44 +00:00
if ( ++ $level > 100 ) {
Logger :: info ( 'Recursion level too deep' , [ 'level' => $level , 'uid' => $uid , 'uri' => $uri , 'fallback' => $fallback ]);
// When the level is too deep we will fallback to the parent uri.
2023-08-22 13:01:12 +00:00
// Allthough the threading won't be correct, we at least had stored all posts and won't try again
2023-06-11 19:24:44 +00:00
return $fallback ;
}
2023-06-03 23:06:31 +00:00
$class = bluesky_get_uri_class ( $uri );
2024-05-16 12:24:34 +00:00
if ( empty ( $class )) {
return $fallback ;
}
2023-06-03 23:06:31 +00:00
$fetch_uri = $class -> uri ;
2023-05-26 20:54:00 +00:00
2023-06-11 19:24:44 +00:00
Logger :: debug ( 'Fetch missing post' , [ 'level' => $level , 'uid' => $uid , 'uri' => $uri ]);
2023-11-25 18:57:03 +00:00
$data = bluesky_xrpc_get ( $fetch_uid , 'app.bsky.feed.getPostThread' , [ 'uri' => $fetch_uri ]);
2023-05-24 05:49:26 +00:00
if ( empty ( $data )) {
2023-06-11 19:24:44 +00:00
Logger :: info ( 'Thread was not fetched' , [ 'level' => $level , 'uid' => $uid , 'uri' => $uri , 'fallback' => $fallback ]);
return $fallback ;
2023-05-26 20:54:00 +00:00
}
2023-08-22 13:01:12 +00:00
2023-10-29 11:39:45 +00:00
Logger :: debug ( 'Reply count' , [ 'level' => $level , 'uid' => $uid , 'uri' => $uri ]);
2023-05-26 20:54:00 +00:00
if ( $causer != 0 ) {
2024-08-12 20:20:32 +00:00
$causer = Contact :: getPublicContactId ( $causer , $uid );
2023-05-24 05:49:26 +00:00
}
2024-02-24 06:48:19 +00:00
return bluesky_process_thread ( $data -> thread , $uid , $fetch_uid , $post_reason , $causer , $level , $last_poll );
2023-06-06 20:33:10 +00:00
}
function bluesky_fetch_post ( string $uri , int $uid ) : string
{
if ( Post :: exists ([ 'uri' => $uri , 'uid' => [ $uid , 0 ]])) {
Logger :: debug ( 'Post exists' , [ 'uri' => $uri ]);
return $uri ;
}
$reply = Post :: selectFirst ([ 'uri' ], [ 'extid' => $uri , 'uid' => [ $uid , 0 ]]);
if ( ! empty ( $reply [ 'uri' ])) {
Logger :: debug ( 'Post with extid exists' , [ 'uri' => $uri ]);
return $reply [ 'uri' ];
}
return '' ;
}
2024-02-23 13:32:24 +00:00
function bluesky_fetch_uri_id ( string $uri , int $uid ) : string
{
$reply = Post :: selectFirst ([ 'uri-id' ], [ 'uri' => $uri , 'uid' => [ $uid , 0 ]]);
if ( ! empty ( $reply [ 'uri-id' ])) {
Logger :: debug ( 'Post with extid exists' , [ 'uri' => $uri ]);
return $reply [ 'uri-id' ];
}
$reply = Post :: selectFirst ([ 'uri-id' ], [ 'extid' => $uri , 'uid' => [ $uid , 0 ]]);
if ( ! empty ( $reply [ 'uri-id' ])) {
Logger :: debug ( 'Post with extid exists' , [ 'uri' => $uri ]);
return $reply [ 'uri-id' ];
}
return 0 ;
}
2024-02-24 06:48:19 +00:00
function bluesky_process_thread ( stdClass $thread , int $uid , int $fetch_uid , int $post_reason , int $causer , int $level , int $last_poll ) : string
2023-06-06 20:33:10 +00:00
{
2023-10-01 04:37:11 +00:00
if ( empty ( $thread -> post )) {
2023-10-18 20:16:59 +00:00
Logger :: info ( 'Invalid post' , [ 'post' => $thread ]);
2023-10-01 04:37:11 +00:00
return '' ;
}
2023-06-06 20:33:10 +00:00
$uri = bluesky_get_uri ( $thread -> post );
2023-10-01 04:37:11 +00:00
2023-06-06 20:33:10 +00:00
$fetched_uri = bluesky_fetch_post ( $uri , $uid );
if ( empty ( $fetched_uri )) {
2024-02-24 06:48:19 +00:00
$uri_id = bluesky_process_post ( $thread -> post , $uid , $fetch_uid , $post_reason , $causer , $level , $last_poll );
2024-02-23 13:32:24 +00:00
if ( $uri_id ) {
Logger :: debug ( 'Post has been processed and stored' , [ 'uri-id' => $uri_id , 'uri' => $uri ]);
return $uri ;
2023-06-11 19:24:44 +00:00
} else {
2024-02-23 13:32:24 +00:00
Logger :: info ( 'Post has not not been stored' , [ 'uri' => $uri ]);
2023-06-11 19:24:44 +00:00
return '' ;
2023-05-24 05:49:26 +00:00
}
2023-06-06 20:33:10 +00:00
} else {
Logger :: debug ( 'Post exists' , [ 'uri' => $uri ]);
$uri = $fetched_uri ;
}
2023-06-11 19:24:44 +00:00
foreach ( $thread -> replies ? ? [] as $reply ) {
2024-02-24 08:26:06 +00:00
$reply_uri = bluesky_process_thread ( $reply , $uid , $fetch_uid , Item :: PR_FETCHED , $causer , $level , $last_poll );
2023-06-11 19:24:44 +00:00
Logger :: debug ( 'Reply has been processed' , [ 'uri' => $uri , 'reply' => $reply_uri ]);
2023-05-24 05:49:26 +00:00
}
2023-05-26 20:54:00 +00:00
return $uri ;
2023-05-24 05:49:26 +00:00
}
2023-06-03 23:06:31 +00:00
function bluesky_get_contact ( stdClass $author , int $uid , int $fetch_uid ) : array
2023-05-23 05:23:13 +00:00
{
2024-06-05 03:37:43 +00:00
$condition = [ 'network' => Protocol :: BLUESKY , 'uid' => 0 , 'nurl' => $author -> did ];
2023-06-03 23:06:31 +00:00
$contact = Contact :: selectFirst ([ 'id' , 'updated' ], $condition );
2023-05-23 05:23:13 +00:00
2023-06-03 23:06:31 +00:00
$update = empty ( $contact ) || $contact [ 'updated' ] < DateTimeFormat :: utc ( 'now -24 hours' );
2024-02-23 13:32:24 +00:00
$public_fields = $fields = bluesky_get_contact_fields ( $author , $uid , $fetch_uid , $update );
2023-05-23 05:23:13 +00:00
2023-06-03 23:06:31 +00:00
$public_fields [ 'uid' ] = 0 ;
$public_fields [ 'rel' ] = Contact :: NOTHING ;
2023-05-23 05:23:13 +00:00
if ( empty ( $contact )) {
2023-06-03 23:06:31 +00:00
$cid = Contact :: insert ( $public_fields );
2023-05-23 05:23:13 +00:00
} else {
$cid = $contact [ 'id' ];
2023-06-03 23:06:31 +00:00
Contact :: update ( $public_fields , [ 'id' => $cid ], true );
2023-05-23 05:23:13 +00:00
}
2023-06-03 23:06:31 +00:00
if ( $uid != 0 ) {
2024-06-05 03:37:43 +00:00
$condition = [ 'network' => Protocol :: BLUESKY , 'uid' => $uid , 'nurl' => $author -> did ];
2023-05-23 05:23:13 +00:00
2023-06-03 23:06:31 +00:00
$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 ;
2023-05-23 05:23:13 +00:00
}
}
2023-06-03 23:06:31 +00:00
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 {
Logger :: debug ( 'Get public contact' , [ 'id' => $cid , 'uid' => $uid , 'update' => $update ]);
}
2023-05-23 05:23:13 +00:00
if ( ! empty ( $author -> avatar )) {
Contact :: updateAvatar ( $cid , $author -> avatar );
}
return Contact :: getById ( $cid );
}
2024-02-23 13:32:24 +00:00
function bluesky_get_contact_fields ( stdClass $author , int $uid , int $fetch_uid , bool $update ) : array
2023-05-23 05:23:13 +00:00
{
2024-02-24 06:48:19 +00:00
$nick = $author -> handle ? ? $author -> did ;
2024-02-28 03:05:57 +00:00
$name = $author -> displayName ? ? $nick ;
2023-05-23 05:23:13 +00:00
$fields = [
'uid' => $uid ,
'network' => Protocol :: BLUESKY ,
'priority' => 1 ,
'writable' => true ,
'blocked' => false ,
'readonly' => false ,
'pending' => false ,
'url' => $author -> did ,
'nurl' => $author -> did ,
2024-02-23 13:32:24 +00:00
'alias' => BLUESKY_WEB . '/profile/' . $nick ,
2024-02-28 03:05:57 +00:00
'name' => $name ? : $nick ,
2024-02-23 13:32:24 +00:00
'nick' => $nick ,
'addr' => $nick ,
2023-05-23 05:23:13 +00:00
];
2023-06-03 23:06:31 +00:00
if ( ! $update ) {
Logger :: debug ( 'Got contact fields' , [ 'uid' => $uid , 'url' => $fields [ 'url' ]]);
return $fields ;
}
2024-09-03 11:28:49 +00:00
$data = bluesky_get ( BLUESKY_DIRECTORY . '/' . $author -> did );
if ( ! empty ( $data )) {
$fields [ 'baseurl' ] = bluesky_get_pds ( '' , $data );
if ( ! empty ( $fields [ 'baseurl' ])) {
GServer :: check ( $fields [ 'baseurl' ], Protocol :: BLUESKY );
$fields [ 'gsid' ] = GServer :: getID ( $fields [ 'baseurl' ], true );
}
$fields [ 'pubkey' ] = bluesky_get_public_key ( '' , $data );
2023-11-19 18:55:05 +00:00
}
2024-02-23 13:32:24 +00:00
$data = bluesky_xrpc_get ( $fetch_uid , 'app.bsky.actor.getProfile' , [ 'actor' => $author -> did ]);
2023-05-23 05:23:13 +00:00
if ( empty ( $data )) {
2023-06-03 23:06:31 +00:00
Logger :: debug ( 'Error fetching contact fields' , [ 'uid' => $uid , 'url' => $fields [ 'url' ]]);
return $fields ;
2023-05-23 05:23:13 +00:00
}
2023-06-03 23:06:31 +00:00
$fields [ 'updated' ] = DateTimeFormat :: utcNow ( DateTimeFormat :: MYSQL );
2023-05-23 05:23:13 +00:00
2023-05-26 20:54:00 +00:00
if ( ! empty ( $data -> description )) {
$fields [ 'about' ] = HTML :: toBBCode ( $data -> description );
}
2023-05-23 05:23:13 +00:00
if ( ! empty ( $data -> banner )) {
$fields [ 'header' ] = $data -> banner ;
}
2023-06-03 23:06:31 +00:00
if ( ! empty ( $data -> viewer )) {
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 ;
2023-05-21 18:54:02 +00:00
}
2023-06-05 04:36:50 +00:00
function bluesky_get_feeds ( int $uid ) : array
{
$type = '$type' ;
$preferences = bluesky_get_preferences ( $uid );
2024-06-13 04:32:00 +00:00
if ( empty ( $preferences ) || empty ( $preferences -> preferences )) {
return [];
}
2023-06-05 04:36:50 +00:00
foreach ( $preferences -> preferences as $preference ) {
if ( $preference -> $type == 'app.bsky.actor.defs#savedFeedsPref' ) {
return $preference -> pinned ? ? [];
}
}
return [];
}
2024-06-13 04:32:00 +00:00
function bluesky_get_preferences ( int $uid ) : ? stdClass
2023-06-05 04:36:50 +00:00
{
$cachekey = 'bluesky:preferences:' . $uid ;
$data = DI :: cache () -> get ( $cachekey );
if ( ! is_null ( $data )) {
return $data ;
}
2023-06-13 20:43:51 +00:00
$data = bluesky_xrpc_get ( $uid , 'app.bsky.actor.getPreferences' );
2024-06-13 04:32:00 +00:00
if ( empty ( $data )) {
return null ;
}
2023-06-05 04:36:50 +00:00
DI :: cache () -> set ( $cachekey , $data , Duration :: HOUR );
return $data ;
}
2024-09-03 11:28:49 +00:00
function bluesky_get_did_by_profile ( string $url ) : string
{
try {
$curlResult = DI :: httpClient () -> get ( $url , HttpClientAccept :: HTML , [ HttpClientOptions :: REQUEST => HttpClientRequest :: CONTACTINFO ]);
} catch ( \Throwable $th ) {
return '' ;
}
if ( ! $curlResult -> isSuccess ()) {
return '' ;
}
$profile = $curlResult -> getBodyString ();
2024-09-10 10:26:05 +00:00
if ( empty ( $profile )) {
return '' ;
}
2024-09-03 11:28:49 +00:00
$doc = new DOMDocument ();
2024-09-10 10:26:05 +00:00
try {
@ $doc -> loadHTML ( $profile );
} catch ( \Throwable $th ) {
return '' ;
}
2024-09-03 11:28:49 +00:00
$xpath = new DOMXPath ( $doc );
$list = $xpath -> query ( '//p[@id]' );
foreach ( $list as $node ) {
foreach ( $node -> attributes as $attribute ) {
if ( $attribute -> name == 'id' ) {
$ids [ $attribute -> value ] = $node -> textContent ;
}
}
}
if ( empty ( $ids [ 'bsky_handle' ]) || empty ( $ids [ 'bsky_did' ])) {
return '' ;
}
if ( ! bluesky_valid_did ( $ids [ 'bsky_did' ], $ids [ 'bsky_handle' ])) {
Logger :: notice ( 'Invalid DID' , [ 'handle' => $ids [ 'bsky_handle' ], 'did' => $ids [ 'bsky_did' ]]);
return '' ;
}
return $ids [ 'bsky_did' ];
}
2024-03-02 13:32:11 +00:00
function bluesky_get_did_by_wellknown ( string $handle ) : string
{
$curlResult = DI :: httpClient () -> get ( 'http://' . $handle . '/.well-known/atproto-did' );
if ( $curlResult -> isSuccess () && substr ( $curlResult -> getBodyString (), 0 , 4 ) == 'did:' ) {
$did = $curlResult -> getBodyString ();
if ( ! bluesky_valid_did ( $did , $handle )) {
2024-04-28 10:36:47 +00:00
Logger :: notice ( 'Invalid DID' , [ 'handle' => $handle , 'did' => $did ]);
2024-03-02 13:32:11 +00:00
return '' ;
}
return $did ;
}
return '' ;
}
function bluesky_get_did_by_dns ( string $handle ) : string
{
$records = @ dns_get_record ( '_atproto.' . $handle . '.' , DNS_TXT );
if ( empty ( $records )) {
return '' ;
}
foreach ( $records as $record ) {
if ( ! empty ( $record [ 'txt' ]) && substr ( $record [ 'txt' ], 0 , 4 ) == 'did=' ) {
$did = substr ( $record [ 'txt' ], 4 );
if ( ! bluesky_valid_did ( $did , $handle )) {
2024-04-28 10:36:47 +00:00
Logger :: notice ( 'Invalid DID' , [ 'handle' => $handle , 'did' => $did ]);
2024-03-02 13:32:11 +00:00
return '' ;
}
return $did ;
}
}
return '' ;
}
2024-09-03 11:28:49 +00:00
function bluesky_get_did ( string $handle , int $uid ) : string
2023-05-21 18:54:02 +00:00
{
2024-06-13 04:32:00 +00:00
if ( $handle == '' ) {
return '' ;
}
if ( strpos ( $handle , '.' ) === false ) {
$handle .= '.' . BLUESKY_HOSTNAME ;
}
2024-09-03 11:28:49 +00:00
// At first we use the user PDS. That should cover most cases.
$pds = DI :: pConfig () -> get ( $uid , 'bluesky' , 'pds' );
if ( ! empty ( $pds )) {
$data = bluesky_get ( $pds . '/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode ( $handle ));
if ( ! empty ( $data ) && ! empty ( $data -> did )) {
Logger :: debug ( 'Got DID by user PDS call' , [ 'handle' => $handle , 'did' => $data -> did ]);
return $data -> did ;
}
2024-09-01 11:39:59 +00:00
}
2024-09-03 11:28:49 +00:00
// Then we query the DNS, which is used for third party handles (DNS should be faster than wellknown)
2024-09-01 11:39:59 +00:00
$did = bluesky_get_did_by_dns ( $handle );
if ( $did != '' ) {
2024-09-03 11:28:49 +00:00
Logger :: debug ( 'Got DID by DNS' , [ 'handle' => $handle , 'did' => $did ]);
return $did ;
}
// Then we query wellknown, which should mostly cover the rest.
$did = bluesky_get_did_by_wellknown ( $handle );
if ( $did != '' ) {
Logger :: debug ( 'Got DID by wellknown' , [ 'handle' => $handle , 'did' => $did ]);
2024-09-01 11:39:59 +00:00
return $did ;
2023-05-21 18:54:02 +00:00
}
2024-09-03 11:28:49 +00:00
// And finally we use the default PDS from Bluesky.
$data = bluesky_get ( BLUESKY_PDS . '/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode ( $handle ));
if ( ! empty ( $data ) && ! empty ( $data -> did )) {
Logger :: debug ( 'Got DID by system PDS call' , [ 'handle' => $handle , 'did' => $data -> did ]);
return $data -> did ;
}
Logger :: notice ( 'No DID detected' , [ 'handle' => $handle ]);
return '' ;
2023-05-21 18:54:02 +00:00
}
2024-05-04 01:00:44 +00:00
function bluesky_get_user_did ( int $uid , bool $refresh = false ) : ? string
2024-04-28 10:36:47 +00:00
{
if ( ! $refresh ) {
$did = DI :: pConfig () -> get ( $uid , 'bluesky' , 'did' );
if ( ! empty ( $did )) {
return $did ;
}
}
$handle = DI :: pConfig () -> get ( $uid , 'bluesky' , 'handle' );
2024-05-12 00:53:46 +00:00
if ( empty ( $handle )) {
return null ;
2024-05-04 01:00:44 +00:00
}
2024-05-12 00:53:46 +00:00
2024-09-03 11:28:49 +00:00
$did = bluesky_get_did ( $handle , $uid );
2024-04-28 10:36:47 +00:00
if ( empty ( $did )) {
2024-05-12 00:53:46 +00:00
return null ;
2024-04-28 10:36:47 +00:00
}
2024-05-12 00:53:46 +00:00
2024-04-28 10:36:47 +00:00
Logger :: debug ( 'Got DID for user' , [ 'uid' => $uid , 'handle' => $handle , 'did' => $did ]);
DI :: pConfig () -> set ( $uid , 'bluesky' , 'did' , $did );
return $did ;
}
2024-05-12 00:53:46 +00:00
function bluesky_get_user_pds ( int $uid ) : ? string
2023-11-19 18:55:05 +00:00
{
$pds = DI :: pConfig () -> get ( $uid , 'bluesky' , 'pds' );
if ( ! empty ( $pds )) {
return $pds ;
}
2024-05-12 00:53:46 +00:00
2024-04-28 10:36:47 +00:00
$did = bluesky_get_user_did ( $uid );
2024-05-12 00:53:46 +00:00
if ( empty ( $did )) {
return null ;
2023-11-25 18:57:03 +00:00
}
2024-05-12 00:53:46 +00:00
$pds = bluesky_get_pds ( $did );
2024-04-28 10:36:47 +00:00
if ( empty ( $pds )) {
2024-05-12 00:53:46 +00:00
return null ;
2024-04-28 10:36:47 +00:00
}
2024-05-12 00:53:46 +00:00
2023-11-19 18:55:05 +00:00
DI :: pConfig () -> set ( $uid , 'bluesky' , 'pds' , $pds );
return $pds ;
}
2024-09-03 11:28:49 +00:00
function bluesky_get_pds ( string $did , stdClass $data = null ) : ? string
2023-11-19 18:55:05 +00:00
{
2024-09-03 11:28:49 +00:00
if ( empty ( $data )) {
$data = bluesky_get ( BLUESKY_DIRECTORY . '/' . $did );
}
2023-11-19 18:55:05 +00:00
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 ;
}
2024-09-03 11:28:49 +00:00
function bluesky_get_public_key ( string $did , stdClass $data = null ) : ? string
{
if ( empty ( $data )) {
$data = bluesky_get ( BLUESKY_DIRECTORY . '/' . $did );
}
if ( empty ( $data ) || empty ( $data -> verificationMethod )) {
return null ;
}
foreach ( $data -> verificationMethod as $method ) {
if ( ! empty ( $method -> publicKeyMultibase )) {
return $method -> publicKeyMultibase ;
}
}
return null ;
}
2024-03-02 13:32:11 +00:00
function bluesky_valid_did ( string $did , string $handle ) : bool
{
$data = bluesky_get ( BLUESKY_DIRECTORY . '/' . $did );
if ( empty ( $data ) || empty ( $data -> alsoKnownAs )) {
return false ;
}
return in_array ( 'at://' . $handle , $data -> alsoKnownAs );
}
2023-05-21 18:54:02 +00:00
function bluesky_get_token ( int $uid ) : string
{
$token = DI :: pConfig () -> get ( $uid , 'bluesky' , 'access_token' );
$created = DI :: pConfig () -> get ( $uid , 'bluesky' , 'token_created' );
if ( empty ( $token )) {
2023-05-21 19:25:57 +00:00
return '' ;
2023-05-21 18:54:02 +00:00
}
if ( $created + 300 < time ()) {
return bluesky_refresh_token ( $uid );
}
return $token ;
}
function bluesky_refresh_token ( int $uid ) : string
{
$token = DI :: pConfig () -> get ( $uid , 'bluesky' , 'refresh_token' );
$data = bluesky_post ( $uid , '/xrpc/com.atproto.server.refreshSession' , '' , [ 'Authorization' => [ 'Bearer ' . $token ]]);
if ( empty ( $data )) {
return '' ;
}
Logger :: debug ( 'Refreshed token' , [ 'return' => $data ]);
DI :: pConfig () -> set ( $uid , 'bluesky' , 'access_token' , $data -> accessJwt );
DI :: pConfig () -> set ( $uid , 'bluesky' , 'refresh_token' , $data -> refreshJwt );
DI :: pConfig () -> set ( $uid , 'bluesky' , 'token_created' , time ());
return $data -> accessJwt ;
}
2023-05-21 19:25:57 +00:00
function bluesky_create_token ( int $uid , string $password ) : string
2023-05-21 18:54:02 +00:00
{
2024-04-28 10:36:47 +00:00
$did = bluesky_get_user_did ( $uid );
2024-05-12 00:53:46 +00:00
if ( empty ( $did )) {
return '' ;
}
2023-05-21 18:54:02 +00:00
$data = bluesky_post ( $uid , '/xrpc/com.atproto.server.createSession' , json_encode ([ 'identifier' => $did , 'password' => $password ]), [ 'Content-type' => 'application/json' ]);
if ( empty ( $data )) {
2023-12-06 06:31:52 +00:00
DI :: pConfig () -> set ( $uid , 'bluesky' , 'status' , BLUEKSY_STATUS_TOKEN_FAIL );
2023-05-21 18:54:02 +00:00
return '' ;
}
Logger :: debug ( 'Created token' , [ 'return' => $data ]);
DI :: pConfig () -> set ( $uid , 'bluesky' , 'access_token' , $data -> accessJwt );
DI :: pConfig () -> set ( $uid , 'bluesky' , 'refresh_token' , $data -> refreshJwt );
DI :: pConfig () -> set ( $uid , 'bluesky' , 'token_created' , time ());
2023-12-06 06:31:52 +00:00
DI :: pConfig () -> set ( $uid , 'bluesky' , 'status' , BLUEKSY_STATUS_TOKEN_OK );
2023-05-21 18:54:02 +00:00
return $data -> accessJwt ;
}
2023-06-13 20:43:51 +00:00
function bluesky_xrpc_post ( int $uid , string $url , $parameters ) : ? stdClass
{
2024-08-12 20:20:32 +00:00
$data = bluesky_post ( $uid , '/xrpc/' . $url , json_encode ( $parameters ), [ 'Content-type' => 'application/json' , 'Authorization' => [ 'Bearer ' . bluesky_get_token ( $uid )]]);
if ( ! empty ( $data )) {
Item :: incrementOutbound ( Protocol :: BLUESKY );
}
return $data ;
2023-06-13 20:43:51 +00:00
}
2023-05-21 18:54:02 +00:00
function bluesky_post ( int $uid , string $url , string $params , array $headers ) : ? stdClass
{
2024-05-12 00:53:46 +00:00
$pds = bluesky_get_user_pds ( $uid );
if ( empty ( $pds )) {
return null ;
}
2023-05-21 18:54:02 +00:00
try {
2024-05-12 00:53:46 +00:00
$curlResult = DI :: httpClient () -> post ( $pds . $url , $params , $headers );
2023-05-21 18:54:02 +00:00
} catch ( \Exception $e ) {
Logger :: notice ( 'Exception on post' , [ 'exception' => $e ]);
2023-12-06 06:31:52 +00:00
DI :: pConfig () -> set ( $uid , 'bluesky' , 'status' , BLUEKSY_STATUS_API_FAIL );
2023-05-21 18:54:02 +00:00
return null ;
}
if ( ! $curlResult -> isSuccess ()) {
2024-01-12 06:16:01 +00:00
Logger :: notice ( 'API Error' , [ 'error' => json_decode ( $curlResult -> getBodyString ()) ? : $curlResult -> getBodyString ()]);
2023-12-06 06:31:52 +00:00
DI :: pConfig () -> set ( $uid , 'bluesky' , 'status' , BLUEKSY_STATUS_API_FAIL );
2023-05-21 18:54:02 +00:00
return null ;
}
2023-12-06 06:31:52 +00:00
DI :: pConfig () -> set ( $uid , 'bluesky' , 'status' , BLUEKSY_STATUS_SUCCESS );
2024-01-12 06:16:01 +00:00
return json_decode ( $curlResult -> getBodyString ());
2023-05-21 18:54:02 +00:00
}
2023-06-13 20:43:51 +00:00
function bluesky_xrpc_get ( int $uid , string $url , array $parameters = []) : ? stdClass
{
if ( ! empty ( $parameters )) {
$url .= '?' . http_build_query ( $parameters );
}
2024-05-12 00:53:46 +00:00
$pds = bluesky_get_user_pds ( $uid );
if ( empty ( $pds )) {
return null ;
}
$data = bluesky_get ( $pds . '/xrpc/' . $url , HttpClientAccept :: JSON , [ HttpClientOptions :: HEADERS => [ 'Authorization' => [ 'Bearer ' . bluesky_get_token ( $uid )]]]);
2023-12-06 06:31:52 +00:00
DI :: pConfig () -> set ( $uid , 'bluesky' , 'status' , is_null ( $data ) ? BLUEKSY_STATUS_API_FAIL : BLUEKSY_STATUS_SUCCESS );
return $data ;
2023-06-13 20:43:51 +00:00
}
2023-11-19 18:55:05 +00:00
function bluesky_get ( string $url , string $accept_content = HttpClientAccept :: DEFAULT , array $opts = []) : ? stdClass
2023-05-21 18:54:02 +00:00
{
try {
2023-11-19 18:55:05 +00:00
$curlResult = DI :: httpClient () -> get ( $url , $accept_content , $opts );
2023-05-21 18:54:02 +00:00
} catch ( \Exception $e ) {
2024-04-28 10:36:47 +00:00
Logger :: notice ( 'Exception on get' , [ 'url' => $url , 'exception' => $e ]);
2023-05-21 18:54:02 +00:00
return null ;
}
if ( ! $curlResult -> isSuccess ()) {
2024-04-28 10:36:47 +00:00
Logger :: notice ( 'API Error' , [ 'url' => $url , 'error' => json_decode ( $curlResult -> getBodyString ()) ? : $curlResult -> getBodyString ()]);
2023-05-21 18:54:02 +00:00
return null ;
}
2024-08-12 20:20:32 +00:00
Item :: incrementInbound ( Protocol :: BLUESKY );
2024-01-12 06:16:01 +00:00
return json_decode ( $curlResult -> getBodyString ());
2023-08-22 13:01:12 +00:00
}