diff --git a/README.md b/README.md index 3c349bc32..f2fbba8d8 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,23 @@ -**ZAP** +**OSADA** -Zap is a social networking app running under the Zot/6 protocol and the LAMP web stack. +Osada is a social networking app and gateway which allows communication between the Zot/6 protocol/network to the ActivityPub protocol/network using the LAMP web stack. -Protocol documentation is located here: +Zot/6 protocol documentation is located here: https://macgirvin.com/wiki/mike/Zot%2BVI/Home -Zap is based on Red, which in turn is based on Hubzilla. It is otherwise unrelated to those projects and the software has a completely different scope and purpose. + +ActivityPub and many other protocols do not work well with Zot or Zot/6 because they do not understand the concept of nomadic identity. Osada provides a gateway between these services to smooth out the differences. It is ultimately designed to bridge ActivityPub with Zap, which is a pure Zot/6 social network application. + +Osada is based on Zap which is based on Red, which in turn is based on Hubzilla. It is otherwise unrelated to those projects and the software has a completely different scope and purpose. -01-August-2018 +20-August-2018 ============== Most of the basic functionality is now present and working. This is still "use at your own risk", but it shouldn't burn down the house. -19-July-2018 -============ - -There is a lot more work yet to be done, but the basic Zap application is nearing alpha quality. - -TODO before alpha release: - -* convert mail to ActivityStreams -* test nomadic channels -* correct any links in the documentation and remove the descriptions of Hubzilla features which do not exist in Zap - - - - - - -**Things you should know** - -Zap is nomadic and does not federate with any other platform or protocol currently. It will only **ever** federate with nomadic-aware services/protocols. Full stop. Full federation support will eventually be provided by creating bridging identities in a companion project Osada; which provides a bridge between nomadic and non-nomadic networks. Osada identities cannot be nomadic since they federate with non-nomadic services, but they can be linked to Zap nomadic identities using Zot6 identity linking. - -If you are looking for a specific Hubzilla feature, you came to the wrong place. - -If you are looking for ActivityPub support, you came to the wrong place. - -If you are looking for stable software, check back in a few months. - If you encounter issues, fix them and submit a pull request. Pull requests which add unnecessary features will be ignored. These should be implemented using apps and/or addons. diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index 6ddbbb9db..3a188974c 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -149,7 +149,7 @@ class Activity { - static function encode_item($i) { + static function encode_item($i, $activitypub = false) { $ret = []; @@ -164,14 +164,43 @@ class Activity { $ret['type'] = $objtype; + + /** + * If the destination is activitypub, see if the content needs conversion to + * Mastodon "quirks" mode. This will be the case if there is any markup beyond + * links or images OR if the number of images exceeds 1. This content may be + * purified into oblivion when using the Note type so turn it into an Article. + */ + + $convert_to_article = false; + $images = false; + + if($activitypub && $ret['type'] === 'Note') { + $bbtags = false; + $num_bbtags = preg_match_all('/\[([a-z]?)(.*?)/ism',$i['body'],$bbtags,PREG_SET_ORDER); + if($num_bbtags) { + foreach($bbtags as $t) { + if(in_array($t[1],['url','zrl','img','zmg'])) { + continue; + } + $convert_to_article = true; + } + } + $has_images = preg_match_all('/\[[zi]mg(.*?)\](.*?)\[/ism',$i['body'],$images,PREG_SET_ORDER); + if($has_images > 1) { + $convert_to_article = true; + } + if($convert_to_article) { + $ret['type'] = 'Article'; + } + } + $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : 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']) + 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'] ]; } @@ -195,11 +224,11 @@ class Activity { if($i['mimetype'] === 'text/bbcode') { if($i['title']) - $ret['name'] = bbcode($i['title']); + $ret['name'] = $i['title']; if($i['summary']) $ret['summary'] = bbcode($i['summary']); $ret['content'] = bbcode($i['body']); - $ret['source'] = [ 'content' => $i['body'], 'mediaType' => 'text/bbcode' ]; + $ret['source'] = [ 'content' => $i['body'], 'summary' => $i['summary'], 'mediaType' => 'text/bbcode' ]; } $actor = self::encode_person($i['author'],false); @@ -218,6 +247,17 @@ class Activity { $ret['attachment'] = $a; } + if($activitypub && $has_images && $ret['type'] === 'Note') { + $img = []; + foreach($images as $match) { + $img[] = [ 'type' => 'Image', 'url' => $match[2] ]; + } + if(! $ret['attachment']) + $ret['attachment'] = []; + + $ret['attachment'] = array_merge($img,$ret['attachment']); + } + return $ret; } @@ -333,7 +373,7 @@ class Activity { - static function encode_activity($i) { + static function encode_activity($i,$activitypub = false) { $ret = []; $reply = false; @@ -348,11 +388,13 @@ class Activity { $ret['type'] = self::activity_mapper($i['verb']); $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/activity/' . urlencode($i['mid'])); - if($i['title']) - $ret['name'] = html2plain(bbcode($i['title'])); + if($i['title']) { + $ret['name'] = $i['title']; + } - if($i['summary']) + if($i['summary']) { $ret['summary'] = bbcode($i['summary']); + } if($ret['type'] === 'Announce') { $tmp = preg_replace('/\[share(.*?)\[\/share\]/ism',EMPTY_STR, $i['body']); @@ -442,6 +484,62 @@ class Activity { return []; } + if($activitypub) { + if($i['item_private']) { + if($reply) { + if($i['author_xchan'] === $i['owner_xchan']) { + $m = self::map_acl($i,(($i['allow_gid']) ? false : true)); + $ret['tag'] = (($ret['tag']) ? array_merge($ret['tag'],$m) : $m); + } + else { + if($is_directmessage) { + $m = [ + 'type' => 'Mention', + 'href' => $reply_url, + 'name' => '@' . $reply_addr + ]; + $ret['tag'] = (($ret['tag']) ? array_merge($ret['tag'],$m) : $m); + } + else { + $ret['to'] = [ $reply_url ]; + } + } + } + else { + /* Add mentions only if the targets are individuals */ + $m = self::map_acl($i,(($i['allow_gid']) ? false : true)); + $ret['tag'] = (($ret['tag']) ? array_merge($ret['tag'],$m) : $m); + } + } + else { + if($reply) { + $ret['to'] = [ z_root() . '/followers/' . substr($i['author']['xchan_addr'],0,strpos($i['author']['xchan_addr'],'@')) ]; + $ret['cc'] = [ ACTIVITY_PUBLIC_INBOX ]; + } + else { + $ret['to'] = [ ACTIVITY_PUBLIC_INBOX ]; + $ret['cc'] = [ z_root() . '/followers/' . substr($i['author']['xchan_addr'],0,strpos($i['author']['xchan_addr'],'@')) ]; + } + } + $mentions = self::map_mentions($i); + if(count($mentions) > 0) { + if(! $ret['cc']) { + $ret['cc'] = $mentions; + } + else { + $ret['cc'] = array_merge($ret['cc'], $mentions); + } + } + + if($ret['to']) + $ret['object']['to'] = $ret['to']; + if($ret['cc']) + $ret['object']['cc'] = $ret['cc']; + if($ret['tag']) + $ret['object']['tag'] = $ret['tag']; + + } + return $ret; } @@ -466,6 +564,7 @@ class Activity { $private = false; $list = []; $x = collect_recipients($i,$private); + if($x) { stringify_array_elms($x); if(! $x) @@ -494,7 +593,7 @@ class Activity { } - static function encode_person($p, $extended = true) { + static function encode_person($p, $extended = true, $activitypub = false) { if(! $p['xchan_url']) return []; @@ -531,7 +630,33 @@ class Activity { ] ]; - $arr = [ 'xchan' => $p, 'encoded' => $ret ]; + $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['followers'] = z_root() . '/followers/' . $c['channel_address']; + $ret['following'] = z_root() . '/following/' . $c['channel_address']; + $ret['endpoints'] = [ 'sharedInbox' => z_root() . '/inbox' ]; + + $ret['publicKey'] = [ + 'id' => $p['xchan_url'] . '/public_key_pem', + 'owner' => $p['xchan_url'], + 'publicKeyPem' => $p['xchan_pubkey'] + ]; + } + else { + $collections = get_xconfig($p['xchan_hash'],'activitystreams','collections',[]); + if($collections) { + $ret = array_merge($ret,$collections); + } + else { + $ret['inbox'] = null; + $ret['outbox'] = null; + } + } + + $arr = [ 'xchan' => $p, 'encoded' => $ret, 'activitypub' => $activitypub ]; call_hooks('encode_person', $arr); $ret = $arr['encoded']; @@ -1372,6 +1497,98 @@ class Activity { } + static function store($channel,$observer_hash,$act,$item) { + + + $is_sys_channel = is_sys_channel($channel['channel_id']); + + // Mastodon only allows visibility in public timelines if the public inbox is listed in the 'to' field. + // They are hidden in the public timeline if the public inbox is listed in the 'cc' field. + // This is not part of the activitypub protocol - we might change this to show all public posts in pubstream at some point. + + $pubstream = ((is_array($act->obj) && array_key_exists('to', $act->obj) && in_array(ACTIVITY_PUBLIC_INBOX, $act->obj['to'])) ? true : false); + + if(! perm_is_allowed($channel['channel_id'],$observer_hash,'send_stream') && ! ($is_sys_channel && $pubstream)) { + logger('no permission'); + return; + } + + $content = self::get_content($act->obj); + + if(! $content) { + logger('no content'); + return; + } + + $item['aid'] = $channel['channel_account_id']; + $item['uid'] = $channel['channel_id']; + + if($channel['channel_system']) { + if(! \Zotlabs\Lib\MessageFilter::evaluate($item,get_config('system','pubstream_incl'),get_config('system','pubstream_excl'))) { + logger('post is filtered'); + return; + } + } + + $abook = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1", + dbesc($observer_hash), + intval($channel['channel_id']) + ); + + if($abook) { + if(! post_is_importable($item,$abook[0])) { + logger('post is filtered'); + return; + } + } + + if($act->obj['conversation']) { + set_iconfig($item,'ostatus','conversation',$act->obj['conversation'],1); + } + + set_iconfig($item,'activitypub','recips',$act->raw_recips); + + $r = q("select created, edited from item where mid = '%s' and uid = %d limit 1", + dbesc($item['mid']), + intval($item['uid']) + ); + if($r) { + if($item['edited'] > $r[0]['edited']) { + $x = item_store_update($item); + } + else { + return; + } + } + else { + $x = item_store($item); + } + + + if(is_array($x) && $x['item_id']) { + if($parent) { + if($item['owner_xchan'] === $channel['channel_hash']) { + // We are the owner of this conversation, so send all received comments back downstream + Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$x['item_id'])); + } + $r = q("select * from item where id = %d limit 1", + intval($x['item_id']) + ); + if($r) { + send_status_notifications($x['item_id'],$r[0]); + } + } + sync_an_item($channel['channel_id'],$x['item_id']); + } + + } + + + + + + + static function announce_note($channel,$observer_hash,$act) {