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,14 +19,14 @@
*
* @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/
*/
if (!defined ('GNUSOCIAL')) {
exit (1);
if (!defined('GNUSOCIAL')) {
exit(1);
}
// Import required files by the plugin
@ -37,41 +37,76 @@ 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)) {
if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
// webfinger lookup
try {
return Activitypub_profile::ensure_web_finger ($arg);
return Activitypub_profile::ensure_web_finger($arg);
} catch (Exception $e) {
common_log(LOG_ERR, 'Webfinger lookup failed for ' .
$arg . ': ' . $e->getMessage ());
$arg . ': ' . $e->getMessage());
}
}
// Look for profile URLs, with or without scheme:
$urls = array ();
if (preg_match ('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
$urls = array();
if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
$urls[] = $arg;
}
if (preg_match ('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
$schemes = array ('http', 'https');
if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
$schemes = array('http', 'https');
foreach ($schemes as $scheme) {
$urls[] = "$scheme://$arg";
}
@ -79,10 +114,10 @@ class ActivityPubPlugin extends Plugin
foreach ($urls as $url) {
try {
return Activitypub_profile::get_from_uri ($url);
return Activitypub_profile::get_from_uri($url);
} catch (Exception $e) {
common_log(LOG_ERR, 'Profile lookup failed for ' .
$arg . ': ' . $e->getMessage ());
$arg . ': ' . $e->getMessage());
}
}
return null;
@ -94,31 +129,64 @@ class ActivityPubPlugin extends Plugin
* @param URLMapper $m
* @return void
*/
public function onRouterInitialized (URLMapper $m)
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']
);
}
/**
@ -127,11 +195,11 @@ class ActivityPubPlugin extends Plugin
* @param array $versions
* @return boolean hook true
*/
public function onPluginVersion (array &$versions)
public function onPluginVersion(array &$versions)
{
$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 = 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,17 +354,19 @@ 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.')/',
$wmatches = array();
$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)) {
common_debug (sprintf ('Found %d matches for WebFinger IDs: %s', count ($wmatches), _ve ($wmatches)));
common_log(LOG_ERR, __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error=='.preg_last_error().').');
} elseif (count($wmatches)) {
common_debug(sprintf('Found %d matches for WebFinger IDs: %s', count($wmatches), _ve($wmatches)));
}
return $wmatches[1];
}
@ -187,19 +379,21 @@ 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 ();
$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)) {
common_debug(sprintf('Found %d matches for profile URL mentions: %s', count ($wmatches), _ve ($wmatches)));
common_log(LOG_ERR, __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error=='.preg_last_error().').');
} elseif (count($wmatches)) {
common_debug(sprintf('Found %d matches for profile URL mentions: %s', count($wmatches), _ve($wmatches)));
}
return $wmatches[1];
}
@ -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();
@ -231,39 +425,39 @@ class ActivityPubPlugin extends Plugin
$this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
continue;
}
assert ($profile instanceof Profile);
assert($profile instanceof Profile);
$displayName = !empty ($profile->nickname) && mb_strlen ($profile->nickname) < mb_strlen ($target)
? $profile->getNickname () // TODO: we could do getBestName() or getFullname() here
$displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target)
? $profile->getNickname() // TODO: we could do getBestName() or getFullname() here
: $target;
$url = $profile->getUri ();
if (!common_valid_http_url ($url)) {
$url = $profile->getUrl ();
$url = $profile->getUri();
if (!common_valid_http_url($url)) {
$url = $profile->getUrl();
}
$matches[$pos] = array('mentioned' => array ($profile),
$matches[$pos] = array('mentioned' => array($profile),
'type' => 'mention',
'text' => $displayName,
'position' => $pos,
'length' => mb_strlen ($target),
'length' => mb_strlen($target),
'url' => $url);
}
foreach (self::extractUrlMentions ($text) as $wmatch) {
list ($target, $pos) = $wmatch;
foreach (self::extractUrlMentions($text) as $wmatch) {
list($target, $pos) = $wmatch;
$schemes = array('https', 'http');
foreach ($schemes as $scheme) {
$url = "$scheme://$target";
$this->log(LOG_INFO, "Checking profile address '$url'");
try {
$aprofile = Activitypub_profile::get_from_uri ($url);
$aprofile = Activitypub_profile::get_from_uri($url);
$profile = $aprofile->local_profile();
$displayName = !empty ($profile->nickname) && mb_strlen ($profile->nickname) < mb_strlen ($target) ?
$displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) ?
$profile->nickname : $target;
$matches[$pos] = array('mentioned' => array ($profile),
$matches[$pos] = array('mentioned' => array($profile),
'type' => 'mention',
'text' => $displayName,
'position' => $pos,
'length' => mb_strlen ($target),
'length' => mb_strlen($target),
'url' => $profile->getUrl());
break;
} catch (Exception $e) {
@ -275,9 +469,9 @@ class ActivityPubPlugin extends Plugin
foreach ($mentions as $i => $other) {
// If we share a common prefix with a local user, override it!
$pos = $other['position'];
if (isset ($matches[$pos])) {
if (isset($matches[$pos])) {
$mentions[$i] = $matches[$pos];
unset ($matches[$pos]);
unset($matches[$pos]);
}
}
foreach ($matches as $mention) {
@ -300,10 +494,10 @@ 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);
$aprofile = $this->pull_remote_profile($arg);
$profile = $aprofile->local_profile();
} catch (Exception $e) {
// No remote ActivityPub profile found
@ -322,43 +516,16 @@ 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);
$aprofile = Activitypub_profile::getKV('profile_id', $profile->id);
if ($aprofile instanceof Activitypub_profile) {
$uri = $aprofile->get_uri ();
$uri = $aprofile->get_uri();
return false;
}
return true;
}
/**
* Dummy string on AccountProfileBlock stating that ActivityPub is active
* this is more of a placeholder for eventual useful stuff ._.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
function onEndShowAccountProfileBlock (HTMLOutputter $out, Profile $profile)
{
if ($profile->isLocal()) {
return true;
}
try {
$aprofile = Activitypub_profile::getKV ('profile_id', $profile->id);
} catch (NoResultException $e) {
// Not a remote ActivityPub_profile! Maybe some other network
// that has imported a non-local user (e.g.: OStatus)?
return true;
}
$out->elementStart('dl', 'entity_tags activitypub_profile');
$out->element('dt', null, _m('ActivityPub'));
$out->element('dd', null, _m('Active'));
$out->elementEnd('dl');
}
/**
* Profile from URI.
*
@ -368,29 +535,29 @@ 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
// in the Profile class, since the plugin always runs before
// that local lookup, but since we return false it won't run double.
$user = User::getKV ('uri', $uri);
$user = User::getKV('uri', $uri);
if ($user instanceof User) {
$profile = $user->getProfile();
return false;
} else {
$group = User_group::getKV ('uri', $uri);
$group = User_group::getKV('uri', $uri);
if ($group instanceof User_group) {
$profile = $group->getProfile ();
$profile = $group->getProfile();
return false;
}
}
// Now, check remotely
try {
$aprofile = Activitypub_profile::get_from_uri ($uri);
$profile = $aprofile->local_profile ();
$aprofile = Activitypub_profile::get_from_uri($uri);
$profile = $aprofile->local_profile();
return false;
} catch (Exception $e) {
return true; // It's not an ActivityPub profile as far as we know, continue event handling
@ -411,21 +578,21 @@ 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 ()) {
if (!$profile->isLocal() || $other->isLocal()) {
return true;
}
try {
$other = Activitypub_profile::from_profile ($other);
$other = Activitypub_profile::from_profile($other);
} catch (Exception $e) {
return true;
}
$postman = new Activitypub_postman ($profile, array ($other));
$postman = new Activitypub_postman($profile, array($other));
$postman->follow ();
$postman->follow();
return true;
}
@ -438,21 +605,21 @@ 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 ()) {
if (!$profile->isLocal() || $other->isLocal()) {
return true;
}
try {
$other = Activitypub_profile::from_profile ($other);
$other = Activitypub_profile::from_profile($other);
} catch (Exception $e) {
return true;
}
$postman = new Activitypub_postman ($profile, array ($other));
$postman = new Activitypub_postman($profile, array($other));
$postman->undo_follow ();
$postman->undo_follow();
return true;
}
@ -465,38 +632,38 @@ 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.
if (!$profile->isLocal ()) {
if (!$profile->isLocal()) {
return true;
}
$other = array ();
$other = array();
try {
$other[] = Activitypub_profile::from_profile($notice->getProfile ());
$other[] = Activitypub_profile::from_profile($notice->getProfile());
} catch (Exception $e) {
// Local user can be ignored
}
foreach ($notice->getAttentionProfiles() as $to_profile) {
try {
$other[] = Activitypub_profile::from_profile ($to_profile);
$other[] = Activitypub_profile::from_profile($to_profile);
} catch (Exception $e) {
// Local user can be ignored
}
}
if ($notice->reply_to) {
try {
$other[] = Activitypub_profile::from_profile ($notice->getParent ()->getProfile ());
$other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile());
} catch (Exception $e) {
// Local user can be ignored
}
try {
$mentions = $notice->getParent ()->getAttentionProfiles ();
$mentions = $notice->getParent()->getAttentionProfiles();
foreach ($mentions as $to_profile) {
try {
$other[] = Activitypub_profile::from_profile ($to_profile);
$other[] = Activitypub_profile::from_profile($to_profile);
} catch (Exception $e) {
// Local user can be ignored
}
@ -509,9 +676,9 @@ class ActivityPubPlugin extends Plugin
}
}
$postman = new Activitypub_postman ($profile, $other);
$postman = new Activitypub_postman($profile, $other);
$postman->like ($notice);
$postman->like($notice);
return true;
}
@ -524,38 +691,38 @@ 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.
if (!$profile->isLocal ()) {
if (!$profile->isLocal()) {
return true;
}
$other = array ();
$other = array();
try {
$other[] = Activitypub_profile::from_profile($notice->getProfile ());
$other[] = Activitypub_profile::from_profile($notice->getProfile());
} catch (Exception $e) {
// Local user can be ignored
}
foreach ($notice->getAttentionProfiles() as $to_profile) {
try {
$other[] = Activitypub_profile::from_profile ($to_profile);
$other[] = Activitypub_profile::from_profile($to_profile);
} catch (Exception $e) {
// Local user can be ignored
}
}
if ($notice->reply_to) {
try {
$other[] = Activitypub_profile::from_profile ($notice->getParent ()->getProfile ());
$other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile());
} catch (Exception $e) {
// Local user can be ignored
}
try {
$mentions = $notice->getParent ()->getAttentionProfiles ();
$mentions = $notice->getParent()->getAttentionProfiles();
foreach ($mentions as $to_profile) {
try {
$other[] = Activitypub_profile::from_profile ($to_profile);
$other[] = Activitypub_profile::from_profile($to_profile);
} catch (Exception $e) {
// Local user can be ignored
}
@ -568,9 +735,9 @@ class ActivityPubPlugin extends Plugin
}
}
$postman = new Activitypub_postman ($profile, $other);
$postman = new Activitypub_postman($profile, $other);
$postman->undo_like ($notice);
$postman->undo_like($notice);
return true;
}
@ -581,36 +748,36 @@ 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 ();
$profile = $user->getProfile();
// Only distribute local users' delete actions, remote users
// will have already distributed theirs.
if (!$profile->isLocal ()) {
if (!$profile->isLocal()) {
return true;
}
$other = array ();
$other = array();
foreach ($notice->getAttentionProfiles() as $to_profile) {
try {
$other[] = Activitypub_profile::from_profile ($to_profile);
$other[] = Activitypub_profile::from_profile($to_profile);
} catch (Exception $e) {
// Local user can be ignored
}
}
if ($notice->reply_to) {
try {
$other[] = Activitypub_profile::from_profile ($notice->getParent ()->getProfile ());
$other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile());
} catch (Exception $e) {
// Local user can be ignored
}
try {
$mentions = $notice->getParent ()->getAttentionProfiles ();
$mentions = $notice->getParent()->getAttentionProfiles();
foreach ($mentions as $to_profile) {
try {
$other[] = Activitypub_profile::from_profile ($to_profile);
$other[] = Activitypub_profile::from_profile($to_profile);
} catch (Exception $e) {
// Local user can be ignored
}
@ -623,8 +790,8 @@ class ActivityPubPlugin extends Plugin
}
}
$postman = new Activitypub_postman ($profile, $other);
$postman->delete ($notice);
$postman = new Activitypub_postman($profile, $other);
$postman->delete($notice);
return true;
}
@ -634,48 +801,50 @@ 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
assert($notice->id > 0); // Ignore if not a valid notice
$profile = Profile::getKV ($notice->profile_id);
$profile = Profile::getKV($notice->profile_id);
$other = array ();
$other = array();
try {
$other[] = Activitypub_profile::from_profile($notice->getProfile ());
$other[] = Activitypub_profile::from_profile($notice->getProfile());
} catch (Exception $e) {
// Local user can be ignored
}
foreach ($notice->getAttentionProfiles() as $to_profile) {
try {
$other[] = Activitypub_profile::from_profile ($to_profile);
$other[] = Activitypub_profile::from_profile($to_profile);
} catch (Exception $e) {
// Local user can be ignored
}
}
// Is Announce
if ($notice->isRepeat ()) {
$repeated_notice = Notice::getKV ('id', $notice->repeat_of);
if ($notice->isRepeat()) {
$repeated_notice = Notice::getKV('id', $notice->repeat_of);
if ($repeated_notice instanceof Notice) {
try {
$other[] = Activitypub_profile::from_profile ($repeated_notice->getProfile ());
$other[] = Activitypub_profile::from_profile($repeated_notice->getProfile());
} catch (Exception $e) {
// Local user can be ignored
}
$postman = new Activitypub_postman ($profile, $other);
$postman = new Activitypub_postman($profile, $other);
// That was it
$postman->announce ($repeated_notice);
$postman->announce($repeated_notice);
return true;
}
}
// Ignore for activity/non-post-verb notices
if (method_exists ('ActivityUtils', 'compareVerbs')) {
$is_post_verb = ActivityUtils::compareVerbs ($notice->verb,
array (ActivityVerb::POST));
if (method_exists('ActivityUtils', 'compareVerbs')) {
$is_post_verb = ActivityUtils::compareVerbs(
$notice->verb,
array(ActivityVerb::POST)
);
} else {
$is_post_verb = ($notice->verb == ActivityVerb::POST ? true : false);
}
@ -686,15 +855,15 @@ class ActivityPubPlugin extends Plugin
// Create
if ($notice->reply_to) {
try {
$other[] = Activitypub_profile::from_profile ($notice->getParent ()->getProfile ());
$other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile());
} catch (Exception $e) {
// Local user can be ignored
}
try {
$mentions = $notice->getParent ()->getAttentionProfiles ();
$mentions = $notice->getParent()->getAttentionProfiles();
foreach ($mentions as $to_profile) {
try {
$other[] = Activitypub_profile::from_profile ($to_profile);
$other[] = Activitypub_profile::from_profile($to_profile);
} catch (Exception $e) {
// Local user can be ignored
}
@ -706,10 +875,10 @@ class ActivityPubPlugin extends Plugin
common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage());
}
}
$postman = new Activitypub_postman ($profile, $other);
$postman = new Activitypub_postman($profile, $other);
// That was it
$postman->create ($notice);
$postman->create($notice);
return true;
}
@ -724,10 +893,10 @@ 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'))) {
if (!in_array($notice->source, array('ActivityPub', 'share'))) {
return true;
}
@ -755,32 +924,6 @@ class ActivityPubPlugin extends Plugin
}
}
/**
* Overwrites variables in URL-mapping
*/
class ActivityPubURLMapperOverwrite extends URLMapper
{
static function overwrite_variable ($m, $path, $args, $paramPatterns, $newaction) {
$mimes = [
'application/activity+json',
'application/ld+json',
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
];
if (in_array ($_SERVER["HTTP_ACCEPT"], $mimes) == false) {
return true;
}
$m->connect ($path, array('action' => $newaction), $paramPatterns);
$regex = self::makeRegex($path, $paramPatterns);
foreach ($m->variables as $n => $v) {
if ($v[1] == $regex) {
$m->variables[$n][0]['action'] = $newaction;
}
}
}
}
/**
* Plugin return handler
*/
@ -793,10 +936,10 @@ class ActivityPubReturn
* @param array $res
* @return void
*/
static function answer ($res)
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));
header('Content-Type: application/activity+json');
echo json_encode($res, JSON_UNESCAPED_SLASHES | (isset($_GET["pretty"]) ? JSON_PRETTY_PRINT : null));
exit;
}
@ -808,12 +951,39 @@ class ActivityPubReturn
* @param int32 $code
* @return void
*/
static function error ($m, $code = 500)
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);
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
{
public static function overwrite_variable($m, $path, $args, $paramPatterns, $newaction)
{
$mimes = [
'application/activity+json',
'application/ld+json',
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
];
if (in_array($_SERVER["HTTP_ACCEPT"], $mimes) == false) {
return true;
}
$m->connect($path, array('action' => $newaction), $paramPatterns);
$regex = self::makeRegex($path, $paramPatterns);
foreach ($m->variables as $n => $v) {
if ($v[1] == $regex) {
$m->variables[$n][0]['action'] = $newaction;
}
}
}
}

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

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -49,51 +49,49 @@ class apActorFollowersAction extends ManagedAction
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
protected function handle ()
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"])) {
if (!isset($_GET["page"])) {
$page = 1;
} else {
$page = intval ($this->trimmed ('page'));
$page = intval($this->trimmed('page'));
}
if ($page <= 0) {
ActivityPubReturn::error ('Invalid page number.');
ActivityPubReturn::error('Invalid page number.');
}
/* Fetch Followers */
try {
$since = ($page - 1) * PROFILES_PER_MINILIST;
$limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST;
$sub = $profile->getSubscribers ($since, $limit);
$sub = $profile->getSubscribers($since, $limit);
} catch (NoResultException $e) {
ActivityPubReturn::error ('This user has no followers.');
ActivityPubReturn::error('This user has no followers.');
}
/* Calculate total items */
$total_subs = $profile->subscriberCount ();
$total_pages = ceil ($total_subs / PROFILES_PER_MINILIST);
$total_subs = $profile->subscriberCount();
$total_pages = ceil($total_subs / PROFILES_PER_MINILIST);
if ($total_pages == 0) {
ActivityPubReturn::error ('This user has no followers.');
ActivityPubReturn::error('This user has no followers.');
}
if ($page > $total_pages) {
ActivityPubReturn::error ("There are only {$total_pages} pages.");
ActivityPubReturn::error("There are only {$total_pages} pages.");
}
/* Get followers' URLs */
$subs = array ();
while ($sub->fetch ()) {
$subs = array();
while ($sub->fetch()) {
$subs[] = $sub->profileurl;
}
@ -110,6 +108,6 @@ class apActorFollowersAction extends ManagedAction
'orderedItems' => $subs
];
ActivityPubReturn::answer ($res);
ActivityPubReturn::answer($res);
}
}

