Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

764 lines
24 KiB

3 years ago
3 years ago
  1. <?php
  2. /**
  3. * @file src/Protocol/ActivityPub/Receiver.php
  4. */
  5. namespace Friendica\Protocol\ActivityPub;
  6. use Friendica\Database\DBA;
  7. use Friendica\Util\HTTPSignature;
  8. use Friendica\Core\Protocol;
  9. use Friendica\Model\Contact;
  10. use Friendica\Model\APContact;
  11. use Friendica\Model\Item;
  12. use Friendica\Model\User;
  13. use Friendica\Util\JsonLD;
  14. use Friendica\Util\LDSignature;
  15. use Friendica\Protocol\ActivityPub;
  16. use Friendica\Model\Conversation;
  17. use Friendica\Util\DateTimeFormat;
  18. /**
  19. * @brief ActivityPub Receiver Protocol class
  20. *
  21. * To-Do:
  22. * - Undo Announce
  23. *
  24. * Check what this is meant to do:
  25. * - Add
  26. * - Block
  27. * - Flag
  28. * - Remove
  29. * - Undo Block
  30. */
  31. class Receiver
  32. {
  33. const PUBLIC_COLLECTION = 'as:Public';
  34. const ACCOUNT_TYPES = ['as:Person', 'as:Organization', 'as:Service', 'as:Group', 'as:Application'];
  35. const CONTENT_TYPES = ['as:Note', 'as:Article', 'as:Video', 'as:Image', 'as:Event'];
  36. const ACTIVITY_TYPES = ['as:Like', 'as:Dislike', 'as:Accept', 'as:Reject', 'as:TentativeAccept'];
  37. /**
  38. * Checks if the web request is done for the AP protocol
  39. *
  40. * @return is it AP?
  41. */
  42. public static function isRequest()
  43. {
  44. return stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json') ||
  45. stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/ld+json');
  46. }
  47. /**
  48. * Checks incoming message from the inbox
  49. *
  50. * @param $body
  51. * @param $header
  52. * @param integer $uid User ID
  53. */
  54. public static function processInbox($body, $header, $uid)
  55. {
  56. $http_signer = HTTPSignature::getSigner($body, $header);
  57. if (empty($http_signer)) {
  58. logger('Invalid HTTP signature, message will be discarded.', LOGGER_DEBUG);
  59. return;
  60. } else {
  61. logger('HTTP signature is signed by ' . $http_signer, LOGGER_DEBUG);
  62. }
  63. $activity = json_decode($body, true);
  64. if (empty($activity)) {
  65. logger('Invalid body.', LOGGER_DEBUG);
  66. return;
  67. }
  68. $ldactivity = JsonLD::compact($activity);
  69. $actor = JsonLD::fetchElement($ldactivity, 'as:actor');
  70. logger('Message for user ' . $uid . ' is from actor ' . $actor, LOGGER_DEBUG);
  71. if (LDSignature::isSigned($activity)) {
  72. $ld_signer = LDSignature::getSigner($activity);
  73. if (empty($ld_signer)) {
  74. logger('Invalid JSON-LD signature from ' . $actor, LOGGER_DEBUG);
  75. }
  76. if (!empty($ld_signer && ($actor == $http_signer))) {
  77. logger('The HTTP and the JSON-LD signature belong to ' . $ld_signer, LOGGER_DEBUG);
  78. $trust_source = true;
  79. } elseif (!empty($ld_signer)) {
  80. logger('JSON-LD signature is signed by ' . $ld_signer, LOGGER_DEBUG);
  81. $trust_source = true;
  82. } elseif ($actor == $http_signer) {
  83. logger('Bad JSON-LD signature, but HTTP signer fits the actor.', LOGGER_DEBUG);
  84. $trust_source = true;
  85. } else {
  86. logger('Invalid JSON-LD signature and the HTTP signer is different.', LOGGER_DEBUG);
  87. $trust_source = false;
  88. }
  89. } elseif ($actor == $http_signer) {
  90. logger('Trusting post without JSON-LD signature, The actor fits the HTTP signer.', LOGGER_DEBUG);
  91. $trust_source = true;
  92. } else {
  93. logger('No JSON-LD signature, different actor.', LOGGER_DEBUG);
  94. $trust_source = false;
  95. }
  96. self::processActivity($ldactivity, $body, $uid, $trust_source);
  97. }
  98. /**
  99. * Fetches the object type for a given object id
  100. *
  101. * @param array $activity
  102. * @param string $object_id Object ID of the the provided object
  103. *
  104. * @return string with object type
  105. */
  106. private static function fetchObjectType($activity, $object_id)
  107. {
  108. if (!empty($activity['as:object'])) {
  109. $object_type = JsonLD::fetchElement($activity['as:object'], '@type');
  110. if (!empty($object_type)) {
  111. return $object_type;
  112. }
  113. }
  114. if (Item::exists(['uri' => $object_id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]])) {
  115. // We just assume "note" since it doesn't make a difference for the further processing
  116. return 'as:Note';
  117. }
  118. $profile = APContact::getByURL($object_id);
  119. if (!empty($profile['type'])) {
  120. return 'as:' . $profile['type'];
  121. }
  122. $data = ActivityPub::fetchContent($object_id);
  123. if (!empty($data)) {
  124. $object = JsonLD::compact($data);
  125. $type = JsonLD::fetchElement($object, '@type');
  126. if (!empty($type)) {
  127. return $type;
  128. }
  129. }
  130. return null;
  131. }
  132. /**
  133. * Prepare the object array
  134. *
  135. * @param array $activity
  136. * @param integer $uid User ID
  137. * @param $trust_source
  138. *
  139. * @return array with object data
  140. */
  141. private static function prepareObjectData($activity, $uid, &$trust_source)
  142. {
  143. $actor = JsonLD::fetchElement($activity, 'as:actor');
  144. if (empty($actor)) {
  145. logger('Empty actor', LOGGER_DEBUG);
  146. return [];
  147. }
  148. $type = JsonLD::fetchElement($activity, '@type');
  149. // Fetch all receivers from to, cc, bto and bcc
  150. $receivers = self::getReceivers($activity, $actor);
  151. // When it is a delivery to a personal inbox we add that user to the receivers
  152. if (!empty($uid)) {
  153. $owner = User::getOwnerDataById($uid);
  154. $additional = ['uid:' . $uid => $uid];
  155. $receivers = array_merge($receivers, $additional);
  156. }
  157. logger('Receivers: ' . json_encode($receivers), LOGGER_DEBUG);
  158. $object_id = JsonLD::fetchElement($activity, 'as:object');
  159. if (empty($object_id)) {
  160. logger('No object found', LOGGER_DEBUG);
  161. return [];
  162. }
  163. $object_type = self::fetchObjectType($activity, $object_id);
  164. // Fetch the content only on activities where this matters
  165. if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) {
  166. if ($type == 'as:Announce') {
  167. $trust_source = false;
  168. }
  169. $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source);
  170. if (empty($object_data)) {
  171. logger("Object data couldn't be processed", LOGGER_DEBUG);
  172. return [];
  173. }
  174. // We had been able to retrieve the object data - so we can trust the source
  175. $trust_source = true;
  176. } elseif (in_array($type, ['as:Like', 'as:Dislike'])) {
  177. // Create a mostly empty array out of the activity data (instead of the object).
  178. // This way we later don't have to check for the existence of ech individual array element.
  179. $object_data = self::processObject($activity);
  180. $object_data['name'] = $type;
  181. $object_data['author'] = JsonLD::fetchElement($activity, 'as:actor');
  182. $object_data['object_id'] = $object_id;
  183. $object_data['object_type'] = ''; // Since we don't fetch the object, we don't know the type
  184. } else {
  185. $object_data = [];
  186. $object_data['id'] = JsonLD::fetchElement($activity, '@id');
  187. $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object');
  188. $object_data['object_actor'] = JsonLD::fetchElement($activity['as:object'], 'as:actor');
  189. $object_data['object_object'] = JsonLD::fetchElement($activity['as:object'], 'as:object');
  190. $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type');
  191. // An Undo is done on the object of an object, so we need that type as well
  192. if ($type == 'as:Undo') {
  193. $object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object']);
  194. }
  195. }
  196. $object_data = self::addActivityFields($object_data, $activity);
  197. if (empty($object_data['object_type'])) {
  198. $object_data['object_type'] = $object_type;
  199. }
  200. $object_data['type'] = $type;
  201. $object_data['actor'] = $actor;
  202. $object_data['receiver'] = array_merge(defaults($object_data, 'receiver', []), $receivers);
  203. logger('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], LOGGER_DEBUG);
  204. return $object_data;
  205. }
  206. /**
  207. * Store the unprocessed data into the conversation table
  208. * This has to be done outside the regular function,
  209. * since we store everything - not only item posts.
  210. *
  211. * @param array $activity Array with activity data
  212. * @param string $body The raw message
  213. */
  214. private static function storeConversation($activity, $body)
  215. {
  216. if (empty($body) || empty($activity['id'])) {
  217. return;
  218. }
  219. $conversation = [
  220. 'protocol' => Conversation::PARCEL_ACTIVITYPUB,
  221. 'item-uri' => $activity['id'],
  222. 'reply-to-uri' => defaults($activity, 'reply-to-id', ''),
  223. 'conversation-href' => defaults($activity, 'context', ''),
  224. 'conversation-uri' => defaults($activity, 'conversation', ''),
  225. 'source' => $body,
  226. 'received' => DateTimeFormat::utcNow()];
  227. DBA::insert('conversation', $conversation, true);
  228. }
  229. /**
  230. * Processes the activity object
  231. *
  232. * @param array $activity Array with activity data
  233. * @param string $body
  234. * @param integer $uid User ID
  235. * @param boolean $trust_source Do we trust the source?
  236. */
  237. public static function processActivity($activity, $body = '', $uid = null, $trust_source = false)
  238. {
  239. $type = JsonLD::fetchElement($activity, '@type');
  240. if (!$type) {
  241. logger('Empty type', LOGGER_DEBUG);
  242. return;
  243. }
  244. if (!JsonLD::fetchElement($activity, 'as:object')) {
  245. logger('Empty object', LOGGER_DEBUG);
  246. return;
  247. }
  248. if (!JsonLD::fetchElement($activity, 'as:actor')) {
  249. logger('Empty actor', LOGGER_DEBUG);
  250. return;
  251. }
  252. // $trust_source is called by reference and is set to true if the content was retrieved successfully
  253. $object_data = self::prepareObjectData($activity, $uid, $trust_source);
  254. if (empty($object_data)) {
  255. logger('No object data found', LOGGER_DEBUG);
  256. return;
  257. }
  258. if (!$trust_source) {
  259. logger('No trust for activity type "' . $type . '", so we quit now.', LOGGER_DEBUG);
  260. return;
  261. }
  262. self::storeConversation($object_data, $body);
  263. // Internal flag for thread completion. See Processor.php
  264. if (!empty($activity['thread-completion'])) {
  265. $object_data['thread-completion'] = $activity['thread-completion'];
  266. }
  267. switch ($type) {
  268. case 'as:Create':
  269. case 'as:Announce':
  270. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  271. ActivityPub\Processor::createItem($object_data);
  272. }
  273. break;
  274. case 'as:Like':
  275. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  276. ActivityPub\Processor::createActivity($object_data, ACTIVITY_LIKE);
  277. }
  278. break;
  279. case 'as:Dislike':
  280. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  281. ActivityPub\Processor::createActivity($object_data, ACTIVITY_DISLIKE);
  282. }
  283. break;
  284. case 'as:TentativeAccept':
  285. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  286. ActivityPub\Processor::createActivity($object_data, ACTIVITY_ATTENDMAYBE);
  287. }
  288. break;
  289. case 'as:Update':
  290. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  291. ActivityPub\Processor::updateItem($object_data);
  292. } elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
  293. ActivityPub\Processor::updatePerson($object_data, $body);
  294. }
  295. break;
  296. case 'as:Delete':
  297. if ($object_data['object_type'] == 'as:Tombstone') {
  298. ActivityPub\Processor::deleteItem($object_data, $body);
  299. } elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
  300. ActivityPub\Processor::deletePerson($object_data, $body);
  301. }
  302. break;
  303. case 'as:Follow':
  304. if (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
  305. ActivityPub\Processor::followUser($object_data);
  306. }
  307. break;
  308. case 'as:Accept':
  309. if ($object_data['object_type'] == 'as:Follow') {
  310. ActivityPub\Processor::acceptFollowUser($object_data);
  311. } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  312. ActivityPub\Processor::createActivity($object_data, ACTIVITY_ATTEND);
  313. }
  314. break;
  315. case 'as:Reject':
  316. if ($object_data['object_type'] == 'as:Follow') {
  317. ActivityPub\Processor::rejectFollowUser($object_data);
  318. } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  319. ActivityPub\Processor::createActivity($object_data, ACTIVITY_ATTENDNO);
  320. }
  321. break;
  322. case 'as:Undo':
  323. if (($object_data['object_type'] == 'as:Follow') &&
  324. in_array($object_data['object_object_type'], self::ACCOUNT_TYPES)) {
  325. ActivityPub\Processor::undoFollowUser($object_data);
  326. } elseif (($object_data['object_type'] == 'as:Accept') &&
  327. in_array($object_data['object_object_type'], self::ACCOUNT_TYPES)) {
  328. ActivityPub\Processor::rejectFollowUser($object_data);
  329. } elseif (in_array($object_data['object_type'], self::ACTIVITY_TYPES) &&
  330. in_array($object_data['object_object_type'], self::CONTENT_TYPES)) {
  331. ActivityPub\Processor::undoActivity($object_data);
  332. }
  333. break;
  334. default:
  335. logger('Unknown activity: ' . $type . ' ' . $object_data['object_type'], LOGGER_DEBUG);
  336. break;
  337. }
  338. }
  339. /**
  340. * Fetch the receiver list from an activity array
  341. *
  342. * @param array $activity
  343. * @param string $actor
  344. *
  345. * @return array with receivers (user id)
  346. */
  347. private static function getReceivers($activity, $actor)
  348. {
  349. $receivers = [];
  350. // When it is an answer, we inherite the receivers from the parent
  351. $replyto = JsonLD::fetchElement($activity, 'as:inReplyTo');
  352. if (!empty($replyto)) {
  353. $parents = Item::select(['uid'], ['uri' => $replyto]);
  354. while ($parent = Item::fetch($parents)) {
  355. $receivers['uid:' . $parent['uid']] = $parent['uid'];
  356. }
  357. }
  358. if (!empty($actor)) {
  359. $profile = APContact::getByURL($actor);
  360. $followers = defaults($profile, 'followers', '');
  361. logger('Actor: ' . $actor . ' - Followers: ' . $followers, LOGGER_DEBUG);
  362. } else {
  363. logger('Empty actor', LOGGER_DEBUG);
  364. $followers = '';
  365. }
  366. foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) {
  367. $receiver_list = JsonLD::fetchElementArray($activity, $element);
  368. if (empty($receiver_list)) {
  369. continue;
  370. }
  371. foreach ($receiver_list as $receiver) {
  372. if ($receiver == self::PUBLIC_COLLECTION) {
  373. $receivers['uid:0'] = 0;
  374. }
  375. if (($receiver == self::PUBLIC_COLLECTION) && !empty($actor)) {
  376. // This will most likely catch all OStatus connections to Mastodon
  377. $condition = ['alias' => [$actor, normalise_link($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND]
  378. , 'archive' => false, 'pending' => false];
  379. $contacts = DBA::select('contact', ['uid'], $condition);
  380. while ($contact = DBA::fetch($contacts)) {
  381. if ($contact['uid'] != 0) {
  382. $receivers['uid:' . $contact['uid']] = $contact['uid'];
  383. }
  384. }
  385. DBA::close($contacts);
  386. }
  387. if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) {
  388. $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
  389. $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
  390. 'network' => $networks, 'archive' => false, 'pending' => false];
  391. $contacts = DBA::select('contact', ['uid'], $condition);
  392. while ($contact = DBA::fetch($contacts)) {
  393. if ($contact['uid'] != 0) {
  394. $receivers['uid:' . $contact['uid']] = $contact['uid'];
  395. }
  396. }
  397. DBA::close($contacts);
  398. continue;
  399. }
  400. $condition = ['self' => true, 'nurl' => normalise_link($receiver)];
  401. $contact = DBA::selectFirst('contact', ['uid'], $condition);
  402. if (!DBA::isResult($contact)) {
  403. continue;
  404. }
  405. $receivers['uid:' . $contact['uid']] = $contact['uid'];
  406. }
  407. }
  408. self::switchContacts($receivers, $actor);
  409. return $receivers;
  410. }
  411. /**
  412. * Switches existing contacts to ActivityPub
  413. *
  414. * @param integer $cid Contact ID
  415. * @param integer $uid User ID
  416. * @param string $url Profile URL
  417. */
  418. public static function switchContact($cid, $uid, $url)
  419. {
  420. $profile = ActivityPub::probeProfile($url);
  421. if (empty($profile)) {
  422. return;
  423. }
  424. logger('Switch contact ' . $cid . ' (' . $profile['url'] . ') for user ' . $uid . ' to ActivityPub');
  425. $photo = defaults($profile, 'photo', null);
  426. unset($profile['photo']);
  427. unset($profile['baseurl']);
  428. $profile['nurl'] = normalise_link($profile['url']);
  429. DBA::update('contact', $profile, ['id' => $cid]);
  430. Contact::updateAvatar($photo, $uid, $cid);
  431. // Send a new follow request to be sure that the connection still exists
  432. if (($uid != 0) && DBA::exists('contact', ['id' => $cid, 'rel' => [Contact::SHARING, Contact::FRIEND]])) {
  433. ActivityPub\Transmitter::sendActivity('Follow', $profile['url'], $uid);
  434. logger('Send a new follow request to ' . $profile['url'] . ' for user ' . $uid, LOGGER_DEBUG);
  435. }
  436. }
  437. /**
  438. *
  439. *
  440. * @param $receivers
  441. * @param $actor
  442. */
  443. private static function switchContacts($receivers, $actor)
  444. {
  445. if (empty($actor)) {
  446. return;
  447. }
  448. foreach ($receivers as $receiver) {
  449. $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'nurl' => normalise_link($actor)]);
  450. if (DBA::isResult($contact)) {
  451. self::switchContact($contact['id'], $receiver, $actor);
  452. }
  453. $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'alias' => [normalise_link($actor), $actor]]);
  454. if (DBA::isResult($contact)) {
  455. self::switchContact($contact['id'], $receiver, $actor);
  456. }
  457. }
  458. }
  459. /**
  460. *
  461. *
  462. * @param $object_data
  463. * @param array $activity
  464. *
  465. * @return
  466. */
  467. private static function addActivityFields($object_data, $activity)
  468. {
  469. if (!empty($activity['published']) && empty($object_data['published'])) {
  470. $object_data['published'] = JsonLD::fetchElement($activity, 'as:published', '@value');
  471. }
  472. if (!empty($activity['diaspora:guid']) && empty($object_data['diaspora:guid'])) {
  473. $object_data['diaspora:guid'] = JsonLD::fetchElement($activity, 'diaspora:guid');
  474. }
  475. $object_data['service'] = JsonLD::fetchElement($activity, 'as:instrument', 'as:name', '@type', 'as:Service');
  476. return $object_data;
  477. }
  478. /**
  479. * Fetches the object data from external ressources if needed
  480. *
  481. * @param string $object_id Object ID of the the provided object
  482. * @param array $object The provided object array
  483. * @param boolean $trust_source Do we trust the provided object?
  484. *
  485. * @return array with trusted and valid object data
  486. */
  487. private static function fetchObject($object_id, $object = [], $trust_source = false)
  488. {
  489. // By fetching the type we check if the object is complete.
  490. $type = JsonLD::fetchElement($object, '@type');
  491. if (!$trust_source || empty($type)) {
  492. $data = ActivityPub::fetchContent($object_id);
  493. if (!empty($data)) {
  494. $object = JsonLD::compact($data);
  495. logger('Fetched content for ' . $object_id, LOGGER_DEBUG);
  496. } else {
  497. logger('Empty content for ' . $object_id . ', check if content is available locally.', LOGGER_DEBUG);
  498. $item = Item::selectFirst([], ['uri' => $object_id]);
  499. if (!DBA::isResult($item)) {
  500. logger('Object with url ' . $object_id . ' was not found locally.', LOGGER_DEBUG);
  501. return false;
  502. }
  503. logger('Using already stored item for url ' . $object_id, LOGGER_DEBUG);
  504. $data = ActivityPub\Transmitter::createNote($item);
  505. $object = JsonLD::compact($data);
  506. }
  507. } else {
  508. logger('Using original object for url ' . $object_id, LOGGER_DEBUG);
  509. }
  510. $type = JsonLD::fetchElement($object, '@type');
  511. if (empty($type)) {
  512. logger('Empty type', LOGGER_DEBUG);
  513. return false;
  514. }
  515. if (in_array($type, self::CONTENT_TYPES)) {
  516. return self::processObject($object);
  517. }
  518. if ($type == 'as:Announce') {
  519. $object_id = JsonLD::fetchElement($object, 'object');
  520. if (empty($object_id)) {
  521. return false;
  522. }
  523. return self::fetchObject($object_id);
  524. }
  525. logger('Unhandled object type: ' . $type, LOGGER_DEBUG);
  526. }
  527. /**
  528. * Convert tags from JSON-LD format into a simplified format
  529. *
  530. * @param array $tags Tags in JSON-LD format
  531. *
  532. * @return array with tags in a simplified format
  533. */
  534. private static function processTags($tags)
  535. {
  536. $taglist = [];
  537. if (empty($tags)) {
  538. return [];
  539. }
  540. foreach ($tags as $tag) {
  541. if (empty($tag)) {
  542. continue;
  543. }
  544. $taglist[] = ['type' => str_replace('as:', '', JsonLD::fetchElement($tag, '@type')),
  545. 'href' => JsonLD::fetchElement($tag, 'as:href'),
  546. 'name' => JsonLD::fetchElement($tag, 'as:name')];
  547. }
  548. return $taglist;
  549. }
  550. /**
  551. * Convert attachments from JSON-LD format into a simplified format
  552. *
  553. * @param array $attachments Attachments in JSON-LD format
  554. *
  555. * @return array with attachmants in a simplified format
  556. */
  557. private static function processAttachments($attachments)
  558. {
  559. $attachlist = [];
  560. if (empty($attachments)) {
  561. return [];
  562. }
  563. foreach ($attachments as $attachment) {
  564. if (empty($attachment)) {
  565. continue;
  566. }
  567. $attachlist[] = ['type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')),
  568. 'mediaType' => JsonLD::fetchElement($attachment, 'as:mediaType'),
  569. 'name' => JsonLD::fetchElement($attachment, 'as:name'),
  570. 'url' => JsonLD::fetchElement($attachment, 'as:url')];
  571. }
  572. return $attachlist;
  573. }
  574. /**
  575. * Fetches data from the object part of an activity
  576. *
  577. * @param array $object
  578. *
  579. * @return array
  580. */
  581. private static function processObject($object)
  582. {
  583. if (!JsonLD::fetchElement($object, '@id')) {
  584. return false;
  585. }
  586. $object_data = [];
  587. $object_data['object_type'] = JsonLD::fetchElement($object, '@type');
  588. $object_data['id'] = JsonLD::fetchElement($object, '@id');
  589. $object_data['reply-to-id'] = JsonLD::fetchElement($object, 'as:inReplyTo');
  590. if (empty($object_data['reply-to-id'])) {
  591. $object_data['reply-to-id'] = $object_data['id'];
  592. }
  593. $object_data['published'] = JsonLD::fetchElement($object, 'as:published', '@value');
  594. $object_data['updated'] = JsonLD::fetchElement($object, 'as:updated', '@value');
  595. if (empty($object_data['updated'])) {
  596. $object_data['updated'] = $object_data['published'];
  597. }
  598. if (empty($object_data['published']) && !empty($object_data['updated'])) {
  599. $object_data['published'] = $object_data['updated'];
  600. }
  601. $actor = JsonLD::fetchElement($object, 'as:attributedTo');
  602. if (empty($actor)) {
  603. $actor = JsonLD::fetchElement($object, 'as:actor');
  604. }
  605. $object_data['diaspora:guid'] = JsonLD::fetchElement($object, 'diaspora:guid');
  606. $object_data['diaspora:comment'] = JsonLD::fetchElement($object, 'diaspora:comment');
  607. $object_data['actor'] = $object_data['author'] = $actor;
  608. $object_data['context'] = JsonLD::fetchElement($object, 'as:context');
  609. $object_data['conversation'] = JsonLD::fetchElement($object, 'ostatus:conversation');
  610. $object_data['sensitive'] = JsonLD::fetchElement($object, 'as:sensitive');
  611. $object_data['name'] = JsonLD::fetchElement($object, 'as:name');
  612. $object_data['summary'] = JsonLD::fetchElement($object, 'as:summary');
  613. $object_data['content'] = JsonLD::fetchElement($object, 'as:content');
  614. $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/bbcode');
  615. $object_data['start-time'] = JsonLD::fetchElement($object, 'as:startTime', '@value');
  616. $object_data['end-time'] = JsonLD::fetchElement($object, 'as:endTime', '@value');
  617. $object_data['location'] = JsonLD::fetchElement($object, 'as:location', 'as:name', '@type', 'as:Place');
  618. $object_data['latitude'] = JsonLD::fetchElement($object, 'as:location', 'as:latitude', '@type', 'as:Place');
  619. $object_data['latitude'] = JsonLD::fetchElement($object_data, 'latitude', '@value');
  620. $object_data['longitude'] = JsonLD::fetchElement($object, 'as:location', 'as:longitude', '@type', 'as:Place');
  621. $object_data['longitude'] = JsonLD::fetchElement($object_data, 'longitude', '@value');
  622. $object_data['attachments'] = self::processAttachments(JsonLD::fetchElementArray($object, 'as:attachment'));
  623. $object_data['tags'] = self::processTags(JsonLD::fetchElementArray($object, 'as:tag'));
  624. $object_data['generator'] = JsonLD::fetchElement($object, 'as:generator', 'as:name', '@type', 'as:Application');
  625. $object_data['alternate-url'] = JsonLD::fetchElement($object, 'as:url');
  626. // Special treatment for Hubzilla links
  627. if (is_array($object_data['alternate-url'])) {
  628. $object_data['alternate-url'] = JsonLD::fetchElement($object_data['alternate-url'], 'as:href');
  629. if (!is_string($object_data['alternate-url'])) {
  630. $object_data['alternate-url'] = JsonLD::fetchElement($object['as:url'], 'as:href');
  631. }
  632. }
  633. $object_data['receiver'] = self::getReceivers($object, $object_data['actor']);
  634. // Common object data:
  635. // Unhandled
  636. // @context, type, actor, signature, mediaType, duration, replies, icon
  637. // Also missing: (Defined in the standard, but currently unused)
  638. // audience, preview, endTime, startTime, image
  639. // Data in Notes:
  640. // Unhandled
  641. // contentMap, announcement_count, announcements, context_id, likes, like_count
  642. // inReplyToStatusId, shares, quoteUrl, statusnetConversationId
  643. // Data in video:
  644. // To-Do?
  645. // category, licence, language, commentsEnabled
  646. // Unhandled
  647. // views, waitTranscoding, state, support, subtitleLanguage
  648. // likes, dislikes, shares, comments
  649. return $object_data;
  650. }
  651. }