Fix remote notice grabber

Fixed bugs and simplified logic
This commit is contained in:
Diogo Cordeiro 2018-08-02 01:42:15 +01:00
parent 9f61de3c87
commit 51ebdcae30
14 changed files with 261 additions and 131 deletions

View File

@ -39,7 +39,11 @@ require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "ex
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "postman.php";
// So that this isn't hardcoded everywhere
define('ACTIVITYPUB_BASE_INSTANCE_URI', common_root_url()."index.php/user/");
define('ACTIVITYPUB_BASE_INSTANCE_URI', common_root_url().'index.php/user/');
const ACTIVITYPUB_PUBLIC_TO = ['https://www.w3.org/ns/activitystreams#Public',
'Public',
'as:Public'
];
/**
* @category Plugin
@ -81,32 +85,74 @@ class ActivityPubPlugin extends Plugin
}
/**
* Returns a notice from its URL since GNU Social doesn't provide
* this functionality
* Returns a notice from its URL.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $url Notice's URL
* @return Notice The Notice object
* @throws Exception This function or provides a Notice or fails with exception
*/
public static function get_local_notice_from_url($url)
public static function grab_notice_from_url($url)
{
/* Offline Grabbing */
try {
// Look for a know remote notice
return Notice::getByUri($url);
} catch (Exception $e) {
// Look for a local notice (unfortunately GNU Social doesn't
// provide this functionality)
try {
$candidate = Notice::getByID(intval(substr($url, strlen(common_local_url('shownotice', ['notice' => ''])))));
if ($candidate->getUrl() == $url) { // Sanity check
return $candidate;
} else {
common_debug ('ActivityPubPlugin Notice Grabber: '.$candidate->getUrl(). ' is different of '.$url);
throw new Exception('Notice not found.');
common_debug('ActivityPubPlugin Notice Grabber: '.$candidate->getUrl(). ' is different of '.$url);
}
} catch (Exception $e) {
common_debug ('ActivityPubPlugin Notice Grabber failed to find: '.$url);
throw new Exception('Notice not found.');
common_debug('ActivityPubPlugin Notice Grabber failed to find: '.$url. 'offline.');
}
}
/* Online Grabbing */
$client = new HTTPClient();
$headers = [];
$headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
$headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social';
$response = $client->get($url, $headers);
$res = json_decode($response->getBody(), JSON_UNESCAPED_SLASHES);
$settings = [];
try {
Activitypub_notice::validate_remote_notice($res);
} catch (Exception $e) {
common_debug('ActivityPub Explorer: Invalid potential remote notice while processing id: '.$url. '. He returned the following: '.json_encode($res, JSON_UNESCAPED_SLASHES));
throw $e;
}
if (isset($res->inReplyTo)) {
$settings['inReplyTo'] = $res->inReplyTo;
}
if (isset($res->latitude)) {
$settings['latitude'] = $res->latitude;
}
if (isset($res->longitude)) {
$settings['longitude'] = $res->longitude;
}
try {
return Activitypub_notice::create_notice(
ActivityPub_explorer::get_profile_from_url($res->atributtedTo),
$res->id,
$res->url,
$res->content,
$res->cc,
$settings
);
} catch (Exception $e) {
common_debug('ActivityPubPlugin Notice Grabber failed to find: '.$url. 'online.');
throw $e;
}
// When all the above failed in its quest of grabbing the Notice
throw new Exception('Notice not found.');
}
/**

View File

@ -81,15 +81,12 @@ class apActorInboxAction extends ManagedAction
ActivityPubReturn::error('Object was not specified.');
}
$discovery = new Activitypub_explorer;
// Get valid Actor object
try {
$actor_profile = $discovery->lookup($data->actor);
$actor_profile = $actor_profile[0];
$actor_profile = ActivityPub_explorer::get_profile_from_url($data->actor);
} catch (Exception $e) {
ActivityPubReturn::error('Invalid Actor.', 404);
ActivityPubReturn::error($e->getMessage(), 404);
}
unset($discovery);
// Public To:
$public_to = ['https://www.w3.org/ns/activitystreams#Public',

View File

@ -71,15 +71,12 @@ class apSharedInboxAction extends ManagedAction
ActivityPubReturn::error('Object was not specified.');
}
$discovery = new Activitypub_explorer;
// Get valid Actor object
try {
$actor_profile = $discovery->lookup($data->actor);
$actor_profile = $actor_profile[0];
$actor_profile = ActivityPub_explorer::get_profile_from_url($data->actor);
} catch (Exception $e) {
ActivityPubReturn::error('Invalid Actor.', 404);
ActivityPubReturn::error($e->getMessage(), 404);
}
unset($discovery);
// Public To:
$public_to = ['https://www.w3.org/ns/activitystreams#Public',

View File

@ -31,7 +31,7 @@ if (!defined('GNUSOCIAL')) {
try {
try {
$object_notice = ActivityPubPlugin::get_local_notice_from_url($data->object);
$object_notice = ActivityPubPlugin::grab_notice_from_url($data->object);
} catch (Exception $e) {
ActivityPubReturn::error('Invalid Object specified.');
}

View File

@ -31,102 +31,38 @@ if (!defined('GNUSOCIAL')) {
$valid_object_types = ['Note'];
// Validate data
if (!isset($data->object->id)) {
ActivityPubReturn::error('Object ID not specified.');
} elseif (!filter_var($data->object->id, FILTER_VALIDATE_URL)) {
ActivityPubReturn::error('Invalid Object ID.');
}
if (!(isset($data->object->type) && in_array($data->object->type, $valid_object_types))) {
ActivityPubReturn::error('Invalid Object type.');
}
if (!isset($data->object->content)) {
ActivityPubReturn::error('Object content was not specified.');
}
if (!isset($data->object->url)) {
ActivityPubReturn::error('Object url was not specified.');
} elseif (!filter_var($data->object->url, FILTER_VALIDATE_URL)) {
ActivityPubReturn::error('Invalid Object URL.');
}
if (!isset($data->object->to)) {
ActivityPubReturn::error('Object To was not specified.');
}
$content = $data->object->content;
$act = new Activity();
$act->verb = ActivityVerb::POST;
$act->time = time();
$act->actor = $actor_profile->asActivityObject();
$act->context = new ActivityContext();
// Is this a reply?
if (isset($data->object->inReplyTo)) {
try {
$inReplyTo = ActivityPubPlugin::get_local_notice_from_url($data->object->inReplyTo);
} catch (Exception $e) {
ActivityPubReturn::error('Invalid Object inReplyTo value.');
}
$act->context->replyToID = $inReplyTo->getUri();
$act->context->replyToUrl = $inReplyTo->getUrl();
} else {
$inReplyTo = null;
}
$discovery = new Activitypub_explorer;
// Generate Cc objects
if (isset($data->object->cc) && is_array($data->object->cc)) {
// Remove duplicates from Cc actors set
array_unique($data->object->cc);
foreach ($data->object->cc as $cc_url) {
try {
$to_profiles = array_merge($to_profiles, $discovery->lookup($cc_url));
} catch (Exception $e) {
// Invalid actor found, just let it go. // TODO: Fallback to OStatus
}
}
} elseif (empty($data->object->cc) || in_array($data->object->cc, $public_to)) {
// No need to do anything else at this point, let's just break out the if
} else {
try {
$to_profiles = array_merge($to_profiles, $discovery->lookup($data->object->cc));
} catch (Exception $e) {
// Invalid actor found, just let it go. // TODO: Fallback to OStatus
}
}
unset($discovery);
foreach ($to_profiles as $tp) {
$act->context->attention[ActivityPubPlugin::actor_uri($tp)] = 'http://activitystrea.ms/schema/1.0/person';
}
// Add location if that is set
if (isset($data->object->latitude, $data->object->longitude)) {
$act->context->location = Location::fromLatLon($data->object->latitude, $data->object->longitude);
}
// Reject notice if it is too long (without the HTML)
if (Notice::contentTooLong($content)) {
ActivityPubReturn::error("That's too long. Maximum notice size is %d character.");
}
$options = array('source' => 'ActivityPub', 'uri' => $data->object->id, 'url' => $data->object->url);
// $options gets filled with possible scoping settings
ToSelector::fillActivity($this, $act, $options);
$actobj = new ActivityObject();
$actobj->type = ActivityObject::NOTE;
$actobj->content = strip_tags($content,'<p><b><i><u><a><ul><ol><li>');
// Finally add the activity object to our activity
$act->objects[] = $actobj;
$res = $data->object;
try {
Notice::saveActivity($act, $actor_profile, $options);
ActivityPubReturn::answer();
Activitypub_notice::validate_remote_notice($res);
} catch (Exception $e) {
common_debug('ActivityPub Inbox Create Note: Invalid note: '.$e->getMessage());
ActivityPubReturn::error($e->getMessage());
}
$settings = [];
if (isset($res->inReplyTo)) {
$settings['inReplyTo'] = $res->inReplyTo;
}
if (isset($res->latitude)) {
$settings['latitude'] = $res->latitude;
}
if (isset($res->longitude)) {
$settings['longitude'] = $res->longitude;
}
try {
Activitypub_notice::create_notice(
ActivityPub_explorer::get_profile_from_url($res->atributtedTo),
$res->id,
$res->url,
$res->content,
$res->cc,
$settings
);
ActivityPubReturn::answer();
} catch (Exception $e) {
common_debug('ActivityPub Inbox Create Note: Failed Create Note: '.$e->getMessage());
ActivityPubReturn::error($e->getMessage());
}

View File

@ -30,7 +30,7 @@ if (!defined('GNUSOCIAL')) {
}
try {
$notice = ActivityPubPlugin::get_local_notice_from_url($data->object->id);
$notice = ActivityPubPlugin::grab_notice_from_url($data->object->id);
$notice->deleteAs($actor_profile);
ActivityPubReturn::answer();
} catch (Exception $e) {

View File

@ -36,12 +36,12 @@ if (!filter_var($data->object, FILTER_VALIDATE_URL)) {
// Ensure valid Object profile
try {
if (!isset ($profile)) {
if (!isset($profile)) {
$object_profile = new Activitypub_explorer;
$object_profile = $object_profile->lookup($data->object)[0];
} else {
$object_profile = $profile;
unset ($profile);
unset($profile);
}
} catch (Exception $e) {
ActivityPubReturn::error('Invalid Object Actor URL.', 404);
@ -62,4 +62,4 @@ if (!Subscription::exists($actor_profile, $object_profile)) {
} else {
common_debug('ActivityPubPlugin: Received a repeated Follow request from '.$data->actor.' to '.$data->object);
ActivityPubReturn::error('Already following.', 409);
}
}

View File

@ -31,7 +31,7 @@ if (!defined('GNUSOCIAL')) {
try {
try {
$object_notice = ActivityPubPlugin::get_local_notice_from_url($data->object);
$object_notice = ActivityPubPlugin::grab_notice_from_url($data->object);
} catch (Exception $e) {
ActivityPubReturn::error('Invalid Object specified.');
}

View File

@ -41,7 +41,7 @@ switch ($data->object->type) {
if (!isset($data->object->object)) {
ActivityPubReturn::error('Notice URI was not specified.');
}
Fave::removeEntry($actor_profile, ActivityPubPlugin::get_local_notice_from_url($data->object->object));
Fave::removeEntry($actor_profile, ActivityPubPlugin::grab_notice_from_url($data->object->object));
// Notice disfavorited successfully.
ActivityPubReturn::answer();
} catch (Exception $e) {
@ -73,6 +73,7 @@ switch ($data->object->type) {
case 'Announce':
// This is a dummy entry point as GNU Social doesn't allow Undo Announce
ActivityPubReturn::answer();
// no break
default:
ActivityPubReturn::error('Invalid object type.');
break;

View File

@ -52,7 +52,7 @@ class Activitypub_create extends Managed_DataObject
{
$res = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $id,
'id' => $id.'/create',
'type' => 'Create',
'to' => $object['to'],
'cc' => $object['cc'],

View File

@ -49,11 +49,13 @@ class Activitypub_delete extends Managed_DataObject
*/
public static function delete_to_array($object)
{
$res = array("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Delete",
"actor" => $object["actor"],
"object" => $object
);
$res = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $object['id'].'/delete',
'type' => 'Delete',
'actor' => $object['actor'],
'object' => $object
];
return $res;
}
}

