Add Remote Flow (#43) and PHP Unit Tests from dansup (#37)

Now the code is following most of PSR
Various changes from various branches (some testing will be required)
Fixed various issues
This commit is contained in:
Diogo Cordeiro 2018-07-26 21:12:13 +00:00
parent e377b87ff7
commit eaad9423dd
45 changed files with 4723 additions and 1918 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/vendor/

View File

@ -19,8 +19,8 @@
*
* @category Plugin
* @package GNUsocial
* @author Daniel Supernault <danielsupernault@gmail.com>
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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/
@ -37,23 +37,58 @@ require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "po
/**
* @category Plugin
* @package GNUsocial
* @author Daniel Supernault <danielsupernault@gmail.com>
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class ActivityPubPlugin extends Plugin
{
/**
* Returns a Actor's URI from its local $profile
* Works both for local and remote users.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $profile Actor's local profile
* @return string Actor's URI
*/
public static function actor_uri($profile)
{
if ($profile->isLocal()) {
return common_root_url()."index.php/user/".$profile->getID();
} else {
return $profile->getUri();
}
}
/**
* Returns a Actor's URL from its local $profile
* Works both for local and remote users.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Profile $profile Actor's local profile
* @return string Actor's URL
*/
public static function actor_url($profile)
{
return actor_uri($profile)."/";
}
public static function stripUrlPath($url)
{
$urlParts = parse_url($url);
$newUrl = $urlParts['scheme'] . "://" . $urlParts['host'] . "/";
return $newUrl;
}
/**
* 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)
public static function pull_remote_profile($arg)
{
if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
// webfinger lookup
@ -96,29 +131,62 @@ class ActivityPubPlugin extends Plugin
*/
public function onRouterInitialized(URLMapper $m)
{
ActivityPubURLMapperOverwrite::overwrite_variable ($m, ':nickname',
ActivityPubURLMapperOverwrite::overwrite_variable(
$m,
'user/:id',
['action' => 'showstream'],
['id' => '[0-9]+'],
'apActorProfile'
);
// Special route for webfinger purposes
ActivityPubURLMapperOverwrite::overwrite_variable(
$m,
':nickname',
['action' => 'showstream'],
['nickname' => Nickname::DISPLAY_FMT],
'apActorProfile');
'apActorProfile'
);
$m->connect (':nickname/liked.json',
['action' => 'apActorLikedCollection'],
['nickname' => Nickname::DISPLAY_FMT]);
$m->connect(
':nickname/remote_follow',
['action' => 'apRemoteFollow'],
['nickname' => '[A-Za-z0-9_-]+']
);
$m->connect (':nickname/followers.json',
$m->connect(
'activitypub/authorize_follow',
['action' => 'apAuthorizeRemoteFollow']
);
$m->connect(
'user/:id/liked.json',
['action' => 'apActorLiked'],
['id' => '[0-9]+']
);
$m->connect(
'user/:id/followers.json',
['action' => 'apActorFollowers'],
['nickname' => Nickname::DISPLAY_FMT]);
['id' => '[0-9]+']
);
$m->connect (':nickname/following.json',
$m->connect(
'user/:id/following.json',
['action' => 'apActorFollowing'],
['nickname' => Nickname::DISPLAY_FMT]);
['id' => '[0-9]+']
);
$m->connect (':nickname/inbox.json',
$m->connect(
'user/:id/inbox.json',
['action' => 'apActorInbox'],
['nickname' => Nickname::DISPLAY_FMT]);
['id' => '[0-9]+']
);
$m->connect ('inbox.json',
array('action' => 'apSharedInbox'));
$m->connect(
'inbox.json',
['action' => 'apSharedInbox']
);
}
/**
@ -131,7 +199,7 @@ class ActivityPubPlugin extends Plugin
{
$versions[] = [ 'name' => 'ActivityPub',
'version' => GNUSOCIAL_VERSION,
'author' => 'Daniel Supernault, Diogo Cordeiro',
'author' => 'Diogo Cordeiro, Daniel Supernault',
'homepage' => 'https://www.gnu.org/software/social/',
'rawdescription' =>
// Todo: Translation
@ -145,13 +213,135 @@ class ActivityPubPlugin extends Plugin
*
* @return boolean hook true
*/
function onCheckSchema ()
public function onCheckSchema()
{
$schema = Schema::get();
$schema->ensureTable('Activitypub_profile', Activitypub_profile::schemaDef());
$schema->ensureTable('Activitypub_pending_follow_requests', Activitypub_pending_follow_requests::schemaDef());
return true;
}
/********************************************************
* Remote Subscription Events *
********************************************************/
/**
* Add in an ActivityPub subscribe button
*
* @author GNU Social
* @param type $output
* @param type $profile
* @return boolean hook false
*/
public function onStartProfileRemoteSubscribe($output, $profile)
{
$this->onStartProfileListItemActionElements($output, $profile);
return false;
}
/**
* Add in an ActivityPub subscribe button
*
* @author GNU Social
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Action|Widget $item
* @return boolean hook return value
* @throws ServerException
*/
public function onStartProfileListItemActionElements($item)
{ // FIXME: This one can accept both an Action and a Widget. Confusing! Refactor to (HTMLOutputter $out, Profile $target)!
if (common_logged_in()) {
// only non-logged in users get to see the "remote subscribe" form
return true;
} elseif (!$item->getTarget()->isLocal()) {
// we can (for now) only provide remote subscribe forms for local users
return true;
}
if ($item instanceof ProfileAction) {
$output = $item;
} elseif ($item instanceof Widget) {
$output = $item->out;
} else {
// Bad $item class, don't know how to use this for outputting!
throw new ServerException('Bad item type for onStartProfileListItemActionElements');
}
// Add an ActivityPub subscribe
$output->elementStart('li', 'entity_subscribe');
$url = common_local_url(
'apRemoteFollow',
array('nickname' => $item->getTarget()->getNickname())
);
$output->element(
'a',
array('href' => $url,
'class' => 'entity_remote_subscribe'),
// TRANS: Link text for a user to subscribe to an OStatus user.
_m('Subscribe')
);
$output->elementEnd('li');
return true;
}
/**
* Add in an ActivityPub subscribe button
*
* @author GNU Social
* @param type $action
* @param string $target
* @return boolean hook return value
*/
public function showEntityRemoteSubscribe($action, $target='apRemoteFollow')
{
if (!$action->getScoped() instanceof Profile) {
// early return if we're not logged in
return true;
}
if ($action->getScoped()->sameAs($action->getTarget())) {
$action->elementStart('div', 'entity_actions');
$action->elementStart('p', array('id' => 'entity_remote_subscribe',
'class' => 'entity_subscribe'));
$action->element(
'a',
array('href' => common_local_url($target),
'class' => 'entity_remote_subscribe'),
// TRANS: Link text for link to remote subscribe.
_m('Remote')
);
$action->elementEnd('p');
$action->elementEnd('div');
}
}
/**
* 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 boolean hook return value
*/
public 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');
}
/********************************************************
* Discovery Events *
********************************************************/
@ -164,13 +354,15 @@ class ActivityPubPlugin extends Plugin
* @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='@')
public static function extractWebfingerIds($text, $preMention='@')
{
$wmatches = array();
$result = preg_match_all ('/(?<!\S)'.preg_quote ($preMention, '/').'('.Nickname::WEBFINGER_FMT.')/',
$result = preg_match_all(
'/(?<!\S)'.preg_quote($preMention, '/').'('.Nickname::WEBFINGER_FMT.')/',
$text,
$wmatches,
PREG_OFFSET_CAPTURE);
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)) {
@ -187,15 +379,17 @@ class ActivityPubPlugin extends Plugin
* @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='@')
public 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.']*)*)/',
$result = preg_match_all(
'/(?:^|\s+)'.preg_quote($preMention, '/').'('.URL_REGEX_DOMAIN_NAME.'(?:\/['.URL_REGEX_VALID_PATH_CHARS.']*)*)/',
$text,
$wmatches,
PREG_OFFSET_CAPTURE);
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)) {
@ -216,7 +410,7 @@ class ActivityPubPlugin extends Plugin
* @param array &$mention in/out param: set of found mentions
* @return boolean hook return value
*/
function onEndFindMentions(Profile $sender, $text, &$mentions)
public function onEndFindMentions(Profile $sender, $text, &$mentions)
{
$matches = array();
@ -300,7 +494,7 @@ class ActivityPubPlugin extends Plugin
* @param Profile &$profile
* @return hook return code
*/
function onStartCommandGetProfile ($command, $arg, &$profile)
public function onStartCommandGetProfile($command, $arg, &$profile)
{
try {
$aprofile = $this->pull_remote_profile($arg);
@ -322,7 +516,7 @@ class ActivityPubPlugin extends Plugin
* @param string $uri in/out
* @return mixed hook return code
*/
function onStartGetProfileUri ($profile, &$uri)
public function onStartGetProfileUri($profile, &$uri)
{
$aprofile = Activitypub_profile::getKV('profile_id', $profile->id);
if ($aprofile instanceof Activitypub_profile) {
@ -332,33 +526,6 @@ class ActivityPubPlugin extends Plugin
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.
*
@ -368,7 +535,7 @@ class ActivityPubPlugin extends Plugin
* @param Profile &$profile in/out param: Profile got from URI
* @return mixed hook return code
*/
function onStartGetProfileFromURI ($uri, &$profile)
public 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
@ -411,7 +578,7 @@ class ActivityPubPlugin extends Plugin
* @return hook return value
* @throws Exception
*/
function onEndSubscribe (Profile $profile, Profile $other)
public function onStartSubscribe(Profile $profile, Profile $other)
{
if (!$profile->isLocal() || $other->isLocal()) {
return true;
@ -438,7 +605,7 @@ class ActivityPubPlugin extends Plugin
* @param Profile $other
* @return hook return value
*/
function onEndUnsubscribe (Profile $profile, Profile $other)
public function onStartUnsubscribe(Profile $profile, Profile $other)
{
if (!$profile->isLocal() || $other->isLocal()) {
return true;
@ -465,7 +632,7 @@ class ActivityPubPlugin extends Plugin
* @param Notice $notice Notice being favored
* @return hook return value
*/
function onEndFavorNotice (Profile $profile, Notice $notice)
public function onEndFavorNotice(Profile $profile, Notice $notice)
{
// Only distribute local users' favor actions, remote users
// will have already distributed theirs.
@ -524,7 +691,7 @@ class ActivityPubPlugin extends Plugin
* @param Notice $notice Notice being favored
* @return hook return value
*/
function onEndDisfavorNotice (Profile $profile, Notice $notice)
public function onEndDisfavorNotice(Profile $profile, Notice $notice)
{
// Only distribute local users' favor actions, remote users
// will have already distributed theirs.
@ -581,7 +748,7 @@ class ActivityPubPlugin extends Plugin
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return boolean hook flag
*/
public function onEndDeleteOwnNotice ($user, $notice)
public function onStartDeleteOwnNotice($user, $notice)
{
$profile = $user->getProfile();
@ -634,7 +801,7 @@ class ActivityPubPlugin extends Plugin
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return boolean hook flag
*/
function onStartNoticeDistribute ($notice)
public function onStartNoticeDistribute($notice)
{
assert($notice->id > 0); // Ignore if not a valid notice
@ -674,8 +841,10 @@ class ActivityPubPlugin extends Plugin
// Ignore for activity/non-post-verb notices
if (method_exists('ActivityUtils', 'compareVerbs')) {
$is_post_verb = ActivityUtils::compareVerbs ($notice->verb,
array (ActivityVerb::POST));
$is_post_verb = ActivityUtils::compareVerbs(
$notice->verb,
array(ActivityVerb::POST)
);
} else {
$is_post_verb = ($notice->verb == ActivityVerb::POST ? true : false);
}
@ -724,7 +893,7 @@ class ActivityPubPlugin extends Plugin
* @param string out &$title
* @return mixed hook return code
*/
function onStartNoticeSourceLink ($notice, &$name, &$url, &$title)
public function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
{
// If we don't handle this, keep the event handler going
if (!in_array($notice->source, array('ActivityPub', 'share'))) {
@ -755,12 +924,50 @@ class ActivityPubPlugin extends Plugin
}
}
/**
* Plugin return handler
*/
class ActivityPubReturn
{
/**
* Return a valid answer
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $res
* @return void
*/
public static function answer($res)
{
header('Content-Type: application/activity+json');
echo json_encode($res, JSON_UNESCAPED_SLASHES | (isset($_GET["pretty"]) ? JSON_PRETTY_PRINT : null));
exit;
}
/**
* Return an error
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $m
* @param int32 $code
* @return void
*/
public static function error($m, $code = 500)
{
http_response_code($code);
header('Content-Type: application/activity+json');
$res[] = Activitypub_error::error_message_to_array($m);
echo json_encode($res, JSON_UNESCAPED_SLASHES);
exit;
}
}
/**
* Overwrites variables in URL-mapping
*/
class ActivityPubURLMapperOverwrite extends URLMapper
{
static function overwrite_variable ($m, $path, $args, $paramPatterns, $newaction) {
public static function overwrite_variable($m, $path, $args, $paramPatterns, $newaction)
{
$mimes = [
'application/activity+json',
'application/ld+json',
@ -780,40 +987,3 @@ class ActivityPubURLMapperOverwrite extends URLMapper
}
}
}
/**
* Plugin return handler
*/
class ActivityPubReturn
{
/**
* Return a valid answer
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $res
* @return void
*/
static function answer ($res)
{
header ('Content-Type: application/activity+json');
echo json_encode ($res, JSON_UNESCAPED_SLASHES | (isset ($_GET["pretty"]) ? JSON_PRETTY_PRINT : null));
exit;
}
/**
* Return an error
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $m
* @param int32 $code
* @return void
*/
static function error ($m, $code = 500)
{
http_response_code ($code);
header ('Content-Type: application/activity+json');
$res[] = Activitypub_error::error_message_to_array ($m);
echo json_encode ($res, JSON_UNESCAPED_SLASHES);
exit;
}
}

View File

@ -6,11 +6,10 @@ email, or any other method with the owners of this repository before making a ch
Please note we have a code of conduct, please follow it in all your interactions with the project.
# Coding Style
- We are using [K&R Variant: Linux kernel](https://en.wikipedia.org/wiki/Indentation_style#Variant:_Linux_kernel) with spaces before every `(`.
- Every function has a docblock explaining what it does and stating the author, parameters, types, return and exceptions
- We use snake_case
- We follow every [PSR-2](https://www.php-fig.org/psr/psr-2/) ...
- ... except camelCase, that's too bad, we use snake_case
## Pull Request Process
## Merge Request Process
1. Ensure you strip any trailing spaces off
2. Increase the version numbers in any examples files and the README.md to the new version that this

View File

@ -67,4 +67,3 @@ License along with this program, in the file "COPYING". If not, see
to your users under the same license. This is a legal requirement
of using the software, and if you do not wish to share your
modifications, *YOU MAY NOT USE THIS PLUGIN*.

View File

@ -51,13 +51,11 @@ class apActorFollowersAction extends ManagedAction
*/
protected function handle()
{
$nickname = $this->trimmed ('nickname');
try {
$user = User::getByNickname ($nickname);
$profile = $user->getProfile ();
$url = $profile->profileurl;
$profile = Profile::getByID($this->trimmed('id'));
$url = ActivityPubPlugin::actor_url($profile);
} catch (Exception $e) {
ActivityPubReturn::error ('Invalid username.');
ActivityPubReturn::error('Invalid Actor URI.', 404);
}
if (!isset($_GET["page"])) {

View File

@ -51,13 +51,11 @@ class apActorFollowingAction extends ManagedAction
*/
protected function handle()
{
$nickname = $this->trimmed ('nickname');
try {
$user = User::getByNickname ($nickname);
$profile = $user->getProfile ();
$url = $profile->profileurl;
$profile = Profile::getByID($this->trimmed('id'));
$url = ActivityPubPlugin::actor_url($profile);
} catch (Exception $e) {
ActivityPubReturn::error ('Invalid username.');
ActivityPubReturn::error('Invalid Actor URI.', 404);
}
if (!isset($_GET["page"])) {

View File

@ -51,13 +51,10 @@ class apActorInboxAction extends ManagedAction
*/
protected function handle()
{
$nickname = $this->trimmed ('nickname');
try {
$user = User::getByNickname ($nickname);
$profile = $user->getProfile ();
$url = $profile->profileurl;
$profile = Profile::getByID($this->trimmed('id'));
} catch (Exception $e) {
ActivityPubReturn::error ("Invalid username.");
ActivityPubReturn::error('Invalid Actor URI.', 404);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
@ -87,7 +84,12 @@ class apActorInboxAction extends ManagedAction
ActivityPubReturn::error("Invalid Actor.", 404);
}
$to_profiles = array ($user);
// Public To:
$public_to = array("https://www.w3.org/ns/activitystreams#Public",
"Public",
"as:Public");
$to_profiles = array($profile);
// Process request
switch ($data->type) {
@ -109,6 +111,12 @@ class apActorInboxAction extends ManagedAction
case "Announce":
require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Announce.php";
break;
case "Accept":
require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Accept.php";
break;
case "Reject":
require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Reject.php";
break;
default:
ActivityPubReturn::error("Invalid type value.");
}

152
actions/apactorliked.php Normal file
View File

@ -0,0 +1,152 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* Actor's Liked Collection
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class apActorLikedAction extends ManagedAction
{
protected $needLogin = false;
protected $canPost = true;
/**
* Handle the Liked Collection request
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
protected function handle()
{
$nickname = $this->trimmed('nickname');
try {
$user = User::getByNickname($nickname);
$profile = $user->getProfile();
$url = $profile->profileurl;
} catch (Exception $e) {
ActivityPubReturn::error('Invalid username.');
}
$limit = intval($this->trimmed('limit'));
$since_id = intval($this->trimmed('since_id'));
$max_id = intval($this->trimmed('max_id'));
$limit = empty($limit) ? 40 : $limit; // Default is 40
$since_id = empty($since_id) ? null : $since_id;
$max_id = empty($max_id) ? null : $max_id;
// Max is 80
if ($limit > 80) {
$limit = 80;
}
$fave = $this->fetch_faves($user->getID(), $limit, $since_id, $max_id);
$faves = array();
while ($fave->fetch()) {
$faves[] = $this->pretty_fave(clone ($fave));
}
$res = [
'@context' => [
"https://www.w3.org/ns/activitystreams",
[
"@language" => "en"
]
],
'id' => "{$url}/liked.json",
'type' => 'OrderedCollection',
'totalItems' => Fave::countByProfile($profile),
'orderedItems' => $faves
];
ActivityPubReturn::answer($res);
}
/**
* 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
*/
protected function pretty_fave($fave_object)
{
$res = array("uri" => $fave_object->uri,
"created" => $fave_object->created,
"object" => Activitypub_notice::notice_to_array(Notice::getByID($fave_object->notice_id)));
return $res;
}
/**
* Fetch faves
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param int32 $user_id
* @param int32 $limit
* @param int32 $since_id
* @param int32 $max_id
* @return Fave fetchable fave collection
*/
private static function fetch_faves(
$user_id,
$limit = 40,
$since_id = null,
$max_id = null
) {
$fav = new Fave();
$fav->user_id = $user_id;
$fav->orderBy('modified DESC');
if ($since_id != null) {
$fav->whereAdd("notice_id > {$since_id}");
}
if ($max_id != null) {
$fav->whereAdd("notice_id < {$max_id}");
}
$fav->limit($limit);
$fav->find();
return $fav;
}
}

View File

@ -1,149 +0,0 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* Actor's Liked Collection
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class apActorLikedCollectionAction extends ManagedAction
{
protected $needLogin = false;
protected $canPost = true;
/**
* Handle the Liked Collection request
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
protected function handle ()
{
$nickname = $this->trimmed ('nickname');
try {
$user = User::getByNickname ($nickname);
$profile = $user->getProfile ();
$url = $profile->profileurl;
} catch (Exception $e) {
ActivityPubReturn::error ('Invalid username.');
}
$limit = intval ($this->trimmed ('limit'));
$since_id = intval ($this->trimmed ('since_id'));
$max_id = intval ($this->trimmed ('max_id'));
$limit = empty ($limit) ? 40 : $limit; // Default is 40
$since_id = empty ($since_id) ? null : $since_id;
$max_id = empty ($max_id) ? null : $max_id;
// Max is 80
if ($limit > 80) {
$limit = 80;
}
$fave = $this->fetch_faves ($user->getID(), $limit, $since_id, $max_id);
$faves = array ();
while ($fave->fetch ()) {
$faves[] = $this->pretty_fave (clone ($fave));
}
$res = [
'@context' => [
"https://www.w3.org/ns/activitystreams",
[
"@language" => "en"
]
],
'id' => "{$url}/liked.json",
'type' => 'OrderedCollection',
'totalItems' => Fave::countByProfile ($profile),
'orderedItems' => $faves
];
ActivityPubReturn::answer ($res);
}
/**
* 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
*/
protected function pretty_fave ($fave_object)
{
$res = array("uri" => $fave_object->uri,
"created" => $fave_object->created,
"object" => Activitypub_notice::notice_to_array (Notice::getByID ($fave_object->notice_id)));
return $res;
}
/**
* Fetch faves
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param int32 $user_id
* @param int32 $limit
* @param int32 $since_id
* @param int32 $max_id
* @return Fave fetchable fave collection
*/
private static function fetch_faves ($user_id, $limit = 40, $since_id = null,
$max_id = null)
{
$fav = new Fave ();
$fav->user_id = $user_id;
$fav->orderBy ('modified DESC');
if ($since_id != null) {
$fav->whereAdd ("notice_id > {$since_id}");
}
if ($max_id != null) {
$fav->whereAdd ("notice_id < {$max_id}");
}
$fav->limit ($limit);
$fav->find ();
return $fav;
}
}

View File

@ -19,8 +19,8 @@
*
* @category Plugin
* @package GNUsocial
* @author Daniel Supernault <danielsupernault@gmail.com>
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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/
@ -34,7 +34,6 @@ if (!defined ('GNUSOCIAL')) {
*
* @category Plugin
* @package GNUsocial
* @author Daniel Supernault <danielsupernault@gmail.com>
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
@ -47,19 +46,29 @@ class apActorProfileAction extends ManagedAction
/**
* Handle the Actor Profile request
*
* @author Daniel Supernault <danielsupernault@gmail.com>
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
protected function handle()
{
$nickname = $this->trimmed ('nickname');
if (!empty($id = $this->trimmed('id'))) {
try {
$user = User::getByNickname ($nickname);
$profile = $user->getProfile ();
$profile = Profile::getByID($id);
} catch (Exception $e) {
ActivityPubReturn::error('Invalid Actor URI.', 404);
}
catch (Exception $e) {
unset($id);
} else {
try {
$profile = User::getByNickname($this->trimmed('nickname'))->getProfile();
} catch (Exception $e) {
ActivityPubReturn::error('Invalid username.', 404);
}
}
if (!$profile->isLocal()) {
ActivityPubReturn::error("This is not a local user.");
}
$res = Activitypub_profile::profile_to_array($profile);

View File

@ -0,0 +1,92 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* Authorize Remote Follow
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class apAuthorizeRemoteFollowAction extends Action
{
/**
* Prepare to handle the Authorize Remote Follow request.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $args
* @return boolean
*/
protected function prepare(array $args=array())
{
parent::prepare($args);
if (!common_logged_in()) {
// XXX: selfURL() didn't work. :<
common_set_returnto($_SERVER['REQUEST_URI']);
if (Event::handle('RedirectToLogin', array($this, null))) {
common_redirect(common_local_url('login'), 303);
}
return false;
} else {
if (!isset($_GET["acct"])) {
return false;
}
}
return true;
}
/**
* Handle the Authorize Remote Follow Request.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
*/
protected function handle()
{
$other = Activitypub_profile::get_from_uri($_GET["acct"]);
$actor_profile = common_current_user()->getProfile();
$object_profile = $other->local_profile();
if (!Subscription::exists($actor_profile, $object_profile)) {
Subscription::start($actor_profile, $object_profile);
}
try {
$postman = new Activitypub_postman($actor_profile, [$other]);
$postman->follow();
} catch (Exception $e) {
// Meh, let the exception go on its merry way, it shouldn't be all
// that important really.
}
common_redirect(common_local_url('userbyid', array('id' => $other->profile_id)), 303);
}
}

239
actions/apremotefollow.php Normal file
View File

@ -0,0 +1,239 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* Remote Follow
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class apRemoteFollowAction extends Action
{
public $nickname;
public $local_profile;
public $remote_identifier;
public $err;
/**
* Prepare to handle the Remote Follow request.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $args
* @return boolean
*/
protected function prepare(array $args=array())
{
parent::prepare($args);
if (common_logged_in()) {
// TRANS: Client error.
$this->clientError(_m('You can use the local subscription!'));
}
// Local user the remote wants to subscribe to
$this->nickname = $this->trimmed('nickname');
$this->local_profile = User::getByNickname($this->nickname)->getProfile();
// Webfinger or profile URL of the remote user
$this->remote_identifier = $this->trimmed('remote_identifier');
return true;
}
/**
* Handle the Remote Follow Request.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
*/
protected function handle()
{
parent::handle();
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
/* Use a session token for CSRF protection. */
$token = $this->trimmed('token');
if (!$token || $token != common_session_token()) {
// TRANS: Client error displayed when the session token does not match or is not given.
$this->showForm(_m('There was a problem with your session token. '.
'Try again, please.'));
return;
}
$this->activitypub_connect();
} else {
$this->showForm();
}
}
/**
* Form.
*
* @author GNU Social
* @param string|null $err
*/
public function showForm($err = null)
{
$this->err = $err;
if ($this->boolean('ajax')) {
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
// TRANS: Form title.
$this->element('title', null, _m('TITLE', 'Subscribe to user'));
$this->elementEnd('head');
$this->elementStart('body');
$this->showContent();
$this->elementEnd('body');
$this->endHTML();
} else {
$this->showPage();
}
}
/**
* Page content.
*
* @author GNU Social
*/
public function showContent()
{
// TRANS: Form legend. %s is a nickname.
$header = sprintf(_m('Subscribe to %s'), $this->nickname);
// TRANS: Button text to subscribe to a profile.
$submit = _m('BUTTON', 'Subscribe');
$this->elementStart(
'form',
['id' => 'form_activitypub_connect',
'method' => 'post',
'class' => 'form_settings',
'action' => common_local_url(
'apRemoteFollow',
['nickname' => $this->nickname]
)
]
);
$this->elementStart('fieldset');
$this->element('legend', null, $header);
$this->hidden('token', common_session_token());
$this->elementStart('ul', 'form_data');
$this->elementStart('li', array('id' => 'activitypub_nickname'));
// TRANS: Field label.
$this->input(
'nickname',
_m('User nickname'),
$this->nickname,
// TRANS: Field title.
_m('Nickname of the user you want to follow.')
);
$this->elementEnd('li');
$this->elementStart('li', array('id' => 'activitypub_profile'));
// TRANS: Field label.
$this->input(
'remote_identifier',
_m('Profile Account'),
$this->remote_identifier,
// TRANS: Tooltip for field label "Profile Account".
_m('Your account ID (e.g. user@example.net).')
);
$this->elementEnd('li');
$this->elementEnd('ul');
$this->submit('submit', $submit);
$this->elementEnd('fieldset');
$this->elementEnd('form');
}
/**
* Start connecting the two instances (will be finished with the authorization)
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
public function activitypub_connect()
{
$remote_profile = null;
try { // Try with ActivityPub system
$remote_profile = Activitypub_profile::get_from_uri($this->remote_identifier);
} catch (Exception $e) { // Fallback to compatibility WebFinger system
$validate = new Validate();
$opts = array('allowed_schemes' => array('http', 'https', 'acct'));
if ($validate->uri($this->remote_identifier, $opts)) {
$bits = parse_url($this->remote_identifier);
if ($bits['scheme'] == 'acct') {
$remote_profile = $this->connect_webfinger($bits['path']);
}
} elseif (strpos($this->remote_identifier, '@') !== false) {
$remote_profile = $this->connect_webfinger($this->remote_identifier);
}
}
if (!empty($remote_profile)) {
$url = ActivityPubPlugin::stripUrlPath($remote_profile->get_uri())."activitypub/authorize_follow?acct=".$this->local_profile->getUri();
common_log(LOG_INFO, "Sending remote subscriber $this->remote_identifier to $url");
common_redirect($url, 303);
return;
}
// TRANS: Client error.
$this->clientError(_m('Must provide a remote profile.'));
}
/**
* This function is used by activitypub_connect () and
* is a step of the process
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param type $acct
* @return Profile Profile resulting of WebFinger connection
*/
private function connect_webfinger($acct)
{
$link = ActivityPubPlugin::pull_remote_profile($acct);
if (!is_null($link)) {
return $link;
}
// TRANS: Client error.
$this->clientError(_m('Could not confirm remote profile address.'));
}
/**
* Page title
*
* @return string Page title
*/
public function title()
{
// TRANS: Page title.
return _m('ActivityPub Connect');
}
}

View File

@ -69,7 +69,6 @@ class apSharedInboxAction extends ManagedAction
}
$discovery = new Activitypub_explorer;
// Get valid Actor object
try {
$actor_profile = $discovery->lookup($data->actor);
@ -77,43 +76,19 @@ class apSharedInboxAction extends ManagedAction
} catch (Exception $e) {
ActivityPubReturn::error("Invalid Actor.", 404);
}
unset($discovery);
// Public To:
$public_to = array ("https://www.w3.org/ns/activitystreams#Public",
$public_to = ["https://www.w3.org/ns/activitystreams#Public",
"Public",
"as:Public");
"as:Public"
];
$to_profiles = "https://www.w3.org/ns/activitystreams#Public";
// Process request
switch ($data->type) {
case "Create":
if (!isset($data->to)) {
ActivityPubReturn::error ("To was not specified.");
}
$discovery = new Activitypub_explorer;
$to_profiles = array ();
// Generate To objects
if (is_array ($data->to)) {
// Remove duplicates from To actors set
array_unique ($data->to);
foreach ($data->to as $to_url) {
try {
$to_profiles = array_merge ($to_profiles, $discovery->lookup ($to_url));
} catch (Exception $e) {
// XXX: Invalid actor found, not sure how we handle those
}
}
} else if (empty ($data->to) || in_array ($data->to, $public_to)) {
// No need to do anything else at this point, let's just break out the if
} else {
try {
$to_profiles[]= $discovery->lookup ($data->to);
} catch (Exception $e) {
ActivityPubReturn::error ("Invalid Actor.", 404);
}
}
unset ($discovery);
require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Create.php";
break;
case "Follow":
@ -131,6 +106,12 @@ class apSharedInboxAction extends ManagedAction
case "Delete":
require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Delete.php";
break;
case "Accept":
require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Accept.php";
break;
case "Reject":
require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Reject.php";
break;
default:
ActivityPubReturn::error("Invalid type value.");
}

58
actions/inbox/Accept.php Normal file
View File

@ -0,0 +1,58 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
// Validate data
if (!isset($data->type)) {
ActivityPubReturn::error("Type was not specified.");
}
switch ($data->object->type) {
case "Follow":
// Validate data
if (!isset($data->object->object)) {
ActivityPubReturn::error("Object Actor URL was not specified.");
}
// Get valid Object profile
try {
$object_profile = new Activitypub_explorer;
$object_profile = $object_profile->lookup($data->object->object)[0];
} catch (Exception $e) {
ActivityPubReturn::error("Invalid Object Actor URL.", 404);
}
$pending_list = new Activitypub_pending_follow_requests($actor_profile->getID(), $object_profile->getID());
$pending_list->remove();
ActivityPubReturn::answer($data); // You are now being followed by this person.
break;
default:
ActivityPubReturn::error("Invalid object type.");
break;
}

View File

@ -30,7 +30,7 @@ if (!defined ('GNUSOCIAL')) {
}
try {
Notice::getByUri ($data->object)->repeat ($actor_profile, "ActivityPub");
Notice::getByUri($data->object->id)->repeat($actor_profile, "ActivityPub");
ActivityPubReturn::answer("Notice repeated successfully.");
} catch (Exception $e) {
ActivityPubReturn::error($e->getMessage(), 403);

View File

@ -32,6 +32,9 @@ if (!defined ('GNUSOCIAL')) {
$valid_object_types = array("Note");
// Validate data
if (!isset($data->id)) {
ActivityPubReturn::error("Id not specified.");
}
if (!(isset($data->object->type) && in_array($data->object->type, $valid_object_types))) {
ActivityPubReturn::error("Invalid Object type.");
}
@ -43,6 +46,9 @@ if (!isset ($data->object->url)) {
} 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;
@ -68,9 +74,34 @@ if (isset ($data->object->reply_to)) {
$act->context->attention = common_get_attentions($content, $actor_profile, $reply_to);
foreach ($to_profiles as $to)
{
$act->context->attention[$to->getUri ()] = "http://activitystrea.ms/schema/1.0/person";
$discovery = new Activitypub_explorer;
if ($to_profiles == "https://www.w3.org/ns/activitystreams#Public") {
$to_profiles = array();
}
// Generate To objects
if (is_array($data->object->to)) {
// Remove duplicates from To actors set
array_unique($data->object->to);
foreach ($data->object->to as $to_url) {
try {
$to_profiles = array_merge($to_profiles, $discovery->lookup($to_url));
} catch (Exception $e) {
// XXX: Invalid actor found, not sure how we handle those
}
}
} elseif (empty($data->object->to) || in_array($data->object->to, $public_to)) {
// No need to do anything else at this point, let's just break out the if
} else {
try {
$to_profiles[]= $discovery->lookup($data->object->to);
} catch (Exception $e) {
ActivityPubReturn::error("Invalid Actor.", 404);
}
}
unset($discovery);
foreach ($to_profiles as $to) {
$act->context->attention[ActivityPubPlugin::actor_uri($to)] = "http://activitystrea.ms/schema/1.0/person";
}
// Reject notice if it is too long (without the HTML)
@ -79,7 +110,7 @@ if (Notice::contentTooLong ($content)) {
ActivityPubReturn::error("That's too long. Maximum notice size is %d character.");
}
$options = array ('source' => 'ActivityPub', 'uri' => $data->id, 'url' => $data->object->url);
$options = array('source' => 'ActivityPub', 'uri' => isset($data->id) ? $data->id : $data->object->url, 'url' => $data->object->url);
// $options gets filled with possible scoping settings
ToSelector::fillActivity($this, $act, $options);
@ -91,12 +122,11 @@ $actobj->content = common_render_content ($content, $actor_profile, $reply_to);
$act->objects[] = $actobj;
try {
$res = array ("@context" => "https://www.w3.org/ns/activitystreams",
"id" => $data->id,
"url" => $data->object->url,
"type" => "Create",
"actor" => $data->actor,
"object" => Activitypub_notice::notice_to_array (Notice::saveActivity ($act, $actor_profile, $options)));
$res = Activitypub_create::create_to_array(
$data->id,
$data->actor,
Activitypub_notice::notice_to_array(Notice::saveActivity($act, $actor_profile, $options))
);
ActivityPubReturn::answer($res);
} catch (Exception $e) {
ActivityPubReturn::error($e->getMessage());

View File

@ -30,12 +30,10 @@ if (!defined ('GNUSOCIAL')) {
}
try {
Notice::getByUri ($data->object)->deleteAs ($actor_profile);
$res = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Delete",
"actor" => $data->actor,
"object" => $data->object);
ActivityPubReturn::answer ($res);
$notice = Notice::getByUri($data->object->id);
$notice_to_array = Activitypub_notice::notice_to_array($notice);
$notice->deleteAs($actor_profile);
ActivityPubReturn::answer(Activitypub_delete::delete_to_array($notice_to_array));
} catch (Exception $e) {
ActivityPubReturn::error($e->getMessage(), 403);
}

View File

@ -45,11 +45,7 @@ try {
try {
if (!Subscription::exists($actor_profile, $object_profile)) {
Subscription::start($actor_profile, $object_profile);
$res = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Follow",
"actor" => $data->actor,
"object" => $data->object);
ActivityPubReturn::answer ($res);
ActivityPubReturn::answer(Activitypub_accept::accept_to_array(Activitypub_follow::follow_to_array($data->actor, $data->object)));
} else {
ActivityPubReturn::error("Already following.", 409);
}

View File

@ -29,13 +29,13 @@ if (!defined ('GNUSOCIAL')) {
exit(1);
}
if (!isset($data->object->id)) {
ActivityPubReturn::error("Id not specified.");
}
try {
Fave::addNew ($actor_profile, Notice::getByUri ($data->object));
$res = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Like",
"actor" => $data->actor,
"object" => $data->object);
ActivityPubReturn::answer ($res);
Fave::addNew($actor_profile, Notice::getByUri($data->object->id));
ActivityPubReturn::answer(Activitypub_like::like_to_array(Activitypub_notice::notice_to_array($data->actor, json_decode($data->object))));
} catch (Exception $e) {
ActivityPubReturn::error($e->getMessage(), 403);
}

32
actions/inbox/Reject.php Normal file
View File

@ -0,0 +1,32 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
// This is a dummy file as there is nothing to do if we fall in this case

View File

@ -38,11 +38,21 @@ switch ($data->object->type) {
case "Like":
try {
// Validate data
if (!isset ($data->object->object)) {
ActivityPubReturn::error ("Object Notice URL was not specified.");
if (!isset($data->object->object->id)) {
ActivityPubReturn::error("Notice ID was not specified.");
}
Fave::removeEntry ($actor_profile, Notice::getByUri ($data->object->object));
ActivityPubReturn::answer ("Notice disfavorited successfully.");
Fave::removeEntry($actor_profile, Notice::getByUri($data->object->object->id));
// Notice disfavorited successfully.
ActivityPubReturn::answer(
Activitypub_undo::undo_to_array(
Activitypub_like::like_to_array(
Activitypub_notice::notice_to_array(
$actor_profile->getUrl(),
$data->object->object
)
)
)
);
} catch (Exception $e) {
ActivityPubReturn::error($e->getMessage(), 403);
}
@ -60,16 +70,22 @@ case "Follow":
ActivityPubReturn::error("Invalid Object Actor URL.", 404);
}
try {
if (Subscription::exists($actor_profile, $object_profile)) {
Subscription::cancel($actor_profile, $object_profile);
ActivityPubReturn::answer ("You are no longer following this person.");
// You are no longer following this person.
ActivityPubReturn::answer(
Activitypub_undo::undo_to_array(
Activitypub_accept::accept_to_array(
Activitypub_follow::follow_to_array(
$actor_profile->getUrl(),
$object_profile->getUrl()
)
)
)
);
} else {
ActivityPubReturn::error("You are not following this person already.", 409);
}
} catch (Exception $e) {
ActivityPubReturn::error ("Invalid Object Actor URL.", 404);
}
break;
default:
ActivityPubReturn::error("Invalid object type.");

View File

@ -0,0 +1,58 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* ActivityPub error representation
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_accept extends Managed_DataObject
{
/**
* Generates an ActivityPub representation of a Accept
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $object
* @return pretty array to be used in a response
*/
public static function accept_to_array($object)
{
$res = array("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Accept",
"object" => $object
);
return $res;
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* ActivityPub error representation
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_announce extends Managed_DataObject
{
/**
* Generates an ActivityPub representation of a Announce
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $object
* @return pretty array to be used in a response
*/
public static function announce_to_array($actor, $object)
{
$res = array("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Announce",
"actor" => $actor,
"object" => $object
);
return $res;
}
}

View File

@ -65,8 +65,7 @@ class Activitypub_attachment extends Managed_DataObject
];
// Image
if (substr ($res["mimetype"], 0, 5) == "image")
{
if (substr($res["mimetype"], 0, 5) == "image") {
$res["meta"]= [
'width' => $attachment->width,
'height' => $attachment->height

View File

@ -0,0 +1,61 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* ActivityPub error representation
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_create extends Managed_DataObject
{
/**
* Generates an ActivityPub representation of a Create
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $actor
* @param array $object
* @return pretty array to be used in a response
*/
public static function create_to_array($id, $actor, $object)
{
$res = array("@context" => "https://www.w3.org/ns/activitystreams",
"id" => $id,
"type" => "Create",
"actor" => $actor,
"object" => $object
);
return $res;
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* ActivityPub error representation
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_delete extends Managed_DataObject
{
/**
* Generates an ActivityPub representation of a Delete
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $object
* @return pretty array to be used in a response
*/
public static function delete_to_array($object)
{
$res = array("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Delete",
"actor" => $object["actor"],
"object" => $object
);
return $res;
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* ActivityPub error representation
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_follow extends Managed_DataObject
{
/**
* Generates an ActivityPub representation of a subscription
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $actor
* @param string $object
* @return pretty array to be used in a response
*/
public static function follow_to_array($actor, $object)
{
$res = array("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Follow",
"actor" => $actor,
"object" => $object
);
return $res;
}
}

View File

@ -0,0 +1,60 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* ActivityPub error representation
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_like extends Managed_DataObject
{
/**
* Generates an ActivityPub representation of a Like
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $actor
* @param array $object
* @return pretty array to be used in a response
*/
public static function like_to_array($actor, $object)
{
$res = array("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Like",
"actor" => $actor,
"object" => $object
);
return $res;
}
}

View File

@ -64,22 +64,22 @@ class Activitypub_notice extends Managed_DataObject
}
$to = array();
foreach ($notice->getAttentionProfileIDs () as $to_id) {
$to[] = Profile::getById ($to_id)->getUri ();
foreach ($notice->getAttentionProfiles() as $to_profile) {
$to[] = $to_profile->getUri();
}
if (!is_null($to)) {
if (empty($to)) {
$to = array("https://www.w3.org/ns/activitystreams#Public");
}
$item = [
'id' => $notice->getUrl (),
'type' => 'Notice',
'id' => $notice->getUri(),
'type' => 'Note',
'actor' => $notice->getProfile()->getUrl(),
'published' => $notice->getCreated(),
'to' => $to,
'content' => $notice->getContent(),
'url' => $notice->getUrl(),
'reply_to' => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUrl (),
'reply_to' => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUri(),
'is_local' => $notice->isLocal(),
'conversation' => intval($notice->conversation),
'attachment' => $attachments,

View File

@ -0,0 +1,105 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* ActivityPub's Pending follow requests
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_pending_follow_requests extends Managed_DataObject
{
public $__table = 'Activitypub_pending_follow_requests';
public $local_profile_id;
public $remote_profile_id;
private $_reldb = null;
/**
* Return table definition for Schema setup and DB_DataObject usage.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return array array of column definitions
*/
public static function schemaDef()
{
return array(
'fields' => array(
'local_profile_id' => array('type' => 'integer', 'not null' => true),
'remote_profile_id' => array('type' => 'integer', 'not null' => true),
'relation_id' => array('type' => 'serial', 'not null' => true),
),
'primary key' => array('relation_id'),
'unique keys' => array(
'Activitypub_pending_follow_requests_relation_id_key' => array('relation_id'),
),
'foreign keys' => array(
'Activitypub_pending_follow_requests_local_profile_id_fkey' => array('profile', array('local_profile_id' => 'id')),
'Activitypub_pending_follow_requests_remote_profile_id_fkey' => array('profile', array('remote_profile_id' => 'id')),
),
);
}
public function __construct($actor, $remote_actor)
{
$this->local_profile_id = $actor;
$this->remote_profile_id = $remote_actor;
}
/**
* Add Follow request to table.
*
* @author Diogo Cordeiro
* @param int32 $actor actor id
* @param int32 $remote_actor remote actor id
*/
public function add()
{
return !$this->exists() && $this->insert();
}
public function exists()
{
$this->_reldb = clone ($this);
if ($this->_reldb->find() > 0) {
$this->_reldb->fetch();
return true;
}
return false;
}
public function remove()
{
return $this->exists() && $this->_reldb->delete();
}
}

View File

@ -35,7 +35,6 @@ if (!defined ('GNUSOCIAL')) {
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
@ -51,7 +50,7 @@ class Activitypub_profile extends Profile
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return array array of column definitions
*/
static function schemaDef ()
public static function schemaDef()
{
return array(
'fields' => array(
@ -82,7 +81,8 @@ class Activitypub_profile extends Profile
*/
public static function profile_to_array($profile)
{
$url = $profile->getURL ();
$uri = ActivityPubPlugin::actor_uri($profile);
$id = $profile->getID();
$res = [
'@context' => [
"https://www.w3.org/ns/activitystreams",
@ -90,29 +90,35 @@ class Activitypub_profile extends Profile
"@language" => "en"
]
],
'id' => $profile->getID (),
'id' => $uri,
'type' => 'Person',
'nickname' => $profile->getNickname (),
'preferredUsername' => $profile->getNickname(),
'is_local' => $profile->isLocal(),
'inbox' => "{$url}/inbox.json",
'sharedInbox' => common_root_url ()."inbox.json",
'outbox' => "{$url}/outbox.json",
'display_name' => $profile->getFullname (),
'followers' => "{$url}/followers.json",
'inbox' => common_local_url("apActorInbox", array("id" => $id)),
'name' => $profile->getFullname(),
'followers' => common_local_url("apActorFollowers", array("id" => $id)),
'followers_count' => $profile->subscriberCount(),
'following' => "{$url}/following.json",
'following' => common_local_url("apActorFollowing", array("id" => $id)),
'following_count' => $profile->subscriptionCount(),
'liked' => "{$url}/liked.json",
'liked' => common_local_url("apActorLiked", array("id" => $id)),
'liked_count' => Fave::countByProfile($profile),
'summary' => ($desc = $profile->getDescription()) == null ? "" : $desc,
'url' => $profile->getURL (),
'avatar' => [
'icon' => [
'type' => 'Image',
'width' => 96,
'height' => 96,
'width' => AVATAR_PROFILE_SIZE,
'height' => AVATAR_PROFILE_SIZE,
'url' => $profile->avatarUrl(AVATAR_PROFILE_SIZE)
]
];
if ($profile->isLocal()) {
$res["sharedInbox"] = common_local_url("apSharedInbox", array("id" => $id));
} else {
$aprofile = new Activitypub_profile();
$aprofile = $aprofile->from_profile($profile);
$res["sharedInbox"] = $aprofile->sharedInboxuri;
}
return $res;
}
@ -129,12 +135,12 @@ class Activitypub_profile extends Profile
$profile->created = $this->created = $this->modified = common_sql_now();
$fields = array (
$fields = [
'uri' => 'profileurl',
'nickname' => 'nickname',
'fullname' => 'fullname',
'bio' => 'bio'
);
];
foreach ($fields as $af => $pf) {
$profile->$pf = $this->$af;
@ -178,7 +184,7 @@ class Activitypub_profile extends Profile
* @return Activitypub_profile
* @throws Exception if no Activitypub_profile exists for given Profile
*/
static function from_profile (Profile $profile)
public static function from_profile(Profile $profile)
{
$profile_id = $profile->getID();
@ -271,10 +277,8 @@ class Activitypub_profile extends Profile
if (!empty($profiles_found)) {
return self::from_profile($profiles_found[0]);
} else {
throw new Exception ('No valid ActivityPub profile found for given URI');
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.
}
/**
@ -325,8 +329,10 @@ class Activitypub_profile extends Profile
throw new Exception(_m('Not a valid webfinger address.'));
}
$hints = array_merge (array ('webfinger' => $addr),
DiscoveryHints::fromXRD ($xrd));
$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)) {

View File

@ -0,0 +1,58 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* ActivityPub error representation
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_reject extends Managed_DataObject
{
/**
* Generates an ActivityPub representation of a Reject
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $object
* @return pretty array to be used in a response
*/
public static function reject_to_array($object)
{
$res = array("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Reject",
"object" => $object
);
return $res;
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* 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 Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 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);
}
/**
* ActivityPub error representation
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_undo extends Managed_DataObject
{
/**
* Generates an ActivityPub representation of a Undo
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array $object
* @return pretty array to be used in a response
*/
public static function undo_to_array($object)
{
$res = array("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Undo",
"actor" => $object["actor"],
"object" => $object
);
return $res;
}
}

25
composer.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "dansup/activity-pub",
"description": "ActivityPub plugin for GNU/Social",
"type": "gnusocial-plugin",
"require": {},
"require-dev": {
"phpunit/phpunit": "^7.2"
},
"license": "AGPL",
"autoload": {
"psr-4": {
"Tests\\": "tests/"
}
},
"authors": [
{
"name": "Daniel Supernault",
"email": "danielsupernault@gmail.com"
},
{
"name": "Diogo Cordeiro",
"email": "diogo@fc.up.pt"
}
]
}

1423
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
phpunit.xml Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
</phpunit>

View File

@ -0,0 +1,28 @@
<?php
namespace Tests;
trait CreatesApplication
{
/**
* Creates the application.
*
* @return todo
*/
public static function createApplication()
{
if (!defined('INSTALLDIR')) {
define('INSTALLDIR', __DIR__ . '/../../../');
}
if (!defined('GNUSOCIAL')) {
define('GNUSOCIAL', true);
}
if (!defined('STATUSNET')) {
define('STATUSNET', true); // compatibility
}
require INSTALLDIR . '/lib/common.php';
return true;
}
}

15
tests/TestCase.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace Tests;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
protected function setUp()
{
$this->createApplication();
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Tests\Unit;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*
* @return void
*/
public function testBasicTest()
{
$this->assertTrue(true);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Tests\Unit;
use Tests\TestCase;
class ProfileObjectTest extends TestCase
{
public function testLibraryInstalled()
{
$this->assertTrue(class_exists('\Activitypub_profile'));
}
public function testProfileObject()
{
// Mimic proper ACCEPT header
$_SERVER['HTTP_ACCEPT'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams';
// Fetch profile
$user = \Profile::getKV('id', 1);
// Fetch ActivityPub Actor Object representation
$profile = \Activitypub_profile::profile_to_array($user);
$this->assertTrue(is_array($profile));
$this->assertTrue(isset($profile['inbox']));
$this->assertTrue(isset($profile['outbox']));
}
}

View File

@ -28,8 +28,9 @@ if (!defined ('GNUSOCIAL')) {
exit(1);
}
class DiscoveryHints {
static function fromXRD(XML_XRD $xrd)
class DiscoveryHints
{
public static function fromXRD(XML_XRD $xrd)
{
$hints = array();
@ -60,7 +61,7 @@ class DiscoveryHints {
return $hints;
}
static function fromHcardUrl($url)
public static function fromHcardUrl($url)
{
$client = new HTTPClient();
$client->setHeader('Accept', 'text/html,application/xhtml+xml');
@ -76,11 +77,13 @@ class DiscoveryHints {
return null;
}
return self::hcardHints($response->getBody(),
$response->getEffectiveUrl());
return self::hcardHints(
$response->getBody(),
$response->getEffectiveUrl()
);
}
static function hcardHints($body, $url)
public static function hcardHints($body, $url)
{
$hcard = self::_hcard($body, $url);
@ -119,7 +122,7 @@ class DiscoveryHints {
return $hints;
}
static function _hcard($body, $url)
public static function _hcard($body, $url)
{
$mf2 = new Mf2\Parser($body, $url);
$mf2 = $mf2->parse();

View File

@ -74,44 +74,75 @@ class Activitypub_explorer
// First check if we already have it locally and, if so, return it
// If the local fetch fails: grab it remotely, store locally and return
if (! ($this->grab_local_user($url) || $this->grab_remote_user($url))) {
throw new Exception ("User not found");
throw new Exception("User not found.");
}
return $this->discovered_actor_profiles;
}
/**
* This ensures that we are using a valid ActivityPub URI
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $url
* @return boolean success state (related to the response)
* @throws Exception (If the HTTP request fails)
*/
private function ensure_proper_remote_uri($url)
{
$client = new HTTPClient();
$headers = array();
$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);
if (!$response->isOk()) {
throw new Exception("Invalid Actor URL.");
}
$res = json_decode($response->getBody(), JSON_UNESCAPED_SLASHES);
if (self::validate_remote_response($res)) {
$this->temp_res = $res;
return true;
}
return false;
}
/**
* 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
* @param string $uri Actor's uri
* @return boolean success state
*/
private function grab_local_user ($url)
private function grab_local_user($uri)
{
if (($actor_profile = self::get_profile_by_url ($url)) != false) {
$this->discovered_actor_profiles[]= $actor_profile;
return true;
// Ensure proper remote URI
// If an exceptiong ocurrs here it's better to just leave everything
// break than to continue processing
if ($this->ensure_proper_remote_uri($uri)) {
$uri = $this->temp_res["id"];
}
try {
// Try standard ActivityPub route
$aprofile = Activitypub_profile::getKV("uri", $uri);
if ($aprofile instanceof Activitypub_profile) {
$profile = $aprofile->local_profile();
} else {
/******************************** XXX: ********************************
* Sometimes it is not true that the user is not locally available, *
* mostly when it is a local user and URLs slightly changed *
* e.g.: GS instance owner changed from standard urls to pretty urls *
* (not sure if this is necessary, but anyway) *
**********************************************************************/
// This potential local user is not a remote user.
// Let's check for pure blood!
$profile = User::getByNickname($this->temp_res["preferredUsername"])->getProfile();
}
// Iff we really are in the same instance
$root_url_len = strlen (common_root_url ());
if (substr ($url, 0, $root_url_len) == common_root_url ()) {
// Grab the nickname and try to get the user
if (($actor_profile = Profile::getKV ("nickname", substr ($url, $root_url_len))) != false) {
$this->discovered_actor_profiles[]= $actor_profile;
// We found something!
$this->discovered_actor_profiles[]= $profile;
unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system
return true;
} catch (Exception $e) {
// We can safely ignore every exception here as we are return false
// when it fails the lookup for existing local representation
}
}
}
return false;
}
@ -125,6 +156,7 @@ class Activitypub_explorer
*/
private function grab_remote_user($url)
{
if (!isset($this->temp_res)) {
$client = new HTTPClient();
$headers = array();
$headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
@ -134,6 +166,10 @@ class Activitypub_explorer
throw new Exception("Invalid Actor URL.");
}
$res = json_decode($response->getBody(), JSON_UNESCAPED_SLASHES);
} else {
$res = $this->temp_res;
unset($this->temp_res);
}
if (isset($res["orderedItems"])) { // It's a potential collection of actors!!!
foreach ($res["orderedItems"] as $profile) {
if ($this->_lookup($profile) == false) {
@ -163,9 +199,9 @@ class Activitypub_explorer
private function store_profile($res)
{
$aprofile = new Activitypub_profile;
$aprofile->uri = $res["url"];
$aprofile->nickname = $res["nickname"];
$aprofile->fullname = $res["display_name"];
$aprofile->uri = $res["id"];
$aprofile->nickname = $res["preferredUsername"];
$aprofile->fullname = $res["name"];
$aprofile->bio = substr($res["summary"], 0, 1000);
$aprofile->inboxuri = $res["inbox"];
$aprofile->sharedInboxuri = isset($res["sharedInbox"]) ? $res["sharedInbox"] : $res["inbox"];
@ -185,39 +221,13 @@ class Activitypub_explorer
*/
private static function validate_remote_response($res)
{
if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"], $res["inbox"])) {
if (!isset($res["id"], $res["preferredUsername"], $res["name"], $res["summary"], $res["inbox"])) {
return false;
}
return true;
}
/**
* Get a profile from it's profileurl
* Unfortunately GNU Social cache is not truly reliable when handling
* 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
*/
public static function get_profile_by_url ($v)
{
$i = Managed_DataObject::getcached("Profile", "profileurl", $v);
if (empty ($i)) { // false = cache miss
$i = new Profile;
$result = $i->get ("profileurl", $v);
if ($result) {
// Hit!
$i->encache();
} else {
return false;
}
}
return $i;
}
/**
* Given a valid actor profile url returns its inboxes
*

View File

@ -44,7 +44,7 @@ if (!defined ('GNUSOCIAL')) {
class Activitypub_postman
{
private $actor;
private $to = array ();
private $to = [];
private $client;
private $headers;
@ -55,12 +55,12 @@ class Activitypub_postman
* @param Profile of sender
* @param Activitypub_profile $to array of destinataries
*/
public function __construct ($from, $to = array ())
public function __construct($from, $to = [])
{
$this->client = new HTTPClient();
$this->actor = $from;
$this->to = $to;
$this->headers = array();
$this->headers = [];
$this->headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
$this->headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social';
}
@ -69,15 +69,28 @@ class Activitypub_postman
* Send a follow notification to remote instance
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @throws Exception
*/
public function follow()
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Follow",
"actor" => $this->actor->getUrl (),
"object" => $this->to[0]->getUrl ());
$data = Activitypub_follow::follow_to_array($this->actor->getUrl(), $this->to[0]->getUrl());
$this->client->setBody(json_encode($data));
$this->client->post ($this->to[0]->get_inbox (), $this->headers);
$res = $this->client->post($this->to[0]->get_inbox(), $this->headers);
$res_body = json_decode($res->getBody());
if ($res->isOk() || $res->getStatus() == 409) {
$pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
if (! ($res->getStatus() == 409 || $res_body->type == "Accept")) {
$pending_list->add();
throw new Exception("Your follow request is pending acceptation.");
}
$pending_list->remove();
return true;
} elseif (isset($res_body[0]->error)) {
throw new Exception($res_body[0]->error);
}
throw new Exception("An unknown error occurred.");
}
/**
@ -87,16 +100,25 @@ class Activitypub_postman
*/
public function undo_follow()
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Undo",
"actor" => $this->actor->getUrl (),
"object" => array (
"type" => "Follow",
"object" => $this->to[0]->getUrl ()
$data = Activitypub_undo::undo_to_array(
Activitypub_follow::follow_to_array(
$this->actor->getUrl(),
$this->to[0]->getUrl()
)
);
$this->client->setBody(json_encode($data));
$this->client->post ($this->to[0]->get_inbox (), $this->headers);
$res = $this->client->post($this->to[0]->get_inbox(), $this->headers);
$res_body = json_decode($res->getBody());
if ($res->isOk() || $res->getStatus() == 409) {
$pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
$pending_list->remove();
return true;
}
if (isset($res_body[0]->error)) {
throw new Exception($res_body[0]->error);
}
throw new Exception("An unknown error occurred.");
}
/**
@ -107,10 +129,10 @@ class Activitypub_postman
*/
public function like($notice)
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Like",
"actor" => $this->actor->getUrl (),
"object" => $notice->getUri ());
$data = Activitypub_like::like_to_array(
$this->actor->getUrl(),
Activitypub_notice::notice_to_array($notice)
);
$this->client->setBody(json_encode($data));
foreach ($this->to_inbox() as $inbox) {
$this->client->post($inbox, $this->headers);
@ -125,12 +147,10 @@ class Activitypub_postman
*/
public function undo_like($notice)
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Undo",
"actor" => $this->actor->getUrl (),
"object" => array (
"type" => "Like",
"object" => $notice->getUri ()
$data = Activitypub_undo::undo_to_array(
Activitypub_like::like_to_array(
$this->actor->getUrl(),
Activitypub_notice::notice_to_array($notice)
)
);
$this->client->setBody(json_encode($data));
@ -139,28 +159,6 @@ class Activitypub_postman
}
}
/**
* Send a Announce notification to remote instances
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice
*/
public function announce ($notice)
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"id" => $notice->getUri (),
"url" => $notice->getUrl (),
"type" => "Announce",
"actor" => $this->actor->getUrl (),
"to" => "https://www.w3.org/ns/activitystreams#Public",
"object" => $notice->getUri ()
);
$this->client->setBody (json_encode ($data));
foreach ($this->to_inbox () as $inbox) {
$this->client->post ($inbox, $this->headers);
}
}
/**
* Send a Create notification to remote instances
*
@ -169,16 +167,10 @@ class Activitypub_postman
*/
public function create($notice)
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"id" => $notice->getUri (),
"type" => "Create",
"actor" => $this->actor->getUrl (),
"to" => "https://www.w3.org/ns/activitystreams#Public",
"object" => array (
"type" => "Note",
"url" => $notice->getUrl (),
"content" => $notice->getContent ()
)
$data = Activitypub_create::create_to_array(
$notice->getUri(),
$this->actor->getUrl(),
Activitypub_notice::notice_to_array($notice)
);
if (isset($notice->reply_to)) {
$data["object"]["reply_to"] = $notice->getParent()->getUri();
@ -189,6 +181,24 @@ class Activitypub_postman
}
}
/**
* Send a Announce notification to remote instances
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice
*/
public function announce($notice)
{
$data = Activitypub_announce::announce_to_array(
$this->actor->getUrl(),
Activitypub_notice::notice_to_array($notice)
);
$this->client->setBody(json_encode($data));
foreach ($this->to_inbox() as $inbox) {
$this->client->post($inbox, $this->headers);
}
}
/**
* Send a Delete notification to remote instances holding the notice
*
@ -197,14 +207,22 @@ class Activitypub_postman
*/
public function delete($notice)
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Delete",
"actor" => $this->actor->getUrl (),
"object" => $notice->getUri ()
);
$data = Activitypub_delete::delete_to_array(Activitypub_notice::notice_to_array($notice));
$this->client->setBody(json_encode($data));
$errors = array();
foreach ($this->to_inbox() as $inbox) {
$this->client->post ($inbox, $this->headers);
$res = $this->client->post($inbox, $this->headers);
if (!$res->isOk()) {
$res_body = json_decode($res->getBody());
if (isset($res_body[0]->error)) {
$errors[] = ($res_body[0]->error);
continue;
}
$errors[] = ("An unknown error occurred.");
}
}
if (!empty($errors)) {
throw new Exception(json_encode($errors));
}
}