View File

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -49,51 +49,49 @@ class apActorFollowingAction extends ManagedAction
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
protected function handle ()
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"])) {
if (!isset($_GET["page"])) {
$page = 1;
} else {
$page = intval ($this->trimmed ('page'));
$page = intval($this->trimmed('page'));
}
if ($page <= 0) {
ActivityPubReturn::error ('Invalid page number.');
ActivityPubReturn::error('Invalid page number.');
}
/* Fetch Following */
try {
$since = ($page - 1) * PROFILES_PER_MINILIST;
$limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST;
$sub = $profile->getSubscribed ($since, $limit);
$sub = $profile->getSubscribed($since, $limit);
} catch (NoResultException $e) {
ActivityPubReturn::error ('This user is not following anyone.');
ActivityPubReturn::error('This user is not following anyone.');
}
/* Calculate total items */
$total_subs = $profile->subscriptionCount();
$total_pages = ceil ($total_subs / PROFILES_PER_MINILIST);
$total_pages = ceil($total_subs / PROFILES_PER_MINILIST);
if ($total_pages == 0) {
ActivityPubReturn::error ('This user is not following anyone.');
ActivityPubReturn::error('This user is not following anyone.');
}
if ($page > $total_pages) {
ActivityPubReturn::error ("There are only {$total_pages} pages.");
ActivityPubReturn::error("There are only {$total_pages} pages.");
}
/* Get followed' URLs */
$subs = array ();
while ($sub->fetch ()) {
$subs = array();
while ($sub->fetch()) {
$subs[] = $sub->profileurl;
}
@ -110,6 +108,6 @@ class apActorFollowingAction extends ManagedAction
'orderedItems' => $subs
];
ActivityPubReturn::answer ($res);
ActivityPubReturn::answer($res);
}
}