View File

@ -105,4 +105,127 @@ class Activitypub_notice extends Managed_DataObject
return $item;
}
/**
* Create a Notice via ActivityPub data.
* Returns created Notice.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $actor_profile
* @param int32 $id
* @param string $url
* @param string $content
* @param array|string $cc
* @param array $settings possible keys: ['inReplyTo', 'latitude', 'longitude']
* @return Notice
* @throws Exception
*/
public static function create_notice($actor_profile, $id, $url, $content, $cc, $settings)
{
$act = new Activity();
$act->verb = ActivityVerb::POST;
$act->time = time();
$act->actor = $actor_profile->asActivityObject();
$act->context = new ActivityContext();
// Is this a reply?
if (isset($settings['inReplyTo'])) {
try {
$inReplyTo = ActivityPubPlugin::grab_notice_from_url($settings['inReplyTo']);
} catch (Exception $e) {
throw new Exception('Invalid Object inReplyTo value.');
}
$act->context->replyToID = $inReplyTo->getUri();
$act->context->replyToUrl = $inReplyTo->getUrl();
} else {
$inReplyTo = null;
}
$discovery = new Activitypub_explorer;
// Generate Cc objects
if (is_array($cc)) {
// Remove duplicates from Cc actors set
array_unique($cc);
foreach ($cc as $cc_url) {
try {
$cc_profiles = array_merge($cc_profiles, $discovery->lookup($cc_url));
} catch (Exception $e) {
// Invalid actor found, just let it go. // TODO: Fallback to OStatus
}
}
} elseif (empty($cc) || in_array($cc, ACTIVITYPUB_PUBLIC_TO)) {
// No need to do anything else at this point, let's just break out the if
} else {
try {
$cc_profiles = array_merge($cc_profiles, $discovery->lookup($cc));
} catch (Exception $e) {
// Invalid actor found, just let it go. // TODO: Fallback to OStatus
}
}
unset($discovery);
foreach ($cc_profiles as $tp) {
$act->context->attention[ActivityPubPlugin::actor_uri($tp)] = 'http://activitystrea.ms/schema/1.0/person';
}
// Add location if that is set
if (isset($settings['latitude'], $settings['longitude'])) {
$act->context->location = Location::fromLatLon($settings['latitude'], $settings['longitude']);
}
// Reject notice if it is too long (without the HTML)
if (Notice::contentTooLong($content)) {
throw new Exception('That\'s too long. Maximum notice size is %d character.');
}
$options = ['source' => 'ActivityPub', 'uri' => $id, 'url' => $url];
$actobj = new ActivityObject();
$actobj->type = ActivityObject::NOTE;
$actobj->content = strip_tags($content, '<p><b><i><u><a><ul><ol><li>');
// Finally add the activity object to our activity
$act->objects[] = $actobj;
try {
return Notice::saveActivity($act, $actor_profile, $options);
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
}
/**
* Validates a remote notice.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Object $data
* @return boolean true in case of success
* @throws Exception
*/
public static function validate_remote_notice($data)
{
if (!isset($data->id)) {
throw new Exception('Object ID not specified.');
} elseif (!filter_var($data->id, FILTER_VALIDATE_URL)) {
throw new Exception('Invalid Object ID.');
}
if ($data->type !== 'Note') {
throw new Exception('Invalid Object type.');
}
if (!isset($data->content)) {
throw new Exception('Object content was not specified.');
}
if (!isset($data->url)) {
throw new Exception('Object url was not specified.');
} elseif (!filter_var($data->url, FILTER_VALIDATE_URL)) {
throw new Exception('Invalid Object URL.');
}
if (!isset($data->to)) {
throw new Exception('Object To was not specified.');
}
return true;
}
}

