Commit 4bb1b32f authored by Zot's avatar Zot

initial checkin of pubcrawl so that if somebody else needs to finish it they...

initial checkin of pubcrawl so that if somebody else needs to finish it they won't need to start from scratch.
parent 8883bd12
<?php
class ActivityStreams {
public $data;
public $valid = false;
public $id = '';
public $type = '';
public $actor = null;
public $obj = null;
public $tgt = null;
function __construct($string) {
$this->data = json_decode($string,true);
if($this->data) {
$this->valid = true;
}
if($this->is_valid()) {
$this->id = $this->get_property_obj('id');
$this->type = $this->get_primary_type();
$this->actor = $this->get_compound_property('actor');
$this->obj = $this->get_compound_property('object');
$this->tgt = $this->get_compound_property('target');
}
}
function is_valid() {
return $this->valid;
}
function get_property_obj($property,$base = '') {
if(! $base) {
$base = $this->data;
}
return $base[$property];
}
function fetch_property($url) {
$redirects = 0;
$x = z_fetch_url($url,true,$redirects,
['headers' => [ 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"']]);
if($x['success'])
return json_decode($x['body'],true);
return null;
}
function get_compound_property($property,$base = '') {
$x = $this->get_property_obj($property,$base);
if($this->is_url($x)) {
$x = $this->fetch_property($x);
}
return $x;
}
function is_url($url) {
if(($url) && (! is_array($url)) && (strpos($url,'http') === 0)) {
return true;
}
return false;
}
function get_primary_type($base = '') {
if(! $base)
$base = $this->data;
$x = $this->get_property_obj('type',$base);
if(is_array($x)) {
foreach($x as $y) {
if(strpos($y,':') === false) {
return $y;
}
}
}
return $x;
}
function debug() {
$x = var_export($this,true);
return $x;
}
}
\ No newline at end of file
<?php
namespace Zotlabs\Module;
class Activity extends \Zotlabs\Web\Controller {
function init() {
if(pubcrawl_is_as_request()) {
$item_id = argv(1);
if(! $item_id)
return;
$item_normal = " and item.item_hidden = 0 and item.item_type = 0 and item.item_unpublished = 0
and item.item_delayed = 0 and item.item_blocked = 0 ";
$sql_extra = item_permissions_sql(0);
$r = q("select * from item where mid like '%s' $item_normal $sql_extra limit 1",
dbesc($item_id . '%')
);
if(! $r) {
$r = q("select * from item where mid like '%s' $item_normal limit 1",
dbesc($item_id . '%')
);
if($r) {
http_status_exit(403, 'Forbidden');
}
http_status_exit(404, 'Not found');
}
xchan_query($r,true);
$items = fetch_post_tags($r,true);
$x = array_merge(['@context' => [
'https://www.w3.org/ns/activitystreams',
[ 'me' => 'http://salmon-protocol.org/ns/magic-env' ],
[ 'zot' => 'http://purl.org/zot/protocol' ]
]], asencode_activity($items[0]));
header('Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"');
json_return_and_die($x);
}
}
}
\ No newline at end of file
<?php
namespace Zotlabs\Module;
class Inbox extends \Zotlabs\Web\Controller {
function post() {
if(argc() <= 1)
return;
$is_public = false;
if(argv(1) === '[public]') {
$channel = get_sys_channel();
$is_public = true;
}
else {
$channel = channelx_by_nick(argv(1));
}
if(! $channel)
return;
$data = file_get_contents('php:input'));
if(! $data)
return;
$AS = new ActivityStreams($data);
if(! $AS->is_valid())
return;
$saved_recips = [];
foreach( [ 'to', 'cc', 'audience' ] as $x ) {
if(array_key_exists($x,$AS->data) {
$saved_recips[$x] = $AS->data[$x];
}
}
switch($AS->type) {
case 'Follow':
if($AP->obj & $AP->obj['type'] === 'Person') {
// do follow activity
}
break;
default:
break;
}
// Look up actor
// These activities require permissions
switch($AS->type) {
case 'Create':
case 'Update':
case 'Delete':
case 'Follow':
case 'Add':
case 'Remove':
case 'Like':
case 'Announce':
case 'Undo':
break;
default:
break;
}
}
function get() {
}
}
<?php
namespace Zotlabs\Module;
class Nullbox extends \Zotlabs\Web\Controller {
function init() {
http_status_exit(404,'Permission Denied');
}
}
<?php
namespace Zotlabs\Module;
class Outbox extends \Zotlabs\Web\Controller {
function post() {
}
function get() {
}
}
<?php
/**
* @brief
*
* @param array $items
* @return array
*/
function gen_asld($items) {
$ret = array();
if(! $items)
return $ret;
foreach($items as $item) {
$ret[] = i2asld($item);
}
return $ret;
}
/**
* @brief
*
* @param array $i
* @return array
*/
function i2asld($i) {
if(! $i)
return array();
$ret = array();
$ret['@context'] = array( 'https://www.w3.org/ns/activitystreams', 'zot' => 'http://purl.org/zot/protocol');
if($i['verb']) {
if(strpos(dirname($i['verb'],'activitystrea.ms/schema/1.0'))) {
$ret['type'] = ucfirst(basename($i['verb']));
}
elseif(strpos(dirname($i['verb'],'purl.org/zot'))) {
$ret['type'] = 'zot:' . ucfirst(basename($i['verb']));
}
}
$ret['id'] = $i['plink'];
$ret['published'] = datetime_convert('UTC','UTC',$i['created'],ATOM_TIME);
if($i['obj_type'] === ACTIVITY_OBJ_NOTE)
$ret['object'] = asencode_item($i);
$ret['actor'] = asencode_person($i['author']);
return $ret;
}
function asencode_object($x) {
if((substr(0,1,trim($x))) === '{' ) {
$x = json_decode($x,true);
}
if($x['type'] === ACTIVITY_OBJ_PERSON) {
return asfetch_person($x);
}
if($x['type'] === ACTIVITY_OBJ_PERSON) {
return asfetch_profile($x);
}
}
function asfetch_person($x) {
return $x;
}
function asfetch_profile($x) {
return $x;
}
function asencode_item($i) {
$ret = array();
if(intval($i['item_deleted'])) {
$ret['type'] = 'Tombstone';
$ret['formerType'] = 'Note';
$ret['id'] = z_root() . '/item/' . urlencode($i['mid']);
return $ret;
}
$ret['type'] = 'Note';
$ret['id'] = z_root() . '/item/' . urlencode($i['mid']);
if($i['title'])
$ret['title'] = bbcode($i['title']);
$ret['published'] = datetime_convert('UTC','UTC',$i['created'],ATOM_TIME);
if($i['created'] !== $i['edited'])
$ret['updated'] = datetime_convert('UTC','UTC',$i['edited'],ATOM_TIME);
if($i['app']) {
$ret['instrument'] = [ 'type' => 'Service', 'name' => $i['app'] ];
}
if($i['location'] || $i['coord']) {
$ret['location'] = [ 'type' => 'Place' ];
if($i['location']) {
$ret['location']['name'] = $i['location'];
}
if($i['coord']) {
$l = explode(' ',$i['coord']);
$ret['location']['latitude'] = $l[0];
$ret['location']['longitude'] = $l[1];
}
}
if($i['id'] != $i['parent']) {
$ret['inReplyTo'] = z_root() . '/item/' . urlencode($i['parent_mid']);
}
$ret['content'] = bbcode($i['body']);
$ret['zot:owner'] = asencode_person($i['owner']);
$ret['actor'] = asencode_person($i['author']);
$ret['tag'] = [];
$ret['tag'][] = [
'type' => 'zot:messageId',
'id' => z_root() . '/display/' . urlencode($i['mid']),
'name' => $i['mid']
];
return $ret;
}
function asencode_activity($i) {
$ret = array();
if(intval($i['item_deleted'])) {
$ret['type'] = 'Tombstone';
$ret['formerType'] = activity_obj_mapper($i['obj_type']);
$ret['id'] = z_root() . '/item/' . urlencode($i['mid']);
return $ret;
}
$ret['type'] = activity_mapper($i['verb']);
$ret['id'] = z_root() . '/activity/' . urlencode($i['mid']);
if($i['title'])
$ret['title'] = bbcode($i['title']);
$ret['published'] = datetime_convert('UTC','UTC',$i['created'],ATOM_TIME);
if($i['created'] !== $i['edited'])
$ret['updated'] = datetime_convert('UTC','UTC',$i['edited'],ATOM_TIME);
if($i['app']) {
$ret['instrument'] = [ 'type' => 'Service', 'name' => $i['app'] ];
}
if($i['location'] || $i['coord']) {
$ret['location'] = [ 'type' => 'Place' ];
if($i['location']) {
$ret['location']['name'] = $i['location'];
}
if($i['coord']) {
$l = explode(' ',$i['coord']);
$ret['location']['latitude'] = $l[0];
$ret['location']['longitude'] = $l[1];
}
}
if($i['id'] != $i['parent']) {
$ret['inReplyTo'] = z_root() . '/item/' . urlencode($i['parent_mid']);
}
$ret['content'] = bbcode($i['body']);
$ret['zot:owner'] = asencode_person($i['owner']);
$ret['actor'] = asencode_person($i['author']);
if($i['obj']) {
$ret['object'] = asencode_object($i['obj']);
}
else {
$ret['object'] = asencode_item($i);
}
if($i['target']) {
$ret['target'] = asencode_object($i['target']);
}
$ret['tag'] = [];
$ret['tag'][] = [
'type' => 'zot:messageId',
'id' => z_root() . '/display/' . urlencode($i['mid']),
'name' => $i['mid']
];
if(! $i['item_private']) {
$ret['to'] = [ ACTIVITY_PUBLIC_INBOX ];
}
else {
$ret['bto'] = as_map_acl($i);
}
return $ret;
}
function as_map_acl($i) {
$private = false;
$list = [];
$x = collect_recipients($i,$private);
if($x) {
stringify_array_elms($x);
if(! $x)
return;
$strict = get_config('activitypub','compliance');
$sql_extra = (($strict) ? " and xchan_network = 'activitypub' " : '');
$details = q("select xchan_url from xchan where xchan_hash in (" . implode(',',$recipients) . ") $sql_extra");
if($details) {
foreach($details as $d) {
$list[] = $d['xchan_url'];
}
}
}
return $list;
}
function asencode_person($p) {
$ret = [];
$ret['type'] = 'Person';
$ret['id'] = $p['xchan_url'];
$ret['name'] = $p['xchan_name'];
$ret['icon'] = [
[
'type' => 'Image',
'mediaType' => $p['xchan_photo_mimetype'],
'url' => $p['xchan_photo_l'],
'height' => 300,
'width' => 300,
],
[
'type' => 'Image',
'mediaType' => $p['xchan_photo_mimetype'],
'url' => $p['xchan_photo_m'],
'height' => 80,
'width' => 80,
],
[
'type' => 'Image',
'mediaType' => $p['xchan_photo_mimetype'],
'url' => $p['xchan_photo_s'],
'height' => 48,
'width' => 48,
]
];
$ret['url'] = [
'type' => 'Link',
'mediaType' => 'text/html',
'href' => $p['xchan_url']
];
$ret['me:magic_keys'] = [
[
'value' => salmon_key($p['xchan_pubkey']),
'key_id' => base64url_encode(hash('sha256',salmon_key($p['xchan_pubkey'])),true)
]
];
$c = channelx_by_hash($p['xchan_hash']);
if($c) {
$ret['inbox'] = z_root() . '/inbox/' . $c['channel_address'];
$ret['outbox'] = z_root() . '/outbox/' . $c['channel_address'];
$ret['publicInbox'] = z_root() . '/inbox/[public]';
}
else {
$collections = get_xconfig($p['xchan_hash'],'activitystreams','collections',[]);
if($collections) {
$ret = array_merge($ret,$collections);
}
else {
$ret['inbox'] = z_root() . '/nullbox';
$ret['outbox'] = z_root() . '/nullbox';
}
}
return $ret;
}
function activity_mapper($verb) {
$acts = [
'http://activitystrea.ms/schema/1.0/post' => 'Create',
'http://activitystrea.ms/schema/1.0/update' => 'Update',
'http://activitystrea.ms/schema/1.0/like' => 'Like',
'http://activitystrea.ms/schema/1.0/favorite' => 'Like',
'http://purl.org/zot/activity/dislike' => 'Dislike',
'http://activitystrea.ms/schema/1.0/tag' => 'Add',
'http://activitystrea.ms/schema/1.0/follow' => 'Follow',
'http://activitystrea.ms/schema/1.0/unfollow' => 'Unfollow',
];
if(array_key_exists($verb,$acts)) {
return $acts[$verb];
}
return false;
}
function activity_obj_mapper($obj) {
$objs = [
'http://activitystrea.ms/schema/1.0/note' => 'Note',
'http://activitystrea.ms/schema/1.0/comment' => 'Note',
'http://activitystrea.ms/schema/1.0/person' => 'Person',
'http://purl.org/zot/activity/profile' => 'Profile',
'http://activitystrea.ms/schema/1.0/photo' => 'Image',
'http://activitystrea.ms/schema/1.0/profile-photo' => 'Icon',
'http://activitystrea.ms/schema/1.0/event' => 'Event',
'http://activitystrea.ms/schema/1.0/wiki' => 'Document',
'http://purl.org/zot/activity/location' => 'Place',
'http://purl.org/zot/activity/chessgame' => 'Game',
'http://purl.org/zot/activity/tagterm' => 'zot:Tag',
'http://purl.org/zot/activity/thing' => 'zot:Thing',
'http://purl.org/zot/activity/file' => 'zot:File',
'http://purl.org/zot/activity/poke' => 'zot:Action',
'http://purl.org/zot/activity/react' => 'zot:Reaction',
'http://purl.org/zot/activity/mood' => 'zot:Mood',
];
if(array_key_exists($obj,$objs)) {
return $objs[$obj];
}
return false;
}
<?php
/**
* Name: PubCrawl (WIP; developers only - not yet functional)
* Description: An unapologetically non-compliant ActivityPub Protocol implemention
*
*/
require_once('addon/pubcrawl/as.php');
require_once('addon/pubcrawl/ActivityStreams.php');
function pubcrawl_load() {
Zotlabs\Extend\Hook::register_array('addon/pubcrawl/pubcrawl.php', [
'module_loaded' => 'pubcrawl_load_module',
'channel_mod_init' => 'pubcrawl_channel_mod_init',
'profile_mod_init' => 'pubcrawl_profile_mod_init',
'item_mod_init' => 'pubcrawl_item_mod_init'
]);
}
function pubcrawl_unload() {
Zotlabs\Extend\Hook::unregister_by_file('addon/pubcrawl/pubcrawl.php');
}
function pubcrawl_load_module(&$b) {
if($b['module'] === 'inbox') {
require_once('addon/pubcrawl/Mod_Inbox.php');
$b['controller'] = new \Zotlabs\Module\Inbox();
$b['installed'] = true;
}
if($b['module'] === 'outbox') {
require_once('addon/pubcrawl/Mod_Outbox.php');
$b['controller'] = new \Zotlabs\Module\Outbox();
$b['installed'] = true;
}
if($b['module'] === 'activity') {
require_once('addon/pubcrawl/Mod_Activity.php');
$b['controller'] = new \Zotlabs\Module\Activity();
$b['installed'] = true;
}
if($b['module'] === 'nullbox') {
require_once('addon/pubcrawl/Mod_Nullbox.php');
$b['controller'] = new \Zotlabs\Module\Nullbox();
$b['installed'] = true;
}
}
function pubcrawl_is_as_request() {
if($_REQUEST['module_format'] === 'json')
return true;
$x = getBestSupportedMimeType([
'application/ld+json;profile="https://www.w3.org/ns/activitystreams"',
'application/ld+json;profile="http://www.w3.org/ns/activitystreams"',
'application/activity+json'
]);
return(($x) ? true : false);
}
function pubcrawl_magic_env_allowed() {
$x = getBestSupportedMimeType([
'application/magic-envelope+json'
]);
return(($x) ? true : false);
}
function pubcrawl_salmon_sign($data,$channel) {
$data = base64url_encode($data, false); // do not strip padding
$data_type = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
$encoding = 'base64url';
$algorithm = 'RSA-SHA256';
$keyhash = base64url_encode(hash('sha256',salmon_key($channel['channel_pubkey'])),true);
$data = str_replace(array(" ","\t","\r","\n"),array("","","",""),$data);
// precomputed base64url encoding of data_type, encoding, algorithm concatenated with periods
$precomputed = '.' . base64url_encode($data_type,false) . '.YmFzZTY0dXJs.UlNBLVNIQTI1Ng==';
$signature = base64url_encode(rsa_sign($data . $precomputed,$channel['channel_prvkey']));
return ([
'data' => $data,
'data_type' => $data_type,
'encoding' => $encoding,
'alg' => $algorithm,
'sigs' => [
'value' => $signature,
'key_id' => $keyhash
]
]);
}
function pubcrawl_channel_mod_init($x) {
if(pubcrawl_is_as_request()) {
$chan = channelx_by_nick(argv(1));
if(! $chan)
return;
$x = array_merge(['@context' => [
'https://www.w3.org/ns/activitystreams',
[ 'me' => 'http://salmon-protocol.org/ns/magic-env' ]
]], asencode_person($chan));
if(pubcrawl_magic_env_allowed()) {
$x = pubcrawl_salmon_sign(json_encode($x),$chan);
header('Content-Type: application/magic-envelope+json');
json_return_and_die($x);
}
else {
header('Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"');
json_return_and_die($x);
}
}
}
function pubcrawl_profile_mod_init($x) {
if(pubcrawl_is_as_request()) {
$chan = channelx_by_nick(argv(1));
if(! $chan)
return;
$x = [
'@context' => [ 'https://www.w3.org/ns/activitystreams',
[ 'me' => 'http://salmon-protocol.org/ns/magic-env' ]
],
'type' => 'Profile',
'describes' => asencode_person($chan)