Add webfinger integration, bug fixes and minor changes ( #21 )
Added author credits in every function so that we know who to bother when something goes wrong Refer to issues #34 and #35
This commit is contained in:
parent
2d0f3de52a
commit
30c073538c
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "postman.php";
|
||||
/**
|
||||
* GNU social - a federating social network
|
||||
*
|
||||
@ -30,6 +29,11 @@ if (!defined ('GNUSOCIAL')) {
|
||||
exit (1);
|
||||
}
|
||||
|
||||
// Import required files by the plugin
|
||||
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "discoveryhints.php";
|
||||
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php";
|
||||
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "postman.php";
|
||||
|
||||
/**
|
||||
* @category Plugin
|
||||
* @package GNUsocial
|
||||
@ -40,6 +44,50 @@ if (!defined ('GNUSOCIAL')) {
|
||||
*/
|
||||
class ActivityPubPlugin extends Plugin
|
||||
{
|
||||
/**
|
||||
* Get remote user's ActivityPub_profile via a identifier
|
||||
* (https://www.w3.org/TR/activitypub/#obj-id)
|
||||
*
|
||||
* @author GNU Social
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $arg A remote user identifier
|
||||
* @return Activitypub_profile|null Valid profile in success | null otherwise
|
||||
*/
|
||||
protected function pull_remote_profile ($arg)
|
||||
{
|
||||
if (preg_match ('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
|
||||
// webfinger lookup
|
||||
try {
|
||||
return Activitypub_profile::ensure_web_finger ($arg);
|
||||
} catch (Exception $e) {
|
||||
common_log(LOG_ERR, 'Webfinger lookup failed for ' .
|
||||
$arg . ': ' . $e->getMessage ());
|
||||
}
|
||||
}
|
||||
|
||||
// Look for profile URLs, with or without scheme:
|
||||
$urls = array ();
|
||||
if (preg_match ('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
|
||||
$urls[] = $arg;
|
||||
}
|
||||
if (preg_match ('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
|
||||
$schemes = array ('http', 'https');
|
||||
foreach ($schemes as $scheme) {
|
||||
$urls[] = "$scheme://$arg";
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($urls as $url) {
|
||||
try {
|
||||
return Activitypub_profile::get_from_uri ($url);
|
||||
} catch (Exception $e) {
|
||||
common_log(LOG_ERR, 'Profile lookup failed for ' .
|
||||
$arg . ': ' . $e->getMessage ());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route/Reroute urls
|
||||
*
|
||||
@ -77,7 +125,7 @@ class ActivityPubPlugin extends Plugin
|
||||
* Plugin version information
|
||||
*
|
||||
* @param array $versions
|
||||
* @return boolean true
|
||||
* @return boolean hook true
|
||||
*/
|
||||
public function onPluginVersion (array &$versions)
|
||||
{
|
||||
@ -94,6 +142,8 @@ class ActivityPubPlugin extends Plugin
|
||||
|
||||
/**
|
||||
* Make sure necessary tables are filled out.
|
||||
*
|
||||
* @return boolean hook true
|
||||
*/
|
||||
function onCheckSchema ()
|
||||
{
|
||||
@ -102,6 +152,251 @@ class ActivityPubPlugin extends Plugin
|
||||
return true;
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* Discovery Events *
|
||||
********************************************************/
|
||||
|
||||
/**
|
||||
* Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz
|
||||
*
|
||||
* @author GNU Social
|
||||
* @param string $text The text from which to extract webfinger IDs
|
||||
* @param string $preMention Character(s) that signals a mention ('@', '!'...)
|
||||
* @return array The matching IDs (without $preMention) and each respective position in the given string.
|
||||
*/
|
||||
static function extractWebfingerIds ($text, $preMention='@')
|
||||
{
|
||||
$wmatches = array ();
|
||||
$result = preg_match_all ('/(?<!\S)'.preg_quote ($preMention, '/').'('.Nickname::WEBFINGER_FMT.')/',
|
||||
$text,
|
||||
$wmatches,
|
||||
PREG_OFFSET_CAPTURE);
|
||||
if ($result === false) {
|
||||
common_log (LOG_ERR, __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error=='.preg_last_error ().').');
|
||||
} elseif (count ($wmatches)) {
|
||||
common_debug (sprintf ('Found %d matches for WebFinger IDs: %s', count ($wmatches), _ve ($wmatches)));
|
||||
}
|
||||
return $wmatches[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile URL matches: @example.com/mublog/user
|
||||
*
|
||||
* @author GNU Social
|
||||
* @param string $text The text from which to extract URL mentions
|
||||
* @param string $preMention Character(s) that signals a mention ('@', '!'...)
|
||||
* @return array The matching URLs (without @ or acct:) and each respective position in the given string.
|
||||
*/
|
||||
static function extractUrlMentions ($text, $preMention='@')
|
||||
{
|
||||
$wmatches = array ();
|
||||
// In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged
|
||||
// with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important)
|
||||
$result = preg_match_all ('/(?:^|\s+)'.preg_quote ($preMention, '/').'('.URL_REGEX_DOMAIN_NAME.'(?:\/['.URL_REGEX_VALID_PATH_CHARS.']*)*)/',
|
||||
$text,
|
||||
$wmatches,
|
||||
PREG_OFFSET_CAPTURE);
|
||||
if ($result === false) {
|
||||
common_log (LOG_ERR, __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error=='.preg_last_error ().').');
|
||||
} elseif (count ($wmatches)) {
|
||||
common_debug(sprintf('Found %d matches for profile URL mentions: %s', count ($wmatches), _ve ($wmatches)));
|
||||
}
|
||||
return $wmatches[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any explicit remote mentions. Accepted forms:
|
||||
* Webfinger: @user@example.com
|
||||
* Profile link: @example.com/mublog/user
|
||||
*
|
||||
* @author GNU Social
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $sender
|
||||
* @param string $text input markup text
|
||||
* @param array &$mention in/out param: set of found mentions
|
||||
* @return boolean hook return value
|
||||
*/
|
||||
function onEndFindMentions(Profile $sender, $text, &$mentions)
|
||||
{
|
||||
$matches = array();
|
||||
|
||||
foreach (self::extractWebfingerIds($text, '@') as $wmatch) {
|
||||
list($target, $pos) = $wmatch;
|
||||
$this->log(LOG_INFO, "Checking webfinger person '$target'");
|
||||
$profile = null;
|
||||
try {
|
||||
$aprofile = Activitypub_profile::ensure_web_finger($target);
|
||||
$profile = $aprofile->local_profile();
|
||||
} catch (Exception $e) {
|
||||
$this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
assert ($profile instanceof Profile);
|
||||
|
||||
$displayName = !empty ($profile->nickname) && mb_strlen ($profile->nickname) < mb_strlen ($target)
|
||||
? $profile->getNickname () // TODO: we could do getBestName() or getFullname() here
|
||||
: $target;
|
||||
$url = $profile->getUri ();
|
||||
if (!common_valid_http_url ($url)) {
|
||||
$url = $profile->getUrl ();
|
||||
}
|
||||
$matches[$pos] = array('mentioned' => array ($profile),
|
||||
'type' => 'mention',
|
||||
'text' => $displayName,
|
||||
'position' => $pos,
|
||||
'length' => mb_strlen ($target),
|
||||
'url' => $url);
|
||||
}
|
||||
|
||||
foreach (self::extractUrlMentions ($text) as $wmatch) {
|
||||
list ($target, $pos) = $wmatch;
|
||||
$schemes = array('https', 'http');
|
||||
foreach ($schemes as $scheme) {
|
||||
$url = "$scheme://$target";
|
||||
$this->log(LOG_INFO, "Checking profile address '$url'");
|
||||
try {
|
||||
$aprofile = Activitypub_profile::get_from_uri ($url);
|
||||
$profile = $aprofile->local_profile();
|
||||
$displayName = !empty ($profile->nickname) && mb_strlen ($profile->nickname) < mb_strlen ($target) ?
|
||||
$profile->nickname : $target;
|
||||
$matches[$pos] = array('mentioned' => array ($profile),
|
||||
'type' => 'mention',
|
||||
'text' => $displayName,
|
||||
'position' => $pos,
|
||||
'length' => mb_strlen ($target),
|
||||
'url' => $profile->getUrl());
|
||||
break;
|
||||
} catch (Exception $e) {
|
||||
$this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($mentions as $i => $other) {
|
||||
// If we share a common prefix with a local user, override it!
|
||||
$pos = $other['position'];
|
||||
if (isset ($matches[$pos])) {
|
||||
$mentions[$i] = $matches[$pos];
|
||||
unset ($matches[$pos]);
|
||||
}
|
||||
}
|
||||
foreach ($matches as $mention) {
|
||||
$mentions[] = $mention;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow remote profile references to be used in commands:
|
||||
* sub update@status.net
|
||||
* whois evan@identi.ca
|
||||
* reply http://identi.ca/evan hey what's up
|
||||
*
|
||||
* @author GNU Social
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Command $command
|
||||
* @param string $arg
|
||||
* @param Profile &$profile
|
||||
* @return hook return code
|
||||
*/
|
||||
function onStartCommandGetProfile ($command, $arg, &$profile)
|
||||
{
|
||||
try {
|
||||
$aprofile = $this->pull_remote_profile ($arg);
|
||||
$profile = $aprofile->local_profile();
|
||||
} catch (Exception $e) {
|
||||
// No remote ActivityPub profile found
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile URI for remote profiles.
|
||||
*
|
||||
* @author GNU Social
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $profile
|
||||
* @param string $uri in/out
|
||||
* @return mixed hook return code
|
||||
*/
|
||||
function onStartGetProfileUri ($profile, &$uri)
|
||||
{
|
||||
$aprofile = Activitypub_profile::getKV ('profile_id', $profile->id);
|
||||
if ($aprofile instanceof Activitypub_profile) {
|
||||
$uri = $aprofile->get_uri ();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dummy string on AccountProfileBlock stating that ActivityPub is active
|
||||
* this is more of a placeholder for eventual useful stuff ._.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return void
|
||||
*/
|
||||
function onEndShowAccountProfileBlock (HTMLOutputter $out, Profile $profile)
|
||||
{
|
||||
if ($profile->isLocal()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
$aprofile = Activitypub_profile::getKV ('profile_id', $profile->id);
|
||||
} catch (NoResultException $e) {
|
||||
// Not a remote ActivityPub_profile! Maybe some other network
|
||||
// that has imported a non-local user (e.g.: OStatus)?
|
||||
return true;
|
||||
}
|
||||
|
||||
$out->elementStart('dl', 'entity_tags activitypub_profile');
|
||||
$out->element('dt', null, _m('ActivityPub'));
|
||||
$out->element('dd', null, _m('Active'));
|
||||
$out->elementEnd('dl');
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile from URI.
|
||||
*
|
||||
* @author GNU Social
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $uri
|
||||
* @param Profile &$profile in/out param: Profile got from URI
|
||||
* @return mixed hook return code
|
||||
*/
|
||||
function onStartGetProfileFromURI ($uri, &$profile)
|
||||
{
|
||||
// Don't want to do Web-based discovery on our own server,
|
||||
// so we check locally first. This duplicates the functionality
|
||||
// in the Profile class, since the plugin always runs before
|
||||
// that local lookup, but since we return false it won't run double.
|
||||
|
||||
$user = User::getKV ('uri', $uri);
|
||||
if ($user instanceof User) {
|
||||
$profile = $user->getProfile();
|
||||
return false;
|
||||
} else {
|
||||
$group = User_group::getKV ('uri', $uri);
|
||||
if ($group instanceof User_group) {
|
||||
$profile = $group->getProfile ();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Now, check remotely
|
||||
try {
|
||||
$aprofile = Activitypub_profile::get_from_uri ($uri);
|
||||
$profile = $aprofile->local_profile ();
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
return true; // It's not an ActivityPub profile as far as we know, continue event handling
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* Delivery Events *
|
||||
********************************************************/
|
||||
@ -110,6 +405,7 @@ class ActivityPubPlugin extends Plugin
|
||||
* Having established a remote subscription, send a notification to the
|
||||
* remote ActivityPub profile's endpoint.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $profile subscriber
|
||||
* @param Profile $other subscribee
|
||||
* @return hook return value
|
||||
@ -137,6 +433,7 @@ class ActivityPubPlugin extends Plugin
|
||||
/**
|
||||
* Notify remote server on unsubscribe.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $profile
|
||||
* @param Profile $other
|
||||
* @return hook return value
|
||||
@ -163,6 +460,7 @@ class ActivityPubPlugin extends Plugin
|
||||
/**
|
||||
* Notify remote users when their notices get favorited.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $profile of local user doing the faving
|
||||
* @param Notice $notice Notice being favored
|
||||
* @return hook return value
|
||||
@ -221,6 +519,7 @@ class ActivityPubPlugin extends Plugin
|
||||
/**
|
||||
* Notify remote users when their notices get de-favorited.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $profile of local user doing the de-faving
|
||||
* @param Notice $notice Notice being favored
|
||||
* @return hook return value
|
||||
@ -279,6 +578,7 @@ class ActivityPubPlugin extends Plugin
|
||||
/**
|
||||
* Notify remote users when their notices get deleted
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return boolean hook flag
|
||||
*/
|
||||
public function onEndDeleteOwnNotice ($user, $notice)
|
||||
@ -331,6 +631,7 @@ class ActivityPubPlugin extends Plugin
|
||||
/**
|
||||
* Insert notifications for replies, mentions and repeats
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return boolean hook flag
|
||||
*/
|
||||
function onStartNoticeDistribute ($notice)
|
||||
@ -416,6 +717,7 @@ class ActivityPubPlugin extends Plugin
|
||||
* Override the "from ActivityPub" bit in notice lists to link to the
|
||||
* original post and show the domain it came from.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Notice in $notice
|
||||
* @param string out &$name
|
||||
* @param string out &$url
|
||||
@ -451,23 +753,6 @@ class ActivityPubPlugin extends Plugin
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile URI for remote profiles.
|
||||
*
|
||||
* @param Profile $profile
|
||||
* @param string $uri in/out
|
||||
* @return mixed hook return code
|
||||
*/
|
||||
function onStartGetProfileUri ($profile, &$uri)
|
||||
{
|
||||
$aprofile = Activitypub_profile::getKV ('profile_id', $profile->id);
|
||||
if ($aprofile instanceof Activitypub_profile) {
|
||||
$uri = $aprofile->uri;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -504,6 +789,7 @@ class ActivityPubReturn
|
||||
/**
|
||||
* Return a valid answer
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param array $res
|
||||
* @return void
|
||||
*/
|
||||
@ -517,6 +803,7 @@ class ActivityPubReturn
|
||||
/**
|
||||
* Return an error
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $m
|
||||
* @param int32 $code
|
||||
* @return void
|
||||
|
@ -46,6 +46,7 @@ class apActorFollowersAction extends ManagedAction
|
||||
/**
|
||||
* Handle the Followers Collection request
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return void
|
||||
*/
|
||||
protected function handle ()
|
||||
|
@ -46,6 +46,7 @@ class apActorFollowingAction extends ManagedAction
|
||||
/**
|
||||
* Handle the Following Collection request
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return void
|
||||
*/
|
||||
protected function handle ()
|
||||
|
@ -46,6 +46,7 @@ class apActorInboxAction extends ManagedAction
|
||||
/**
|
||||
* Handle the Actor Inbox request
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return void
|
||||
*/
|
||||
protected function handle ()
|
||||
|
@ -46,6 +46,7 @@ class apActorLikedCollectionAction extends ManagedAction
|
||||
/**
|
||||
* Handle the Liked Collection request
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return void
|
||||
*/
|
||||
protected function handle ()
|
||||
@ -99,6 +100,7 @@ class apActorLikedCollectionAction extends ManagedAction
|
||||
* Take a fave object and turns it in a pretty array to be used
|
||||
* as a plugin answer
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Fave $fave_object
|
||||
* @return array pretty array representating a Fave
|
||||
*/
|
||||
@ -114,6 +116,7 @@ class apActorLikedCollectionAction extends ManagedAction
|
||||
/**
|
||||
* Fetch faves
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param int32 $user_id
|
||||
* @param int32 $limit
|
||||
* @param int32 $since_id
|
||||
|
@ -47,6 +47,7 @@ class apActorProfileAction extends ManagedAction
|
||||
/**
|
||||
* Handle the Actor Profile request
|
||||
*
|
||||
* @author Daniel Supernault <danielsupernault@gmail.com>
|
||||
* @return void
|
||||
*/
|
||||
protected function handle()
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once dirname (__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php";
|
||||
/**
|
||||
* GNU social - a federating social network
|
||||
*
|
||||
@ -47,6 +46,7 @@ class apSharedInboxAction extends ManagedAction
|
||||
/**
|
||||
* Handle the Shared Inbox request
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return void
|
||||
*/
|
||||
protected function handle ()
|
||||
|
@ -40,6 +40,8 @@ if (!isset ($data->object->content)) {
|
||||
}
|
||||
if (!isset ($data->object->url)) {
|
||||
ActivityPubReturn::error ("Object url was not specified.");
|
||||
} else if (!filter_var ($data->object->url, FILTER_VALIDATE_URL)) {
|
||||
ActivityPubReturn::error ("Invalid Object Url.");
|
||||
}
|
||||
|
||||
$content = $data->object->content;
|
||||
|
@ -43,6 +43,7 @@ class Activitypub_attachment extends Managed_DataObject
|
||||
/**
|
||||
* Generates a pretty array from an Attachment object
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Attachment $attachment
|
||||
* @return pretty array to be used in a response
|
||||
*/
|
||||
|
@ -43,6 +43,7 @@ class Activitypub_error extends Managed_DataObject
|
||||
/**
|
||||
* Generates a pretty error from a string
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $m
|
||||
* @return pretty array to be used in a response
|
||||
*/
|
||||
|
@ -44,6 +44,8 @@ class Activitypub_notice extends Managed_DataObject
|
||||
/**
|
||||
* Generates a pretty notice from a Notice object
|
||||
*
|
||||
* @author Daniel Supernault <danielsupernault@gmail.com>
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Notice $notice
|
||||
* @return pretty array to be used in a response
|
||||
*/
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
require_once dirname (__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php";
|
||||
/**
|
||||
* GNU social - a federating social network
|
||||
*
|
||||
@ -49,6 +48,7 @@ class Activitypub_profile extends Profile
|
||||
/**
|
||||
* Return table definition for Schema setup and DB_DataObject usage.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return array array of column definitions
|
||||
*/
|
||||
static function schemaDef ()
|
||||
@ -76,6 +76,7 @@ class Activitypub_profile extends Profile
|
||||
/**
|
||||
* Generates a pretty profile from a Profile object
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $profile
|
||||
* @return pretty array to be used in a response
|
||||
*/
|
||||
@ -118,6 +119,7 @@ class Activitypub_profile extends Profile
|
||||
/**
|
||||
* Insert the current objects variables into the database
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @access public
|
||||
* @throws ServerException
|
||||
*/
|
||||
@ -154,6 +156,8 @@ class Activitypub_profile extends Profile
|
||||
|
||||
/**
|
||||
* Fetch the locally stored profile for this Activitypub_profile
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return Profile
|
||||
* @throws NoProfileException if it was not found
|
||||
*/
|
||||
@ -169,6 +173,7 @@ class Activitypub_profile extends Profile
|
||||
/**
|
||||
* Generates an Activitypub_profile from a Profile
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $profile
|
||||
* @return Activitypub_profile
|
||||
* @throws Exception if no Activitypub_profile exists for given Profile
|
||||
@ -177,14 +182,14 @@ class Activitypub_profile extends Profile
|
||||
{
|
||||
$profile_id = $profile->getID ();
|
||||
|
||||
$aprofile = Activitypub_profile::getKV ('profile_id', $profile_id);
|
||||
$aprofile = self::getKV ('profile_id', $profile_id);
|
||||
if (!$aprofile instanceof Activitypub_profile) {
|
||||
// No Activitypub_profile for this profile_id,
|
||||
if (!$profile->isLocal ()) {
|
||||
// create one!
|
||||
$aprofile = self::create_from_local_profile ($profile);
|
||||
} else {
|
||||
throw new Exception ('No Activitypub_profile for Profile ID: '.$profile_id. ', this probably is a local profile.');
|
||||
throw new Exception ('No Activitypub_profile for Profile ID: '.$profile_id. ', this is a local user.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,6 +205,7 @@ class Activitypub_profile extends Profile
|
||||
* One must be careful not to give a user profile to this function
|
||||
* as only remote users have ActivityPub_profiles on local instance
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $profile
|
||||
* @return Activitypub_profile
|
||||
*/
|
||||
@ -227,6 +233,7 @@ class Activitypub_profile extends Profile
|
||||
/**
|
||||
* Returns sharedInbox if possible, inbox otherwise
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return string Inbox URL
|
||||
*/
|
||||
public function get_inbox ()
|
||||
@ -237,4 +244,122 @@ class Activitypub_profile extends Profile
|
||||
|
||||
return $this->sharedInboxuri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for uri property
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return string URI
|
||||
*/
|
||||
public function get_uri ()
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a valid Activitypub_profile when provided with a valid URI.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $url
|
||||
* @return Activitypub_profile
|
||||
* @throws Exception if it isn't possible to return an Activitypub_profile
|
||||
*/
|
||||
public static function get_from_uri ($url)
|
||||
{
|
||||
$explorer = new Activitypub_explorer ();
|
||||
$profiles_found = $explorer->lookup ($url);
|
||||
if (!empty ($profiles_found)) {
|
||||
return self::from_profile ($profiles_found[0]);
|
||||
} else {
|
||||
throw new Exception ('No valid ActivityPub profile found for given URI');
|
||||
}
|
||||
// If it doesn't return a valid Activitypub_profile an exception will
|
||||
// have been thrown before getting to this point.
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up, and if necessary create, an Activitypub_profile for the remote
|
||||
* entity with the given webfinger address.
|
||||
* This should never return null -- you will either get an object or
|
||||
* an exception will be thrown.
|
||||
*
|
||||
* @author GNU Social
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $addr webfinger address
|
||||
* @return Activitypub_profile
|
||||
* @throws Exception on error conditions
|
||||
*/
|
||||
public static function ensure_web_finger ($addr)
|
||||
{
|
||||
// Normalize $addr, i.e. add 'acct:' if missing
|
||||
$addr = Discovery::normalize ($addr);
|
||||
|
||||
// Try the cache
|
||||
$uri = self::cacheGet (sprintf ('activitypub_profile:webfinger:%s', $addr));
|
||||
|
||||
if ($uri !== false) {
|
||||
if (is_null ($uri)) {
|
||||
// Negative cache entry
|
||||
// TRANS: Exception.
|
||||
throw new Exception (_m ('Not a valid webfinger address (via cache).'));
|
||||
}
|
||||
try {
|
||||
return self::get_from_uri ($uri);
|
||||
} catch (Exception $e) {
|
||||
common_log (LOG_ERR, sprintf (__METHOD__ . ': Webfinger address cache inconsistent with database, did not find Activitypub_profile uri==%s', $uri));
|
||||
self::cacheSet (sprintf ('activitypub_profile:webfinger:%s', $addr), false);
|
||||
}
|
||||
}
|
||||
|
||||
// Now, try some discovery
|
||||
|
||||
$disco = new Discovery ();
|
||||
|
||||
try {
|
||||
$xrd = $disco->lookup ($addr);
|
||||
} catch (Exception $e) {
|
||||
// Save negative cache entry so we don't waste time looking it up again.
|
||||
// @todo FIXME: Distinguish temporary failures?
|
||||
self::cacheSet (sprintf ('activitypub_profile:webfinger:%s', $addr), null);
|
||||
// TRANS: Exception.
|
||||
throw new Exception (_m ('Not a valid webfinger address.'));
|
||||
}
|
||||
|
||||
$hints = array_merge (array ('webfinger' => $addr),
|
||||
DiscoveryHints::fromXRD ($xrd));
|
||||
|
||||
// If there's an Hcard, let's grab its info
|
||||
if (array_key_exists ('hcard', $hints)) {
|
||||
if (!array_key_exists ('profileurl', $hints) ||
|
||||
$hints['hcard'] != $hints['profileurl']) {
|
||||
$hcardHints = DiscoveryHints::fromHcardUrl ($hints['hcard']);
|
||||
$hints = array_merge ($hcardHints, $hints);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a profile page, try that!
|
||||
$profileUrl = null;
|
||||
if (array_key_exists ('profileurl', $hints)) {
|
||||
$profileUrl = $hints['profileurl'];
|
||||
try {
|
||||
common_log (LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl");
|
||||
$aprofile = self::get_from_uri ($hints['profileurl']);
|
||||
self::cacheSet (sprintf ('activitypub_profile:webfinger:%s', $addr), $aprofile->get_uri ());
|
||||
return $aprofile;
|
||||
} catch (Exception $e) {
|
||||
common_log (LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage ());
|
||||
// keep looking
|
||||
//
|
||||
// @todo FIXME: This means an error discovering from profile page
|
||||
// may give us a corrupt entry using the webfinger URI, which
|
||||
// will obscure the correct page-keyed profile later on.
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: try hcard
|
||||
// XXX: try FOAF
|
||||
|
||||
// TRANS: Exception. %s is a webfinger address.
|
||||
throw new Exception (sprintf (_m ('Could not find a valid profile for "%s".'), $addr));
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ class Activitypub_tag extends Managed_DataObject
|
||||
/**
|
||||
* Generates a pretty tag from a Tag object
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Tag $tag
|
||||
* @return pretty array to be used in a response
|
||||
*/
|
||||
|
154
utils/discoveryhints.php
Normal file
154
utils/discoveryhints.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* GNU social - a federating social network
|
||||
*
|
||||
* Some utilities for generating hint data
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category Plugin
|
||||
* @package GNUsocial
|
||||
* @author GNUsocial
|
||||
* @copyright 2010 Free Software Foundation http://fsf.org
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link https://www.gnu.org/software/social/
|
||||
*/
|
||||
if (!defined ('GNUSOCIAL')) {
|
||||
exit (1);
|
||||
}
|
||||
|
||||
class DiscoveryHints {
|
||||
static function fromXRD(XML_XRD $xrd)
|
||||
{
|
||||
$hints = array();
|
||||
|
||||
if (Event::handle('StartDiscoveryHintsFromXRD', array($xrd, &$hints))) {
|
||||
foreach ($xrd->links as $link) {
|
||||
switch ($link->rel) {
|
||||
case WebFingerResource_Profile::PROFILEPAGE:
|
||||
$hints['profileurl'] = $link->href;
|
||||
break;
|
||||
$hints['salmon'] = $link->href;
|
||||
break;
|
||||
case Discovery::UPDATESFROM:
|
||||
if (empty($link->type) || $link->type == 'application/atom+xml') {
|
||||
$hints['feedurl'] = $link->href;
|
||||
}
|
||||
break;
|
||||
case Discovery::HCARD:
|
||||
case Discovery::MF2_HCARD:
|
||||
$hints['hcard'] = $link->href;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::handle('EndDiscoveryHintsFromXRD', array($xrd, &$hints));
|
||||
}
|
||||
|
||||
return $hints;
|
||||
}
|
||||
|
||||
static function fromHcardUrl($url)
|
||||
{
|
||||
$client = new HTTPClient();
|
||||
$client->setHeader('Accept', 'text/html,application/xhtml+xml');
|
||||
try {
|
||||
$response = $client->get($url);
|
||||
|
||||
if (!$response->isOk()) {
|
||||
return null;
|
||||
}
|
||||
} catch (HTTP_Request2_Exception $e) {
|
||||
// Any HTTPClient error that might've been thrown
|
||||
common_log(LOG_ERR, __METHOD__ . ':'.$e->getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::hcardHints($response->getBody(),
|
||||
$response->getEffectiveUrl());
|
||||
}
|
||||
|
||||
static function hcardHints($body, $url)
|
||||
{
|
||||
$hcard = self::_hcard($body, $url);
|
||||
|
||||
if (empty($hcard)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$hints = array();
|
||||
|
||||
// XXX: don't copy stuff into an array and then copy it again
|
||||
|
||||
if (array_key_exists('nickname', $hcard) && !empty($hcard['nickname'][0])) {
|
||||
$hints['nickname'] = $hcard['nickname'][0];
|
||||
}
|
||||
|
||||
if (array_key_exists('name', $hcard) && !empty($hcard['name'][0])) {
|
||||
$hints['fullname'] = $hcard['name'][0];
|
||||
}
|
||||
|
||||
if (array_key_exists('photo', $hcard) && count($hcard['photo'])) {
|
||||
$hints['avatar'] = $hcard['photo'][0];
|
||||
}
|
||||
|
||||
if (array_key_exists('note', $hcard) && !empty($hcard['note'][0])) {
|
||||
$hints['bio'] = $hcard['note'][0];
|
||||
}
|
||||
|
||||
if (array_key_exists('adr', $hcard) && !empty($hcard['adr'][0])) {
|
||||
$hints['location'] = $hcard['adr'][0]['value'];
|
||||
}
|
||||
|
||||
if (array_key_exists('url', $hcard) && !empty($hcard['url'][0])) {
|
||||
$hints['homepage'] = $hcard['url'][0];
|
||||
}
|
||||
|
||||
return $hints;
|
||||
}
|
||||
|
||||
static function _hcard($body, $url)
|
||||
{
|
||||
$mf2 = new Mf2\Parser($body, $url);
|
||||
$mf2 = $mf2->parse();
|
||||
|
||||
if (empty($mf2['items'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hcards = array();
|
||||
|
||||
foreach ($mf2['items'] as $item) {
|
||||
if (!in_array('h-card', $item['type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We found a match, return it immediately
|
||||
if (isset($item['properties']['url']) && in_array($url, $item['properties']['url'])) {
|
||||
return $item['properties'];
|
||||
}
|
||||
|
||||
// Let's keep all the hcards for later, to return one of them at least
|
||||
$hcards[] = $item['properties'];
|
||||
}
|
||||
|
||||
// No match immediately for the url we expected, but there were h-cards found
|
||||
if (count($hcards) > 0) {
|
||||
return $hcards[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -49,6 +49,7 @@ class Activitypub_explorer
|
||||
* This function cleans the $this->discovered_actor_profiles array
|
||||
* so that there is no erroneous data
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $url User's url
|
||||
* @return array of Profile objects
|
||||
*/
|
||||
@ -64,6 +65,7 @@ class Activitypub_explorer
|
||||
* This is a recursive function that will accumulate the results on
|
||||
* $discovered_actor_profiles array
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $url User's url
|
||||
* @return array of Profile objects
|
||||
*/
|
||||
@ -83,6 +85,7 @@ class Activitypub_explorer
|
||||
* Get a local user profiles from its URL and joins it on
|
||||
* $this->discovered_actor_profiles
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $url User's url
|
||||
* @return boolean success state
|
||||
*/
|
||||
@ -116,6 +119,7 @@ class Activitypub_explorer
|
||||
* Get a remote user(s) profile(s) from its URL and joins it on
|
||||
* $this->discovered_actor_profiles
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $url User's url
|
||||
* @return boolean success state
|
||||
*/
|
||||
@ -152,6 +156,7 @@ class Activitypub_explorer
|
||||
/**
|
||||
* Save remote user profile in local instance
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param array $res remote response
|
||||
* @return Profile remote Profile object
|
||||
*/
|
||||
@ -174,6 +179,7 @@ class Activitypub_explorer
|
||||
* Validates a remote response in order to determine whether this
|
||||
* response is a valid profile or not
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param array $res remote response
|
||||
* @return boolean success state
|
||||
*/
|
||||
@ -192,12 +198,13 @@ class Activitypub_explorer
|
||||
* potential ActivityPub remote profiles, as so it is important to use
|
||||
* this hacky workaround (at least for now)
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $v URL
|
||||
* @return boolean|Profile false if fails | Profile object if successful
|
||||
*/
|
||||
static function get_profile_by_url ($v)
|
||||
public static function get_profile_by_url ($v)
|
||||
{
|
||||
$i = Managed_DataObject::getcached(Profile, "profileurl", $v);
|
||||
$i = Managed_DataObject::getcached("Profile", "profileurl", $v);
|
||||
if (empty ($i)) { // false = cache miss
|
||||
$i = new Profile;
|
||||
$result = $i->get ("profileurl", $v);
|
||||
@ -214,10 +221,11 @@ class Activitypub_explorer
|
||||
/**
|
||||
* Given a valid actor profile url returns its inboxes
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $url of Actor profile
|
||||
* @return boolean|array false if fails | array with inbox and shared inbox if successful
|
||||
*/
|
||||
static function get_actor_inboxes_uri ($url)
|
||||
public static function get_actor_inboxes_uri ($url)
|
||||
{
|
||||
$client = new HTTPClient ();
|
||||
$headers = array();
|
||||
|
@ -51,6 +51,7 @@ class Activitypub_postman
|
||||
/**
|
||||
* Create a postman to deliver something to someone
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile of sender
|
||||
* @param Activitypub_profile $to array of destinataries
|
||||
*/
|
||||
@ -66,6 +67,8 @@ class Activitypub_postman
|
||||
|
||||
/**
|
||||
* Send a follow notification to remote instance
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function follow ()
|
||||
{
|
||||
@ -79,6 +82,8 @@ class Activitypub_postman
|
||||
|
||||
/**
|
||||
* Send a Undo Follow notification to remote instance
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function undo_follow ()
|
||||
{
|
||||
@ -97,6 +102,7 @@ class Activitypub_postman
|
||||
/**
|
||||
* Send a Like notification to remote instances holding the notice
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Notice $notice
|
||||
*/
|
||||
public function like ($notice)
|
||||
@ -114,6 +120,7 @@ class Activitypub_postman
|
||||
/**
|
||||
* Send a Undo Like notification to remote instances holding the notice
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Notice $notice
|
||||
*/
|
||||
public function undo_like ($notice)
|
||||
@ -135,6 +142,7 @@ class Activitypub_postman
|
||||
/**
|
||||
* Send a Announce notification to remote instances
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Notice $notice
|
||||
*/
|
||||
public function announce ($notice)
|
||||
@ -156,6 +164,7 @@ class Activitypub_postman
|
||||
/**
|
||||
* Send a Create notification to remote instances
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Notice $notice
|
||||
*/
|
||||
public function create ($notice)
|
||||
@ -183,6 +192,7 @@ class Activitypub_postman
|
||||
/**
|
||||
* Send a Delete notification to remote instances holding the notice
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Notice $notice
|
||||
*/
|
||||
public function delete ($notice)
|
||||
@ -201,6 +211,7 @@ class Activitypub_postman
|
||||
/**
|
||||
* Clean list of inboxes to deliver messages
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return array To Inbox URLs
|
||||
*/
|
||||
private function to_inbox ()
|
||||
|
Reference in New Issue
Block a user