X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FTransmitter.php;h=2be621cb93e9b09ec4c28b570880e32c6a14f1a9;hb=abed3ba90607392d0e84abf08e247bf7bea437dd;hp=a3b8b679e3b9d0350fe6704880a49ea43992ab23;hpb=c1f99c70b1c7d62120723f3b142e843ba25ab338;p=friendica.git diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index a3b8b679e3..2be621cb93 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -6,30 +6,32 @@ namespace Friendica\Protocol\ActivityPub; use Friendica\BaseObject; use Friendica\Content\Feature; -use Friendica\Database\DBA; +use Friendica\Content\Text\BBCode; +use Friendica\Content\Text\Plaintext; +use Friendica\Core\Cache; use Friendica\Core\Config; use Friendica\Core\Logger; -use Friendica\Core\System; -use Friendica\Util\HTTPSignature; use Friendica\Core\Protocol; -use Friendica\Model\Conversation; -use Friendica\Model\Contact; +use Friendica\Core\System; +use Friendica\Database\DBA; use Friendica\Model\APContact; +use Friendica\Model\Contact; +use Friendica\Model\Conversation; use Friendica\Model\Item; +use Friendica\Model\Profile; +use Friendica\Model\Photo; use Friendica\Model\Term; use Friendica\Model\User; +use Friendica\Protocol\Activity; +use Friendica\Protocol\ActivityPub; use Friendica\Util\DateTimeFormat; -use Friendica\Content\Text\BBCode; -use Friendica\Content\Text\Plaintext; -use Friendica\Util\XML; +use Friendica\Util\HTTPSignature; +use Friendica\Util\Images; use Friendica\Util\JsonLD; use Friendica\Util\LDSignature; -use Friendica\Model\Profile; -use Friendica\Object\Image; -use Friendica\Protocol\ActivityPub; -use Friendica\Core\Cache; use Friendica\Util\Map; use Friendica\Util\Network; +use Friendica\Util\XML; require_once 'include/api.php'; require_once 'mod/share.php'; @@ -71,6 +73,7 @@ class Transmitter if (empty($page)) { $data['first'] = System::baseUrl() . '/followers/' . $owner['nickname'] . '?page=1'; } else { + $data['type'] = 'OrderedCollectionPage'; $list = []; $contacts = DBA::select('contact', ['url'], $condition, ['limit' => [($page - 1) * 100, 100]]); @@ -119,6 +122,7 @@ class Transmitter if (empty($page)) { $data['first'] = System::baseUrl() . '/following/' . $owner['nickname'] . '?page=1'; } else { + $data['type'] = 'OrderedCollectionPage'; $list = []; $contacts = DBA::select('contact', ['url'], $condition, ['limit' => [($page - 1) * 100, 100]]); @@ -165,15 +169,18 @@ class Transmitter if (empty($page)) { $data['first'] = System::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=1'; } else { + $data['type'] = 'OrderedCollectionPage'; $list = []; $condition['parent-network'] = Protocol::NATIVE_SUPPORT; $items = Item::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]); while ($item = Item::fetch($items)) { - $object = self::createObjectFromItemID($item['id']); - unset($object['@context']); - $list[] = $object; + $activity = self::createActivityFromItem($item['id'], true); + // Only list "Create" activity objects here, no reshares + if (is_array($activity['object']) && ($activity['type'] == 'Create')) { + $list[] = $activity['object']; + } } if (!empty($list)) { @@ -358,7 +365,7 @@ class Transmitter if (Config::get('debug', 'total_ap_delivery')) { // Will be activated in a later step - $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]; + $networks = Protocol::FEDERATED; } else { // For now only send to these contacts: $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS]; @@ -374,6 +381,15 @@ class Transmitter $terms = Term::tagArrayFromItemId($item['id'], [Term::MENTION, Term::IMPLICIT_MENTION]); + // Directly mention the original author upon a quoted reshare. + // Else just ensure that the original author receives the reshare. + $announce = self::getAnnounceObject($item); + if (!empty($announce['comment'])) { + $data['to'][] = $announce['actor']['url']; + } elseif (!empty($announce)) { + $data['cc'][] = $announce['actor']['url']; + } + if (!$item['private']) { $data = array_merge($data, self::fetchPermissionBlockFromConversation($item)); @@ -386,7 +402,7 @@ class Transmitter } } } else { - $receiver_list = Item::enumeratePermissions($item); + $receiver_list = Item::enumeratePermissions($item, true); foreach ($terms as $term) { $cid = Contact::getIdForURL($term['url'], $item['uid']); @@ -530,7 +546,7 @@ class Transmitter if (Config::get('debug', 'total_ap_delivery')) { // Will be activated in a later step - $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]; + $networks = Protocol::FEDERATED; } else { // For now only send to these contacts: $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS]; @@ -544,6 +560,10 @@ class Transmitter $contacts = DBA::select('contact', ['url', 'network', 'protocol'], $condition); while ($contact = DBA::fetch($contacts)) { + if (Contact::isLocal($contact['url'])) { + continue; + } + if (!in_array($contact['network'], $networks) && ($contact['protocol'] != Protocol::ACTIVITYPUB)) { continue; } @@ -611,6 +631,10 @@ class Transmitter if ($receiver == $item_profile['followers']) { $inboxes = array_merge($inboxes, self::fetchTargetInboxesforUser($uid, $personal)); } else { + if (Contact::isLocal($receiver)) { + continue; + } + $profile = APContact::getByURL($receiver, false); if (!empty($profile)) { if (empty($profile['sharedinbox']) || $personal || $blindcopy) { @@ -640,6 +664,9 @@ class Transmitter public static function ItemArrayFromMail($mail_id) { $mail = DBA::selectFirst('mail', [], ['id' => $mail_id]); + if (!DBA::isResult($mail)) { + return []; + } $reply = DBA::selectFirst('mail', ['uri'], ['parent-uri' => $mail['parent-uri'], 'reply' => false]); @@ -683,11 +710,6 @@ class Transmitter $mail = self::ItemArrayFromMail($mail_id); $object = self::createNote($mail); - $object['to'] = $object['cc']; - unset($object['cc']); - - $object['tag'] = [['type' => 'Mention', 'href' => $object['to'][0], 'name' => 'test']]; - if (!$object_mode) { $data = ['@context' => ActivityPub::CONTEXT]; } else { @@ -713,6 +735,8 @@ class Transmitter unset($data['bcc']); $object['to'] = $data['to']; + $object['tag'] = [['type' => 'Mention', 'href' => $object['to'][0], 'name' => '']]; + unset($object['cc']); unset($object['bcc']); @@ -744,31 +768,31 @@ class Transmitter // Only check for a reshare, if it is a real reshare and no quoted reshare if (strpos($item['body'], "[share") === 0) { - $announce = api_share_as_retweet($item); - $reshared = !empty($announce['plink']); + $announce = self::getAnnounceObject($item); + $reshared = !empty($announce); } if ($reshared) { $type = 'Announce'; - } elseif ($item['verb'] == ACTIVITY_POST) { + } elseif ($item['verb'] == Activity::POST) { if ($item['created'] == $item['edited']) { $type = 'Create'; } else { $type = 'Update'; } - } elseif ($item['verb'] == ACTIVITY_LIKE) { + } elseif ($item['verb'] == Activity::LIKE) { $type = 'Like'; - } elseif ($item['verb'] == ACTIVITY_DISLIKE) { + } elseif ($item['verb'] == Activity::DISLIKE) { $type = 'Dislike'; - } elseif ($item['verb'] == ACTIVITY_ATTEND) { + } elseif ($item['verb'] == Activity::ATTEND) { $type = 'Accept'; - } elseif ($item['verb'] == ACTIVITY_ATTENDNO) { + } elseif ($item['verb'] == Activity::ATTENDNO) { $type = 'Reject'; - } elseif ($item['verb'] == ACTIVITY_ATTENDMAYBE) { + } elseif ($item['verb'] == Activity::ATTENDMAYBE) { $type = 'TentativeAccept'; - } elseif ($item['verb'] == ACTIVITY_FOLLOW) { + } elseif ($item['verb'] == Activity::FOLLOW) { $type = 'Follow'; - } elseif ($item['verb'] == ACTIVITY_TAG) { + } elseif ($item['verb'] == Activity::TAG) { $type = 'Add'; } else { $type = ''; @@ -906,29 +930,6 @@ class Transmitter /// @todo Create "conversation" entry } - /** - * Creates an object array for a given item id - * - * @param integer $item_id - * - * @return array with the object data - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function createObjectFromItemID($item_id) - { - $item = Item::selectFirst([], ['id' => $item_id, 'parent-network' => Protocol::NATIVE_SUPPORT]); - - if (!DBA::isResult($item)) { - return false; - } - - $data = ['@context' => ActivityPub::CONTEXT]; - $data = array_merge($data, self::createNote($item)); - - return $data; - } - /** * Creates a location entry for a given item array * @@ -991,6 +992,13 @@ class Transmitter $tags[] = ['type' => 'Mention', 'href' => $term['url'], 'name' => $mention]; } } + + $announce = self::getAnnounceObject($item); + // Mention the original author upon commented reshares + if (!empty($announce['comment'])) { + $tags[] = ['type' => 'Mention', 'href' => $announce['actor']['url'], 'name' => '@' . $announce['actor']['addr']]; + } + return $tags; } @@ -1007,6 +1015,37 @@ class Transmitter { $attachments = []; + // Currently deactivated, since it creates side effects on Mastodon and Pleroma. + // It will be reactivated, once this cleared. + /* + $attach_data = BBCode::getAttachmentData($item['body']); + if (!empty($attach_data['url'])) { + $attachment = ['type' => 'Page', + 'mediaType' => 'text/html', + 'url' => $attach_data['url']]; + + if (!empty($attach_data['title'])) { + $attachment['name'] = $attach_data['title']; + } + + if (!empty($attach_data['description'])) { + $attachment['summary'] = $attach_data['description']; + } + + if (!empty($attach_data['image'])) { + $imgdata = Images::getInfoFromURLCached($attach_data['image']); + if ($imgdata) { + $attachment['icon'] = ['type' => 'Image', + 'mediaType' => $imgdata['mime'], + 'width' => $imgdata[0], + 'height' => $imgdata[1], + 'url' => $attach_data['image']]; + } + } + + $attachments[] = $attachment; + } + */ $arr = explode('[/attach],', $item['attach']); if (count($arr)) { foreach ($arr as $r) { @@ -1034,10 +1073,10 @@ class Transmitter // Simplify image codes $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $item['body']); - // Grab all pictures and create attachments out of them + // Grab all pictures without alternative descriptions and create attachments out of them if (preg_match_all("/\[img\]([^\[\]]*)\[\/img\]/Usi", $body, $pictures)) { foreach ($pictures[1] as $picture) { - $imgdata = Image::getInfoFromURL($picture); + $imgdata = Images::getInfoFromURLCached($picture); if ($imgdata) { $attachments[] = ['type' => 'Document', 'mediaType' => $imgdata['mime'], @@ -1047,6 +1086,19 @@ class Transmitter } } + // Grab all pictures with alternative description and create attachments out of them + if (preg_match_all("/\[img=([^\[\]]*)\]([^\[\]]*)\[\/img\]/Usi", $body, $pictures, PREG_SET_ORDER)) { + foreach ($pictures as $picture) { + $imgdata = Images::getInfoFromURLCached($picture[1]); + if ($imgdata) { + $attachments[] = ['type' => 'Document', + 'mediaType' => $imgdata['mime'], + 'url' => $picture[1], + 'name' => $picture[2]]; + } + } + } + return $attachments; } @@ -1072,19 +1124,34 @@ class Transmitter } /** - * Remove image elements and replaces them with links to the image + * Remove image elements since they are added as attachment * * @param string $body * - * @return string with replaced elements + * @return string with removed images */ private static function removePictures($body) { // Simplify image codes $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body); + $body = preg_replace("/\[img\=(.*?)\](.*?)\[\/img\]/ism", '[img]$1[/img]', $body); + + // Now remove local links + $body = preg_replace_callback( + '/\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]/Usi', + function ($match) { + // We remove the link when it is a link to a local photo page + if (Photo::isLocalPage($match[1])) { + return ''; + } + // otherwise we just return the link + return '[url]' . $match[1] . '[/url]'; + }, + $body + ); - $body = preg_replace("/\[url=([^\[\]]*)\]\[img\](.*)\[\/img\]\[\/url\]/Usi", '[url]$1[/url]', $body); - $body = preg_replace("/\[img\]([^\[\]]*)\[\/img\]/Usi", '[url]$1[/url]', $body); + // Remove all pictures + $body = preg_replace("/\[img\]([^\[\]]*)\[\/img\]/Usi", '', $body); return $body; } @@ -1162,6 +1229,10 @@ class Transmitter */ public static function createNote($item) { + if (empty($item)) { + return []; + } + if ($item['event-type'] == 'event') { $type = 'Event'; } elseif (!empty($item['title'])) { @@ -1229,6 +1300,13 @@ class Transmitter $data['content'] = BBCode::convert($body, false, 9); } + $regexp = "/[@!]\[url\=([^\[\]]*)\].*?\[\/url\]/ism"; + $richbody = preg_replace_callback($regexp, ['self', 'mentionCallback'], $item['body']); + $richbody = BBCode::removeAttachment($richbody); + + $data['contentMap']['text/html'] = BBCode::convert($richbody, false); + $data['contentMap']['text/markdown'] = BBCode::toMarkdown($item["body"]); + $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"]; if (!empty($item['signed_text']) && ($item['uri'] != $item['thr-parent'])) { @@ -1287,29 +1365,71 @@ class Transmitter */ private static function createAnnounce($item, $data) { - $announce = api_share_as_retweet($item); - if (empty($announce['plink'])) { + $orig_body = $item['body']; + $announce = self::getAnnounceObject($item); + if (empty($announce)) { $data['type'] = 'Create'; $data['object'] = self::createNote($item); return $data; } // Fetch the original id of the object - $activity = ActivityPub::fetchContent($announce['plink'], $item['uid']); + $activity = ActivityPub::fetchContent($announce['id'], $item['uid']); if (!empty($activity)) { $ldactivity = JsonLD::compact($activity); $id = JsonLD::fetchElement($ldactivity, '@id'); + $type = str_replace('as:', '', JsonLD::fetchElement($ldactivity, '@type')); if (!empty($id)) { - $data['object'] = $id; + if (empty($announce['comment'])) { + // Pure announce, without a quote + $data['type'] = 'Announce'; + $data['object'] = $id; + return $data; + } + + // Quote + $data['type'] = 'Create'; + $item['body'] = $announce['comment'] . "\n" . $id; + $data['object'] = self::createNote($item); + + /// @todo Finally descide how to implement this in AP. This is a possible way: + $data['object']['attachment'][] = ['type' => $type, 'id' => $id]; + + $data['object']['source']['content'] = $orig_body; return $data; } } + $item['body'] = $orig_body; $data['type'] = 'Create'; $data['object'] = self::createNote($item); return $data; } + /** + * Return announce related data if the item is an annunce + * + * @param array $item + * + * @return array + */ + public static function getAnnounceObject($item) + { + $announce = api_share_as_retweet($item); + if (empty($announce['plink'])) { + return []; + } + + /// @ToDo Check if the announced item is an AP object + + $profile = APContact::getByURL($announce['author-link'], false); + if (empty($profile)) { + return []; + } + + return ['id' => $announce['plink'], 'actor' => $profile, 'comment' => trim($announce['share-pre-body'])]; + } + /** * Creates an activity id for a given contact id * @@ -1529,7 +1649,7 @@ class Transmitter $uid = $first_user['uid']; } - $condition = ['verb' => ACTIVITY_FOLLOW, 'uid' => 0, 'parent-uri' => $object, + $condition = ['verb' => Activity::FOLLOW, 'uid' => 0, 'parent-uri' => $object, 'author-id' => Contact::getPublicIdByUserId($uid)]; if (Item::exists($condition)) { Logger::log('Follow for ' . $object . ' for user ' . $uid . ' does already exist.', Logger::DEBUG);