diff --git a/.homeinstall/zotserver-setup.sh b/.homeinstall/zotserver-setup.sh index 797ac354e..0321c14de 100755 --- a/.homeinstall/zotserver-setup.sh +++ b/.homeinstall/zotserver-setup.sh @@ -9,6 +9,7 @@ # - misty : https://zotlabs.com/misty/ # - osada : https://codeberg.org/zot/osada # - redmatrix : https://codeberg.org/zot/redmatrix +# - roadhouse : https://codeberg.org/zot/roadhouse # under Debian Linux "Buster" # # 1) Copy the file "zotserver-config.txt.template" to "zotserver-config.txt" @@ -532,8 +533,11 @@ function zotserver_name { elif git remote -v | grep -i "origin.*redmatrix.*" then zotserver=redmatrix + elif git remote -v | grep -i "origin.*roadhouse.*" + then + zotserver=roadhouse else - die "neither redmatrix, osada, misty, zap nor hubzilla repository > did not install redmatrix/osada/misty/zap/hubzilla" + die "neither roadhouse, redmatrix, osada, misty, zap nor hubzilla repository > did not install roadhouse/redmatrix/osada/misty/zap/hubzilla" fi } @@ -560,8 +564,12 @@ function install_zotserver { then print_info "redmatrix" util/add_addon_repo https://codeberg.org/zot/redmatrix-addons.git raddons + elif [ $zotserver = "roadhouse" ] + then + print_info "roadhouse" + util/add_addon_repo https://codeberg.org/zot/roadhouse-addons.git rhaddons else - die "neither redmatrix, osada, misty, zap nor hubzilla repository > did not install addons or redmatrix/osada/misty/zap/hubzilla" + die "neither roadhouse, redmatrix, osada, misty, zap nor hubzilla repository > did not install addons or roadhouse/redmatrix/osada/misty/zap/hubzilla" fi mkdir -p "cache/smarty3" mkdir -p "store" diff --git a/FEDERATION.md b/FEDERATION.md index 88690b4cb..d7a296daa 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -61,11 +61,11 @@ The Zot permission system has years of historical use and is different than and Delivery model -This project uses the relay system pioneered by projects such as Friendica, Diaspora, and Hubzilla which attempts to keep entire conversations intact and keeps the conversation initiator in control of the privacy distribution. This is not guaranteed under ActivityPub where conversation members can cc: others who were not in the initial privacy group. We encourage projects to not allow additional recipients or not include their own followers in followups. Followups SHOULD have one recipient - the conversation owner or originator, and are relayed by the owner to the other conversation members. This normally requires the use of LD-Signatures but may also be accessible through authenticated fetch of the activity using HTTP signatures. +This project uses the relay system pioneered by projects such as Friendica, Diaspora, and Hubzilla which attempts to keep entire conversations intact and keeps the conversation initiator in control of the privacy distribution. This is not guaranteed under ActivityPub where conversation members can cc: others who were not in the initial privacy group. We encourage projects to not allow additional recipients or not include their own followers in followups. Followups SHOULD have one recipient - the conversation owner or originator, and are relayed by the owner to the other conversation members. This normally requires the use of LD-Signatures but may also be accessible through authenticated fetch of the activity using HTTP signatures. Content -Content may be rich multi-media and renders nicely as HTML. Bbcode is used internally due to its ease of purification while still providing rich multi-media support. Content is not obviously length-limited and authors MAY use up to the storage maximum of 24MB. In practice bbcode conversion limits the effective length to around 200KB and the default "maximum length of imported content" from other sites is 200KB. This can be changed on a per-site basis but this is rare. A Note may contain one or more images or links. The images are also added as an attachment for the benefit of Mastodon, but remain in the HTML source. When importing content from other sites, if the content contains an image attachment, the content is scanned to see if a link (a) or (img) tag containing that image is already present in the HTML. If not, an image tag is added inline to the end of the incoming content. Multiple images are supported using this mechanism. +Content may be rich multi-media and renders nicely as HTML. Multicode is used and stored internally. Multicode is html, markdown, and bbcode. The multicode interpreter allows you to switch between these or combine them at will. Content is not obviously length-limited and authors MAY use up to the storage maximum of 24MB. In practice markup conversion limits the effective length to around 200KB and the default "maximum length of imported content" from other sites is 200KB. This can be changed on a per-site basis but this is rare. A Note may contain one or more images, other media, and links. HTML purification primarily removes javascript and iframes and allows most legitimate tags and CSS styles through. Inline images are also added as attachments for the benefit of Mastodon (which strips most HTML), but remain in the HTML source. When importing content from other sites, if the content contains an image attachment, the content is scanned to see if a link (a) or (img) tag containing that image is already present in the HTML. If not, an image tag is added inline to the end of the incoming content. Multiple images are supported using this mechanism. Mastodon 'summary' does not invoke any special handling so 'summary' can be used for its intended purpose as a content summary. Mastodon 'sensitive' is honoured and results in invoking whatever mechanisms the user has selected to deal with this type of content. By default images are obscured and are 'click to view'. Sensitive text is not treated specially, but may be obscured using the NSFW plugin or filtered per connection based on string match, tags, patterns, languages, or other criteria. @@ -75,7 +75,9 @@ Edited posts and comments are sent with Update/Note and an 'updated' timestamp a Announce -Announce and relay activities are supported on the inbound side but are not generated. Instead a new message is generated with an embedded rendering of the shared content as the message content. This message may (should) contain additional commentary in order to comply with the Fair Use provisions of copyright law. +Announce and relay activities use two mechanisms. As wll as the Announce activity, a new message is generated with an embedded rendering of the shared content as the message content. This message may (should) contain additional commentary in order to comply with the Fair Use provisions of copyright law. The other reason is our use of comment permissions. Comments to Announce activities are sent to the author (who typically accepts comments only from connections). Comment to embedded forwards are sent to the sender. This difference in behaviour allows groups to work correctly in the presence of comment permissions. + +Discussion (2021-04-17): In the email world this type of conflict is resolved by the use of the reply-to header (e.g. in this case reply to the group rather than to the author) as well as the concept of a 'sender' which is different than 'from' (the author). We will soon be modelling the first one in ActivityPub with the use of 'replyTo'. If you see 'replyTo' in an activity it indicates that replies SHOULD go to that address rather than the author's inbox. We will implement this first and come up with a proposal for 'sender' if this gets any traction. If enough projects support these constructs we can eliminate the multiple relay mechanisms and in the process make ActivityPub much more versatile when it comes to organisational and group communications. Our primary use case for 'sender' is to provide an ActivityPub origin to a message that was imported from another system entirely (such as Diaspora or from RSS source). In this case we would set 'attributedTo' to the remote identity that authored the content, and 'sender' to the person that posted it in ActivityPub. Mastodon Custom Emojis @@ -84,5 +86,14 @@ Mastodon Custom Emojis are only supported for post content. Display names and me Mentions and private mentions -By default the mention format is '@Display Name', but other options are available, such as '@username' and both '@Display Name (username)'. Mentions may also contain an exclamation character, for example '@!Display Name'. This indicates a Direct or private message to 'Display Name' and post addressing/privacy are altered accordingly. +By default the mention format is '@Display Name', but other options are available, such as '@username' and both '@Display Name (username)'. Mentions may also contain an exclamation character, for example '@!Display Name'. This indicates a Direct or private message to 'Display Name' and post addressing/privacy are altered accordingly. All incoming and outgoing content to your stream is re-written to display mentions in your chosen style. This mechanism may need to be adopted by other projects to provide consistency as there are strong feelings on both sides of the debate about which form should be prevalent in the fediverse. + + +(Mastodon) Comment Notifications + +Our projects send comment notifications if somebody replies to a post you either created or have previously interacted with in some way. They also are able to send a "mention" notification if you were mentioned in the post. This differs from Mastodon which does not appear to support comment notifications at all and only provides mention notifications. For this reason, Mastodon users don't typically get notified unless the author they are replying to is mentioned in the post. We provide this mention in the 'tag' field of the generated Activity, but normally don't include it in the message body, as we don't actually require mentions that were created for the sole purpose of triggering a comment notification. + +Conversation Completion + +(2021-04-17) It's easy to fetch missing pieces of a conversation going "upstream", but there is no agreed-on method to fetch a complete conversation from the viewpoint of the origin actor and upstream fetching only provides a single conversation branch, rather than the entire tree. We provide 'context' as a URL to a collection containing the entire conversation (all known branches) as seen by its creator. This requires special treatment and is very similar to ostatus:conversation in that if context is present, it needs to be replicated in conversation descendants. We still support ostatus:conversation but usage is deprecated. We do not use 'replies' to achieve the same purposes because 'replies' only applies to direct descendants at any point in the conversation tree. diff --git a/Zotlabs/Daemon/Notifier.php b/Zotlabs/Daemon/Notifier.php index 855d8e3cc..cb1b20d57 100644 --- a/Zotlabs/Daemon/Notifier.php +++ b/Zotlabs/Daemon/Notifier.php @@ -401,6 +401,35 @@ class Notifier { logger('followup relay (upstream delivery)', LOGGER_DEBUG); $sendto = ($uplink) ? $parent_item['source_xchan'] : $parent_item['owner_xchan']; self::$recipients = [ $sendto ]; + +if (defined('X-REPLY-TO')) { +// experimental until debugging is completed + + if ($parent_item['replyto'] && (! $uplink)) { + $ptr = unserialise($parent_item['replyto']); + if (is_string($ptr)) { + if (ActivityStreams::is_url($sendto)) { + $sendto = $ptr; + self::$recipients = [ $sendto ]; + } + } + elseif (is_array($ptr)) { + $sendto = []; + foreach ($ptr as $rto) { + if (is_string($rto)) { + $sendto[] = $rto; + } + elseif (is_array($rto) && isset($rto['id'])) { + $sendto[] = $rto['id']; + } + } + self::$recipients = $sendto; + } + } + +} +// END defined('X-REPLY-TO') + self::$private = true; $upstream = true; self::$packet_type = 'response'; diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index 219e70aff..18bb6ecf5 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -745,7 +745,10 @@ class Activity { if (! in_array($ret['type'],[ 'Create','Update','Accept','Reject','TentativeAccept','TentativeReject' ])) { $ret['inReplyTo'] = $i['thr_parent']; - $cnv = get_iconfig($i['parent'],'ostatus','conversation'); + $cnv = get_iconfig($i['parent'],'activitypub','context'); + if (! $cnv) { + $cnv = get_iconfig($i['parent'],'ostatus','conversation'); + } if (! $cnv) { $cnv = $ret['parent_mid']; } @@ -753,15 +756,19 @@ class Activity { } if (! (isset($cnv) && $cnv)) { - // This method may be called before the item is actually saved - in which case there is no id and IConfig cannot be used - if ($i['id']) { + $cnv = get_iconfig($i,'activitypub','context'); + if (! $cnv) { $cnv = get_iconfig($i,'ostatus','conversation'); } - else { + if (! $cnv) { $cnv = $i['parent_mid']; } } if (isset($cnv) && $cnv) { + if (strpos($cnv,z_root()) === 0) { + $cnv = str_replace(['/item/','/activity/'],[ '/conversation/', '/conversation/' ], $cnv); + } + $ret['context'] = $cnv; $ret['conversation'] = $cnv; } @@ -775,10 +782,11 @@ class Activity { else return []; - $replyto = self::encode_person($i['owner'],false); -// if ($replyto) { -// $ret['replyTo'] = $replyto; -// } + + $replyto = unserialise($i['replyto']); + if ($replyto) { + $ret['replyTo'] = $replyto; + } if (! isset($ret['url'])) { $urls = []; @@ -1065,7 +1073,10 @@ class Activity { if ($i['mid'] !== $i['parent_mid']) { $ret['inReplyTo'] = $i['thr_parent']; - $cnv = get_iconfig($i['parent'],'ostatus','conversation'); + $cnv = get_iconfig($i['parent'],'activitypub','context'); + if (! $cnv) { + $cnv = get_iconfig($i['parent'],'ostatus','conversation'); + } if (! $cnv) { $cnv = $ret['parent_mid']; } @@ -1091,14 +1102,19 @@ class Activity { } } if (! isset($cnv)) { - if ($i['id']) { + $cnv = get_iconfig($i,'activitypub','context'); + if (! $cnv) { $cnv = get_iconfig($i,'ostatus','conversation'); } - else { + if (! $cnv) { $cnv = $i['parent_mid']; } } - if ($cnv) { + if (isset($cnv) && $cnv) { + if (strpos($cnv,z_root()) === 0) { + $cnv = str_replace(['/item/','/activity/'],[ '/conversation/', '/conversation/' ], $cnv); + } + $ret['context'] = $cnv; $ret['conversation'] = $cnv; } @@ -1151,10 +1167,10 @@ class Activity { } } - $replyto = self::encode_person($i['owner'],false); -// if ($replyto) { -// $ret['replyTo'] = $replyto; -// } + $replyto = unserialise($i['replyto']); + if ($replyto) { + $ret['replyTo'] = $replyto; + } if (! isset($ret['url'])) { $urls = []; @@ -1483,9 +1499,10 @@ class Activity { $ret['following'] = z_root() . '/following/' . $c['channel_address']; $ret['endpoints'] = [ - 'sharedInbox' => z_root() . '/inbox', + 'sharedInbox' => z_root() . '/inbox', + 'oauthRegistrationEndpoint' => z_root() . '/api/client/register', 'oauthAuthorizationEndpoint' => z_root() . '/authorize', - 'oauthTokenEndpoint' => z_root() . '/token' + 'oauthTokenEndpoint' => z_root() . '/token' ]; $ret['discoverable'] = ((1 - intval($p['xchan_hidden'])) ? true : false); @@ -2499,6 +2516,15 @@ class Activity { $s['mid'] = $s['parent_mid'] = $act->id; } + if (isset($act->replyto) && ! empty($act->replyto)) { + if (is_array($act->replyto) && isset($act->replyto['id'])) { + $s['replyto'] = $act->replyto['id']; + } + else { + $s['replyto'] = $act->replyto; + } + } + if (ActivityStreams::is_response_activity($act->type)) { $response_activity = true; @@ -2506,9 +2532,6 @@ class Activity { $s['mid'] = $act->id; $s['parent_mid'] = $act->obj['id']; -// if (isset($act->replyto) && ! empty($act->replyto)) { -// $s['replyto'] = $act->replyto; -// } // over-ride the object timestamp with the activity @@ -3317,11 +3340,14 @@ class Activity { return; } + if ($act->obj['context']) { + set_iconfig($item,'activitypub','context',$act->obj['context'],1); + } + if ($act->obj['conversation']) { set_iconfig($item,'ostatus','conversation',$act->obj['conversation'],1); } - set_iconfig($item,'activitypub','recips',$act->raw_recips); if (! (isset($act->data['inheritPrivacy']) && $act->data['inheritPrivacy'])) { @@ -3790,8 +3816,10 @@ class Activity { 'toot' => 'http://joinmastodon.org/ns#', 'ostatus' => 'http://ostatus.org#', 'schema' => 'http://schema.org#', + 'litepub' => 'http://litepub.social/ns#', 'conversation' => 'ostatus:conversation', 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', + 'oauthRegistrationEndpoint' => 'litepub:oauthRegistrationEndpoint', 'sensitive' => 'as:sensitive', 'movedTo' => 'as:movedTo', 'copiedTo' => 'as:copiedTo', diff --git a/Zotlabs/Module/Activity.php b/Zotlabs/Module/Activity.php index 24564dc5a..2ad2c8373 100644 --- a/Zotlabs/Module/Activity.php +++ b/Zotlabs/Module/Activity.php @@ -232,7 +232,7 @@ class Activity extends Controller { if(! perm_is_allowed($chan['channel_id'],get_observer_hash(),'view_stream')) http_status_exit(403, 'Forbidden'); - $i = ZlibActivity::encode_item_collection($nitems,'conversation/' . $item_id,'OrderedCollection',true); + $i = ZlibActivity::encode_item_collection($nitems,'conversation/' . $item_id,'OrderedCollection',true, count($nitems)); if ($portable_id && (! intval($items[0]['item_private']))) { ThreadListener::store(z_root() . '/activity/' . $item_id,$portable_id); } diff --git a/Zotlabs/Module/Conversation.php b/Zotlabs/Module/Conversation.php new file mode 100644 index 000000000..c51ea1969 --- /dev/null +++ b/Zotlabs/Module/Conversation.php @@ -0,0 +1,162 @@ + 49) { + if($n && intval($n[0]['total']) > 49) { $r = q("select * from notify where uid = %d and seen = 0 order by created desc limit 50", intval(local_channel()) ); - } else { + } + else { $r1 = q("select * from notify where uid = %d and seen = 0 order by created desc limit 50", intval(local_channel()) ); + $r2 = q("select * from notify where uid = %d and seen = 1 order by created desc limit %d", intval(local_channel()), - intval(50 - intval($r[0]['total'])) + intval(50 - intval($n[0]['total'])) ); $r = array_merge($r1,$r2); } @@ -41,7 +43,7 @@ class Notifications extends \Zotlabs\Web\Controller { $notifications_available = 1; foreach ($r as $rr) { $x = strip_tags(bbcode($rr['msg'])); - $notif_content = replace_macros(get_markup_template('notify.tpl'),array( + $notif_content .= replace_macros(get_markup_template('notify.tpl'),array( '$item_link' => z_root().'/notify/view/'. $rr['id'], '$item_image' => $rr['photo'], '$item_text' => $x, diff --git a/boot.php b/boot.php index fcb8ce484..ddefa8dc2 100755 --- a/boot.php +++ b/boot.php @@ -2323,6 +2323,15 @@ function construct_page() { header("X-Content-Type-Options: nosniff"); } + + if (isset(App::$config['system']['perm_policy_header']) && App::$config['system']['perm_policy_header']) { + header("Permissions-Policy: " . App::$config['system']['perm_policy_header']); + } + else { + // opt-out this site from federated browser surveillance + header("Permissions-Policy: interest-cohort=()"); + } + if (isset(App::$config['system']['public_key_pins']) && App::$config['system']['public_key_pins']) { header("Public-Key-Pins: " . App::$config['system']['public_key_pins']); } diff --git a/include/api.php b/include/api.php index 057f2f63e..ec95967db 100644 --- a/include/api.php +++ b/include/api.php @@ -196,8 +196,7 @@ require_once('include/api_zot.php'); $secret = random_string(16); $name = trim(escape_tags($_REQUEST['client_name'])); if (! $name) { - // json_return_and_die($ret); - $name = random_string(8); + json_return_and_die($ret); } if (is_array($_REQUEST['redirect_uris'])) { $redirect = trim($_REQUEST['redirect_uris'][0]); diff --git a/include/bbcode.php b/include/bbcode.php index 74614de88..8839c2f55 100644 --- a/include/bbcode.php +++ b/include/bbcode.php @@ -282,7 +282,7 @@ function bb_parse_crypt($match) { $onclick = 'onclick="' . $f . '(\'' . $algorithm . '\',\'' . $hint . '\',\'' . $match[2] . '\',\'#' . $x . '\');"'; $label = t('Encrypted content'); - $Text = '
' . $label . '