View File

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -49,45 +49,47 @@ class apActorInboxAction extends ManagedAction
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
protected function handle ()
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') {
ActivityPubReturn::error ("C2S not implemented just yet.");
ActivityPubReturn::error("C2S not implemented just yet.");
}
$data = json_decode (file_get_contents ('php://input'));
$data = json_decode(file_get_contents('php://input'));
// Validate data
if (!(isset ($data->type))) {
ActivityPubReturn::error ("Type was not specified.");
if (!(isset($data->type))) {
ActivityPubReturn::error("Type was not specified.");
}
if (!isset ($data->actor)) {
ActivityPubReturn::error ("Actor was not specified.");
if (!isset($data->actor)) {
ActivityPubReturn::error("Actor was not specified.");
}
if (!isset ($data->object)) {
ActivityPubReturn::error ("Object was not specified.");
if (!isset($data->object)) {
ActivityPubReturn::error("Object was not specified.");
}
// Get valid Actor object
try {
require_once dirname (__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php";
require_once dirname(__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php";
$actor_profile = new Activitypub_explorer;
$actor_profile = $actor_profile->lookup ($data->actor);
$actor_profile = $actor_profile->lookup($data->actor);
$actor_profile = $actor_profile[0];
} catch (Exception $e) {
ActivityPubReturn::error ("Invalid Actor.", 404);
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,8 +111,14 @@ 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.");
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,14 +19,14 @@
*
* @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/
*/
if (!defined ('GNUSOCIAL')) {
exit (1);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -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,22 +46,32 @@ 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);
}
unset($id);
} else {
try {
$profile = User::getByNickname($this->trimmed('nickname'))->getProfile();
} catch (Exception $e) {
ActivityPubReturn::error('Invalid username.', 404);
}
catch (Exception $e) {
ActivityPubReturn::error ('Invalid username.', 404);
}
$res = Activitypub_profile::profile_to_array ($profile);
if (!$profile->isLocal()) {
ActivityPubReturn::error("This is not a local user.");
}
ActivityPubReturn::answer ($res);
$res = Activitypub_profile::profile_to_array($profile);
ActivityPubReturn::answer($res);
}
}

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

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -49,71 +49,46 @@ class apSharedInboxAction extends ManagedAction
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
protected function handle ()
protected function handle()
{
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
ActivityPubReturn::error ("Only POST requests allowed.");
ActivityPubReturn::error("Only POST requests allowed.");
}
$data = json_decode (file_get_contents ('php://input'));
$data = json_decode(file_get_contents('php://input'));
// Validate data
if (!isset ($data->type)) {
ActivityPubReturn::error ("Type was not specified.");
if (!isset($data->type)) {
ActivityPubReturn::error("Type was not specified.");
}
if (!isset ($data->actor)) {
ActivityPubReturn::error ("Actor was not specified.");
if (!isset($data->actor)) {
ActivityPubReturn::error("Actor was not specified.");
}
if (!isset ($data->object)) {
ActivityPubReturn::error ("Object was not specified.");
if (!isset($data->object)) {
ActivityPubReturn::error("Object was not specified.");
}
$discovery = new Activitypub_explorer;
// Get valid Actor object
try {
$actor_profile = $discovery->lookup ($data->actor);
$actor_profile = $discovery->lookup($data->actor);
$actor_profile = $actor_profile[0];
} catch (Exception $e) {
ActivityPubReturn::error ("Invalid Actor.", 404);
ActivityPubReturn::error("Invalid Actor.", 404);
}
unset ($discovery);
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,8 +106,14 @@ 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.");
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

@ -25,13 +25,13 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
try {
Notice::getByUri ($data->object)->repeat ($actor_profile, "ActivityPub");
ActivityPubReturn::answer ("Notice repeated successfully.");
Notice::getByUri($data->object->id)->repeat($actor_profile, "ActivityPub");
ActivityPubReturn::answer("Notice repeated successfully.");
} catch (Exception $e) {
ActivityPubReturn::error ($e->getMessage (), 403);
ActivityPubReturn::error($e->getMessage(), 403);
}

View File

@ -25,79 +25,109 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
$valid_object_types = array ("Note");
$valid_object_types = array("Note");
// Validate data
if (!(isset ($data->object->type) && in_array ($data->object->type, $valid_object_types))) {
ActivityPubReturn::error ("Invalid Object type.");
if (!isset($data->id)) {
ActivityPubReturn::error("Id not specified.");
}
if (!isset ($data->object->content)) {
ActivityPubReturn::error ("Object content was not specified.");
if (!(isset($data->object->type) && in_array($data->object->type, $valid_object_types))) {
ActivityPubReturn::error("Invalid Object type.");
}
if (!isset ($data->object->url)) {
ActivityPubReturn::error ("Object url was not specified.");
} else if (!filter_var ($data->object->url, FILTER_VALIDATE_URL)) {
ActivityPubReturn::error ("Invalid Object Url.");
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 = new Activity();
$act->verb = ActivityVerb::POST;
$act->time = time ();
$act->actor = $actor_profile->asActivityObject ();
$act->time = time();
$act->actor = $actor_profile->asActivityObject();
$act->context = new ActivityContext ();
$act->context = new ActivityContext();
// Is this a reply?
if (isset ($data->object->reply_to)) {
if (isset($data->object->reply_to)) {
try {
$reply_to = Notice::getByUri ($data->object->reply_to);
$reply_to = Notice::getByUri($data->object->reply_to);
} catch (Exception $e) {
ActivityPubReturn::error ("Invalid Object reply_to value.");
ActivityPubReturn::error("Invalid Object reply_to value.");
}
$act->context->replyToID = $reply_to->getUri ();
$act->context->replyToUrl = $reply_to->getUrl ();
$act->context->replyToID = $reply_to->getUri();
$act->context->replyToUrl = $reply_to->getUrl();
} else {
$reply_to = null;
}
$act->context->attention = common_get_attentions ($content, $actor_profile, $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)
// This is done after MediaFile::fromUpload etc. just to act the same as the ApiStatusesUpdateAction
if (Notice::contentTooLong ($content)) {
ActivityPubReturn::error ("That's too long. Maximum notice size is %d character.");
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);
ToSelector::fillActivity($this, $act, $options);
$actobj = new ActivityObject ();
$actobj = new ActivityObject();
$actobj->type = ActivityObject::NOTE;
$actobj->content = common_render_content ($content, $actor_profile, $reply_to);
$actobj->content = common_render_content($content, $actor_profile, $reply_to);
// Finally add the activity object to our activity
$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)));
ActivityPubReturn::answer ($res);
$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 ());
ActivityPubReturn::error($e->getMessage());
}

View File

@ -25,17 +25,15 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
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);
ActivityPubReturn::error($e->getMessage(), 403);
}

View File

@ -25,34 +25,30 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
// Validate Object
if (!is_string ($data->object)) {
ActivityPubReturn::error ("Invalid Object object, URL expected.");
if (!is_string($data->object)) {
ActivityPubReturn::error("Invalid Object object, URL expected.");
}
// Get valid Object profile
try {
$object_profile = new Activitypub_explorer;
$object_profile = $object_profile->lookup ($data->object)[0];
} catch(Exception $e) {
ActivityPubReturn::error ("Invalid Object Actor URL.", 404);
$object_profile = $object_profile->lookup($data->object)[0];
} catch (Exception $e) {
ActivityPubReturn::error("Invalid Object Actor URL.", 404);
}
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);
if (!Subscription::exists($actor_profile, $object_profile)) {
Subscription::start($actor_profile, $object_profile);
ActivityPubReturn::answer(Activitypub_accept::accept_to_array(Activitypub_follow::follow_to_array($data->actor, $data->object)));
} else {
ActivityPubReturn::error ("Already following.", 409);
ActivityPubReturn::error("Already following.", 409);
}
} catch (Exception $e) {
ActivityPubReturn::error ("Invalid Object Actor URL.", 404);
ActivityPubReturn::error("Invalid Object Actor URL.", 404);
}

View File

@ -25,17 +25,17 @@
* @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);
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);
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

