diff options
Diffstat (limited to 'Zotlabs/Lib/Activity.php')
-rw-r--r-- | Zotlabs/Lib/Activity.php | 783 |
1 files changed, 481 insertions, 302 deletions
diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index ae43a43b5..296129ea2 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -8,10 +8,12 @@ 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'); require_once('include/items.php'); +require_once('include/markdown.php'); class Activity { @@ -67,10 +69,10 @@ class Activity { if ($j) { xchan_query($j, true); $items = fetch_post_tags($j); - } - if ($items) { - return self::encode_item(array_shift($items)); + if ($items) { + return self::encode_item(array_shift($items)); + } } return null; @@ -164,7 +166,7 @@ class Activity { } else { logger('fetch failed: ' . $url); - logger($x['body']); + logger(print_r($x, true), LOGGER_DEBUG); } @@ -503,15 +505,21 @@ class Activity { $ret['diaspora:guid'] = $i['uuid']; $images = []; + $audios = []; + $videos = []; + $has_images = preg_match_all('/\[[zi]mg(.*?)](.*?)\[/ism', $i['body'], $images, PREG_SET_ORDER); + $has_audios = preg_match_all('/\[zaudio](.*?)\[/ism', $i['body'], $audios, PREG_SET_ORDER); + $has_videos = preg_match_all('/\[zvideo](.*?)\[/ism', $i['body'], $videos, PREG_SET_ORDER); // provide ocap access token for private media. // set this for descendants even if the current item is not private // because it may have been relayed from a private item. $token = IConfig::Get($i, 'ocap', 'relay'); + $matches_processed = []; + if ($token && $has_images) { - $matches_processed = []; for ($n = 0; $n < count($images); $n++) { $match = $images[$n]; if (str_starts_with($match[1], '=http') && str_contains($match[1], z_root() . '/photo/') && !in_array($match[1], $matches_processed)) { @@ -526,6 +534,28 @@ class Activity { } } + if ($token && $has_audios) { + for ($n = 0; $n < count($audios); $n++) { + $match = $audios[$n]; + if (str_contains($match[1], z_root() . '/attach/') && !in_array($match[1], $matches_processed)) { + $i['body'] = str_replace($match[1], $match[1] . '?token=' . $token, $i['body']); + $audios[$n][1] = $match[1] . '?token=' . $token; + $matches_processed[] = $match[1]; + } + } + } + + if ($token && $has_videos) { + for ($n = 0; $n < count($videos); $n++) { + $match = $videos[$n]; + if (str_contains($match[1], z_root() . '/attach/') && !in_array($match[1], $matches_processed)) { + $i['body'] = str_replace($match[1], $match[1] . '?token=' . $token, $i['body']); + $videos[$n][1] = $match[1] . '?token=' . $token; + $matches_processed[] = $match[1]; + } + } + } + if ($i['title']) $ret['name'] = unescape_tags($i['title']); @@ -551,14 +581,12 @@ class Activity { } } - if (intval($i['item_wall'])) { - $ret['commentPolicy'] = map_scope(PermissionLimits::Get($i['uid'], 'post_comments')); - } - if (intval($i['item_private']) === 2) { $ret['directMessage'] = true; } + $ret['commentPolicy'] = (($i['item_wall']) ? map_scope(PermissionLimits::Get($i['uid'], 'post_comments')) : ''); + if (array_key_exists('comments_closed', $i) && $i['comments_closed'] !== EMPTY_STR && $i['comments_closed'] > NULL_DATE) { if ($ret['commentPolicy']) { $ret['commentPolicy'] .= ' '; @@ -570,6 +598,26 @@ 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['contextHistory'] = $cnv; + } + $ret['context'] = $cnv; } if ($i['mimetype'] === 'text/bbcode') { @@ -589,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; } @@ -596,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]; } @@ -627,7 +694,7 @@ class Activity { if (is_array($t) && !array_key_exists('type', $t)) $t['type'] = 'Hashtag'; - if (is_array($t) && (array_key_exists('href', $t) || array_key_exists('id', $t)) && array_key_exists('name', $t)) { + if (is_array($t) && (array_key_exists('href', $t) || array_key_exists('id', $t) || isset($t['icon']['url'])) && array_key_exists('name', $t)) { switch ($t['type']) { case 'Hashtag': $ret[] = ['ttype' => TERM_HASHTAG, 'url' => $t['href'], 'term' => escape_tags((substr($t['name'], 0, 1) === '#') ? substr($t['name'], 1) : $t['name'])]; @@ -642,7 +709,7 @@ class Activity { break; case 'Emoji': - $ret[] = ['ttype' => TERM_EMOJI, 'url' => $t['id'], 'term' => escape_tags($t['name']), 'imgurl' => $t['icon']['url']]; + $ret[] = ['ttype' => TERM_EMOJI, 'url' => $t['id'] ?? $t['icon']['url'], 'term' => escape_tags($t['name']), 'imgurl' => $t['icon']['url']]; break; default: @@ -694,6 +761,8 @@ class Activity { $ret = []; + $token = IConfig::Get($item, 'ocap', 'relay'); + if (!$iconfig && array_key_exists('attach', $item)) { $atts = ((is_array($item['attach'])) ? $item['attach'] : json_decode($item['attach'], true)); if ($atts) { @@ -702,11 +771,17 @@ class Activity { continue; } - if (isset($att['type']) && strpos($att['type'], 'image')) { - $ret[] = ['type' => 'Image', 'mediaType' => $att['type'], 'name' => $att['title'], 'url' => $att['href']]; + if (str_starts_with($att['type'], 'image')) { + $ret[] = ['type' => 'Image', 'mediaType' => $att['type'], 'name' => $att['title'], 'url' => $att['href'] . (($token) ? '?token=' . $token : '')]; + } + elseif (str_starts_with($att['type'], 'audio')) { + $ret[] = ['type' => 'Audio', 'mediaType' => $att['type'], 'name' => $att['title'], 'url' => $att['href'] . (($token) ? '?token=' . $token : '')]; + } + elseif (str_starts_with($att['type'], 'video')) { + $ret[] = ['type' => 'Video', 'mediaType' => $att['type'], 'name' => $att['title'], 'url' => $att['href'] . (($token) ? '?token=' . $token : '')]; } else { - $ret[] = ['type' => 'Link', 'mediaType' => $att['type'], 'name' => $att['title'], 'href' => $att['href']]; + $ret[] = ['type' => 'Link', 'mediaType' => $att['type'], 'name' => $att['title'], 'href' => $att['href'] . (($token) ? '?token=' . $token : '')]; } } } @@ -727,7 +802,7 @@ class Activity { $ret = []; - if (isset($item['attachment'])) { + if (isset($item['attachment']) && is_array($item['attachment'])) { $ptr = $item['attachment']; if (!array_key_exists(0, $ptr)) { $ptr = [$ptr]; @@ -777,6 +852,8 @@ class Activity { $entry['type'] = $att['mediaType']; } elseif (array_key_exists('type', $att) && $att['type'] === 'Image') { $entry['type'] = 'image/jpeg'; + } elseif (array_key_exists('type', $att) && $att['type'] === 'Link') { + $entry['type'] = 'text/uri-list'; } if (array_key_exists('name', $att) && $att['name']) { $entry['name'] = html2plain(purify_html($att['name']), 256); @@ -807,12 +884,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'; @@ -846,36 +943,8 @@ class Activity { } - if ($ret['type'] === 'emojiReaction') { - // There may not be an object for these items for legacy reasons - it should be the conversation parent. - $p = q("select * from item where mid = '%s' and uid = %d", - dbesc($i['parent_mid']), - intval($i['uid']) - ); - if ($p) { - xchan_query($p, true); - $p = fetch_post_tags($p); - $i['obj'] = self::encode_item($p[0]); - - // convert to zot6 emoji reaction encoding which uses the target object to indicate the - // specific emoji instead of overloading the verb or type. - - $im = explode('#', $i['verb']); - if ($im && count($im) > 1) - $emoji = $im[1]; - if (preg_match("/\[img(.*?)\](.*?)\[\/img\]/ism", $i['body'], $match)) { - $ln = $match[2]; - } - - $i['tgt_type'] = 'Image'; - - $i['target'] = [ - 'type' => 'Image', - 'name' => $emoji, - 'url' => (($ln) ? $ln : z_root() . '/images/emoji/' . $emoji . '.png') - ]; - - } + if ($ret['type'] === 'EmojiReact') { + $ret['content'] = $i['body']; } if (strpos($i['mid'], z_root() . '/item/') !== false) { @@ -890,15 +959,15 @@ class Activity { $ret['diaspora:guid'] = $i['uuid']; - if (isset($i['title']) && $i['title']) - $ret['name'] = html2plain(bbcode($i['title'], ['cache' => true])); + if (!empty($i['title'])) + $ret['name'] = html2plain(bbcode($i['title'])); - if (isset($i['summary']) && $i['summary']) - $ret['summary'] = bbcode($i['summary'], ['cache' => true]); + if (!empty($i['summary'])) + $ret['summary'] = bbcode($i['summary']); if ($ret['type'] === 'Announce') { $tmp = preg_replace('/\[share(.*?)\[\/share\]/ism', EMPTY_STR, $i['body']); - $ret['content'] = bbcode($tmp, ['cache' => true]); + $ret['content'] = bbcode($tmp); $ret['source'] = [ 'content' => $i['body'], 'mediaType' => 'text/bbcode' @@ -914,7 +983,7 @@ class Activity { } } - if (isset($i['app']) && $i['app']) { + if (!empty($i['app'])) { $ret['generator'] = ['type' => 'Application', 'name' => $i['app']]; } if (!empty($i['location']) || !empty($i['coord'])) { @@ -935,9 +1004,29 @@ 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['contextHistory'] = $cnv; + } + $ret['context'] = $cnv; } $actor = self::encode_person($i['author'], false); @@ -946,7 +1035,7 @@ class Activity { else return []; - if (isset($i['obj']) && $i['obj']) { + if (!empty($i['obj'])) { if (!is_array($i['obj'])) { $i['obj'] = json_decode($i['obj'], true); } @@ -974,7 +1063,7 @@ class Activity { $ret['type'] = 'Invite'; } - if (isset($i['target']) && $i['target']) { + if (!empty($i['target'])) { if (!is_array($i['target'])) { $i['target'] = json_decode($i['target'], true); } @@ -985,12 +1074,10 @@ class Activity { return []; } -/* this should not be needed $t = self::encode_taxonomy($i); if ($t) { $ret['tag'] = $t; } -*/ $a = self::encode_attachment($i, true); if ($a) { @@ -1001,7 +1088,6 @@ class Activity { $ret['to'] = [ACTIVITY_PUBLIC_INBOX]; } - $hookinfo = [ 'item' => $i, 'encoded' => $ret @@ -1010,6 +1096,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. @@ -1253,81 +1340,6 @@ class Activity { // return false; } - static function activity_decode_mapper($verb) { - - $acts = [ - 'http://activitystrea.ms/schema/1.0/post' => 'Create', - // 'http://activitystrea.ms/schema/1.0/share' => 'Announce', - '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', - 'http://activitystrea.ms/schema/1.0/stop-following' => 'Unfollow', - 'http://purl.org/zot/activity/attendyes' => 'Accept', - 'http://purl.org/zot/activity/attendno' => 'Reject', - 'http://purl.org/zot/activity/attendmaybe' => 'TentativeAccept', - 'Announce' => 'Announce', - 'Invite' => 'Invite', - 'Delete' => 'Delete', - 'Undo' => 'Undo', - 'Add' => 'Add', - 'Remove' => 'Remove' - ]; - - call_hooks('activity_decode_mapper', $acts); - - foreach ($acts as $k => $v) { - if ($verb === $v) { - return $k; - } - } - - logger('Unmapped activity: ' . $verb); - return 'Create'; - - } - - static function activity_obj_decode_mapper($obj) { - - $objs = [ - 'http://activitystrea.ms/schema/1.0/note' => 'Note', - 'http://activitystrea.ms/schema/1.0/note' => 'Article', - '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://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' => 'Object', - 'http://purl.org/zot/activity/file' => 'zot:File', - 'http://purl.org/zot/activity/mood' => 'zot:Mood', - 'Invite' => 'Invite', - 'Question' => 'Question', - 'Document' => 'Document', - 'Audio' => 'Audio', - 'Video' => 'Video', - 'Delete' => 'Delete', - 'Undo' => 'Undo' - ]; - - call_hooks('activity_obj_decode_mapper', $objs); - - foreach ($objs as $k => $v) { - if ($obj === $v) { - return $k; - } - } - - logger('Unmapped activity object: ' . $obj); - return 'Note'; - } - static function activity_obj_mapper($obj) { $objs = [ @@ -1606,9 +1618,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']); @@ -1676,9 +1688,9 @@ class Activity { return; } - $name = $person_obj['name'] ?? ''; + $name = ((isset($person_obj['name'])) ? escape_tags($person_obj['name']) : ''); if (!$name) { - $name = $person_obj['preferredUsername'] ?? ''; + $name = ((isset($person_obj['preferredUsername'])) ? escape_tags($person_obj['preferredUsername']) : ''); } if (!$name) { $name = t('Unknown'); @@ -1687,13 +1699,11 @@ class Activity { $webfinger_addr = ((isset($person_obj['webfinger'])) ? str_replace('acct:', '', $person_obj['webfinger']) : ''); $hostname = ''; $baseurl = ''; - $site_url = ''; $m = parse_url($url); if ($m) { - $hostname = $m['host']; - $baseurl = $m['scheme'] . '://' . $m['host'] . ((isset($m['port'])) ? ':' . $m['port'] : ''); - $site_url = $m['scheme'] . '://' . $m['host']; + $hostname = unparse_url($m, ['host']); + $baseurl = unparse_url($m, ['scheme', 'host', 'port']); } if (!$webfinger_addr && !empty($person_obj['preferredUsername']) && $hostname) { @@ -1795,7 +1805,7 @@ class Activity { q("UPDATE site SET site_update = '%s', site_dead = 0 WHERE site_url = '%s'", dbesc(datetime_convert()), - dbesc($site_url) + dbesc($baseurl) ); // update existing xchan record @@ -1906,129 +1916,173 @@ class Activity { } - static function update_poll($item_id, $post) { + static function update_poll($pollItem, $response) { - $multi = false; - $mid = $post['mid']; - $content = $post['title']; + logger('updating poll'); - if (!$item_id) { + $multi = false; + + 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; - } + $edited = datetime_convert(); + $i[0]['edited'] = $edited; + + // 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($edited) + ); + + 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) { @@ -2064,35 +2118,25 @@ class Activity { $s['owner_xchan'] = $act->actor['id']; $s['author_xchan'] = $act->actor['id']; - $content = []; + $s['mid'] = self::getMessageID($act); - if (is_array($act->obj)) { - $content = self::get_content($act->obj); + if (!$s['mid']) { + return false; } - $s['mid'] = $act->objprop('id'); - - if (!$s['mid'] && is_string($act->obj)) { - $s['mid'] = $act->obj; - } + $s['uuid'] = self::getUUID($act); - // pleroma fetched activities - if (!$s['mid'] && isset($act->obj['data']['id'])) { - $s['mid'] = $act->obj['data']['id']; + if (!$s['uuid']) { + // If we have not found anything useful, create an uuid v5 from the mid + $s['uuid'] = uuid_from_url($s['mid']); } - if ($act->objprop('type') === 'Profile') { - $s['mid'] = $act->id; - } + $content = []; - if (!$s['mid']) { - return false; + if (is_array($act->obj)) { + $content = self::get_content($act->obj); } - // Friendica sends the diaspora guid in a nonstandard field via AP - // If no uuid is provided we will create an uuid v5 from the mid - $s['uuid'] = (($act->objprop('diaspora:guid')) ?: uuid_from_url($s['mid'])); - $s['parent_mid'] = $act->parent_id; if (array_key_exists('published', $act->data)) { @@ -2116,6 +2160,13 @@ class Activity { $s['expires'] = datetime_convert('UTC', 'UTC', $act->obj['expires']); } + if ($act->objprop('location')) { + $s['location'] = ((isset($act->objprop('location')['name'])) ? html2plain(purify_html($act->objprop('location')['name'])) : ''); + if (isset($act->objprop('location')['latitude'], $act->objprop('location')['longitude'])) { + $s['coord'] = floatval($act->objprop('location')['latitude']) . ' ' . floatval($act->objprop('location')['longitude']); + } + } + if (in_array($act->type, ['Invite', 'Create']) && $act->objprop('type') === 'Event') { $s['mid'] = $s['parent_mid'] = $act->id; } @@ -2124,23 +2175,8 @@ class Activity { $response_activity = true; - $s['mid'] = $act->id; - $s['uuid'] = ((!empty($act->data['diaspora:guid'])) ? $act->data['diaspora:guid'] : uuid_from_url($s['mid'])); - $s['parent_mid'] = $act->objprop('id') ?: $act->obj; -/* - if ($act->objprop('inReplyTo')) { - $s['parent_mid'] = $act->objprop('inReplyTo'); - } - - $s['thr_parent'] = $act->objprop('id') ?: $act->obj; - - if (empty($s['parent_mid']) || empty($s['thr_parent'])) { - logger('response activity without parent_mid or thr_parent'); - return; - } -*/ // over-ride the object timestamp with the activity if (isset($act->data['published'])) { @@ -2151,9 +2187,9 @@ class Activity { $s['edited'] = datetime_convert('UTC', 'UTC', $act->data['updated']); } - $obj_actor = $act->objprop('actor') ?: $act->get_actor('attributedTo', $act->obj); + $obj_actor = is_array($act->objprop('actor')) ? $act->objprop('actor') : $act->get_actor('attributedTo', $act->obj); - if (!isset($obj_actor['id'])) { + if (empty($obj_actor['id'])) { return false; } @@ -2189,12 +2225,8 @@ class Activity { $content['content'] = sprintf(t('🔁 Repeated %1$s\'s %2$s'), $mention, $act->obj['type']); } - // TODO: Deprecated - if ($act->type === 'emojiReaction') { - $content['content'] = (($act->tgt && $act->tgt['type'] === 'Image') ? '[img=32x32]' . $act->tgt['url'] . '[/img]' : '&#x' . $act->tgt['name'] . ';'); - } - if (in_array($act->type, ['EmojiReact'])) { + // Pleroma reactions $t = trim(self::get_textfield($act->data, 'content')); @@ -2218,6 +2250,8 @@ class Activity { if ($s['mid'] === $s['parent_mid']) { $s['item_thread_top'] = 1; + $s['item_nocomment'] = 0; + $s['comments_closed'] = NULL_DATE; // it is a parent node - decode the comment policy info if present if ($act->objprop('commentPolicy')) { @@ -2225,7 +2259,7 @@ class Activity { if ($until !== false) { $s['comments_closed'] = datetime_convert('UTC', 'UTC', substr($act->obj['commentPolicy'], $until + 6)); if ($s['comments_closed'] < datetime_convert()) { - $s['nocomment'] = true; + $s['item_nocomment'] = 1; } } @@ -2242,10 +2276,15 @@ class Activity { if (!array_key_exists('edited', $s)) $s['edited'] = $s['created']; - $s['title'] = (($response_activity) ? EMPTY_STR : self::bb_content($content, 'name')); - $s['summary'] = self::bb_content($content, 'summary'); + $s['title'] = (($response_activity) ? EMPTY_STR : html2plain($content['name'])); + $s['summary'] = (($content['summary'] !== $content['content']) ? html2plain($content['summary']) : ''); $s['body'] = ((self::bb_content($content, 'bbcode') && (!$response_activity)) ? self::bb_content($content, 'bbcode') : self::bb_content($content, 'content')); + // peertube quirks + if ($act->objprop('mediaType') === 'text/markdown') { + $s['body'] = markdown_to_bb($act->objprop('content')); + } + if ($act->objprop('quoteUrl')) { $quote_bbcode = self::get_quote_bbcode($act->obj['quoteUrl']); @@ -2277,6 +2316,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); @@ -2367,7 +2416,8 @@ class Activity { } } - $tag = (($poster) ? '[video poster="' . $poster . '"]' : '[video]' ); + $tag = (($poster) ? '[video poster=\'' . $poster . '\']' : '[video]' ); + $ptr = null; if ($act->objprop('url')) { @@ -2582,6 +2632,7 @@ class Activity { } } + if (!$ap_rawmsg && array_key_exists('signed', $raw_arr)) { // zap $ap_rawmsg = json_encode($act->data, JSON_UNESCAPED_SLASHES); @@ -2615,8 +2666,7 @@ class Activity { return $hookinfo['s']; } - - static function store($channel, $observer_hash, $act, $item, $fetch_parents = true, $force = false) { + static function store($channel, $observer_hash, $act, $item, $fetch_parents = true, $force = false, $is_collection_operation = false) { $is_sys_channel = is_sys_channel($channel['channel_id']); $is_child_node = false; $parent = null; @@ -2647,6 +2697,8 @@ class Activity { } $allowed = false; + $relay = false; + $permit_mentions = intval(PConfig::Get($channel['channel_id'], 'system','permit_all_mentions') && i_am_mentioned($channel, $item)); if ($is_child_node) { @@ -2673,13 +2725,22 @@ class Activity { return; } + $relay = $channel['channel_hash'] === $parent[0]['owner_xchan']; + + if (str_contains($parent[0]['tgt_type'], 'Collection') && !$relay && !$is_collection_operation) { + logger('not a collection activity'); + return; + } + if ($parent[0]['obj_type'] === 'Question') { if (in_array($item['obj_type'], ['Note', ACTIVITY_OBJ_COMMENT]) && $item['title'] && (!$item['body'])) { $item['obj_type'] = 'Answer'; + $item['item_hidden'] = 1; } } if ($parent[0]['item_wall']) { + // set the owner to the owner of the parent $item['owner_xchan'] = $parent[0]['owner_xchan']; @@ -2860,6 +2921,13 @@ 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['contextHistory'])) { + IConfig::Set($item, 'activitypub', 'context', $act->obj['contextHistory'], 1); + } + elseif (!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)) { @@ -2891,7 +2959,7 @@ class Activity { if (intval($parent[0]['item_private']) === 0) { if (intval($item['item_private'])) { - $item['item_restrict'] = $item['item_restrict'] | 1; + $item['item_restrict'] = ((isset($item['item_restrict'])) ? $item['item_restrict'] | 1 : 1); $item['allow_cid'] = '<' . $channel['channel_hash'] . '>'; $item['allow_gid'] = $item['deny_cid'] = $item['deny_gid'] = ''; } @@ -2908,7 +2976,7 @@ class Activity { } } } - +/* if (isset($item['term']) && !PConfig::Get($channel['channel_id'], 'system', 'no_smilies')) { foreach ($item['term'] as $t) { if ($t['ttype'] === TERM_EMOJI) { @@ -2922,6 +2990,7 @@ class Activity { } } } +*/ // TODO: not implemented // self::rewrite_mentions($item); @@ -2930,17 +2999,42 @@ class Activity { dbesc($item['mid']), intval($item['uid']) ); + if ($r) { if ($item['edited'] > $r[0]['edited']) { $item['id'] = $r[0]['id']; - $x = item_store_update($item); + $x = item_store_update($item, deliver: false); } else { return; } } else { - $x = item_store($item); + $x = item_store($item, deliver: false, addAndSync: false); + } + + if ($x['success']) { + if ($relay && $channel['channel_hash'] === $x['item']['owner_xchan'] && $x['item']['verb'] !== 'Add' && !$is_collection_operation) { + $approval = Activity::addToCollection($channel, $act->data, $x['item']['parent_mid'], $x['item'], deliver: false); + } + + if (check_item_source($channel['channel_id'], $x['item']) && in_array($x['item']['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) { + event_addtocal($x['item_id'], $channel['channel_id']); + } + + tag_deliver($channel['channel_id'], $x['item_id']); + + if ($relay && $is_child_node) { + // We are the owner of this conversation, so send all received comments back downstream + Master::Summon(['Notifier', 'comment-import', $x['item_id']]); + if (!empty($approval['item_id'])) { + Master::Summon(['Notifier', 'comment-import', $approval['item_id']]); + } + } + + send_status_notifications($x['item_id'], $x['item']); + + sync_an_item($channel['channel_id'], $x['item_id']); } if ($fetch_parents && $parent && !intval($parent[0]['item_private'])) { @@ -2967,28 +3061,6 @@ class Activity { } } } - - if ($x['success']) { - - if (check_item_source($channel['channel_id'], $x['item']) && in_array($x['item']['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) { - event_addtocal($x['item_id'], $channel['channel_id']); - } - - if ($is_child_node) { - if ($item['owner_xchan'] === $channel['channel_hash']) { - // We are the owner of this conversation, so send all received comments back downstream - Master::Summon(['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']); - } - } /** @@ -3278,10 +3350,10 @@ class Activity { if (array_key_exists('startTime', $act) && strpos($act['startTime'], -1, 1) === 'Z') { $adjust = true; $event['adjust'] = 1; - $event['dtstart'] = datetime_convert('UTC', 'UTC', $event['startTime'] . (($adjust) ? '' : 'Z')); + $event['dtstart'] = datetime_convert('UTC', 'UTC', $act['startTime'] . (($adjust) ? '' : 'Z')); } if (array_key_exists('endTime', $act)) { - $event['dtend'] = datetime_convert('UTC', 'UTC', $event['endTime'] . (($adjust) ? '' : 'Z')); + $event['dtend'] = datetime_convert('UTC', 'UTC', $act['endTime'] . (($adjust) ? '' : 'Z')); } else { $event['nofinish'] = true; @@ -3464,7 +3536,7 @@ class Activity { $ret[$collection] = $actor_record[$collection]; } } - if (array_path_exists('endpoints/sharedInbox', $actor_record) && $actor_record['endpoints']['sharedInbox']) { + if (!empty($actor_record['endpoints']['sharedInbox'])) { $ret['sharedInbox'] = $actor_record['endpoints']['sharedInbox']; } @@ -3567,6 +3639,8 @@ class Activity { return [ 'zot' => z_root() . '/apschema#', + + 'contextHistory' => 'https://w3id.org/fep/171b/contextHistory', 'schema' => 'http://schema.org#', 'ostatus' => 'http://ostatus.org#', 'diaspora' => 'https://diasporafoundation.org/ns/', @@ -3590,7 +3664,6 @@ class Activity { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'Hashtag' => 'as:Hashtag' - ]; } @@ -3669,5 +3742,111 @@ 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)) + ->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']) + ->setRestrict($sourceItem['item_restrict']) + ->setHidden($sourceItem['item_hidden']) + ->setDelayed($sourceItem['item_delayed']) + ->setUnpublished($sourceItem['item_unpublished']) + ->setBlocked($sourceItem['item_blocked']) + ->setType($sourceItem['item_type']) + ->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; + } + + + /** + * @brief Retrieves message ID from activity object. + * @param object $act Activity object + * @return string Message ID or empty string if not found + */ + public static function getMessageID($act): string + { + if (ActivityStreams::is_response_activity($act->type) || $act->objprop('type') === 'Profile') { + return $act->id; + } + + return $act->objprop('id', null) + ?? (is_string($act->obj) ? $act->obj : null) + ?? ''; + } + + /** + * @brief Retrieves the UUID from an activity object. + * @param object $act Activity object + * @return string UUID or empty string if not found + */ + public static function getUUID($act): string + { + if (ActivityStreams::is_response_activity($act->type)) { + return $act->data['uuid'] + ?? $act->data['diaspora:guid'] + ?? ''; + } + + return $act->objprop('uuid', null) + ?? $act->objprop('diaspora:guid', null) + ?? ''; + } } |