View File

@ -42,7 +42,28 @@ if (!defined('GNUSOCIAL')) {
*/
class Activitypub_explorer
{
private $discovered_actor_profiles = array();
private $discovered_actor_profiles = [];
/**
* Shortcut function to get a single profile from its URL.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $url
* @return Profile
* @throws Exception
*/
public static function get_profile_from_url($url)
{
$discovery = new Activitypub_explorer;
// Get valid Actor object
try {
$actor_profile = $discovery->lookup($url);
return $actor_profile[0];
} catch (Exception $e) {
throw new Exception('Invalid Actor.');
}
unset($discovery);
}
/**
* Get every profile from the given URL
@ -55,8 +76,12 @@ class Activitypub_explorer
*/
public function lookup($url)
{
if (in_array($url, ACTIVITYPUB_PUBLIC_TO)) {
return [];
}
common_debug('ActivityPub Explorer: Started now looking for '.$url);
$this->discovered_actor_profiles = array();
$this->discovered_actor_profiles = [];
return $this->_lookup($url);
}
@ -195,7 +220,7 @@ class Activitypub_explorer
foreach ($res["orderedItems"] as $profile) {
if ($this->_lookup($profile) == false) {
common_debug('ActivityPub Explorer: Found an inavlid actor for '.$profile);
// XXX: Invalid actor found, not sure how we handle those
// TODO: Invalid actor found, fallback to OStatus
}
}
// Go through entire collection
@ -211,6 +236,7 @@ class Activitypub_explorer
common_debug('ActivityPub Explorer: Invalid potential remote actor while grabbing remotely: '.$url. '. He returned the following: '.json_encode($res, JSON_UNESCAPED_SLASHES));
}
// TODO: Fallback to OStatus
return false;
}
@ -279,10 +305,12 @@ class Activitypub_explorer
$id = $profile->getID();
$imagefile = new ImageFile(null, $temp_filename);
$filename = Avatar::filename($id,
$filename = Avatar::filename(
$id,
image_type_to_extension($imagefile->type),
null,
common_timestamp());
common_timestamp()
);
rename($temp_filename, Avatar::path($filename));
} catch (Exception $e) {
unlink($temp_filename);

View File

@ -227,7 +227,7 @@ class Activitypub_postman
{
$data = Activitypub_announce::announce_to_array(
ActivityPubPlugin::actor_uri($this->actor),
Activitypub_notice::notice_to_array($notice)
$notice->getUri()
);
$data = json_encode($data, JSON_UNESCAPED_SLASHES);