@ -25,53 +25,69 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
// Validate data
if (!isset ($data->type)) {
ActivityPubReturn::error ("Type was not specified.");
if (!isset($data->type)) {
ActivityPubReturn::error("Type was not specified.");
}
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);
ActivityPubReturn::error($e->getMessage(), 403);
}
break;
case "Follow":
// Validate data
if (!isset ($data->object->object)) {
ActivityPubReturn::error ("Object Actor URL was not specified.");
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];
$object_profile = $object_profile->lookup($data->object->object)[0];
} catch (Exception $e) {
ActivityPubReturn::error ("Invalid Object Actor URL.", 404);
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.");
if (Subscription::exists($actor_profile, $object_profile)) {
Subscription::cancel($actor_profile, $object_profile);
// 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);
ActivityPubReturn::error("You are not following this person already.", 409);
}
break;
default:
ActivityPubReturn::error ("Invalid object type.");
ActivityPubReturn::error("Invalid object type.");
break;
}

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

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -47,7 +47,7 @@ class Activitypub_attachment extends Managed_DataObject
* @param Attachment $attachment
* @return pretty array to be used in a response
*/
public static function attachment_to_array ($attachment)
public static function attachment_to_array($attachment)
{
$res = [
'@context' => [
@ -56,17 +56,16 @@ class Activitypub_attachment extends Managed_DataObject
"@language" => "en"
]
],
'id' => $attachment->getID (),
'id' => $attachment->getID(),
'mimetype' => $attachment->mimetype,
'url' => $attachment->getUrl (),
'url' => $attachment->getUrl(),
'size' => intval($attachment->size), // $attachment->getSize ()
'title' => $attachment->getTitle (),
'title' => $attachment->getTitle(),
'meta' => null
];
// 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

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -47,7 +47,7 @@ class Activitypub_error extends Managed_DataObject
* @param string $m
* @return pretty array to be used in a response
*/
public static function error_message_to_array ($m)
public static function error_message_to_array($m)
{
$res = [
'error'=> $m

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

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -49,39 +49,39 @@ class Activitypub_notice extends Managed_DataObject
* @param Notice $notice
* @return pretty array to be used in a response
*/
public static function notice_to_array ($notice)
public static function notice_to_array($notice)
{
$attachments = array ();
foreach($notice->attachments () as $attachment) {
$attachments[] = Activitypub_attachment::attachment_to_array ($attachment);
$attachments = array();
foreach ($notice->attachments() as $attachment) {
$attachments[] = Activitypub_attachment::attachment_to_array($attachment);
}
$tags = array ();
foreach($notice->getTags()as $tag) {
$tags = array();
foreach ($notice->getTags() as $tag) {
if ($tag != "") { // Hacky workaround to avoid stupid outputs
$tags[] = Activitypub_tag::tag_to_array ($tag);
$tags[] = Activitypub_tag::tag_to_array($tag);
}
}
$to = array ();
foreach ($notice->getAttentionProfileIDs () as $to_id) {
$to[] = Profile::getById ($to_id)->getUri ();
$to = array();
foreach ($notice->getAttentionProfiles() as $to_profile) {
$to[] = $to_profile->getUri();
}
if (!is_null($to)) {
$to = array ("https://www.w3.org/ns/activitystreams#Public");
if (empty($to)) {
$to = array("https://www.w3.org/ns/activitystreams#Public");
}
$item = [
'id' => $notice->getUrl (),
'type' => 'Notice',
'actor' => $notice->getProfile ()->getUrl (),
'published' => $notice->getCreated (),
'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 (),
'is_local' => $notice->isLocal (),
'conversation' => intval ($notice->conversation),
'content' => $notice->getContent(),
'url' => $notice->getUrl(),
'reply_to' => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUri(),
'is_local' => $notice->isLocal(),
'conversation' => intval($notice->conversation),
'attachment' => $attachments,
'tag' => $tags
];

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

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -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,24 +50,24 @@ 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 (
'uri' => array ('type' => 'varchar', 'length' => 191, 'not null' => true),
'profile_id' => array ('type' => 'integer'),
'inboxuri' => array ('type' => 'varchar', 'length' => 191),
'sharedInboxuri' => array ('type' => 'varchar', 'length' => 191),
'created' => array ('type' => 'datetime', 'not null' => true),
'modified' => array ('type' => 'datetime', 'not null' => true),
return array(
'fields' => array(
'uri' => array('type' => 'varchar', 'length' => 191, 'not null' => true),
'profile_id' => array('type' => 'integer'),
'inboxuri' => array('type' => 'varchar', 'length' => 191),
'sharedInboxuri' => array('type' => 'varchar', 'length' => 191),
'created' => array('type' => 'datetime', 'not null' => true),
'modified' => array('type' => 'datetime', 'not null' => true),
),
'primary key' => array ('uri'),
'unique keys' => array (
'Activitypub_profile_profile_id_key' => array ('profile_id'),
'Activitypub_profile_inboxuri_key' => array ('inboxuri'),
'primary key' => array('uri'),
'unique keys' => array(
'Activitypub_profile_profile_id_key' => array('profile_id'),
'Activitypub_profile_inboxuri_key' => array('inboxuri'),
),
'foreign keys' => array (
'Activitypub_profile_profile_id_fkey' => array ('profile', array ('profile_id' => 'id')),
'foreign keys' => array(
'Activitypub_profile_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
),
);
}
@ -80,9 +79,10 @@ class Activitypub_profile extends Profile
* @param Profile $profile
* @return pretty array to be used in a response
*/
public static function profile_to_array ($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 (),
'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",
'followers_count' => $profile->subscriberCount (),
'following' => "{$url}/following.json",
'following_count' => $profile->subscriptionCount (),
'liked' => "{$url}/liked.json",
'liked_count' => Fave::countByProfile ($profile),
'summary' => ($desc = $profile->getDescription ()) == null ? "" : $desc,
'url' => $profile->getURL (),
'avatar' => [
'preferredUsername' => $profile->getNickname(),
'is_local' => $profile->isLocal(),
'inbox' => common_local_url("apActorInbox", array("id" => $id)),
'name' => $profile->getFullname(),
'followers' => common_local_url("apActorFollowers", array("id" => $id)),
'followers_count' => $profile->subscriberCount(),
'following' => common_local_url("apActorFollowing", array("id" => $id)),
'following_count' => $profile->subscriptionCount(),
'liked' => common_local_url("apActorLiked", array("id" => $id)),
'liked_count' => Fave::countByProfile($profile),
'summary' => ($desc = $profile->getDescription()) == null ? "" : $desc,
'icon' => [
'type' => 'Image',
'width' => 96,
'height' => 96,
'url' => $profile->avatarUrl (AVATAR_PROFILE_SIZE)
'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;
}
@ -123,34 +129,34 @@ class Activitypub_profile extends Profile
* @access public
* @throws ServerException
*/
public function do_insert ()
public function do_insert()
{
$profile = new Profile ();
$profile = new Profile();
$profile->created = $this->created = $this->modified = common_sql_now ();
$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;
}
$this->profile_id = $profile->insert ();
$this->profile_id = $profile->insert();
if ($this->profile_id === false) {
$profile->query ('ROLLBACK');
throw new ServerException ('Profile insertion failed.');
$profile->query('ROLLBACK');
throw new ServerException('Profile insertion failed.');
}
$ok = $this->insert ();
$ok = $this->insert();
if ($ok === false) {
$profile->query ('ROLLBACK');
throw new ServerException ('Cannot save ActivityPub profile.');
$profile->query('ROLLBACK');
throw new ServerException('Cannot save ActivityPub profile.');
}
}
@ -161,11 +167,11 @@ class Activitypub_profile extends Profile
* @return Profile
* @throws NoProfileException if it was not found
*/
public function local_profile ()
public function local_profile()
{
$profile = Profile::getKV ('id', $this->profile_id);
$profile = Profile::getKV('id', $this->profile_id);
if (!$profile instanceof Profile) {
throw new NoProfileException ($this->profile_id);
throw new NoProfileException($this->profile_id);
}
return $profile;
}
@ -178,18 +184,18 @@ 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 ();
$profile_id = $profile->getID();
$aprofile = self::getKV ('profile_id', $profile_id);
$aprofile = self::getKV('profile_id', $profile_id);
if (!$aprofile instanceof Activitypub_profile) {
// No Activitypub_profile for this profile_id,
if (!$profile->isLocal ()) {
if (!$profile->isLocal()) {
// create one!
$aprofile = self::create_from_local_profile ($profile);
$aprofile = self::create_from_local_profile($profile);
} else {
throw new Exception ('No Activitypub_profile for Profile ID: '.$profile_id. ', this is a local user.');
throw new Exception('No Activitypub_profile for Profile ID: '.$profile_id. ', this is a local user.');
}
}
@ -209,23 +215,23 @@ class Activitypub_profile extends Profile
* @param Profile $profile
* @return Activitypub_profile
*/
private static function create_from_local_profile (Profile $profile)
private static function create_from_local_profile(Profile $profile)
{
$url = $profile->getURL ();
$inboxes = Activitypub_explorer::get_actor_inboxes_uri ($url);
$url = $profile->getURL();
$inboxes = Activitypub_explorer::get_actor_inboxes_uri($url);
$aprofile->created = $aprofile->modified = common_sql_now ();
$aprofile->created = $aprofile->modified = common_sql_now();
$aprofile = new Activitypub_profile;
$aprofile->profile_id = $profile->getID ();
$aprofile->profile_id = $profile->getID();
$aprofile->uri = $url;
$aprofile->nickname = $profile->getNickname ();
$aprofile->fullname = $profile->getFullname ();
$aprofile->bio = substr ($profile->getDescription (), 0, 1000);
$aprofile->nickname = $profile->getNickname();
$aprofile->fullname = $profile->getFullname();
$aprofile->bio = substr($profile->getDescription(), 0, 1000);
$aprofile->inboxuri = $inboxes["inbox"];
$aprofile->sharedInboxuri = $inboxes["sharedInbox"];
$aprofile->insert ();
$aprofile->insert();
return $aprofile;
}
@ -236,9 +242,9 @@ class Activitypub_profile extends Profile
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return string Inbox URL
*/
public function get_inbox ()
public function get_inbox()
{
if (is_null ($this->sharedInboxuri)) {
if (is_null($this->sharedInboxuri)) {
return $this->inboxuri;
}
@ -251,7 +257,7 @@ class Activitypub_profile extends Profile
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return string URI
*/
public function get_uri ()
public function get_uri()
{
return $this->uri;
}
@ -264,17 +270,15 @@ class Activitypub_profile extends Profile
* @return Activitypub_profile
* @throws Exception if it isn't possible to return an Activitypub_profile
*/
public static function get_from_uri ($url)
public static function get_from_uri($url)
{
$explorer = new Activitypub_explorer ();
$profiles_found = $explorer->lookup ($url);
if (!empty ($profiles_found)) {
return self::from_profile ($profiles_found[0]);
$explorer = new Activitypub_explorer();
$profiles_found = $explorer->lookup($url);
if (!empty($profiles_found)) {
return self::from_profile($profiles_found[0]);
} else {
throw new Exception ('No valid ActivityPub profile found for given URI');
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.
}
/**
@ -289,65 +293,67 @@ class Activitypub_profile extends Profile
* @return Activitypub_profile
* @throws Exception on error conditions
*/
public static function ensure_web_finger ($addr)
public static function ensure_web_finger($addr)
{
// Normalize $addr, i.e. add 'acct:' if missing
$addr = Discovery::normalize ($addr);
$addr = Discovery::normalize($addr);
// Try the cache
$uri = self::cacheGet (sprintf ('activitypub_profile:webfinger:%s', $addr));
$uri = self::cacheGet(sprintf('activitypub_profile:webfinger:%s', $addr));
if ($uri !== false) {
if (is_null ($uri)) {
if (is_null($uri)) {
// Negative cache entry
// TRANS: Exception.
throw new Exception (_m ('Not a valid webfinger address (via cache).'));
throw new Exception(_m('Not a valid webfinger address (via cache).'));
}
try {
return self::get_from_uri ($uri);
return self::get_from_uri($uri);
} catch (Exception $e) {
common_log (LOG_ERR, sprintf (__METHOD__ . ': Webfinger address cache inconsistent with database, did not find Activitypub_profile uri==%s', $uri));
self::cacheSet (sprintf ('activitypub_profile:webfinger:%s', $addr), false);
common_log(LOG_ERR, sprintf(__METHOD__ . ': Webfinger address cache inconsistent with database, did not find Activitypub_profile uri==%s', $uri));
self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), false);
}
}
// Now, try some discovery
$disco = new Discovery ();
$disco = new Discovery();
try {
$xrd = $disco->lookup ($addr);
$xrd = $disco->lookup($addr);
} catch (Exception $e) {
// Save negative cache entry so we don't waste time looking it up again.
// @todo FIXME: Distinguish temporary failures?
self::cacheSet (sprintf ('activitypub_profile:webfinger:%s', $addr), null);
self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), null);
// TRANS: Exception.
throw new Exception (_m ('Not a valid webfinger address.'));
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)) {
if (!array_key_exists ('profileurl', $hints) ||
if (array_key_exists('hcard', $hints)) {
if (!array_key_exists('profileurl', $hints) ||
$hints['hcard'] != $hints['profileurl']) {
$hcardHints = DiscoveryHints::fromHcardUrl ($hints['hcard']);
$hints = array_merge ($hcardHints, $hints);
$hcardHints = DiscoveryHints::fromHcardUrl($hints['hcard']);
$hints = array_merge($hcardHints, $hints);
}
}
// If we got a profile page, try that!
$profileUrl = null;
if (array_key_exists ('profileurl', $hints)) {
if (array_key_exists('profileurl', $hints)) {
$profileUrl = $hints['profileurl'];
try {
common_log (LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl");
$aprofile = self::get_from_uri ($hints['profileurl']);
self::cacheSet (sprintf ('activitypub_profile:webfinger:%s', $addr), $aprofile->get_uri ());
common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl");
$aprofile = self::get_from_uri($hints['profileurl']);
self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), $aprofile->get_uri());
return $aprofile;
} catch (Exception $e) {
common_log (LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage ());
common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage());
// keep looking
//
// @todo FIXME: This means an error discovering from profile page
@ -360,6 +366,6 @@ class Activitypub_profile extends Profile
// XXX: try FOAF
// TRANS: Exception. %s is a webfinger address.
throw new Exception (sprintf (_m ('Could not find a valid profile for "%s".'), $addr));
throw new Exception(sprintf(_m('Could not find a valid profile for "%s".'), $addr));
}
}

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

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -47,7 +47,7 @@ class Activitypub_tag extends Managed_DataObject
* @param Tag $tag
* @return pretty array to be used in a response
*/
public static function tag_to_array ($tag)
public static function tag_to_array($tag)
{
$res = [
'@context' => [
@ -57,7 +57,7 @@ class Activitypub_tag extends Managed_DataObject
]
],
'name' => $tag,
'url' => common_local_url ('tag', array('tag' => $tag))
'url' => common_local_url('tag', array('tag' => $tag))
];
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

@ -24,12 +24,13 @@
* @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);
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

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -42,7 +42,7 @@ if (!defined ('GNUSOCIAL')) {
*/
class Activitypub_explorer
{
private $discovered_actor_profiles = array ();
private $discovered_actor_profiles = array();
/**
* Get every profile from the given URL
@ -53,11 +53,11 @@ class Activitypub_explorer
* @param string $url User's url
* @return array of Profile objects
*/
public function lookup ($url)
public function lookup($url)
{
$this->discovered_actor_profiles = array ();
$this->discovered_actor_profiles = array();
return $this->_lookup ($url);
return $this->_lookup($url);
}
/**
@ -69,49 +69,80 @@ class Activitypub_explorer
* @param string $url User's url
* @return array of Profile objects
*/
private function _lookup ($url)
private function _lookup($url)
{
// 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");
if (! ($this->grab_local_user($url) || $this->grab_remote_user($url))) {
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;
}
@ -123,30 +154,35 @@ class Activitypub_explorer
* @param string $url User's url
* @return boolean success state
*/
private function grab_remote_user ($url)
private function grab_remote_user($url)
{
$client = new HTTPClient ();
if (!isset($this->temp_res)) {
$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.");
$response = $client->get($url, $headers);
if (!$response->isOk()) {
throw new Exception("Invalid Actor URL.");
}
$res = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES);
if (isset ($res["orderedItems"])) { // It's a potential collection of actors!!!
$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) {
if ($this->_lookup($profile) == false) {
// XXX: Invalid actor found, not sure how we handle those
}
}
// Go through entire collection
if (!is_null ($res["next"])) {
$this->_lookup ($res["next"]);
if (!is_null($res["next"])) {
$this->_lookup($res["next"]);
}
return true;
} else if (self::validate_remote_response ($res)) {
$this->discovered_actor_profiles[]= $this->store_profile ($res);
} elseif (self::validate_remote_response($res)) {
$this->discovered_actor_profiles[]= $this->store_profile($res);
return true;
}
@ -160,19 +196,19 @@ class Activitypub_explorer
* @param array $res remote response
* @return Profile remote Profile object
*/
private function store_profile ($res)
private function store_profile($res)
{
$aprofile = new Activitypub_profile;
$aprofile->uri = $res["url"];
$aprofile->nickname = $res["nickname"];
$aprofile->fullname = $res["display_name"];
$aprofile->bio = substr ($res["summary"], 0, 1000);
$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"];
$aprofile->sharedInboxuri = isset($res["sharedInbox"]) ? $res["sharedInbox"] : $res["inbox"];
$aprofile->do_insert ();
$aprofile->do_insert();
return $aprofile->local_profile ();
return $aprofile->local_profile();
}
/**
@ -183,41 +219,15 @@ class Activitypub_explorer
* @param array $res remote response
* @return boolean success state
*/
private static function validate_remote_response ($res)
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
*
@ -225,20 +235,20 @@ class Activitypub_explorer
* @param string $url of Actor profile
* @return boolean|array false if fails | array with inbox and shared inbox if successful
*/
public static function get_actor_inboxes_uri ($url)
public static function get_actor_inboxes_uri($url)
{
$client = new HTTPClient ();
$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.");
$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)) {
return array ("inbox" => $res["inbox"],
"sharedInbox" => isset ($res["sharedInbox"]) ? $res["sharedInbox"] : $res["inbox"]);
$res = json_decode($response->getBody(), JSON_UNESCAPED_SLASHES);
if (self::validate_remote_response($res)) {
return array("inbox" => $res["inbox"],
"sharedInbox" => isset($res["sharedInbox"]) ? $res["sharedInbox"] : $res["inbox"]);
}
return false;

View File

@ -25,8 +25,8 @@
* @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);
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
@ -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->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 ()
public function follow()
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Follow",
"actor" => $this->actor->getUrl (),
"object" => $this->to[0]->getUrl ());
$this->client->setBody (json_encode ($data));
$this->client->post ($this->to[0]->get_inbox (), $this->headers);
$data = Activitypub_follow::follow_to_array($this->actor->getUrl(), $this->to[0]->getUrl());
$this->client->setBody(json_encode($data));
$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.");
}
/**
@ -85,18 +98,27 @@ class Activitypub_postman
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
*/
public function undo_follow ()
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);
$this->client->setBody(json_encode($data));
$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.");
}
/**
@ -105,15 +127,15 @@ class Activitypub_postman
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice
*/
public function like ($notice)
public function like($notice)
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Like",
"actor" => $this->actor->getUrl (),
"object" => $notice->getUri ());
$this->client->setBody (json_encode ($data));
foreach ($this->to_inbox () as $inbox) {
$this->client->post ($inbox, $this->headers);
$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);
}
}
@ -123,41 +145,17 @@ class Activitypub_postman
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice
*/
public function undo_like ($notice)
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));
foreach ($this->to_inbox () as $inbox) {
$this->client->post ($inbox, $this->headers);
}
}
/**
* 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);
$this->client->setBody(json_encode($data));
foreach ($this->to_inbox() as $inbox) {
$this->client->post($inbox, $this->headers);
}
}
@ -167,25 +165,37 @@ class Activitypub_postman
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice
*/
public function create ($notice)
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 ();
if (isset($notice->reply_to)) {
$data["object"]["reply_to"] = $notice->getParent()->getUri();
}
$this->client->setBody (json_encode ($data));
foreach ($this->to_inbox () as $inbox) {
$this->client->post ($inbox, $this->headers);
$this->client->setBody(json_encode($data));
foreach ($this->to_inbox() as $inbox) {
$this->client->post($inbox, $this->headers);
}
}
/**
* 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);
}
}
@ -195,16 +205,24 @@ class Activitypub_postman
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param Notice $notice
*/
public function delete ($notice)
public function delete($notice)
{
$data = array ("@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Delete",
"actor" => $this->actor->getUrl (),
"object" => $notice->getUri ()
);
$this->client->setBody (json_encode ($data));
foreach ($this->to_inbox () as $inbox) {
$this->client->post ($inbox, $this->headers);
$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) {
$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));
}
}
@ -214,13 +232,13 @@ class Activitypub_postman
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return array To Inbox URLs
*/
private function to_inbox ()
private function to_inbox()
{
$to_inboxes = array ();
$to_inboxes = array();
foreach ($this->to as $to_profile) {
$to_inboxes[] = $to_profile->get_inbox ();
$to_inboxes[] = $to_profile->get_inbox();
}
return array_unique ($to_inboxes);
return array_unique($to_inboxes);
}
}