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.
 
 
 
 
 
 

1372 lines
46 KiB

  1. <?php
  2. /**
  3. * @copyright Copyright (C) 2020, Friendica
  4. *
  5. * @license GNU AGPL version 3 or any later version
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Affero General Public License as
  9. * published by the Free Software Foundation, either version 3 of the
  10. * License, or (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License
  18. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. *
  20. */
  21. namespace Friendica\Protocol\ActivityPub;
  22. use Friendica\Content\Text\BBCode;
  23. use Friendica\Database\DBA;
  24. use Friendica\Content\Text\HTML;
  25. use Friendica\Content\Text\Markdown;
  26. use Friendica\Core\Logger;
  27. use Friendica\Core\Protocol;
  28. use Friendica\Model\Contact;
  29. use Friendica\Model\APContact;
  30. use Friendica\Model\Item;
  31. use Friendica\Model\User;
  32. use Friendica\Protocol\Activity;
  33. use Friendica\Protocol\ActivityPub;
  34. use Friendica\Util\HTTPSignature;
  35. use Friendica\Util\JsonLD;
  36. use Friendica\Util\LDSignature;
  37. use Friendica\Util\Strings;
  38. /**
  39. * ActivityPub Receiver Protocol class
  40. *
  41. * To-Do:
  42. * @todo Undo Announce
  43. *
  44. * Check what this is meant to do:
  45. * - Add
  46. * - Block
  47. * - Flag
  48. * - Remove
  49. * - Undo Block
  50. */
  51. class Receiver
  52. {
  53. const PUBLIC_COLLECTION = 'as:Public';
  54. const ACCOUNT_TYPES = ['as:Person', 'as:Organization', 'as:Service', 'as:Group', 'as:Application'];
  55. const CONTENT_TYPES = ['as:Note', 'as:Article', 'as:Video', 'as:Image', 'as:Event', 'as:Audio'];
  56. const ACTIVITY_TYPES = ['as:Like', 'as:Dislike', 'as:Accept', 'as:Reject', 'as:TentativeAccept'];
  57. const TARGET_UNKNOWN = 0;
  58. const TARGET_TO = 1;
  59. const TARGET_CC = 2;
  60. const TARGET_BTO = 3;
  61. const TARGET_BCC = 4;
  62. const TARGET_FOLLOWER = 5;
  63. const TARGET_ANSWER = 6;
  64. const TARGET_GLOBAL = 7;
  65. /**
  66. * Checks if the web request is done for the AP protocol
  67. *
  68. * @return bool is it AP?
  69. */
  70. public static function isRequest()
  71. {
  72. return stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/activity+json') ||
  73. stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/ld+json');
  74. }
  75. /**
  76. * Checks incoming message from the inbox
  77. *
  78. * @param $body
  79. * @param $header
  80. * @param integer $uid User ID
  81. * @throws \Exception
  82. */
  83. public static function processInbox($body, $header, $uid)
  84. {
  85. $activity = json_decode($body, true);
  86. if (empty($activity)) {
  87. Logger::warning('Invalid body.');
  88. return;
  89. }
  90. $ldactivity = JsonLD::compact($activity);
  91. $actor = JsonLD::fetchElement($ldactivity, 'as:actor', '@id');
  92. $apcontact = APContact::getByURL($actor);
  93. if (!empty($apcontact) && ($apcontact['type'] == 'Application') && ($apcontact['nick'] == 'relay')) {
  94. self::processRelayPost($ldactivity);
  95. return;
  96. }
  97. $http_signer = HTTPSignature::getSigner($body, $header);
  98. if (empty($http_signer)) {
  99. Logger::warning('Invalid HTTP signature, message will be discarded.');
  100. return;
  101. } else {
  102. Logger::info('Valid HTTP signature', ['signer' => $http_signer]);
  103. }
  104. $signer = [$http_signer];
  105. Logger::info('Message for user ' . $uid . ' is from actor ' . $actor);
  106. if (LDSignature::isSigned($activity)) {
  107. $ld_signer = LDSignature::getSigner($activity);
  108. if (empty($ld_signer)) {
  109. Logger::log('Invalid JSON-LD signature from ' . $actor, Logger::DEBUG);
  110. } elseif ($ld_signer != $http_signer) {
  111. $signer[] = $ld_signer;
  112. }
  113. if (!empty($ld_signer && ($actor == $http_signer))) {
  114. Logger::log('The HTTP and the JSON-LD signature belong to ' . $ld_signer, Logger::DEBUG);
  115. $trust_source = true;
  116. } elseif (!empty($ld_signer)) {
  117. Logger::log('JSON-LD signature is signed by ' . $ld_signer, Logger::DEBUG);
  118. $trust_source = true;
  119. } elseif ($actor == $http_signer) {
  120. Logger::log('Bad JSON-LD signature, but HTTP signer fits the actor.', Logger::DEBUG);
  121. $trust_source = true;
  122. } else {
  123. Logger::log('Invalid JSON-LD signature and the HTTP signer is different.', Logger::DEBUG);
  124. $trust_source = false;
  125. }
  126. } elseif ($actor == $http_signer) {
  127. Logger::log('Trusting post without JSON-LD signature, The actor fits the HTTP signer.', Logger::DEBUG);
  128. $trust_source = true;
  129. } else {
  130. Logger::log('No JSON-LD signature, different actor.', Logger::DEBUG);
  131. $trust_source = false;
  132. }
  133. self::processActivity($ldactivity, $body, $uid, $trust_source, true, $signer);
  134. }
  135. /**
  136. * Process incoming posts from relays
  137. *
  138. * @param array $activity
  139. * @return void
  140. */
  141. private static function processRelayPost(array $activity)
  142. {
  143. $type = JsonLD::fetchElement($activity, '@type');
  144. if (!$type) {
  145. Logger::info('Empty type', ['activity' => $activity]);
  146. return;
  147. }
  148. if ($type != 'as:Announce') {
  149. Logger::info('Not an announcement', ['activity' => $activity]);
  150. return;
  151. }
  152. $object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
  153. if (empty($object_id)) {
  154. Logger::info('No object id found', ['activity' => $activity]);
  155. return;
  156. }
  157. Logger::info('Got relayed message id', ['id' => $object_id]);
  158. $item_id = Item::searchByLink($object_id);
  159. if ($item_id) {
  160. Logger::info('Relayed message already exists', ['id' => $object_id, 'item' => $item_id]);
  161. return;
  162. }
  163. Processor::fetchMissingActivity($object_id);
  164. $item_id = Item::searchByLink($object_id);
  165. if ($item_id) {
  166. Logger::info('Relayed message had been fetched and stored', ['id' => $object_id, 'item' => $item_id]);
  167. } else {
  168. Logger::notice('Relayed message had not been stored', ['id' => $object_id]);
  169. }
  170. }
  171. /**
  172. * Fetches the object type for a given object id
  173. *
  174. * @param array $activity
  175. * @param string $object_id Object ID of the the provided object
  176. * @param integer $uid User ID
  177. *
  178. * @return string with object type
  179. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  180. * @throws \ImagickException
  181. */
  182. private static function fetchObjectType($activity, $object_id, $uid = 0)
  183. {
  184. if (!empty($activity['as:object'])) {
  185. $object_type = JsonLD::fetchElement($activity['as:object'], '@type');
  186. if (!empty($object_type)) {
  187. return $object_type;
  188. }
  189. }
  190. if (Item::exists(['uri' => $object_id, 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]])) {
  191. // We just assume "note" since it doesn't make a difference for the further processing
  192. return 'as:Note';
  193. }
  194. $profile = APContact::getByURL($object_id);
  195. if (!empty($profile['type'])) {
  196. return 'as:' . $profile['type'];
  197. }
  198. $data = ActivityPub::fetchContent($object_id, $uid);
  199. if (!empty($data)) {
  200. $object = JsonLD::compact($data);
  201. $type = JsonLD::fetchElement($object, '@type');
  202. if (!empty($type)) {
  203. return $type;
  204. }
  205. }
  206. return null;
  207. }
  208. /**
  209. * Prepare the object array
  210. *
  211. * @param array $activity Array with activity data
  212. * @param integer $uid User ID
  213. * @param boolean $push Message had been pushed to our system
  214. * @param boolean $trust_source Do we trust the source?
  215. *
  216. * @return array with object data
  217. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  218. * @throws \ImagickException
  219. */
  220. public static function prepareObjectData($activity, $uid, $push, &$trust_source)
  221. {
  222. $id = JsonLD::fetchElement($activity, '@id');
  223. if (!empty($id) && !$trust_source) {
  224. $fetched_activity = ActivityPub::fetchContent($id, $uid ?? 0);
  225. if (!empty($fetched_activity)) {
  226. $object = JsonLD::compact($fetched_activity);
  227. $fetched_id = JsonLD::fetchElement($object, '@id');
  228. if ($fetched_id == $id) {
  229. Logger::info('Activity had been fetched successfully', ['id' => $id]);
  230. $trust_source = true;
  231. $activity = $object;
  232. } else {
  233. Logger::info('Activity id is not equal', ['id' => $id, 'fetched' => $fetched_id]);
  234. }
  235. } else {
  236. Logger::info('Activity could not been fetched', ['id' => $id]);
  237. }
  238. }
  239. $actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
  240. if (empty($actor)) {
  241. Logger::info('Empty actor', ['activity' => $activity]);
  242. return [];
  243. }
  244. $type = JsonLD::fetchElement($activity, '@type');
  245. // Fetch all receivers from to, cc, bto and bcc
  246. $receiverdata = self::getReceivers($activity, $actor);
  247. $receivers = $reception_types = [];
  248. foreach ($receiverdata as $key => $data) {
  249. $receivers[$key] = $data['uid'];
  250. $reception_types[$data['uid']] = $data['type'] ?? 0;
  251. }
  252. // When it is a delivery to a personal inbox we add that user to the receivers
  253. if (!empty($uid)) {
  254. $additional = ['uid:' . $uid => $uid];
  255. $receivers = array_merge($receivers, $additional);
  256. if (empty($reception_types[$uid]) || in_array($reception_types[$uid], [self::TARGET_UNKNOWN, self::TARGET_FOLLOWER, self::TARGET_ANSWER, self::TARGET_GLOBAL])) {
  257. $reception_types[$uid] = self::TARGET_BCC;
  258. }
  259. } else {
  260. // We possibly need some user to fetch private content,
  261. // so we fetch the first out ot the list.
  262. $uid = self::getFirstUserFromReceivers($receivers);
  263. }
  264. $object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
  265. if (empty($object_id)) {
  266. Logger::log('No object found', Logger::DEBUG);
  267. return [];
  268. }
  269. if (!is_string($object_id)) {
  270. Logger::info('Invalid object id', ['object' => $object_id]);
  271. return [];
  272. }
  273. $object_type = self::fetchObjectType($activity, $object_id, $uid);
  274. // Fetch the content only on activities where this matters
  275. if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) {
  276. // Always fetch on "Announce"
  277. $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source && ($type != 'as:Announce'), $uid);
  278. if (empty($object_data)) {
  279. Logger::log("Object data couldn't be processed", Logger::DEBUG);
  280. return [];
  281. }
  282. $object_data['object_id'] = $object_id;
  283. if ($type == 'as:Announce') {
  284. $object_data['push'] = false;
  285. } else {
  286. $object_data['push'] = $push;
  287. }
  288. // Test if it is an answer to a mail
  289. if (DBA::exists('mail', ['uri' => $object_data['reply-to-id']])) {
  290. $object_data['directmessage'] = true;
  291. } else {
  292. $object_data['directmessage'] = JsonLD::fetchElement($activity, 'litepub:directMessage');
  293. }
  294. } elseif (in_array($type, array_merge(self::ACTIVITY_TYPES, ['as:Follow'])) && in_array($object_type, self::CONTENT_TYPES)) {
  295. // Create a mostly empty array out of the activity data (instead of the object).
  296. // This way we later don't have to check for the existence of ech individual array element.
  297. $object_data = self::processObject($activity);
  298. $object_data['name'] = $type;
  299. $object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id');
  300. $object_data['object_id'] = $object_id;
  301. $object_data['object_type'] = ''; // Since we don't fetch the object, we don't know the type
  302. } elseif (in_array($type, ['as:Add'])) {
  303. $object_data = [];
  304. $object_data['id'] = JsonLD::fetchElement($activity, '@id');
  305. $object_data['target_id'] = JsonLD::fetchElement($activity, 'as:target', '@id');
  306. $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id');
  307. $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type');
  308. $object_data['object_content'] = JsonLD::fetchElement($activity['as:object'], 'as:content', '@type');
  309. } else {
  310. $object_data = [];
  311. $object_data['id'] = JsonLD::fetchElement($activity, '@id');
  312. $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id');
  313. $object_data['object_actor'] = JsonLD::fetchElement($activity['as:object'], 'as:actor', '@id');
  314. $object_data['object_object'] = JsonLD::fetchElement($activity['as:object'], 'as:object');
  315. $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type');
  316. // An Undo is done on the object of an object, so we need that type as well
  317. if (($type == 'as:Undo') && !empty($object_data['object_object'])) {
  318. $object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object'], $uid);
  319. }
  320. }
  321. $object_data = self::addActivityFields($object_data, $activity);
  322. if (empty($object_data['object_type'])) {
  323. $object_data['object_type'] = $object_type;
  324. }
  325. $object_data['type'] = $type;
  326. $object_data['actor'] = $actor;
  327. $object_data['item_receiver'] = $receivers;
  328. $object_data['receiver'] = array_merge($object_data['receiver'] ?? [], $receivers);
  329. $object_data['reception_type'] = array_merge($object_data['reception_type'] ?? [], $reception_types);
  330. $author = $object_data['author'] ?? $actor;
  331. if (!empty($author) && !empty($object_data['id'])) {
  332. $author_host = parse_url($author, PHP_URL_HOST);
  333. $id_host = parse_url($object_data['id'], PHP_URL_HOST);
  334. if ($author_host == $id_host) {
  335. Logger::info('Valid hosts', ['type' => $type, 'host' => $id_host]);
  336. } else {
  337. Logger::notice('Differing hosts on author and id', ['type' => $type, 'author' => $author_host, 'id' => $id_host]);
  338. $trust_source = false;
  339. }
  340. }
  341. Logger::log('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], Logger::DEBUG);
  342. return $object_data;
  343. }
  344. /**
  345. * Fetches the first user id from the receiver array
  346. *
  347. * @param array $receivers Array with receivers
  348. * @return integer user id;
  349. */
  350. public static function getFirstUserFromReceivers($receivers)
  351. {
  352. foreach ($receivers as $receiver) {
  353. if (!empty($receiver)) {
  354. return $receiver;
  355. }
  356. }
  357. return 0;
  358. }
  359. /**
  360. * Processes the activity object
  361. *
  362. * @param array $activity Array with activity data
  363. * @param string $body
  364. * @param integer $uid User ID
  365. * @param boolean $trust_source Do we trust the source?
  366. * @param boolean $push Message had been pushed to our system
  367. * @throws \Exception
  368. */
  369. public static function processActivity($activity, string $body = '', int $uid = null, bool $trust_source = false, bool $push = false, array $signer = [])
  370. {
  371. $type = JsonLD::fetchElement($activity, '@type');
  372. if (!$type) {
  373. Logger::info('Empty type', ['activity' => $activity]);
  374. return;
  375. }
  376. if (!JsonLD::fetchElement($activity, 'as:object', '@id')) {
  377. Logger::info('Empty object', ['activity' => $activity]);
  378. return;
  379. }
  380. $actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
  381. if (empty($actor)) {
  382. Logger::info('Empty actor', ['activity' => $activity]);
  383. return;
  384. }
  385. if (is_array($activity['as:object'])) {
  386. $attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo', '@id');
  387. } else {
  388. $attributed_to = '';
  389. }
  390. // Test the provided signatures against the actor and "attributedTo"
  391. if ($trust_source) {
  392. if (!empty($attributed_to) && !empty($actor)) {
  393. $trust_source = (in_array($actor, $signer) && in_array($attributed_to, $signer));
  394. } else {
  395. $trust_source = in_array($actor, $signer);
  396. }
  397. }
  398. // $trust_source is called by reference and is set to true if the content was retrieved successfully
  399. $object_data = self::prepareObjectData($activity, $uid, $push, $trust_source);
  400. if (empty($object_data)) {
  401. Logger::info('No object data found', ['activity' => $activity]);
  402. return;
  403. }
  404. if (!$trust_source) {
  405. Logger::info('Activity trust could not be achieved.', ['id' => $object_data['object_id'], 'type' => $type, 'signer' => $signer, 'actor' => $actor, 'attributedTo' => $attributed_to]);
  406. return;
  407. }
  408. if (!empty($body) && empty($object_data['raw'])) {
  409. $object_data['raw'] = $body;
  410. }
  411. // Internal flag for thread completion. See Processor.php
  412. if (!empty($activity['thread-completion'])) {
  413. $object_data['thread-completion'] = $activity['thread-completion'];
  414. }
  415. switch ($type) {
  416. case 'as:Create':
  417. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  418. $item = ActivityPub\Processor::createItem($object_data);
  419. ActivityPub\Processor::postItem($object_data, $item);
  420. }
  421. break;
  422. case 'as:Add':
  423. if ($object_data['object_type'] == 'as:tag') {
  424. ActivityPub\Processor::addTag($object_data);
  425. }
  426. break;
  427. case 'as:Announce':
  428. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  429. $object_data['thread-completion'] = true;
  430. $item = ActivityPub\Processor::createItem($object_data);
  431. if (empty($item)) {
  432. return;
  433. }
  434. $item['post-type'] = Item::PT_ANNOUNCEMENT;
  435. ActivityPub\Processor::postItem($object_data, $item);
  436. $announce_object_data = self::processObject($activity);
  437. $announce_object_data['name'] = $type;
  438. $announce_object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id');
  439. $announce_object_data['object_id'] = $object_data['object_id'];
  440. $announce_object_data['object_type'] = $object_data['object_type'];
  441. $announce_object_data['push'] = $push;
  442. if (!empty($body)) {
  443. $announce_object_data['raw'] = $body;
  444. }
  445. ActivityPub\Processor::createActivity($announce_object_data, Activity::ANNOUNCE);
  446. }
  447. break;
  448. case 'as:Like':
  449. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  450. ActivityPub\Processor::createActivity($object_data, Activity::LIKE);
  451. }
  452. break;
  453. case 'as:Dislike':
  454. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  455. ActivityPub\Processor::createActivity($object_data, Activity::DISLIKE);
  456. }
  457. break;
  458. case 'as:TentativeAccept':
  459. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  460. ActivityPub\Processor::createActivity($object_data, Activity::ATTENDMAYBE);
  461. }
  462. break;
  463. case 'as:Update':
  464. if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  465. ActivityPub\Processor::updateItem($object_data);
  466. } elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
  467. ActivityPub\Processor::updatePerson($object_data);
  468. }
  469. break;
  470. case 'as:Delete':
  471. if ($object_data['object_type'] == 'as:Tombstone') {
  472. ActivityPub\Processor::deleteItem($object_data);
  473. } elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
  474. ActivityPub\Processor::deletePerson($object_data);
  475. }
  476. break;
  477. case 'as:Follow':
  478. if (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
  479. ActivityPub\Processor::followUser($object_data);
  480. } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  481. $object_data['reply-to-id'] = $object_data['object_id'];
  482. ActivityPub\Processor::createActivity($object_data, Activity::FOLLOW);
  483. }
  484. break;
  485. case 'as:Accept':
  486. if ($object_data['object_type'] == 'as:Follow') {
  487. ActivityPub\Processor::acceptFollowUser($object_data);
  488. } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  489. ActivityPub\Processor::createActivity($object_data, Activity::ATTEND);
  490. }
  491. break;
  492. case 'as:Reject':
  493. if ($object_data['object_type'] == 'as:Follow') {
  494. ActivityPub\Processor::rejectFollowUser($object_data);
  495. } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
  496. ActivityPub\Processor::createActivity($object_data, Activity::ATTENDNO);
  497. }
  498. break;
  499. case 'as:Undo':
  500. if (($object_data['object_type'] == 'as:Follow') &&
  501. in_array($object_data['object_object_type'], self::ACCOUNT_TYPES)) {
  502. ActivityPub\Processor::undoFollowUser($object_data);
  503. } elseif (($object_data['object_type'] == 'as:Accept') &&
  504. in_array($object_data['object_object_type'], self::ACCOUNT_TYPES)) {
  505. ActivityPub\Processor::rejectFollowUser($object_data);
  506. } elseif (in_array($object_data['object_type'], self::ACTIVITY_TYPES) &&
  507. in_array($object_data['object_object_type'], self::CONTENT_TYPES)) {
  508. ActivityPub\Processor::undoActivity($object_data);
  509. }
  510. break;
  511. default:
  512. Logger::log('Unknown activity: ' . $type . ' ' . $object_data['object_type'], Logger::DEBUG);
  513. break;
  514. }
  515. }
  516. /**
  517. * Fetch the receiver list from an activity array
  518. *
  519. * @param array $activity
  520. * @param string $actor
  521. * @param array $tags
  522. * @param boolean $fetch_unlisted
  523. *
  524. * @return array with receivers (user id)
  525. * @throws \Exception
  526. */
  527. private static function getReceivers($activity, $actor, $tags = [], $fetch_unlisted = false)
  528. {
  529. $reply = $receivers = [];
  530. // When it is an answer, we inherite the receivers from the parent
  531. $replyto = JsonLD::fetchElement($activity, 'as:inReplyTo', '@id');
  532. if (!empty($replyto)) {
  533. $reply = [$replyto];
  534. // Fix possibly wrong item URI (could be an answer to a plink uri)
  535. $fixedReplyTo = Item::getURIByLink($replyto);
  536. if (!empty($fixedReplyTo)) {
  537. $reply[] = $fixedReplyTo;
  538. }
  539. }
  540. // Fetch all posts that refer to the object id
  541. $object_id = JsonLD::fetchElement($activity, 'as:object', '@id');
  542. if (!empty($object_id)) {
  543. $reply[] = $object_id;
  544. }
  545. if (!empty($reply)) {
  546. $parents = Item::select(['uid'], ['uri' => $reply]);
  547. while ($parent = Item::fetch($parents)) {
  548. $receivers['uid:' . $parent['uid']] = ['uid' => $parent['uid'], 'type' => self::TARGET_ANSWER];
  549. }
  550. }
  551. if (!empty($actor)) {
  552. $profile = APContact::getByURL($actor);
  553. $followers = $profile['followers'] ?? '';
  554. Logger::log('Actor: ' . $actor . ' - Followers: ' . $followers, Logger::DEBUG);
  555. } else {
  556. Logger::info('Empty actor', ['activity' => $activity]);
  557. $followers = '';
  558. }
  559. foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) {
  560. $receiver_list = JsonLD::fetchElementArray($activity, $element, '@id');
  561. if (empty($receiver_list)) {
  562. continue;
  563. }
  564. foreach ($receiver_list as $receiver) {
  565. if ($receiver == self::PUBLIC_COLLECTION) {
  566. $receivers['uid:0'] = ['uid' => 0, 'type' => self::TARGET_GLOBAL];
  567. }
  568. // Add receiver "-1" for unlisted posts
  569. if ($fetch_unlisted && ($receiver == self::PUBLIC_COLLECTION) && ($element == 'as:cc')) {
  570. $receivers['uid:-1'] = ['uid' => -1, 'type' => self::TARGET_GLOBAL];
  571. }
  572. // Fetch the receivers for the public and the followers collection
  573. if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) {
  574. $receivers = self::getReceiverForActor($actor, $tags, $receivers);
  575. continue;
  576. }
  577. // Fetching all directly addressed receivers
  578. $condition = ['self' => true, 'nurl' => Strings::normaliseLink($receiver)];
  579. $contact = DBA::selectFirst('contact', ['uid', 'contact-type'], $condition);
  580. if (!DBA::isResult($contact)) {
  581. continue;
  582. }
  583. // Check if the potential receiver is following the actor
  584. // Exception: The receiver is targetted via "to" or this is a comment
  585. if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) {
  586. $networks = Protocol::FEDERATED;
  587. $condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
  588. 'network' => $networks, 'archive' => false, 'pending' => false, 'uid' => $contact['uid']];
  589. // Forum posts are only accepted from forum contacts
  590. if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
  591. $condition['rel'] = [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER];
  592. }
  593. if (!DBA::exists('contact', $condition)) {
  594. continue;
  595. }
  596. }
  597. $type = $receivers['uid:' . $contact['uid']]['type'] ?? self::TARGET_UNKNOWN;
  598. if (in_array($type, [self::TARGET_UNKNOWN, self::TARGET_FOLLOWER, self::TARGET_ANSWER, self::TARGET_GLOBAL])) {
  599. switch ($element) {
  600. case 'as:to':
  601. $type = self::TARGET_TO;
  602. break;
  603. case 'as:cc':
  604. $type = self::TARGET_CC;
  605. break;
  606. case 'as:bto':
  607. $type = self::TARGET_BTO;
  608. break;
  609. case 'as:bcc':
  610. $type = self::TARGET_BCC;
  611. break;
  612. }
  613. $receivers['uid:' . $contact['uid']] = ['uid' => $contact['uid'], 'type' => $type];
  614. }
  615. }
  616. }
  617. self::switchContacts($receivers, $actor);
  618. return $receivers;
  619. }
  620. /**
  621. * Fetch the receiver list of a given actor
  622. *
  623. * @param string $actor
  624. * @param array $tags
  625. *
  626. * @return array with receivers (user id)
  627. * @throws \Exception
  628. */
  629. private static function getReceiverForActor($actor, $tags, $receivers)
  630. {
  631. $basecondition = ['rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER],
  632. 'network' => Protocol::FEDERATED, 'archive' => false, 'pending' => false];
  633. $condition = DBA::mergeConditions($basecondition, ['nurl' => Strings::normaliseLink($actor)]);
  634. $contacts = DBA::select('contact', ['uid', 'rel'], $condition);
  635. while ($contact = DBA::fetch($contacts)) {
  636. if (empty($receivers['uid:' . $contact['uid']]) && self::isValidReceiverForActor($contact, $actor, $tags)) {
  637. $receivers['uid:' . $contact['uid']] = ['uid' => $contact['uid'], 'type' => self::TARGET_FOLLOWER];
  638. }
  639. }
  640. DBA::close($contacts);
  641. // The queries are split because of performance issues
  642. $condition = DBA::mergeConditions($basecondition, ["`alias` IN (?, ?)", Strings::normaliseLink($actor), $actor]);
  643. $contacts = DBA::select('contact', ['uid', 'rel'], $condition);
  644. while ($contact = DBA::fetch($contacts)) {
  645. if (empty($receivers['uid:' . $contact['uid']]) && self::isValidReceiverForActor($contact, $actor, $tags)) {
  646. $receivers['uid:' . $contact['uid']] = ['uid' => $contact['uid'], 'type' => self::TARGET_FOLLOWER];
  647. }
  648. }
  649. DBA::close($contacts);
  650. return $receivers;
  651. }
  652. /**
  653. * Tests if the contact is a valid receiver for this actor
  654. *
  655. * @param array $contact
  656. * @param string $actor
  657. * @param array $tags
  658. *
  659. * @return bool with receivers (user id)
  660. * @throws \Exception
  661. */
  662. private static function isValidReceiverForActor($contact, $actor, $tags)
  663. {
  664. // Public contacts are no valid receiver
  665. if ($contact['uid'] == 0) {
  666. return false;
  667. }
  668. // Are we following the contact? Then this is a valid receiver
  669. if (in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) {
  670. return true;
  671. }
  672. // When the possible receiver isn't a community, then it is no valid receiver
  673. $owner = User::getOwnerDataById($contact['uid']);
  674. if (empty($owner) || ($owner['contact-type'] != Contact::TYPE_COMMUNITY)) {
  675. return false;
  676. }
  677. // Is the community account tagged?
  678. foreach ($tags as $tag) {
  679. if ($tag['type'] != 'Mention') {
  680. continue;
  681. }
  682. if ($tag['href'] == $owner['url']) {
  683. return true;
  684. }
  685. }
  686. return false;
  687. }
  688. /**
  689. * Switches existing contacts to ActivityPub
  690. *
  691. * @param integer $cid Contact ID
  692. * @param integer $uid User ID
  693. * @param string $url Profile URL
  694. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  695. * @throws \ImagickException
  696. */
  697. public static function switchContact($cid, $uid, $url)
  698. {
  699. if (DBA::exists('contact', ['id' => $cid, 'network' => Protocol::ACTIVITYPUB])) {
  700. Logger::info('Contact is already ActivityPub', ['id' => $cid, 'uid' => $uid, 'url' => $url]);
  701. return;
  702. }
  703. if (Contact::updateFromProbe($cid)) {
  704. Logger::info('Update was successful', ['id' => $cid, 'uid' => $uid, 'url' => $url]);
  705. }
  706. // Send a new follow request to be sure that the connection still exists
  707. if (($uid != 0) && DBA::exists('contact', ['id' => $cid, 'rel' => [Contact::SHARING, Contact::FRIEND], 'network' => Protocol::ACTIVITYPUB])) {
  708. Logger::info('Contact had been switched to ActivityPub. Sending a new follow request.', ['uid' => $uid, 'url' => $url]);
  709. ActivityPub\Transmitter::sendActivity('Follow', $url, $uid);
  710. }
  711. }
  712. /**
  713. *
  714. *
  715. * @param $receivers
  716. * @param $actor
  717. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  718. * @throws \ImagickException
  719. */
  720. private static function switchContacts($receivers, $actor)
  721. {
  722. if (empty($actor)) {
  723. return;
  724. }
  725. foreach ($receivers as $receiver) {
  726. $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'nurl' => Strings::normaliseLink($actor)]);
  727. if (DBA::isResult($contact)) {
  728. self::switchContact($contact['id'], $receiver['uid'], $actor);
  729. }
  730. $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'alias' => [Strings::normaliseLink($actor), $actor]]);
  731. if (DBA::isResult($contact)) {
  732. self::switchContact($contact['id'], $receiver['uid'], $actor);
  733. }
  734. }
  735. }
  736. /**
  737. *
  738. *
  739. * @param $object_data
  740. * @param array $activity
  741. *
  742. * @return mixed
  743. */
  744. private static function addActivityFields($object_data, $activity)
  745. {
  746. if (!empty($activity['published']) && empty($object_data['published'])) {
  747. $object_data['published'] = JsonLD::fetchElement($activity, 'as:published', '@value');
  748. }
  749. if (!empty($activity['diaspora:guid']) && empty($object_data['diaspora:guid'])) {
  750. $object_data['diaspora:guid'] = JsonLD::fetchElement($activity, 'diaspora:guid', '@value');
  751. }
  752. $object_data['service'] = JsonLD::fetchElement($activity, 'as:instrument', 'as:name', '@type', 'as:Service');
  753. $object_data['service'] = JsonLD::fetchElement($object_data, 'service', '@value');
  754. if (!empty($object_data['object_id'])) {
  755. // Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field.
  756. $objectId = Item::getURIByLink($object_data['object_id']);
  757. if (!empty($objectId) && ($object_data['object_id'] != $objectId)) {
  758. Logger::notice('Fix wrong object-id', ['received' => $object_data['object_id'], 'correct' => $objectId]);
  759. $object_data['object_id'] = $objectId;
  760. }
  761. }
  762. return $object_data;
  763. }
  764. /**
  765. * Fetches the object data from external ressources if needed
  766. *
  767. * @param string $object_id Object ID of the the provided object
  768. * @param array $object The provided object array
  769. * @param boolean $trust_source Do we trust the provided object?
  770. * @param integer $uid User ID for the signature that we use to fetch data
  771. *
  772. * @return array|false with trusted and valid object data
  773. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  774. * @throws \ImagickException
  775. */
  776. private static function fetchObject(string $object_id, array $object = [], bool $trust_source = false, int $uid = 0)
  777. {
  778. // By fetching the type we check if the object is complete.
  779. $type = JsonLD::fetchElement($object, '@type');
  780. if (!$trust_source || empty($type)) {
  781. $data = ActivityPub::fetchContent($object_id, $uid);
  782. if (!empty($data)) {
  783. $object = JsonLD::compact($data);
  784. Logger::log('Fetched content for ' . $object_id, Logger::DEBUG);
  785. } else {
  786. Logger::log('Empty content for ' . $object_id . ', check if content is available locally.', Logger::DEBUG);
  787. $item = Item::selectFirst([], ['uri' => $object_id]);
  788. if (!DBA::isResult($item)) {
  789. Logger::log('Object with url ' . $object_id . ' was not found locally.', Logger::DEBUG);
  790. return false;
  791. }
  792. Logger::log('Using already stored item for url ' . $object_id, Logger::DEBUG);
  793. $data = ActivityPub\Transmitter::createNote($item);
  794. $object = JsonLD::compact($data);
  795. }
  796. $id = JsonLD::fetchElement($object, '@id');
  797. if (empty($id)) {
  798. Logger::info('Empty id');
  799. return false;
  800. }
  801. if ($id != $object_id) {
  802. Logger::info('Fetched id differs from provided id', ['provided' => $object_id, 'fetched' => $id]);
  803. return false;
  804. }
  805. } else {
  806. Logger::log('Using original object for url ' . $object_id, Logger::DEBUG);
  807. }
  808. $type = JsonLD::fetchElement($object, '@type');
  809. if (empty($type)) {
  810. Logger::info('Empty type');
  811. return false;
  812. }
  813. // We currently don't handle 'pt:CacheFile', but with this step we avoid logging
  814. if (in_array($type, self::CONTENT_TYPES) || ($type == 'pt:CacheFile')) {
  815. $object_data = self::processObject($object);
  816. if (!empty($data)) {
  817. $object_data['raw'] = json_encode($data);
  818. }
  819. return $object_data;
  820. }
  821. if ($type == 'as:Announce') {
  822. $object_id = JsonLD::fetchElement($object, 'object', '@id');
  823. if (empty($object_id) || !is_string($object_id)) {
  824. return false;
  825. }
  826. return self::fetchObject($object_id, [], false, $uid);
  827. }
  828. Logger::log('Unhandled object type: ' . $type, Logger::DEBUG);
  829. return false;
  830. }
  831. /**
  832. * Convert tags from JSON-LD format into a simplified format
  833. *
  834. * @param array $tags Tags in JSON-LD format
  835. *
  836. * @return array with tags in a simplified format
  837. */
  838. private static function processTags(array $tags)
  839. {
  840. $taglist = [];
  841. foreach ($tags as $tag) {
  842. if (empty($tag)) {
  843. continue;
  844. }
  845. $element = ['type' => str_replace('as:', '', JsonLD::fetchElement($tag, '@type')),
  846. 'href' => JsonLD::fetchElement($tag, 'as:href', '@id'),
  847. 'name' => JsonLD::fetchElement($tag, 'as:name', '@value')];
  848. if (empty($element['type'])) {
  849. continue;
  850. }
  851. if (empty($element['href'])) {
  852. $element['href'] = $element['name'];
  853. }
  854. $taglist[] = $element;
  855. }
  856. return $taglist;
  857. }
  858. /**
  859. * Convert emojis from JSON-LD format into a simplified format
  860. *
  861. * @param array $emojis
  862. * @return array with emojis in a simplified format
  863. */
  864. private static function processEmojis(array $emojis)
  865. {
  866. $emojilist = [];
  867. foreach ($emojis as $emoji) {
  868. if (empty($emoji) || (JsonLD::fetchElement($emoji, '@type') != 'toot:Emoji') || empty($emoji['as:icon'])) {
  869. continue;
  870. }
  871. $url = JsonLD::fetchElement($emoji['as:icon'], 'as:url', '@id');
  872. $element = ['name' => JsonLD::fetchElement($emoji, 'as:name', '@value'),
  873. 'href' => $url];
  874. $emojilist[] = $element;
  875. }
  876. return $emojilist;
  877. }
  878. /**
  879. * Convert attachments from JSON-LD format into a simplified format
  880. *
  881. * @param array $attachments Attachments in JSON-LD format
  882. *
  883. * @return array Attachments in a simplified format
  884. */
  885. private static function processAttachments(array $attachments)
  886. {
  887. $attachlist = [];
  888. // Removes empty values
  889. $attachments = array_filter($attachments);
  890. foreach ($attachments as $attachment) {
  891. switch (JsonLD::fetchElement($attachment, '@type')) {
  892. case 'as:Page':
  893. $pageUrl = null;
  894. $pageImage = null;
  895. $urls = JsonLD::fetchElementArray($attachment, 'as:url');
  896. foreach ($urls as $url) {
  897. // Single scalar URL case
  898. if (is_string($url)) {
  899. $pageUrl = $url;
  900. continue;
  901. }
  902. $href = JsonLD::fetchElement($url, 'as:href', '@id');
  903. $mediaType = JsonLD::fetchElement($url, 'as:mediaType', '@value');
  904. if (Strings::startsWith($mediaType, 'image')) {
  905. $pageImage = $href;
  906. } else {
  907. $pageUrl = $href;
  908. }
  909. }
  910. $attachlist[] = [
  911. 'type' => 'link',
  912. 'title' => JsonLD::fetchElement($attachment, 'as:name', '@value'),
  913. 'desc' => JsonLD::fetchElement($attachment, 'as:summary', '@value'),
  914. 'url' => $pageUrl,
  915. 'image' => $pageImage,
  916. ];
  917. break;
  918. case 'as:Link':
  919. $attachlist[] = [
  920. 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')),
  921. 'mediaType' => JsonLD::fetchElement($attachment, 'as:mediaType', '@value'),
  922. 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'),
  923. 'url' => JsonLD::fetchElement($attachment, 'as:href', '@id')
  924. ];
  925. break;
  926. case 'as:Image':
  927. $mediaType = JsonLD::fetchElement($attachment, 'as:mediaType', '@value');
  928. $imageFullUrl = JsonLD::fetchElement($attachment, 'as:url', '@id');
  929. $imagePreviewUrl = null;
  930. // Multiple URLs?
  931. if (!$imageFullUrl && ($urls = JsonLD::fetchElementArray($attachment, 'as:url'))) {
  932. $imageVariants = [];
  933. $previewVariants = [];
  934. foreach ($urls as $url) {
  935. // Scalar URL, no discrimination possible
  936. if (is_string($url)) {
  937. $imageFullUrl = $url;
  938. continue;
  939. }
  940. // Not sure what to do with a different Link media type than the base Image, we skip
  941. if ($mediaType != JsonLD::fetchElement($url, 'as:mediaType', '@value')) {
  942. continue;
  943. }
  944. $href = JsonLD::fetchElement($url, 'as:href', '@id');
  945. // Default URL choice if no discriminating width is provided
  946. $imageFullUrl = $href ?? $imageFullUrl;
  947. $width = intval(JsonLD::fetchElement($url, 'as:width', '@value') ?? 1);
  948. if ($href && $width) {
  949. $imageVariants[$width] = $href;
  950. // 632 is the ideal width for full screen frio posts, we compute the absolute distance to it
  951. $previewVariants[abs(632 - $width)] = $href;
  952. }
  953. }
  954. if ($imageVariants) {
  955. // Taking the maximum size image
  956. ksort($imageVariants);
  957. $imageFullUrl = array_pop($imageVariants);
  958. // Taking the minimum number distance to the target distance
  959. ksort($previewVariants);
  960. $imagePreviewUrl = array_shift($previewVariants);
  961. }
  962. unset($imageVariants);
  963. unset($previewVariants);
  964. }
  965. $attachlist[] = [
  966. 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')),
  967. 'mediaType' => $mediaType,
  968. 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'),
  969. 'url' => $imageFullUrl,
  970. 'image' => $imagePreviewUrl !== $imageFullUrl ? $imagePreviewUrl : null,
  971. ];
  972. break;
  973. default:
  974. $attachlist[] = [
  975. 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')),
  976. 'mediaType' => JsonLD::fetchElement($attachment, 'as:mediaType', '@value'),
  977. 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'),
  978. 'url' => JsonLD::fetchElement($attachment, 'as:url', '@id')
  979. ];
  980. }
  981. }
  982. return $attachlist;
  983. }
  984. /**
  985. * Fetch the original source or content with the "language" Markdown or HTML
  986. *
  987. * @param array $object
  988. * @param array $object_data
  989. *
  990. * @return array
  991. * @throws \Exception
  992. */
  993. private static function getSource($object, $object_data)
  994. {
  995. $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/bbcode');
  996. $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value');
  997. if (!empty($object_data['source'])) {
  998. return $object_data;
  999. }
  1000. $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/markdown');
  1001. $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value');
  1002. if (!empty($object_data['source'])) {
  1003. $object_data['source'] = Markdown::toBBCode($object_data['source']);
  1004. return $object_data;
  1005. }
  1006. $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/html');
  1007. $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value');
  1008. if (!empty($object_data['source'])) {
  1009. $object_data['source'] = HTML::toBBCode($object_data['source']);
  1010. return $object_data;
  1011. }
  1012. return $object_data;
  1013. }
  1014. /**
  1015. * Check if the "as:url" element is an array with multiple links
  1016. * This is the case with audio and video posts.
  1017. * Then the links are added as attachments
  1018. *
  1019. * @param array $object The raw object
  1020. * @param array $object_data The parsed object data for later processing
  1021. * @return array the object data
  1022. */
  1023. private static function processAttachmentUrls(array $object, array $object_data) {
  1024. // Check if this is some url with multiple links
  1025. if (empty($object['as:url'])) {
  1026. return $object_data;
  1027. }
  1028. $urls = $object['as:url'];
  1029. $keys = array_keys($urls);
  1030. if (!is_numeric(array_pop($keys))) {
  1031. return $object_data;
  1032. }
  1033. $attachments = [];
  1034. foreach ($urls as $url) {
  1035. if (empty($url['@type']) || ($url['@type'] != 'as:Link')) {
  1036. continue;
  1037. }
  1038. $href = JsonLD::fetchElement($url, 'as:href', '@id');
  1039. if (empty($href)) {
  1040. continue;
  1041. }
  1042. $mediatype = JsonLD::fetchElement($url, 'as:mediaType');
  1043. if (empty($mediatype)) {
  1044. continue;
  1045. }
  1046. if ($mediatype == 'text/html') {
  1047. $object_data['alternate-url'] = $href;
  1048. }
  1049. $filetype = strtolower(substr($mediatype, 0, strpos($mediatype, '/')));
  1050. if ($filetype == 'audio') {
  1051. $attachments[$filetype] = ['type' => $mediatype, 'url' => $href];
  1052. } elseif ($filetype == 'video') {
  1053. $height = (int)JsonLD::fetchElement($url, 'as:height', '@value');
  1054. // We save bandwidth by using a moderate height
  1055. // Peertube normally uses these heights: 240, 360, 480, 720, 1080
  1056. if (!empty($attachments[$filetype]['height']) &&
  1057. (($height > 480) || $height < $attachments[$filetype]['height'])) {
  1058. continue;
  1059. }
  1060. $attachments[$filetype] = ['type' => $mediatype, 'url' => $href, 'height' => $height];
  1061. }
  1062. }
  1063. foreach ($attachments as $type => $attachment) {
  1064. $object_data['attachments'][] = ['type' => $type,
  1065. 'mediaType' => $attachment['type'],
  1066. 'name' => '',
  1067. 'url' => $attachment['url']];
  1068. }
  1069. return $object_data;
  1070. }
  1071. /**
  1072. * Fetches data from the object part of an activity
  1073. *
  1074. * @param array $object
  1075. *
  1076. * @return array
  1077. * @throws \Exception
  1078. */
  1079. private static function processObject($object)
  1080. {
  1081. if (!JsonLD::fetchElement($object, '@id')) {
  1082. return false;
  1083. }
  1084. $object_data = [];
  1085. $object_data['object_type'] = JsonLD::fetchElement($object, '@type');
  1086. $object_data['id'] = JsonLD::fetchElement($object, '@id');
  1087. $object_data['reply-to-id'] = JsonLD::fetchElement($object, 'as:inReplyTo', '@id');
  1088. // An empty "id" field is translated to "./" by the compactor, so we have to check for this content
  1089. if (empty($object_data['reply-to-id']) || ($object_data['reply-to-id'] == './')) {
  1090. $object_data['reply-to-id'] = $object_data['id'];
  1091. } else {
  1092. // Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field.
  1093. $replyToId = Item::getURIByLink($object_data['reply-to-id']);
  1094. if (!empty($replyToId) && ($object_data['reply-to-id'] != $replyToId)) {
  1095. Logger::notice('Fix wrong reply-to', ['received' => $object_data['reply-to-id'], 'correct' => $replyToId]);
  1096. $object_data['reply-to-id'] = $replyToId;
  1097. }
  1098. }
  1099. $object_data['published'] = JsonLD::fetchElement($object, 'as:published', '@value');
  1100. $object_data['updated'] = JsonLD::fetchElement($object, 'as:updated', '@value');
  1101. if (empty($object_data['updated'])) {
  1102. $object_data['updated'] = $object_data['published'];
  1103. }
  1104. if (empty($object_data['published']) && !empty($object_data['updated'])) {
  1105. $object_data['published'] = $object_data['updated'];
  1106. }
  1107. $actor = JsonLD::fetchElement($object, 'as:attributedTo', '@id');
  1108. if (empty($actor)) {
  1109. $actor = JsonLD::fetchElement($object, 'as:actor', '@id');
  1110. }
  1111. $location = JsonLD::fetchElement($object, 'as:location', 'as:name', '@type', 'as:Place');
  1112. $location = JsonLD::fetchElement($location, 'location', '@value');
  1113. if ($location) {
  1114. // Some AP software allow formatted text in post location, so we run all the text converters we have to boil
  1115. // down to HTML and then finally format to plaintext.
  1116. $location = Markdown::convert($location);
  1117. $location = BBCode::convert($location);
  1118. $location = HTML::toPlaintext($location);
  1119. }
  1120. $object_data['sc:identifier'] = JsonLD::fetchElement($object, 'sc:identifier', '@value');
  1121. $object_data['diaspora:guid'] = JsonLD::fetchElement($object, 'diaspora:guid', '@value');
  1122. $object_data['diaspora:comment'] = JsonLD::fetchElement($object, 'diaspora:comment', '@value');
  1123. $object_data['diaspora:like'] = JsonLD::fetchElement($object, 'diaspora:like', '@value');
  1124. $object_data['actor'] = $object_data['author'] = $actor;
  1125. $object_data['context'] = JsonLD::fetchElement($object, 'as:context', '@id');
  1126. $object_data['conversation'] = JsonLD::fetchElement($object, 'ostatus:conversation', '@id');
  1127. $object_data['sensitive'] = JsonLD::fetchElement($object, 'as:sensitive');
  1128. $object_data['name'] = JsonLD::fetchElement($object, 'as:name', '@value');
  1129. $object_data['summary'] = JsonLD::fetchElement($object, 'as:summary', '@value');
  1130. $object_data['content'] = JsonLD::fetchElement($object, 'as:content', '@value');
  1131. $object_data = self::getSource($object, $object_data);
  1132. $object_data['start-time'] = JsonLD::fetchElement($object, 'as:startTime', '@value');
  1133. $object_data['end-time'] = JsonLD::fetchElement($object, 'as:endTime', '@value');
  1134. $object_data['location'] = $location;
  1135. $object_data['latitude'] = JsonLD::fetchElement($object, 'as:location', 'as:latitude', '@type', 'as:Place');
  1136. $object_data['latitude'] = JsonLD::fetchElement($object_data, 'latitude', '@value');
  1137. $object_data['longitude'] = JsonLD::fetchElement($object, 'as:location', 'as:longitude', '@type', 'as:Place');
  1138. $object_data['longitude'] = JsonLD::fetchElement($object_data, 'longitude', '@value');
  1139. $object_data['attachments'] = self::processAttachments(JsonLD::fetchElementArray($object, 'as:attachment') ?? []);
  1140. $object_data['tags'] = self::processTags(JsonLD::fetchElementArray($object, 'as:tag') ?? []);
  1141. $object_data['emojis'] = self::processEmojis(JsonLD::fetchElementArray($object, 'as:tag', 'toot:Emoji') ?? []);
  1142. $object_data['generator'] = JsonLD::fetchElement($object, 'as:generator', 'as:name', '@type', 'as:Application');
  1143. $object_data['generator'] = JsonLD::fetchElement($object_data, 'generator', '@value');
  1144. $object_data['alternate-url'] = JsonLD::fetchElement($object, 'as:url', '@id');
  1145. // Special treatment for Hubzilla links
  1146. if (is_array($object_data['alternate-url'])) {
  1147. $object_data['alternate-url'] = JsonLD::fetchElement($object_data['alternate-url'], 'as:href', '@id');
  1148. if (!is_string($object_data['alternate-url'])) {
  1149. $object_data['alternate-url'] = JsonLD::fetchElement($object['as:url'], 'as:href', '@id');
  1150. }
  1151. }
  1152. if (in_array($object_data['object_type'], ['as:Audio', 'as:Video'])) {
  1153. $object_data = self::processAttachmentUrls($object, $object_data);
  1154. }
  1155. $receiverdata = self::getReceivers($object, $object_data['actor'], $object_data['tags'], true);
  1156. $receivers = $reception_types = [];
  1157. foreach ($receiverdata as $key => $data) {
  1158. $receivers[$key] = $data['uid'];
  1159. $reception_types[$data['uid']] = $data['type'] ?? 0;
  1160. }
  1161. $object_data['receiver'] = $receivers;
  1162. $object_data['reception_type'] = $reception_types;
  1163. $object_data['unlisted'] = in_array(-1, $object_data['receiver']);
  1164. unset($object_data['receiver']['uid:-1']);
  1165. // Common object data:
  1166. // Unhandled
  1167. // @context, type, actor, signature, mediaType, duration, replies, icon
  1168. // Also missing: (Defined in the standard, but currently unused)
  1169. // audience, preview, endTime, startTime, image
  1170. // Data in Notes:
  1171. // Unhandled
  1172. // contentMap, announcement_count, announcements, context_id, likes, like_count
  1173. // inReplyToStatusId, shares, quoteUrl, statusnetConversationId
  1174. // Data in video:
  1175. // To-Do?
  1176. // category, licence, language, commentsEnabled
  1177. // Unhandled
  1178. // views, waitTranscoding, state, support, subtitleLanguage
  1179. // likes, dislikes, shares, comments
  1180. return $object_data;
  1181. }
  1182. }