'; + $Text = '
' . $label . '


' . bb_parse_b64_crypt($match); return $Text; } @@ -298,9 +298,11 @@ function bb_parse_b64_crypt($match) { if(empty($match[2])) return; - $r .= '----- ENCRYPTED CONTENT -----' . PHP_EOL; - $r .= $match[2] . PHP_EOL; - $r .= '----- END ENCRYPTED CONTENT -----'; + $r .= '----- ENCRYPTED CONTENT -----' . "\n"; + $r .= base64_encode($match[1]) . "." . $match[2] . "\n"; + $r .= '----- END ENCRYPTED CONTENT -----' . "\n"; + + $r = '' . str_replace("\n",'
',wordwrap($r,75,"\n",true)) . '
'; return $r; diff --git a/include/items.php b/include/items.php index 39c39c1ce..24ec4bc68 100644 --- a/include/items.php +++ b/include/items.php @@ -724,6 +724,7 @@ function get_item_elements($x,$allow_code = false) { } $arr['attach'] = activity_sanitise($x['attach']); + $arr['replyto'] = activity_sanitise($c['replyto']); $arr['term'] = decode_tags($x['tags']); $arr['iconfig'] = decode_item_meta($x['meta']); @@ -1115,6 +1116,7 @@ function encode_item($item,$mirror = false) { $x['longlat'] = $item['coord']; $x['signature'] = $item['sig']; $x['route'] = $item['route']; + $x['replyto'] = $item['replyto']; $x['owner'] = encode_item_xchan($item['owner']); $x['author'] = encode_item_xchan($item['author']); @@ -3058,6 +3060,9 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false $arr['deny_gid'] = $channel['channel_deny_gid']; $arr['comment_policy'] = map_scope(PermissionLimits::Get($channel['channel_id'],'post_comments')); + $arr['replyto'] = z_root() . '/channel/' . $channel['channel_address']; + + if ($arr['id']) { $post = item_store_update($arr); } @@ -3117,12 +3122,13 @@ function start_delivery_chain($channel, $item, $item_id, $parent, $group = false $title = $item['title']; $body = $item['body']; - $r = q("update item set item_uplink = %d, item_nocomment = %d, item_flags = %d, owner_xchan = '%s', allow_cid = '%s', allow_gid = '%s', + $r = q("update item set item_uplink = %d, item_nocomment = %d, item_flags = %d, owner_xchan = '%s', replyto = '%s', allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s', item_private = %d, comment_policy = '%s', title = '%s', body = '%s', item_wall = %d, item_origin = %d where id = %d", intval($item_uplink), intval($item_nocomment), intval($flag_bits), dbesc($channel['channel_hash']), + dbesc(channel_url($channel)), dbesc($channel['channel_allow_cid']), dbesc($channel['channel_allow_gid']), dbesc($channel['channel_deny_cid']),