2018-03-25 21:16:43 +01:00
< ? php
/**
* GNU social - a federating social network
*
2018-07-09 23:43:34 +01:00
* ActivityPubPlugin implementation for GNU Social
2018-03-25 21:16:43 +01:00
*
* 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
2018-04-29 13:23:54 +01:00
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
2018-07-26 22:12:13 +01:00
* @ author Daniel Supernault < danielsupernault @ gmail . com >
2018-07-09 23:43:34 +01:00
* @ copyright 2018 Free Software Foundation http :// fsf . org
2018-03-25 21:16:43 +01:00
* @ 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 /
*/
2018-07-26 22:12:13 +01:00
if ( ! defined ( 'GNUSOCIAL' )) {
exit ( 1 );
2018-07-09 23:43:34 +01:00
}
2018-03-25 21:16:43 +01:00
2018-07-31 20:09:47 +01:00
// Ensure proper timezone
date_default_timezone_set ( 'UTC' );
2018-07-20 16:04:17 +01:00
// Import required files by the plugin
2018-08-03 00:41:08 +01:00
require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php' ;
require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'discoveryhints.php' ;
require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'explorer.php' ;
require_once __DIR__ . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'postman.php' ;
2018-07-20 16:04:17 +01:00
2018-07-29 02:35:04 +01:00
// So that this isn't hardcoded everywhere
2018-08-02 18:02:28 +01:00
define ( 'ACTIVITYPUB_BASE_ACTOR_URI' , common_root_url () . 'index.php/user/' );
2018-08-02 01:42:15 +01:00
const ACTIVITYPUB_PUBLIC_TO = [ 'https://www.w3.org/ns/activitystreams#Public' ,
'Public' ,
'as:Public'
];
2018-07-29 02:35:04 +01:00
2018-07-09 23:43:34 +01:00
/**
* @ 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 /
*/
2018-03-25 21:16:43 +01:00
class ActivityPubPlugin extends Plugin
{
2018-07-26 22:12:13 +01:00
/**
* 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 ()) {
2018-08-02 18:02:28 +01:00
return ACTIVITYPUB_BASE_ACTOR_URI . $profile -> getID ();
2018-07-26 22:12:13 +01:00
} 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 )
{
2018-07-28 15:52:47 +01:00
return ActivityPubPlugin :: actor_uri ( $profile ) . " / " ;
2018-07-26 22:12:13 +01:00
}
/**
2018-08-02 01:42:15 +01:00
* Returns a notice from its URL .
2018-07-26 22:12:13 +01:00
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
2018-07-29 02:35:04 +01:00
* @ param string $url Notice ' s URL
* @ return Notice The Notice object
* @ throws Exception This function or provides a Notice or fails with exception
2018-07-26 22:12:13 +01:00
*/
2018-08-02 01:42:15 +01:00
public static function grab_notice_from_url ( $url )
2018-07-26 22:12:13 +01:00
{
2018-08-02 01:42:15 +01:00
/* Offline Grabbing */
2018-07-29 02:35:04 +01:00
try {
2018-08-02 05:00:35 +01:00
// Look for a known remote notice
2018-07-31 22:41:40 +01:00
return Notice :: getByUri ( $url );
2018-07-29 02:35:04 +01:00
} catch ( Exception $e ) {
2018-08-02 01:42:15 +01:00
// Look for a local notice (unfortunately GNU Social doesn't
2018-08-02 05:00:35 +01:00
// provide this functionality natively)
2018-07-26 22:12:13 +01:00
try {
2018-07-29 02:35:04 +01:00
$candidate = Notice :: getByID ( intval ( substr ( $url , strlen ( common_local_url ( 'shownotice' , [ 'notice' => '' ])))));
2018-07-31 20:09:47 +01:00
if ( $candidate -> getUrl () == $url ) { // Sanity check
2018-07-29 02:35:04 +01:00
return $candidate ;
} else {
2018-08-02 01:42:15 +01:00
common_debug ( 'ActivityPubPlugin Notice Grabber: ' . $candidate -> getUrl () . ' is different of ' . $url );
2018-07-29 02:35:04 +01:00
}
2018-07-26 22:12:13 +01:00
} catch ( Exception $e ) {
2018-08-02 02:34:08 +01:00
common_debug ( 'ActivityPubPlugin Notice Grabber: failed to find: ' . $url . ' offline.' );
2018-07-26 22:12:13 +01:00
}
}
2018-08-02 01:42:15 +01:00
/* Online Grabbing */
$client = new HTTPClient ();
$headers = [];
$headers [] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ;
$headers [] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social' ;
$response = $client -> get ( $url , $headers );
2018-08-02 05:00:35 +01:00
$res = json_decode ( $response -> getBody (), true );
2018-08-02 01:42:15 +01:00
$settings = [];
2018-08-02 07:01:39 +01:00
try {
Activitypub_notice :: validate_remote_notice ( $res );
} catch ( Exception $e ) {
2018-08-02 02:14:43 +01:00
common_debug ( 'ActivityPubPlugin Notice Grabber: Invalid potential remote notice while processing id: ' . $url . '. He returned the following: ' . json_encode ( $res , JSON_UNESCAPED_SLASHES ));
2018-08-02 07:01:39 +01:00
throw $e ;
2018-08-02 01:42:15 +01:00
}
if ( isset ( $res -> inReplyTo )) {
$settings [ 'inReplyTo' ] = $res -> inReplyTo ;
}
if ( isset ( $res -> latitude )) {
$settings [ 'latitude' ] = $res -> latitude ;
}
if ( isset ( $res -> longitude )) {
$settings [ 'longitude' ] = $res -> longitude ;
}
try {
return Activitypub_notice :: create_notice (
2018-08-02 07:33:22 +01:00
ActivityPub_explorer :: get_profile_from_url ( $res [ 'attributedTo' ]),
2018-08-02 07:36:28 +01:00
$res [ 'id' ],
$res [ 'url' ],
$res [ 'content' ],
$res [ 'cc' ],
2018-08-02 01:42:15 +01:00
$settings
);
} catch ( Exception $e ) {
2018-08-02 02:34:08 +01:00
common_debug ( 'ActivityPubPlugin Notice Grabber: failed to find: ' . $url . ' online.' );
2018-08-02 01:42:15 +01:00
throw $e ;
}
// When all the above failed in its quest of grabbing the Notice
throw new Exception ( 'Notice not found.' );
2018-07-26 22:12:13 +01:00
}
/**
* Route / Reroute urls
*
* @ param URLMapper $m
* @ return void
*/
public function onRouterInitialized ( URLMapper $m )
{
2018-07-31 20:09:47 +01:00
ActivityPubURLMapperOverwrite :: variable (
2018-07-26 22:12:13 +01:00
$m ,
'user/:id' ,
2018-07-31 20:09:47 +01:00
[ 'id' => '[0-9]+' ],
2018-07-29 02:35:04 +01:00
'apActorProfile'
);
2018-07-26 22:12:13 +01:00
// Special route for webfinger purposes
2018-07-31 20:09:47 +01:00
ActivityPubURLMapperOverwrite :: variable (
2018-07-26 22:12:13 +01:00
$m ,
':nickname' ,
2018-07-29 02:35:04 +01:00
[ 'nickname' => Nickname :: DISPLAY_FMT ],
'apActorProfile'
2018-07-26 22:12:13 +01:00
);
2018-07-31 20:09:47 +01:00
ActivityPubURLMapperOverwrite :: variable (
$m ,
'notice/:id' ,
[ 'id' => '[0-9]+' ],
'apNotice'
);
2018-07-26 22:12:13 +01:00
$m -> connect (
'user/:id/liked.json' ,
[ 'action' => 'apActorLiked' ],
[ 'id' => '[0-9]+' ]
);
$m -> connect (
'user/:id/followers.json' ,
[ 'action' => 'apActorFollowers' ],
[ 'id' => '[0-9]+' ]
);
$m -> connect (
'user/:id/following.json' ,
[ 'action' => 'apActorFollowing' ],
[ 'id' => '[0-9]+' ]
);
$m -> connect (
'user/:id/inbox.json' ,
[ 'action' => 'apActorInbox' ],
[ 'id' => '[0-9]+' ]
);
$m -> connect (
'inbox.json' ,
[ 'action' => 'apSharedInbox' ]
);
}
/**
* Plugin version information
*
* @ param array $versions
* @ return boolean hook true
*/
public function onPluginVersion ( array & $versions )
{
$versions [] = [ 'name' => 'ActivityPub' ,
2018-07-09 23:43:34 +01:00
'version' => GNUSOCIAL_VERSION ,
2018-07-26 22:12:13 +01:00
'author' => 'Diogo Cordeiro, Daniel Supernault' ,
2018-07-09 23:43:34 +01:00
'homepage' => 'https://www.gnu.org/software/social/' ,
2018-07-31 20:09:47 +01:00
'rawdescription' => 'Adds ActivityPub Support' ];
2018-07-09 23:43:34 +01:00
2018-07-26 22:12:13 +01:00
return true ;
}
2018-07-27 19:18:31 +01:00
/**
* 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' ));
2018-08-02 18:02:28 +01:00
$out -> element ( 'dd' , null , _m ( 'Remote Profile' ));
2018-07-27 19:18:31 +01:00
$out -> elementEnd ( 'dl' );
}
2018-07-26 22:12:13 +01:00
/**
* Make sure necessary tables are filled out .
*
* @ return boolean hook true
*/
public function onCheckSchema ()
{
$schema = Schema :: get ();
$schema -> ensureTable ( 'Activitypub_profile' , Activitypub_profile :: schemaDef ());
2018-07-28 02:11:58 +01:00
$schema -> ensureTable ( 'Activitypub_rsa' , Activitypub_rsa :: schemaDef ());
2018-07-26 22:12:13 +01:00
$schema -> ensureTable ( 'Activitypub_pending_follow_requests' , Activitypub_pending_follow_requests :: schemaDef ());
return true ;
}
/********************************************************
2018-07-27 15:42:30 +01:00
* WebFinger Events *
2018-07-26 22:12:13 +01:00
********************************************************/
2018-07-29 02:35:04 +01:00
/**
* Get remote user ' s ActivityPub_profile via a identifier
*
* @ 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
*/
public static function pull_remote_profile ( $arg )
{
if ( preg_match ( '!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!' , $arg )) {
// webfinger lookup
try {
return Activitypub_profile :: ensure_web_finger ( $arg );
} catch ( Exception $e ) {
common_log ( LOG_ERR , 'Webfinger lookup failed for ' .
$arg . ': ' . $e -> getMessage ());
}
}
// Look for profile URLs, with or without scheme:
$urls = array ();
if ( preg_match ( '!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!' , $arg )) {
$urls [] = $arg ;
}
if ( preg_match ( '!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!' , $arg )) {
$schemes = array ( 'http' , 'https' );
foreach ( $schemes as $scheme ) {
$urls [] = " $scheme :// $arg " ;
}
}
foreach ( $urls as $url ) {
try {
return Activitypub_profile :: fromUri ( $url );
} catch ( Exception $e ) {
common_log ( LOG_ERR , 'Profile lookup failed for ' .
$arg . ': ' . $e -> getMessage ());
}
}
return null ;
}
2018-07-26 22:12:13 +01:00
/**
* Webfinger matches : @ user @ example . com or even @ user -- one . george_orwell @ 1984. biz
*
* @ author GNU Social
* @ param string $text The text from which to extract webfinger IDs
* @ param string $preMention Character ( s ) that signals a mention ( '@' , '!' ... )
* @ return array The matching IDs ( without $preMention ) and each respective position in the given string .
*/
public static function extractWebfingerIds ( $text , $preMention = '@' )
{
2018-07-28 02:11:58 +01:00
$wmatches = [];
2018-07-26 22:12:13 +01:00
$result = preg_match_all (
'/(?<!\S)' . preg_quote ( $preMention , '/' ) . '(' . Nickname :: WEBFINGER_FMT . ')/' ,
2018-07-28 02:11:58 +01:00
$text ,
$wmatches ,
PREG_OFFSET_CAPTURE
2018-07-26 22:12:13 +01:00
);
if ( $result === false ) {
common_log ( LOG_ERR , __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error==' . preg_last_error () . ').' );
2018-07-28 02:11:58 +01:00
return [];
} elseif ( $n_matches = count ( $wmatches )) {
common_debug ( sprintf ( 'Found %d matches for WebFinger IDs: %s' , $n_matches , _ve ( $wmatches )));
2018-07-26 22:12:13 +01:00
}
return $wmatches [ 1 ];
}
/**
* Profile URL matches : @ example . com / mublog / user
*
* @ author GNU Social
* @ param string $text The text from which to extract URL mentions
* @ param string $preMention Character ( s ) that signals a mention ( '@' , '!' ... )
* @ return array The matching URLs ( without @ or acct : ) and each respective position in the given string .
*/
public static function extractUrlMentions ( $text , $preMention = '@' )
{
$wmatches = array ();
// In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged
// with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important)
$result = preg_match_all (
'/(?:^|\s+)' . preg_quote ( $preMention , '/' ) . '(' . URL_REGEX_DOMAIN_NAME . '(?:\/[' . URL_REGEX_VALID_PATH_CHARS . ']*)*)/' ,
2018-07-20 16:04:17 +01:00
$text ,
$wmatches ,
2018-07-26 22:12:13 +01:00
PREG_OFFSET_CAPTURE
);
if ( $result === false ) {
common_log ( LOG_ERR , __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error==' . preg_last_error () . ').' );
} elseif ( count ( $wmatches )) {
common_debug ( sprintf ( 'Found %d matches for profile URL mentions: %s' , count ( $wmatches ), _ve ( $wmatches )));
}
return $wmatches [ 1 ];
}
2018-07-27 15:42:30 +01:00
/**
* Add activity + json mimetype on WebFinger
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param XML_XRD $xrd
* @ param Managed_DataObject $object
*/
public function onEndWebFingerProfileLinks ( XML_XRD & $xrd , Managed_DataObject $object )
{
if ( $object -> isPerson ()) {
2018-07-27 21:45:43 +01:00
$link = new XML_XRD_Element_Link (
'self' ,
2018-07-27 15:42:30 +01:00
ActivityPubPlugin :: actor_uri ( $object -> getProfile ()),
2018-07-27 21:45:43 +01:00
'application/activity+json'
);
2018-07-27 15:42:30 +01:00
$xrd -> links [] = clone ( $link );
}
}
2018-07-26 22:12:13 +01:00
/**
* Find any explicit remote mentions . Accepted forms :
* Webfinger : @ user @ example . com
* Profile link : @ example . com / mublog / user
*
* @ author GNU Social
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param Profile $sender
* @ param string $text input markup text
* @ param array & $mention in / out param : set of found mentions
* @ return boolean hook return value
*/
public function onEndFindMentions ( Profile $sender , $text , & $mentions )
{
$matches = array ();
foreach ( self :: extractWebfingerIds ( $text , '@' ) as $wmatch ) {
list ( $target , $pos ) = $wmatch ;
$this -> log ( LOG_INFO , " Checking webfinger person ' $target ' " );
$profile = null ;
try {
$aprofile = Activitypub_profile :: ensure_web_finger ( $target );
$profile = $aprofile -> local_profile ();
} catch ( Exception $e ) {
$this -> log ( LOG_ERR , " Webfinger check failed: " . $e -> getMessage ());
continue ;
}
assert ( $profile instanceof Profile );
$displayName = ! empty ( $profile -> nickname ) && mb_strlen ( $profile -> nickname ) < mb_strlen ( $target )
? $profile -> getNickname () // TODO: we could do getBestName() or getFullname() here
2018-07-20 16:04:17 +01:00
: $target ;
2018-07-26 22:12:13 +01:00
$url = $profile -> getUri ();
if ( ! common_valid_http_url ( $url )) {
$url = $profile -> getUrl ();
}
$matches [ $pos ] = array ( 'mentioned' => array ( $profile ),
2018-07-20 16:04:17 +01:00
'type' => 'mention' ,
'text' => $displayName ,
'position' => $pos ,
2018-07-26 22:12:13 +01:00
'length' => mb_strlen ( $target ),
2018-07-20 16:04:17 +01:00
'url' => $url );
2018-07-26 22:12:13 +01:00
}
2018-07-20 16:04:17 +01:00
2018-07-26 22:12:13 +01:00
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 {
2018-07-29 02:35:04 +01:00
$aprofile = Activitypub_profile :: fromUri ( $url );
2018-07-26 22:12:13 +01:00
$profile = $aprofile -> local_profile ();
$displayName = ! empty ( $profile -> nickname ) && mb_strlen ( $profile -> nickname ) < mb_strlen ( $target ) ?
2018-07-20 16:04:17 +01:00
$profile -> nickname : $target ;
2018-07-26 22:12:13 +01:00
$matches [ $pos ] = array ( 'mentioned' => array ( $profile ),
2018-07-20 16:04:17 +01:00
'type' => 'mention' ,
'text' => $displayName ,
'position' => $pos ,
2018-07-26 22:12:13 +01:00
'length' => mb_strlen ( $target ),
2018-07-20 16:04:17 +01:00
'url' => $profile -> getUrl ());
2018-07-26 22:12:13 +01:00
break ;
} catch ( Exception $e ) {
$this -> log ( LOG_ERR , " Profile check failed: " . $e -> getMessage ());
2018-07-20 16:04:17 +01:00
}
2018-07-26 22:12:13 +01:00
}
}
2018-07-20 16:04:17 +01:00
2018-07-26 22:12:13 +01:00
foreach ( $mentions as $i => $other ) {
// If we share a common prefix with a local user, override it!
$pos = $other [ 'position' ];
if ( isset ( $matches [ $pos ])) {
$mentions [ $i ] = $matches [ $pos ];
unset ( $matches [ $pos ]);
}
}
foreach ( $matches as $mention ) {
$mentions [] = $mention ;
2018-07-20 16:04:17 +01:00
}
2018-07-26 22:12:13 +01:00
return true ;
}
/**
* Allow remote profile references to be used in commands :
* sub update @ status . net
* whois evan @ identi . ca
* reply http :// identi . ca / evan hey what ' s up
*
* @ author GNU Social
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param Command $command
* @ param string $arg
* @ param Profile & $profile
* @ return hook return code
*/
public function onStartCommandGetProfile ( $command , $arg , & $profile )
{
try {
$aprofile = $this -> pull_remote_profile ( $arg );
$profile = $aprofile -> local_profile ();
} catch ( Exception $e ) {
// No remote ActivityPub profile found
return true ;
}
2018-07-20 16:04:17 +01:00
2018-07-26 22:12:13 +01:00
return false ;
}
2018-07-27 15:42:30 +01:00
/********************************************************
* Discovery Events *
********************************************************/
2018-07-26 22:12:13 +01:00
/**
* Profile URI for remote profiles .
*
* @ author GNU Social
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param Profile $profile
* @ param string $uri in / out
* @ return mixed hook return code
*/
2018-08-02 18:02:28 +01:00
public function onStartGetProfileUri ( Profile $profile , & $uri )
2018-07-26 22:12:13 +01:00
{
$aprofile = Activitypub_profile :: getKV ( 'profile_id' , $profile -> id );
if ( $aprofile instanceof Activitypub_profile ) {
2018-07-29 02:35:04 +01:00
$uri = $aprofile -> getUri ();
2018-07-26 22:12:13 +01:00
return false ;
}
return true ;
}
/**
* Profile from URI .
*
* @ author GNU Social
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param string $uri
* @ param Profile & $profile in / out param : Profile got from URI
* @ return mixed hook return code
*/
public function onStartGetProfileFromURI ( $uri , & $profile )
{
try {
2018-07-29 02:35:04 +01:00
$explorer = new Activitypub_explorer ();
$profile = $explorer -> lookup ( $uri )[ 0 ];
2018-07-26 22:12:13 +01:00
return false ;
} catch ( Exception $e ) {
return true ; // It's not an ActivityPub profile as far as we know, continue event handling
}
}
/********************************************************
* Delivery Events *
********************************************************/
/**
* Having established a remote subscription , send a notification to the
* remote ActivityPub profile ' s endpoint .
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param Profile $profile subscriber
* @ param Profile $other subscribee
* @ return hook return value
* @ throws Exception
*/
public function onStartSubscribe ( Profile $profile , Profile $other )
{
2018-07-28 02:11:58 +01:00
if ( ! $profile -> isLocal () && $other -> isLocal ()) {
2018-07-26 22:12:13 +01:00
return true ;
2018-07-20 16:04:17 +01:00
}
2018-07-26 22:12:13 +01:00
try {
$other = Activitypub_profile :: from_profile ( $other );
} catch ( Exception $e ) {
return true ;
2018-07-20 16:04:17 +01:00
}
2018-07-26 22:12:13 +01:00
$postman = new Activitypub_postman ( $profile , array ( $other ));
2018-07-13 00:20:18 +01:00
2018-07-26 22:12:13 +01:00
$postman -> follow ();
2018-07-13 00:20:18 +01:00
2018-07-26 22:12:13 +01:00
return true ;
}
2018-07-13 00:20:18 +01:00
2018-07-26 22:12:13 +01:00
/**
* Notify remote server on unsubscribe .
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param Profile $profile
* @ param Profile $other
* @ return hook return value
*/
public function onStartUnsubscribe ( Profile $profile , Profile $other )
{
2018-07-28 02:11:58 +01:00
if ( ! $profile -> isLocal () && $other -> isLocal ()) {
2018-07-26 22:12:13 +01:00
return true ;
}
2018-07-13 00:20:18 +01:00
2018-07-26 22:12:13 +01:00
try {
$other = Activitypub_profile :: from_profile ( $other );
} catch ( Exception $e ) {
return true ;
2018-07-13 00:20:18 +01:00
}
2018-07-26 22:12:13 +01:00
$postman = new Activitypub_postman ( $profile , array ( $other ));
$postman -> undo_follow ();
return true ;
}
/**
* Notify remote users when their notices get favorited .
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param Profile $profile of local user doing the faving
* @ param Notice $notice Notice being favored
* @ return hook return value
*/
public function onEndFavorNotice ( Profile $profile , Notice $notice )
{
// Only distribute local users' favor actions, remote users
// will have already distributed theirs.
if ( ! $profile -> isLocal ()) {
return true ;
}
2018-07-13 00:20:18 +01:00
2018-07-26 22:12:13 +01:00
$other = array ();
try {
$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 );
} catch ( Exception $e ) {
// Local user can be ignored
}
}
if ( $notice -> reply_to ) {
try {
$other [] = Activitypub_profile :: from_profile ( $notice -> getParent () -> getProfile ());
} catch ( Exception $e ) {
// Local user can be ignored
}
try {
$mentions = $notice -> getParent () -> getAttentionProfiles ();
foreach ( $mentions as $to_profile ) {
try {
$other [] = Activitypub_profile :: from_profile ( $to_profile );
} catch ( Exception $e ) {
// Local user can be ignored
}
2018-07-13 00:20:18 +01:00
}
2018-07-26 22:12:13 +01:00
} catch ( NoParentNoticeException $e ) {
// This is not a reply to something (has no parent)
} catch ( NoResultException $e ) {
// Parent author's profile not found! Complain louder?
common_log ( LOG_ERR , " Parent notice's author not found: " . $e -> getMessage ());
}
2018-07-13 00:20:18 +01:00
}
2018-07-14 00:13:28 +01:00
2018-07-26 22:12:13 +01:00
$postman = new Activitypub_postman ( $profile , $other );
$postman -> like ( $notice );
return true ;
}
/**
* Notify remote users when their notices get de - favorited .
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param Profile $profile of local user doing the de - faving
* @ param Notice $notice Notice being favored
* @ return hook return value
*/
public function onEndDisfavorNotice ( Profile $profile , Notice $notice )
{
// Only distribute local users' favor actions, remote users
// will have already distributed theirs.
if ( ! $profile -> isLocal ()) {
return true ;
}
2018-07-14 00:13:28 +01:00
2018-07-26 22:12:13 +01:00
$other = array ();
try {
$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 );
} catch ( Exception $e ) {
// Local user can be ignored
}
}
if ( $notice -> reply_to ) {
try {
$other [] = Activitypub_profile :: from_profile ( $notice -> getParent () -> getProfile ());
} catch ( Exception $e ) {
// Local user can be ignored
}
try {
$mentions = $notice -> getParent () -> getAttentionProfiles ();
foreach ( $mentions as $to_profile ) {
try {
$other [] = Activitypub_profile :: from_profile ( $to_profile );
} catch ( Exception $e ) {
2018-07-15 02:13:46 +01:00
// Local user can be ignored
2018-07-26 22:12:13 +01:00
}
2018-07-15 02:13:46 +01:00
}
2018-07-26 22:12:13 +01:00
} catch ( NoParentNoticeException $e ) {
// This is not a reply to something (has no parent)
} catch ( NoResultException $e ) {
// Parent author's profile not found! Complain louder?
common_log ( LOG_ERR , " Parent notice's author not found: " . $e -> getMessage ());
}
2018-07-14 00:13:28 +01:00
}
2018-07-26 22:12:13 +01:00
$postman = new Activitypub_postman ( $profile , $other );
2018-07-14 00:13:28 +01:00
2018-07-26 22:12:13 +01:00
$postman -> undo_like ( $notice );
2018-07-15 02:13:46 +01:00
2018-07-26 22:12:13 +01:00
return true ;
}
2018-07-14 00:13:28 +01:00
2018-07-26 22:12:13 +01:00
/**
* Notify remote users when their notices get deleted
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ return boolean hook flag
*/
public function onStartDeleteOwnNotice ( $user , $notice )
{
$profile = $user -> getProfile ();
2018-07-14 00:13:28 +01:00
2018-07-26 22:12:13 +01:00
// Only distribute local users' delete actions, remote users
// will have already distributed theirs.
if ( ! $profile -> isLocal ()) {
return true ;
2018-07-14 00:13:28 +01:00
}
2018-07-14 01:18:31 +01:00
2018-07-26 22:12:13 +01:00
$other = array ();
2018-07-15 02:13:46 +01:00
2018-07-26 22:12:13 +01:00
foreach ( $notice -> getAttentionProfiles () as $to_profile ) {
try {
$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 ());
} catch ( Exception $e ) {
// Local user can be ignored
}
try {
$mentions = $notice -> getParent () -> getAttentionProfiles ();
foreach ( $mentions as $to_profile ) {
try {
$other [] = Activitypub_profile :: from_profile ( $to_profile );
} catch ( Exception $e ) {
// Local user can be ignored
}
2018-07-15 02:13:46 +01:00
}
2018-07-26 22:12:13 +01:00
} catch ( NoParentNoticeException $e ) {
// This is not a reply to something (has no parent)
} catch ( NoResultException $e ) {
// Parent author's profile not found! Complain louder?
common_log ( LOG_ERR , " Parent notice's author not found: " . $e -> getMessage ());
}
2018-07-15 02:13:46 +01:00
}
2018-07-26 22:12:13 +01:00
$postman = new Activitypub_postman ( $profile , $other );
$postman -> delete ( $notice );
return true ;
}
/**
* Insert notifications for replies , mentions and repeats
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ return boolean hook flag
*/
public function onStartNoticeDistribute ( $notice )
{
assert ( $notice -> id > 0 ); // Ignore if not a valid notice
$profile = Profile :: getKV ( $notice -> profile_id );
2018-07-28 02:11:58 +01:00
if ( ! $profile -> isLocal ()) {
return true ;
}
2018-07-26 22:12:13 +01:00
$other = array ();
try {
$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 );
} catch ( Exception $e ) {
// Local user can be ignored
}
}
2018-07-15 02:13:46 +01:00
2018-07-26 22:12:13 +01:00
// Is Announce
if ( $notice -> isRepeat ()) {
$repeated_notice = Notice :: getKV ( 'id' , $notice -> repeat_of );
if ( $repeated_notice instanceof Notice ) {
2018-07-15 02:13:46 +01:00
try {
2018-07-26 22:12:13 +01:00
$other [] = Activitypub_profile :: from_profile ( $repeated_notice -> getProfile ());
2018-07-15 02:13:46 +01:00
} catch ( Exception $e ) {
2018-07-26 22:12:13 +01:00
// Local user can be ignored
2018-07-15 02:13:46 +01:00
}
2018-07-26 22:12:13 +01:00
$postman = new Activitypub_postman ( $profile , $other );
2018-07-14 01:18:31 +01:00
2018-07-15 02:13:46 +01:00
// That was it
2018-07-26 22:12:13 +01:00
$postman -> announce ( $repeated_notice );
2018-07-14 01:18:31 +01:00
return true ;
2018-07-26 22:12:13 +01:00
}
2018-07-14 01:18:31 +01:00
}
2018-07-15 03:04:22 +01:00
2018-07-26 22:12:13 +01:00
// Ignore for activity/non-post-verb notices
if ( method_exists ( 'ActivityUtils' , 'compareVerbs' )) {
$is_post_verb = ActivityUtils :: compareVerbs (
$notice -> verb ,
2018-07-31 20:09:47 +01:00
[ ActivityVerb :: POST ]
2018-07-26 22:12:13 +01:00
);
} else {
$is_post_verb = ( $notice -> verb == ActivityVerb :: POST ? true : false );
}
if ( $notice -> source == 'activity' || ! $is_post_verb ) {
return true ;
}
2018-07-15 03:04:22 +01:00
2018-07-26 22:12:13 +01:00
// Create
if ( $notice -> reply_to ) {
try {
$other [] = Activitypub_profile :: from_profile ( $notice -> getParent () -> getProfile ());
} catch ( Exception $e ) {
// Local user can be ignored
}
2018-07-15 03:04:22 +01:00
try {
2018-07-26 22:12:13 +01:00
$mentions = $notice -> getParent () -> getAttentionProfiles ();
foreach ( $mentions as $to_profile ) {
try {
$other [] = Activitypub_profile :: from_profile ( $to_profile );
} catch ( Exception $e ) {
// Local user can be ignored
2018-07-15 03:04:22 +01:00
}
2018-07-26 22:12:13 +01:00
}
} catch ( NoParentNoticeException $e ) {
// This is not a reply to something (has no parent)
} catch ( NoResultException $e ) {
// Parent author's profile not found! Complain louder?
common_log ( LOG_ERR , " Parent notice's author not found: " . $e -> getMessage ());
}
}
$postman = new Activitypub_postman ( $profile , $other );
// That was it
$postman -> create ( $notice );
return true ;
}
/**
* Override the " from ActivityPub " bit in notice lists to link to the
* original post and show the domain it came from .
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param Notice in $notice
* @ param string out & $name
* @ param string out & $url
* @ param string out & $title
* @ return mixed hook return code
*/
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' ))) {
return true ;
}
2018-07-15 03:04:22 +01:00
2018-07-26 22:12:13 +01:00
try {
$url = $notice -> getUrl ();
// If getUrl() throws exception, $url is never set
2018-07-15 03:04:22 +01:00
2018-07-26 22:12:13 +01:00
$bits = parse_url ( $url );
$domain = $bits [ 'host' ];
if ( substr ( $domain , 0 , 4 ) == 'www.' ) {
$name = substr ( $domain , 4 );
} else {
$name = $domain ;
2018-07-15 03:04:22 +01:00
}
2018-07-26 22:12:13 +01:00
// TRANS: Title. %s is a domain name.
$title = sprintf ( _m ( 'Sent from %s via ActivityPub' ), $domain );
// Abort event handler, we have a name and URL!
return false ;
} catch ( InvalidUrlException $e ) {
// This just means we don't have the notice source data
return true ;
2018-07-15 03:04:22 +01:00
}
2018-07-26 22:12:13 +01:00
}
}
/**
* Plugin return handler
*/
class ActivityPubReturn
{
/**
* Return a valid answer
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param array $res
2018-08-01 16:20:14 +01:00
* @ param int 32 $code Status Code
2018-07-26 22:12:13 +01:00
* @ return void
*/
2018-08-01 16:20:14 +01:00
public static function answer ( $res = '' , $code = 202 )
2018-07-26 22:12:13 +01:00
{
2018-07-31 20:55:24 +01:00
http_response_code ( $code );
2018-07-26 22:12:13 +01:00
header ( 'Content-Type: application/activity+json' );
echo json_encode ( $res , JSON_UNESCAPED_SLASHES | ( isset ( $_GET [ " pretty " ]) ? JSON_PRETTY_PRINT : null ));
exit ;
}
/**
* Return an error
*
* @ author Diogo Cordeiro < diogo @ fc . up . pt >
* @ param string $m
2018-08-01 16:20:14 +01:00
* @ param int32 $code Status Code
2018-07-26 22:12:13 +01:00
* @ return void
*/
public static function error ( $m , $code = 500 )
{
http_response_code ( $code );
header ( 'Content-Type: application/activity+json' );
$res [] = Activitypub_error :: error_message_to_array ( $m );
echo json_encode ( $res , JSON_UNESCAPED_SLASHES );
exit ;
}
2018-07-31 20:09:47 +01:00
/**
* Select content type from HTTP Accept header
*
* @ author Maciej Łebkowski < m . lebkowski @ gmail . com >
* @ param array $mimeTypes Supported Types
* @ return array | null of supported mime types sorted | null if none valid
*/
2018-08-02 18:02:28 +01:00
public static function getBestSupportedMimeType ( $mimeTypes )
2018-07-31 20:09:47 +01:00
{
2018-08-02 18:02:28 +01:00
// XXX: This function needs improvement!
2018-07-31 23:02:32 +01:00
if ( ! isset ( $_SERVER [ 'HTTP_ACCEPT' ])) {
return null ;
}
2018-08-02 18:02:28 +01:00
// This mime type was messing everything, thus the special case
if ( $_SERVER [ 'HTTP_ACCEPT' ] == 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ) {
return true ;
}
2018-07-31 20:09:47 +01:00
// Values will be stored in this array
2018-07-31 23:02:32 +01:00
$AcceptTypes = [];
2018-07-31 20:09:47 +01:00
// Accept header is case insensitive, and whitespace isn’ t important
$accept = strtolower ( str_replace ( ' ' , '' , $_SERVER [ 'HTTP_ACCEPT' ]));
// divide it into parts in the place of a ","
$accept = explode ( ',' , $accept );
foreach ( $accept as $a ) {
// the default quality is 1.
$q = 1 ;
// check if there is a different quality
if ( strpos ( $a , ';q=' )) {
2018-08-02 18:02:28 +01:00
// divide "mime/type;q=X" into two parts: "mime/type" and "X"
2018-07-31 20:09:47 +01:00
list ( $a , $q ) = explode ( ';q=' , $a );
}
// mime-type $a is accepted with the quality $q
// WARNING: $q == 0 means, that mime-type isn’ t supported!
$AcceptTypes [ $a ] = $q ;
}
arsort ( $AcceptTypes );
2018-08-02 18:02:28 +01:00
$mimeTypes = array_map ( 'strtolower' , $mimeTypes );
2018-07-31 20:09:47 +01:00
// let’ s check our supported types:
foreach ( $AcceptTypes as $mime => $q ) {
if ( $q && in_array ( $mime , $mimeTypes )) {
return $mime ;
}
}
2018-08-02 18:02:28 +01:00
2018-07-31 20:09:47 +01:00
// no mime-type found
return null ;
}
2018-03-25 21:16:43 +01:00
}
2018-05-06 00:25:02 +01:00
2018-07-10 01:47:52 +01:00
/**
* Overwrites variables in URL - mapping
*/
class ActivityPubURLMapperOverwrite extends URLMapper
{
2018-07-31 20:09:47 +01:00
public static function variable ( $m , $path , $paramPatterns , $newaction )
2018-07-26 22:12:13 +01:00
{
$mimes = [
2018-07-31 20:09:47 +01:00
'application/json' ,
'application/activity+json' ,
'application/ld+json' ,
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
];
2018-07-10 01:47:52 +01:00
2018-07-31 20:09:47 +01:00
if ( is_null ( ActivityPubReturn :: getBestSupportedMimeType ( $mimes ))) {
2018-07-26 22:12:13 +01:00
return true ;
2018-07-10 01:47:52 +01:00
}
2018-07-26 22:12:13 +01:00
$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 ;
}
2018-07-09 23:43:34 +01:00
}
2018-07-26 22:12:13 +01:00
}
2018-07-06 11:37:13 +01:00
}