aboutsummaryrefslogtreecommitdiffstats
path: root/Zotlabs/Lib
diff options
context:
space:
mode:
Diffstat (limited to 'Zotlabs/Lib')
-rw-r--r--Zotlabs/Lib/Activity.php398
-rw-r--r--Zotlabs/Lib/BaseObject.php80
-rw-r--r--Zotlabs/Lib/Libzot.php131
-rw-r--r--Zotlabs/Lib/ThreadItem.php3
4 files changed, 474 insertions, 138 deletions
diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php
index 9a8ac4a39..9e4498099 100644
--- a/Zotlabs/Lib/Activity.php
+++ b/Zotlabs/Lib/Activity.php
@@ -8,6 +8,7 @@ use Zotlabs\Access\PermissionRoles;
use Zotlabs\Access\Permissions;
use Zotlabs\Daemon\Master;
use Zotlabs\Web\HTTPSig;
+use Zotlabs\Entity\Item;
require_once('include/event.php');
require_once('include/html2plain.php');
@@ -598,6 +599,25 @@ class Activity {
if ($i['mid'] !== $i['parent_mid']) {
$ret['inReplyTo'] = ((strpos($i['thr_parent'], 'http') === 0) ? $i['thr_parent'] : z_root() . '/item/' . urlencode($i['thr_parent']));
+
+ $cnv = IConfig::Get($i['parent'], 'activitypub', 'context');
+ if (!$cnv) {
+ $cnv = $i['parent_mid'];
+ }
+ }
+
+ if (empty($cnv)) {
+ $cnv = IConfig::Get($i, 'activitypub', 'context');
+ if (!$cnv) {
+ $cnv = $i['parent_mid'];
+ }
+ }
+
+ if (!empty($cnv)) {
+ if (is_string($cnv) && str_starts_with($cnv, z_root())) {
+ $cnv = str_replace(['/item/', '/activity/'], ['/conversation/', '/conversation/'], $cnv);
+ }
+ $ret['context'] = $cnv;
}
if ($i['mimetype'] === 'text/bbcode') {
@@ -617,6 +637,12 @@ class Activity {
$t = self::encode_taxonomy($i);
if ($t) {
+ foreach($t as $tag) {
+ if (strcasecmp($tag['name'], '#nsfw') === 0 || strcasecmp($tag['name'], '#sensitive') === 0) {
+ $ret['sensitive'] = true;
+ }
+ }
+
$ret['tag'] = $t;
}
@@ -624,7 +650,20 @@ class Activity {
if ($a) {
$ret['attachment'] = $a;
}
-
+/*
+ if ($i['target']) {
+ if (is_string($i['target'])) {
+ $tmp = json_decode($i['target'], true);
+ if ($tmp !== null) {
+ $i['target'] = $tmp;
+ }
+ }
+ $tgt = self::encode_object($i['target']);
+ if ($tgt) {
+ $ret['target'] = $tgt;
+ }
+ }
+*/
if (intval($i['item_private']) === 0) {
$ret['to'] = [ACTIVITY_PUBLIC_INBOX];
}
@@ -843,12 +882,32 @@ class Activity {
$ret['type'] = self::activity_mapper($i['verb']);
if ((isset($i['item_deleted']) && intval($i['item_deleted'])) && !$recurse) {
- $is_response = false;
- if (ActivityStreams::is_response_activity($ret['type'])) {
+ if ($i['verb'] === 'Add' && str_contains($i['tgt_type'], 'Collection')) {
+ $ret['id'] = str_replace('/item/', '/activity/', $i['mid']) . '#Remove';
+ $ret['type'] = 'Remove';
+ if (is_string($i['obj'])) {
+ $obj = json_decode($i['obj'], true);
+ }
+ elseif(is_array($i['obj'])) {
+ $obj = $i['obj'];
+ }
+ if (isset($obj['id'])) {
+ $ret['object'] = $obj['id'];
+ }
+ else {
+ $ret['object'] = str_replace('/item/', '/activity/', $i['mid']);
+ }
+ $ret['target'] = is_array($i['target']) ? $i['target'] : json_decode($i['target'], true);
+
+ return $ret;
+ }
+
+ $is_response = ActivityStreams::is_response_activity($ret['type']);
+
+ if ($is_response) {
$ret['type'] = 'Undo';
$fragment = 'undo';
- $is_response = true;
}
else {
$ret['type'] = 'Delete';
@@ -971,9 +1030,28 @@ class Activity {
// inReplyTo needs to be set in the activity for followup actions (Like, Dislike, Announce, etc.),
// but *not* for comments and RSVPs, where it should only be present in the object
- if (!in_array($ret['type'], ['Create', 'Update', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject'])) {
+ if (!in_array($ret['type'], ['Create', 'Update', 'Add', 'Remove', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject'])) {
$ret['inReplyTo'] = ((strpos($i['thr_parent'], 'http') === 0) ? $i['thr_parent'] : z_root() . '/item/' . urlencode($i['thr_parent']));
}
+
+ $cnv = IConfig::Get($i['parent'], 'activitypub', 'context');
+ if (!$cnv) {
+ $cnv = $i['parent_mid'];
+ }
+ }
+
+ if (empty($cnv)) {
+ $cnv = IConfig::Get($i, 'activitypub', 'context');
+ if (!$cnv) {
+ $cnv = $i['parent_mid'];
+ }
+ }
+
+ if (!empty($cnv)) {
+ if (is_string($cnv) && str_starts_with($cnv, z_root())) {
+ $cnv = str_replace(['/item/', '/activity/'], ['/conversation/', '/conversation/'], $cnv);
+ }
+ $ret['context'] = $cnv;
}
$actor = self::encode_person($i['author'], false);
@@ -1046,6 +1124,7 @@ class Activity {
call_hooks('encode_activity', $hookinfo);
return $hookinfo['encoded'];
+
}
// Returns an array of URLS for any mention tags found in the item array $i.
@@ -1642,9 +1721,9 @@ class Activity {
}
if (in_array($observer, [$r[0]['author_xchan'], $r[0]['owner_xchan']])) {
- drop_item($r[0]['id'], false, (($r[0]['item_wall']) ? DROPITEM_PHASE1 : DROPITEM_NORMAL));
+ drop_item($r[0]['id'], (($r[0]['item_wall']) ? DROPITEM_PHASE1 : DROPITEM_NORMAL), observer_hash: $observer);
} elseif (in_array($act->actor['id'], [$r[0]['author_xchan'], $r[0]['owner_xchan']])) {
- drop_item($r[0]['id'], false, (($r[0]['item_wall']) ? DROPITEM_PHASE1 : DROPITEM_NORMAL));
+ drop_item($r[0]['id'], (($r[0]['item_wall']) ? DROPITEM_PHASE1 : DROPITEM_NORMAL));
}
sync_an_item($channel['channel_id'], $r[0]['id']);
@@ -1942,129 +2021,170 @@ class Activity {
}
- static function update_poll($item_id, $post) {
+ static function update_poll($pollItem, $response) {
+
+ logger('updating poll');
- $multi = false;
- $mid = $post['mid'];
- $content = $post['title'];
+ $multi = false;
- if (!$item_id) {
+ if (!$pollItem) {
+ logger('no item');
return false;
}
- if (intval($post['item_blocked']) === ITEM_MODERATED) {
+ if (intval($pollItem['item_blocked']) === ITEM_MODERATED) {
+ logger('item blocked');
return false;
}
+ $channel = channelx_by_n($pollItem['uid']);
+ if (!$channel) {
+ logger('no channel');
+ return false;
+ }
+
+ $relatedItem = find_related($pollItem);
+
+ $ids = (($relatedItem) ? $pollItem['id'] . ',' . $relatedItem['id'] : $pollItem['id']);
+
dbq("START TRANSACTION");
+ // Using the provided items as is will produce desastrous race conditions
+ // in case of multiple choice polls - hence:
- $item = q("SELECT * FROM item WHERE id = %d FOR UPDATE",
- intval($item_id)
- );
+ $items = dbq("SELECT * FROM item WHERE id in ($ids) FOR UPDATE");
- if (!$item) {
- dbq("COMMIT");
- return false;
+ foreach ($items as $item) {
+ if ($item['id'] === $pollItem['id']) {
+ $pollItem = $item;
+ }
+ if (!empty($relatedItem['id']) && $item['id'] === $relatedItem['id']) {
+ $relatedItem = $item;
+ }
}
- $item = $item[0];
+ $o = json_decode($pollItem['obj'], true);
- $o = json_decode($item['obj'], true);
if ($o && array_key_exists('anyOf', $o)) {
$multi = true;
}
- $r = q("select mid, title from item where parent_mid = '%s' and author_xchan = '%s'",
- dbesc($item['mid']),
- dbesc($post['author_xchan'])
- );
+ if ($response) {
+ $mid = $response['mid'];
+ $content = trim($response['title']);
- // prevent any duplicate votes by same author for oneOf and duplicate votes with same author and same answer for anyOf
+ $r = q("select mid, title from item where parent_mid = '%s' and author_xchan = '%s' and mid != parent_mid ",
+ dbesc($pollItem['mid']),
+ dbesc($response['author_xchan'])
+ );
- if ($r) {
- if ($multi) {
- foreach ($r as $rv) {
- if ($rv['title'] === $content && $rv['mid'] !== $mid) {
- return false;
+ // prevent any duplicate votes by same author for oneOf and duplicate votes with same author and same answer for anyOf
+
+ if ($r) {
+ if ($multi) {
+ foreach ($r as $rv) {
+ if (trim($rv['title']) === $content && $rv['mid'] !== $mid) {
+ logger('already voted multi');
+ return false;
+ }
}
- }
- }
- else {
- foreach ($r as $rv) {
- if ($rv['mid'] !== $mid) {
- return false;
+ } else {
+ foreach ($r as $rv) {
+ if ($rv['mid'] !== $mid && $content) {
+ logger('already voted');
+ return false;
+ }
}
}
}
- }
- $answer_found = false;
- $found = false;
- if ($multi) {
- for ($c = 0; $c < count($o['anyOf']); $c++) {
- if ($o['anyOf'][$c]['name'] === $content) {
- $answer_found = true;
- if (is_array($o['anyOf'][$c]['replies'])) {
- foreach ($o['anyOf'][$c]['replies'] as $reply) {
- if (is_array($reply) && array_key_exists('id', $reply) && $reply['id'] === $mid) {
- $found = true;
+ $answer_found = false;
+ $foundPrevious = false;
+ if ($multi) {
+ for ($c = 0; $c < count($o['anyOf']); $c++) {
+ if (trim($o['anyOf'][$c]['name']) === $content) {
+ $answer_found = true;
+
+
+ if (is_array($o['anyOf'][$c]['replies'])) {
+ foreach ($o['anyOf'][$c]['replies'] as $reply) {
+ if (is_array($reply) && array_key_exists('id', $reply) && $reply['id'] === $mid) {
+ $foundPrevious = true;
+ }
}
}
- }
- if (!$found) {
- $o['anyOf'][$c]['replies']['totalItems']++;
- $o['anyOf'][$c]['replies']['items'][] = ['id' => $mid, 'type' => 'Note'];
+ if (!$foundPrevious) {
+ $o['anyOf'][$c]['replies']['totalItems']++;
+ $o['anyOf'][$c]['replies']['items'][] = ['id' => $mid, 'type' => 'Note'];
+ }
}
}
- }
- }
- else {
- for ($c = 0; $c < count($o['oneOf']); $c++) {
- if ($o['oneOf'][$c]['name'] === $content) {
- $answer_found = true;
- if (is_array($o['oneOf'][$c]['replies'])) {
- foreach ($o['oneOf'][$c]['replies'] as $reply) {
- if (is_array($reply) && array_key_exists('id', $reply) && $reply['id'] === $mid) {
- $found = true;
+ } else {
+ for ($c = 0; $c < count($o['oneOf']); $c++) {
+ if (trim($o['oneOf'][$c]['name']) === $content) {
+ $answer_found = true;
+ if (is_array($o['oneOf'][$c]['replies'])) {
+ foreach ($o['oneOf'][$c]['replies'] as $reply) {
+ if (is_array($reply) && array_key_exists('id', $reply) && $reply['id'] === $mid) {
+ $foundPrevious = true;
+ }
}
}
- }
- if (!$found) {
- $o['oneOf'][$c]['replies']['totalItems']++;
- $o['oneOf'][$c]['replies']['items'][] = ['id' => $mid, 'type' => 'Note'];
+ if (!$foundPrevious) {
+ $o['oneOf'][$c]['replies']['totalItems']++;
+ $o['oneOf'][$c]['replies']['items'][] = ['id' => $mid, 'type' => 'Note'];
+ }
}
}
}
}
- logger('updated_poll: ' . print_r($o, true), LOGGER_DATA);
- if ($answer_found && !$found) {
- $u = q("update item set obj = '%s', edited = '%s' where id = %d",
- dbesc(json_encode($o)),
- dbesc(datetime_convert()),
- intval($item['id'])
- );
+ if ($pollItem['comments_closed'] > NULL_DATE) {
+ if ($pollItem['comments_closed'] > datetime_convert()) {
+ $o['closed'] = datetime_convert('UTC', 'UTC', $pollItem['comments_closed'], ATOM_TIME);
+ // set this to force an update
+ $answer_found = true;
+ }
+ }
- if ($u) {
- dbq("COMMIT");
+ // A change was made locally
+ if ($response && $answer_found && !$foundPrevious) {
- if ($multi) {
- // wait some seconds for possible multiple answers to be processed
- // before calling the notifier
- sleep(3);
- }
+ // update this copy
+ $i = [$pollItem];
+ xchan_query($i, true);
+ $i = fetch_post_tags($i);
+ $i[0]['obj'] = $o;
- Master::Summon(['Notifier', 'wall-new', $item['id']]);
- return true;
- }
+ // create the new object
+ $newObj = self::build_packet(self::encode_activity($i[0]), $channel, true);
- dbq("ROLLBACK");
+ // and immediately update the db
+ $u = q("UPDATE item
+ SET obj = (
+ CASE
+ WHEN item.id = %d THEN '%s'
+ WHEN item.id = %d THEN '%s'
+ END
+ ),
+ edited = '%s'
+ WHERE id IN ($ids)",
+ intval($pollItem['id']),
+ dbesc(json_encode($o)),
+ intval($relatedItem['id']),
+ dbesc($newObj),
+ dbesc(datetime_convert())
+ );
+
+ dbq("COMMIT");
+ Master::Summon(['Notifier', 'edit_post', $pollItem['id'], $response['mid']]);
+ if (!empty($relatedItem['id'])) {
+ Master::Summon(['Notifier', 'edit_post', $relatedItem['id'], $response['mid']]);
+ }
}
- dbq("COMMIT");
- return false;
+ return true;
}
static function decode_note($act) {
@@ -2320,6 +2440,16 @@ class Activity {
$s['obj']['actor'] = $s['obj']['actor']['id'];
}
+ if (is_array($act->tgt) && $act->tgt) {
+ if (array_key_exists('type', $act->tgt)) {
+ $s['tgt_type'] = self::activity_obj_mapper($act->tgt['type']);
+ }
+ // We shouldn't need to store collection contents which could be large. We will often only require the meta-data
+ if (isset($s['tgt_type']) && str_contains($s['tgt_type'], 'Collection')) {
+ $s['target'] = ['id' => $act->tgt['id'], 'type' => $s['tgt_type'], 'attributedTo' => $act->tgt['attributedTo'] ?? $act->tgt['actor']];
+ }
+ }
+
$generator = $act->get_property_obj('generator');
if ((!$generator) && (!$response_activity)) {
$generator = $act->get_property_obj('generator', $act->obj);
@@ -2903,6 +3033,10 @@ class Activity {
// This isn't perfect but the best we can do for now.
$item['comment_policy'] = ((isset($act->data['commentPolicy'])) ? $act->data['commentPolicy'] : 'authenticated');
+ if (!empty($act->obj['context'])) {
+ IConfig::Set($item, 'activitypub', 'context', $act->obj['context'], 1);
+ }
+
IConfig::Set($item, 'activitypub', 'recips', $act->raw_recips);
if (intval($act->sigok)) {
@@ -3090,24 +3224,6 @@ class Activity {
}
$a = new ActivityStreams($n);
-
- logger($a->debug(), LOGGER_DATA);
-
- if (!$a->is_valid()) {
- logger('not a valid activity');
- break;
- }
-
- if (in_array($a->type, ['Add', 'Remove'])
- && is_array($a->obj)
- && array_key_exists('object', $a->obj)
- && array_key_exists('actor', $a->obj)
- && !empty($a->tgt)) {
-
- logger('unsupported collection operation', LOGGER_DEBUG);
- return;
- }
-
if ($a->type === 'Announce' && is_array($a->obj)
&& array_key_exists('object', $a->obj) && array_key_exists('actor', $a->obj)) {
// This is a relayed/forwarded Activity (as opposed to a shared/boosted object)
@@ -3116,6 +3232,13 @@ class Activity {
$a = new ActivityStreams($a->obj);
}
+ logger($a->debug(), LOGGER_DATA);
+
+ if (!$a->is_valid()) {
+ logger('not a valid activity');
+ break;
+ }
+
$item = Activity::decode_note($a);
if (!$item) {
@@ -3723,5 +3846,72 @@ class Activity {
}
}
+ public static function addToCollection($channel, $object, $target, $sourceItem = null, $deliver = true) {
+ if (!isset($channel['xchan_hash'])) {
+ $channel = channelx_by_hash($channel['channel_hash']);
+ }
+
+ $item = ((new Item())
+ ->setUid($channel['channel_id'])
+ ->setVerb('Add')
+ ->setAuthorXchan($channel['channel_hash'])
+ ->setOwnerXchan($channel['channel_hash'])
+ ->setObj($object)
+ ->setObjType($object['type'])
+ ->setParentMid(str_replace('/conversation/','/item/', $target))
+ ->setThrParent(str_replace('/conversation/','/item/', $target))
+ // ->setApproved($object['object']['id'] ?? '')
+ // ->setReplyto(z_root() . '/channel/' . $channel['channel_address'])
+ ->setTgtType('Collection')
+ ->setTarget([
+ 'id' => str_replace('/item/','/conversation/', $target),
+ 'type' => 'Collection',
+ 'attributedTo' => z_root() . '/channel/' . $channel['channel_address'],
+ ])
+ );
+ if ($sourceItem) {
+ $item->setSourceXchan($sourceItem['source_xchan'])
+ ->setAllowCid($sourceItem['allow_cid'])
+ ->setAllowGid($sourceItem['allow_gid'])
+ ->setDenyCid($sourceItem['deny_cid'])
+ ->setDenyGid($sourceItem['deny_gid'])
+ ->setPrivate($sourceItem['item_private'])
+ ->setNocomment($sourceItem['item_nocomment'])
+ ->setCommentPolicy($sourceItem['comment_policy'])
+ ->setPublicPolicy($sourceItem['public_policy'])
+ ->setPostopts($sourceItem['postopts']);
+ }
+ $result = post_activity_item($item->toArray(), deliver: $deliver, channel: $channel, observer: $channel, addAndSync: false);
+ logger('addToCollection: ' . print_r($result, true));
+ return $result;
+ }
+
+ public static function removeFromCollection($channel, $object, $target, $deliver = true) {
+ if (!isset($channel['xchan_hash'])) {
+ $channel = channelx_by_hash($channel['channel_hash']);
+ }
+
+ $item = ((new Item())
+ ->setUid($channel['channel_id'])
+ ->setVerb('Remove')
+ ->setAuthorXchan($channel['channel_hash'])
+ ->setOwnerXchan($channel['channel_hash'])
+ ->setObj($object)
+ ->setObjType($object['type'])
+ ->setParentMid(str_replace('/conversation/','/item/', $target))
+ ->setThrParent(str_replace('/conversation/','/item/', $target))
+ ->setReplyto(z_root() . '/channel/' . $channel['channel_address'])
+ ->setTgtType('Collection')
+ ->setTarget([
+ 'id' => str_replace('/item/','/conversation/', $target),
+ 'type' => 'Collection',
+ 'attributedTo' => z_root() . '/channel/' . $channel['channel_address']
+ ])
+ );
+
+ $result = post_activity_item($item->toArray(), deliver: $deliver, channel: $channel, observer: $channel, addAndSync: false);
+ logger('removeFromCollection: ' . print_r($result, true));
+ return $result;
+ }
}
diff --git a/Zotlabs/Lib/BaseObject.php b/Zotlabs/Lib/BaseObject.php
new file mode 100644
index 000000000..7125d34cb
--- /dev/null
+++ b/Zotlabs/Lib/BaseObject.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Zotlabs\Lib;
+
+use Zotlabs\ActivityStreams\UnhandledElementException;
+
+class BaseObject
+{
+
+ public $string;
+ public $ldContext;
+
+ /**
+ * @param $input
+ * @param $strict
+ * @throws UnhandledElementException if $strict
+ */
+
+ public function __construct($input = null, $strict = false)
+ {
+ if (isset($input)) {
+ if (is_string($input)) {
+ $this->string = $input;
+ }
+ elseif(is_array($input)) {
+ foreach ($input as $key => $value) {
+ $key = ($key === '@context') ? 'ldContext' : $key;
+ if ($strict && !property_exists($this, $key)) {
+ throw new UnhandledElementException("Unhandled element: $key");
+ }
+ $this->{$key} = $value;
+ }
+ }
+ }
+ return $this;
+ }
+
+ public function getDataType($element, $object = null)
+ {
+ $object = $object ?? $this;
+ $type = gettype($object[$element]);
+ if ($type === 'array' && array_is_list($object[$element])) {
+ return 'list';
+ }
+ return $type;
+ }
+
+ public function toArray()
+ {
+ if ($this->string) {
+ return $this->string;
+ }
+ $returnValue = [];
+ foreach ((array) $this as $key => $value) {
+ if (isset($value)) {
+ $key = ($key === 'ldContext') ? '@context' : $key;
+ $returnValue[$key] = (($value instanceof BaseObject) ? $value->toArray() : $value);
+ }
+ }
+ return $returnValue;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getLdContext()
+ {
+ return $this->ldContext;
+ }
+
+ /**
+ * @param mixed $ldContext
+ * @return BaseObject
+ */
+ public function setLdContext($ldContext)
+ {
+ $this->ldContext = $ldContext;
+ return $this;
+ }
+}
diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php
index 05134f433..be2305c04 100644
--- a/Zotlabs/Lib/Libzot.php
+++ b/Zotlabs/Lib/Libzot.php
@@ -1134,6 +1134,7 @@ class Libzot {
}
$message_request = false;
+ $is_collection_operation = false;
$has_data = array_key_exists('data', $env) && $env['data'];
@@ -1141,26 +1142,37 @@ class Libzot {
$AS = null;
+
if ($env['encoding'] === 'activitystreams') {
$AS = new ActivityStreams($data);
- if (!$AS->is_valid()) {
- logger('Activity rejected: ' . print_r($data, true));
- return;
- }
+ // process add/remove from collection separately, as it requires a target.
+ // use the raw object, as it will not include actor expansion
if (in_array($AS->type, ['Add', 'Remove'])
&& is_array($AS->obj)
&& array_key_exists('object', $AS->obj)
&& array_key_exists('actor', $AS->obj)
&& !empty($AS->tgt)) {
- logger('unsupported collection operation', LOGGER_DEBUG);
- return;
+ logger('relayed collection operation', LOGGER_DEBUG);
+ $is_collection_operation = true;
+
+ $original_id = $AS->id;
+ $original_type = $AS->type;
+
+ $raw_activity = $AS->data;
+
+ $AS = new ActivityStreams($raw_activity['object'], portable_id: $env['sender']);
+
+ // Store the original activity id and type for later usage
+ $AS->meta['original_id'] = $original_id;
+ $AS->meta['original_type'] = $original_type;
}
if (is_array($AS->obj)) {
$item = Activity::decode_note($AS);
+
if (!$item) {
logger('Could not decode activity: ' . print_r($AS, true));
return;
@@ -1170,6 +1182,11 @@ class Libzot {
$item = [];
}
+ if (!$AS->is_valid()) {
+ logger('Activity rejected: ' . print_r($data, true));
+ return;
+ }
+
logger($AS->debug(), LOGGER_DATA);
}
@@ -1313,7 +1330,7 @@ class Libzot {
$relay = (($env['type'] === 'response') ? true : false);
- $result = self::process_delivery($env['sender'], $AS, $item, $deliveries, $relay, false, $message_request);
+ $result = self::process_delivery($env['sender'], $AS, $item, $deliveries, $relay, false, $message_request, false, $is_collection_operation);
Activity::init_background_fetch($env['sender']);
}
@@ -1529,7 +1546,7 @@ class Libzot {
* @return array
*/
- static function process_delivery($sender, $act, $arr, $deliveries, $relay, $public = false, $request = false, $force = false) {
+ static function process_delivery($sender, $act, $arr, $deliveries, $relay, $public = false, $request = false, $force = false, $is_collection_operation = false) {
$result = [];
// We've validated the sender. Now make sure that the sender is the owner or author
@@ -1557,6 +1574,14 @@ class Libzot {
$DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>');
+ $conversation_operation = $is_collection_operation && isset($arr['target']['attributedTo']);
+
+ if (str_contains($arr['tgt_type'], 'Collection') && !$relay && !$conversation_operation) {
+ $DR->update('not a collection activity');
+ $result[] = $DR->get();
+ continue;
+ }
+
if (($act) && ($act->obj) && (!is_array($act->obj))) {
// The initial object fetch failed using the sys channel credentials.
// Try again using the delivery channel credentials.
@@ -1590,6 +1615,8 @@ class Libzot {
*
*/
+
+
if ($sender === $channel['channel_hash'] && $arr['author_xchan'] === $channel['channel_hash'] && !str_starts_with($arr['mid'], z_root())) {
$DR->update('self delivery ignored');
$result[] = $DR->get();
@@ -1838,11 +1865,19 @@ class Libzot {
dbesc($arr['author_xchan'])
);
- // reactions such as like and dislike could have an mid with /activity/ in it.
+ // If we import an add/remove activity ($is_collection_operation) we strip off the
+ // add/remove part and only process the object.
+ // When looking up the item to pass it to the notifier for relay, we need to look up
+ // the original (stripped off) message id which we stored in $act->meta.
+
+ $sql_mid = (($is_collection_operation && $relay && $channel['channel_hash'] === $arr['owner_xchan']) ? $act->meta['original_id'] : $arr['mid']);
+
+ // Reactions such as like and dislike could have an mid with /activity/ in it.
// Check for both forms in order to prevent duplicates.
- $r = q("select * from item where mid in ('%s','%s') and uid = %d limit 1",
- dbesc($arr['mid']),
- dbesc(str_replace(z_root() . '/activity/', z_root() . '/item/', $arr['mid'])),
+
+ $r = q("select * from item where mid in ('%s', '%s') and uid = %d limit 1",
+ dbesc($sql_mid),
+ dbesc(reverse_activity_mid($sql_mid)),
intval($channel['channel_id'])
);
@@ -1871,21 +1906,29 @@ class Libzot {
$DR->update('update ignored');
$result[] = $DR->get();
}
+
+ if ($relay && $channel['channel_hash'] === $item_result['item']['owner_xchan'] && $item_result['item']['verb'] !== 'Add' && !$is_collection_operation) {
+ $approval = Activity::addToCollection($channel, $act->data, $item_result['item']['parent_mid'], $item_result['item'], deliver: false);
+ }
+
}
else {
$DR->update('update ignored');
$result[] = $DR->get();
- // We need this line to ensure wall-to-wall comments are relayed (by falling through to the relay bit),
+ // We need this line to ensure wall-to-wall comments and add/remove activities are relayed (by falling through to the relay bit),
// and at the same time not relay any other relayable posts more than once, because to do so is very wasteful.
if (!intval($r[0]['item_origin']))
continue;
}
+
+
}
else {
$arr['aid'] = $channel['channel_account_id'];
$arr['uid'] = $channel['channel_id'];
+
// if it's a sourced post, call the post_local hooks as if it were
// posted locally so that crosspost connectors will be triggered.
$item_source = check_item_source($arr['uid'], $arr);
@@ -1914,10 +1957,15 @@ class Libzot {
}
if (post_is_importable($arr['uid'], $arr, $abook)) {
- $item_result = item_store($arr);
+ $item_result = item_store($arr, addAndSync: false);
+
if ($item_result['success']) {
$item_id = $item_result['item_id'];
+ if ($relay && $channel['channel_hash'] === $item_result['item']['owner_xchan'] && $item_result['item']['verb'] !== 'Add' && !$is_collection_operation) {
+ $approval = Activity::addToCollection($channel, $act->data, $item_result['item']['parent_mid'], $item_result['item'], deliver: false);
+ }
+
if ($item_source && in_array($item_result['item']['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) {
event_addtocal($item_id, $channel['channel_id']);
}
@@ -1966,6 +2014,10 @@ class Libzot {
if ($relay && $item_id && $stored['item_blocked'] !== ITEM_MODERATED) {
logger('Invoking relay');
Master::Summon(['Notifier', 'relay', intval($item_id)]);
+ if (!empty($approval) && $approval['item_id']) {
+ Master::Summon(['Notifier', 'relay', intval($approval['item_id'])]);
+ }
+
$DR->addto_update('relayed');
$result[] = $DR->get();
}
@@ -2019,12 +2071,7 @@ class Libzot {
$AS = new ActivityStreams($activity);
- if (!$AS->is_valid()) {
- logger('Fetched activity rejected: ' . print_r($activity, true));
- continue;
- }
-
- if ($AS->type === 'Announce' && is_array($AS->obj)
+ if ($AS->is_valid() && $AS->type === 'Announce' && is_array($AS->obj)
&& array_key_exists('object', $AS->obj) && array_key_exists('actor', $AS->obj)) {
// This is a relayed/forwarded Activity (as opposed to a shared/boosted object)
// Reparse the encapsulated Activity and use that instead
@@ -2032,14 +2079,33 @@ class Libzot {
$AS = new ActivityStreams($AS->obj);
}
+ // process add/remove from collection separately, as it requires a target.
+ // use the raw object, as it will not include actor expansion
if (in_array($AS->type, ['Add', 'Remove'])
&& is_array($AS->obj)
&& array_key_exists('object', $AS->obj)
&& array_key_exists('actor', $AS->obj)
&& !empty($AS->tgt)) {
- logger('unsupported collection operation', LOGGER_DEBUG);
- return;
+ logger('relayed collection operation', LOGGER_DEBUG);
+
+ $is_collection_operation = true;
+
+ $original_id = $AS->id;
+ $original_type = $AS->type;
+
+ $raw_activity = $AS->data;
+
+ $AS = new ActivityStreams($raw_activity['object'], portable_id: $env['sender']);
+
+ // Store the original activity id and type for later usage
+ $AS->meta['original_id'] = $original_id;
+ $AS->meta['original_type'] = $original_type;
+ }
+
+ if (!$AS->is_valid()) {
+ logger('Fetched activity rejected: ' . print_r($activity, true));
+ continue;
}
// logger($AS->debug());
@@ -2230,7 +2296,7 @@ class Libzot {
}
- $x = item_store_update($item);
+ $x = item_store_update($item, addAndSync: false);
// If we're updating an event that we've saved locally, we store the item info first
// because event_addtocal will parse the body to get the 'new' event details
@@ -2346,21 +2412,20 @@ class Libzot {
);
}
} else {
- if ($stored['id'] !== $stored['parent']) {
- q(
- "update item set commented = '%s', changed = '%s' where id = %d",
- dbesc(datetime_convert()),
- dbesc(datetime_convert()),
- intval($stored['parent'])
- );
- }
- }
+ if ($stored['id'] !== $stored['parent']) {
+ q("update item set commented = '%s', changed = '%s' where id = %d",
+ dbesc(datetime_convert()),
+ dbesc(datetime_convert()),
+ intval($stored['parent'])
+ );
+ }
+ }
// Use phased deletion to set the deleted flag, call both tag_deliver and the notifier to notify downstream channels
// and then clean up after ourselves with a cron job after several days to do the delete_item_lowlevel() (DROPITEM_PHASE2).
- drop_item($post_id, false, DROPITEM_PHASE1);
+ drop_item($post_id, DROPITEM_PHASE1, uid: $uid);
tag_deliver($uid, $post_id);
}
diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php
index d21d85105..90a3d3fc8 100644
--- a/Zotlabs/Lib/ThreadItem.php
+++ b/Zotlabs/Lib/ThreadItem.php
@@ -544,7 +544,8 @@ class ThreadItem {
'moderate_delete' => t('Delete'),
'rtl' => in_array($item['lang'], rtl_languages()),
'reactions_allowed' => $reactions_allowed,
- 'reaction_str' => [t('Add yours'), t('Remove yours')]
+ 'reaction_str' => [t('Add yours'), t('Remove yours')],
+ 'is_contained' => $this->is_toplevel() && str_contains($item['tgt_type'], 'Collection')
);
$arr = array('item' => $item, 'output' => $tmp_item);