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.
 
 
 
 
 
 

963 lines
28 KiB

  1. <?php
  2. /**
  3. * @file src/Protocol/ActivityPub/Processor.php
  4. */
  5. namespace Friendica\Protocol\ActivityPub;
  6. use Friendica\Content\Text\BBCode;
  7. use Friendica\Content\Text\HTML;
  8. use Friendica\Core\Config;
  9. use Friendica\Core\Logger;
  10. use Friendica\Core\Protocol;
  11. use Friendica\Database\DBA;
  12. use Friendica\DI;
  13. use Friendica\Model\APContact;
  14. use Friendica\Model\Contact;
  15. use Friendica\Model\Event;
  16. use Friendica\Model\Item;
  17. use Friendica\Model\Mail;
  18. use Friendica\Model\Term;
  19. use Friendica\Model\User;
  20. use Friendica\Protocol\Activity;
  21. use Friendica\Protocol\ActivityPub;
  22. use Friendica\Util\DateTimeFormat;
  23. use Friendica\Util\JsonLD;
  24. use Friendica\Util\Strings;
  25. /**
  26. * ActivityPub Processor Protocol class
  27. */
  28. class Processor
  29. {
  30. /**
  31. * Converts mentions from Pleroma into the Friendica format
  32. *
  33. * @param string $body
  34. *
  35. * @return string converted body
  36. */
  37. private static function convertMentions($body)
  38. {
  39. $URLSearchString = "^\[\]";
  40. $body = preg_replace("/\[url\=([$URLSearchString]*)\]([#@!])(.*?)\[\/url\]/ism", '$2[url=$1]$3[/url]', $body);
  41. return $body;
  42. }
  43. /**
  44. * Replaces emojis in the body
  45. *
  46. * @param array $emojis
  47. * @param string $body
  48. *
  49. * @return string with replaced emojis
  50. */
  51. private static function replaceEmojis($body, array $emojis)
  52. {
  53. foreach ($emojis as $emoji) {
  54. $replace = '[class=emoji mastodon][img=' . $emoji['href'] . ']' . $emoji['name'] . '[/img][/class]';
  55. $body = str_replace($emoji['name'], $replace, $body);
  56. }
  57. return $body;
  58. }
  59. /**
  60. * Constructs a string with tags for a given tag array
  61. *
  62. * @param array $tags
  63. * @param boolean $sensitive
  64. * @return string with tags
  65. */
  66. private static function constructTagString(array $tags = null, $sensitive = false)
  67. {
  68. if (empty($tags)) {
  69. return '';
  70. }
  71. $tag_text = '';
  72. foreach ($tags as $tag) {
  73. if (in_array($tag['type'] ?? '', ['Mention', 'Hashtag'])) {
  74. if (!empty($tag_text)) {
  75. $tag_text .= ',';
  76. }
  77. $tag_text .= substr($tag['name'], 0, 1) . '[url=' . $tag['href'] . ']' . substr($tag['name'], 1) . '[/url]';
  78. }
  79. }
  80. /// @todo add nsfw for $sensitive
  81. return $tag_text;
  82. }
  83. /**
  84. * Add attachment data to the item array
  85. *
  86. * @param array $activity
  87. * @param array $item
  88. *
  89. * @return array array
  90. */
  91. private static function constructAttachList($activity, $item)
  92. {
  93. if (empty($activity['attachments'])) {
  94. return $item;
  95. }
  96. foreach ($activity['attachments'] as $attach) {
  97. $filetype = strtolower(substr($attach['mediaType'], 0, strpos($attach['mediaType'], '/')));
  98. if ($filetype == 'image') {
  99. if (!empty($activity['source']) && strpos($activity['source'], $attach['url'])) {
  100. continue;
  101. }
  102. if (empty($attach['name'])) {
  103. $item['body'] .= "\n[img]" . $attach['url'] . '[/img]';
  104. } else {
  105. $item['body'] .= "\n[img=" . $attach['url'] . ']' . $attach['name'] . '[/img]';
  106. }
  107. } else {
  108. if (!empty($item["attach"])) {
  109. $item["attach"] .= ',';
  110. } else {
  111. $item["attach"] = '';
  112. }
  113. if (!isset($attach['length'])) {
  114. $attach['length'] = "0";
  115. }
  116. $item["attach"] .= '[attach]href="'.$attach['url'].'" length="'.$attach['length'].'" type="'.$attach['mediaType'].'" title="'.($attach['name'] ?? '') .'"[/attach]';
  117. }
  118. }
  119. return $item;
  120. }
  121. /**
  122. * Updates a message
  123. *
  124. * @param array $activity Activity array
  125. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  126. */
  127. public static function updateItem($activity)
  128. {
  129. $item = Item::selectFirst(['uri', 'thr-parent', 'gravity'], ['uri' => $activity['id']]);
  130. if (!DBA::isResult($item)) {
  131. Logger::warning('Unknown item', ['uri' => $activity['id']]);
  132. return;
  133. }
  134. $item['changed'] = DateTimeFormat::utcNow();
  135. $item['edited'] = DateTimeFormat::utc($activity['updated']);
  136. $item = self::processContent($activity, $item);
  137. if (empty($item)) {
  138. return;
  139. }
  140. Item::update($item, ['uri' => $activity['id']]);
  141. }
  142. /**
  143. * Prepares data for a message
  144. *
  145. * @param array $activity Activity array
  146. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  147. * @throws \ImagickException
  148. */
  149. public static function createItem($activity)
  150. {
  151. $item = [];
  152. $item['verb'] = Activity::POST;
  153. $item['thr-parent'] = $activity['reply-to-id'];
  154. if ($activity['reply-to-id'] == $activity['id']) {
  155. $item['gravity'] = GRAVITY_PARENT;
  156. $item['object-type'] = Activity\ObjectType::NOTE;
  157. } else {
  158. $item['gravity'] = GRAVITY_COMMENT;
  159. $item['object-type'] = Activity\ObjectType::COMMENT;
  160. // Ensure that the comment reaches all receivers of the referring post
  161. $activity['receiver'] = self::addReceivers($activity);
  162. }
  163. if (empty($activity['directmessage']) && ($activity['id'] != $activity['reply-to-id']) && !Item::exists(['uri' => $activity['reply-to-id']])) {
  164. Logger::log('Parent ' . $activity['reply-to-id'] . ' not found. Try to refetch it.');
  165. self::fetchMissingActivity($activity['reply-to-id'], $activity);
  166. }
  167. $item['diaspora_signed_text'] = $activity['diaspora:comment'] ?? '';
  168. self::postItem($activity, $item);
  169. }
  170. /**
  171. * Delete items
  172. *
  173. * @param array $activity
  174. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  175. * @throws \ImagickException
  176. */
  177. public static function deleteItem($activity)
  178. {
  179. $owner = Contact::getIdForURL($activity['actor']);
  180. Logger::log('Deleting item ' . $activity['object_id'] . ' from ' . $owner, Logger::DEBUG);
  181. Item::delete(['uri' => $activity['object_id'], 'owner-id' => $owner]);
  182. }
  183. /**
  184. * Prepare the item array for an activity
  185. *
  186. * @param array $activity Activity array
  187. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  188. * @throws \ImagickException
  189. */
  190. public static function addTag($activity)
  191. {
  192. if (empty($activity['object_content']) || empty($activity['object_id'])) {
  193. return;
  194. }
  195. foreach ($activity['receiver'] as $receiver) {
  196. $item = Item::selectFirst(['id', 'tag', 'origin', 'author-link'], ['uri' => $activity['target_id'], 'uid' => $receiver]);
  197. if (!DBA::isResult($item)) {
  198. // We don't fetch missing content for this purpose
  199. continue;
  200. }
  201. if (($item['author-link'] != $activity['actor']) && !$item['origin']) {
  202. Logger::info('Not origin, not from the author, skipping update', ['id' => $item['id'], 'author' => $item['author-link'], 'actor' => $activity['actor']]);
  203. continue;
  204. }
  205. // To-Do:
  206. // - Check if "blocktag" is set
  207. // - Check if actor is a contact
  208. if (!stristr($item['tag'], trim($activity['object_content']))) {
  209. $tag = $item['tag'] . (strlen($item['tag']) ? ',' : '') . '#[url=' . $activity['object_id'] . ']'. $activity['object_content'] . '[/url]';
  210. Item::update(['tag' => $tag], ['id' => $item['id']]);
  211. Logger::info('Tagged item', ['id' => $item['id'], 'tag' => $activity['object_content'], 'uri' => $activity['target_id'], 'actor' => $activity['actor']]);
  212. }
  213. }
  214. }
  215. /**
  216. * Add users to the receiver list of the given public activity.
  217. * This is used to ensure that the activity will be stored in every thread.
  218. *
  219. * @param array $activity Activity array
  220. * @return array Modified receiver list
  221. */
  222. private static function addReceivers(array $activity)
  223. {
  224. if (!in_array(0, $activity['receiver'])) {
  225. // Private activities will not be modified
  226. return $activity['receiver'];
  227. }
  228. // Add all owners of the referring item to the receivers
  229. $original = $receivers = $activity['receiver'];
  230. $items = Item::select(['uid'], ['uri' => $activity['object_id']]);
  231. while ($item = DBA::fetch($items)) {
  232. $receivers['uid:' . $item['uid']] = $item['uid'];
  233. }
  234. DBA::close($items);
  235. if (count($original) != count($receivers)) {
  236. Logger::info('Improved data', ['id' => $activity['id'], 'object' => $activity['object_id'], 'original' => $original, 'improved' => $receivers]);
  237. }
  238. return $receivers;
  239. }
  240. /**
  241. * Prepare the item array for an activity
  242. *
  243. * @param array $activity Activity array
  244. * @param string $verb Activity verb
  245. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  246. * @throws \ImagickException
  247. */
  248. public static function createActivity($activity, $verb)
  249. {
  250. $item = [];
  251. $item['verb'] = $verb;
  252. $item['thr-parent'] = $activity['object_id'];
  253. $item['gravity'] = GRAVITY_ACTIVITY;
  254. $item['object-type'] = Activity\ObjectType::NOTE;
  255. $item['diaspora_signed_text'] = $activity['diaspora:like'] ?? '';
  256. $activity['receiver'] = self::addReceivers($activity);
  257. self::postItem($activity, $item);
  258. }
  259. /**
  260. * Create an event
  261. *
  262. * @param array $activity Activity array
  263. * @param array $item
  264. * @throws \Exception
  265. */
  266. public static function createEvent($activity, $item)
  267. {
  268. $event['summary'] = HTML::toBBCode($activity['name']);
  269. $event['desc'] = HTML::toBBCode($activity['content']);
  270. $event['start'] = $activity['start-time'];
  271. $event['finish'] = $activity['end-time'];
  272. $event['nofinish'] = empty($event['finish']);
  273. $event['location'] = $activity['location'];
  274. $event['adjust'] = true;
  275. $event['cid'] = $item['contact-id'];
  276. $event['uid'] = $item['uid'];
  277. $event['uri'] = $item['uri'];
  278. $event['edited'] = $item['edited'];
  279. $event['private'] = $item['private'];
  280. $event['guid'] = $item['guid'];
  281. $event['plink'] = $item['plink'];
  282. $condition = ['uri' => $item['uri'], 'uid' => $item['uid']];
  283. $ev = DBA::selectFirst('event', ['id'], $condition);
  284. if (DBA::isResult($ev)) {
  285. $event['id'] = $ev['id'];
  286. }
  287. $event_id = Event::store($event);
  288. Logger::log('Event '.$event_id.' was stored', Logger::DEBUG);
  289. }
  290. /**
  291. * Process the content
  292. *
  293. * @param array $activity Activity array
  294. * @param array $item
  295. * @return array|bool Returns the item array or false if there was an unexpected occurrence
  296. * @throws \Exception
  297. */
  298. private static function processContent($activity, $item)
  299. {
  300. $item['title'] = HTML::toBBCode($activity['name']);
  301. if (!empty($activity['source'])) {
  302. $item['body'] = $activity['source'];
  303. } else {
  304. $content = HTML::toBBCode($activity['content']);
  305. if (!empty($activity['emojis'])) {
  306. $content = self::replaceEmojis($content, $activity['emojis']);
  307. }
  308. $content = self::convertMentions($content);
  309. if (empty($activity['directmessage']) && ($item['thr-parent'] != $item['uri']) && ($item['gravity'] == GRAVITY_COMMENT)) {
  310. $item_private = !in_array(0, $activity['item_receiver']);
  311. $parent = Item::selectFirst(['id', 'private', 'author-link', 'alias'], ['uri' => $item['thr-parent']]);
  312. if (!DBA::isResult($parent)) {
  313. Logger::warning('Unknown parent item.', ['uri' => $item['thr-parent']]);
  314. return false;
  315. }
  316. if ($item_private && !$parent['private']) {
  317. Logger::warning('Item is private but the parent is not. Dropping.', ['item-uri' => $item['uri'], 'thr-parent' => $item['thr-parent']]);
  318. return false;
  319. }
  320. $potential_implicit_mentions = self::getImplicitMentionList($parent);
  321. $content = self::removeImplicitMentionsFromBody($content, $potential_implicit_mentions);
  322. $activity['tags'] = self::convertImplicitMentionsInTags($activity['tags'], $potential_implicit_mentions);
  323. }
  324. $item['content-warning'] = HTML::toBBCode($activity['summary']);
  325. $item['body'] = $content;
  326. if (($activity['object_type'] == 'as:Video') && !empty($activity['alternate-url'])) {
  327. $item['body'] .= "\n[video]" . $activity['alternate-url'] . '[/video]';
  328. }
  329. }
  330. $item['tag'] = self::constructTagString($activity['tags'], $activity['sensitive']);
  331. $item['location'] = $activity['location'];
  332. if (!empty($item['latitude']) && !empty($item['longitude'])) {
  333. $item['coord'] = $item['latitude'] . ' ' . $item['longitude'];
  334. }
  335. $item['app'] = $activity['generator'];
  336. return $item;
  337. }
  338. /**
  339. * Creates an item post
  340. *
  341. * @param array $activity Activity data
  342. * @param array $item item array
  343. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  344. * @throws \ImagickException
  345. */
  346. private static function postItem($activity, $item)
  347. {
  348. /// @todo What to do with $activity['context']?
  349. if (empty($activity['directmessage']) && ($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['thr-parent']])) {
  350. Logger::info('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]);
  351. return;
  352. }
  353. $item['network'] = Protocol::ACTIVITYPUB;
  354. $item['private'] = !in_array(0, $activity['receiver']);
  355. $item['author-link'] = $activity['author'];
  356. $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true);
  357. $item['owner-link'] = $activity['actor'];
  358. $item['owner-id'] = Contact::getIdForURL($activity['actor'], 0, true);
  359. $isForum = false;
  360. if (!empty($activity['thread-completion'])) {
  361. // Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
  362. $item['causer-link'] = $item['owner-link'];
  363. $item['causer-id'] = $item['owner-id'];
  364. Logger::info('Ignoring actor because of thread completion.', ['actor' => $item['owner-link']]);
  365. $item['owner-link'] = $item['author-link'];
  366. $item['owner-id'] = $item['author-id'];
  367. } else {
  368. $actor = APContact::getByURL($item['owner-link'], false);
  369. $isForum = ($actor['type'] == 'Group');
  370. }
  371. $item['uri'] = $activity['id'];
  372. $item['created'] = DateTimeFormat::utc($activity['published']);
  373. $item['edited'] = DateTimeFormat::utc($activity['updated']);
  374. $item['guid'] = $activity['diaspora:guid'];
  375. $item = self::processContent($activity, $item);
  376. if (empty($item)) {
  377. return;
  378. }
  379. $item['plink'] = $activity['alternate-url'] ?? $item['uri'];
  380. $item = self::constructAttachList($activity, $item);
  381. $stored = false;
  382. foreach ($activity['receiver'] as $receiver) {
  383. $item['uid'] = $receiver;
  384. if ($isForum) {
  385. $item['contact-id'] = Contact::getIdForURL($activity['actor'], $receiver, true);
  386. } else {
  387. $item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, true);
  388. }
  389. if (($receiver != 0) && empty($item['contact-id'])) {
  390. $item['contact-id'] = Contact::getIdForURL($activity['author'], 0, true);
  391. }
  392. if (!empty($activity['directmessage'])) {
  393. self::postMail($activity, $item);
  394. continue;
  395. }
  396. if (DI::pConfig()->get($receiver, 'system', 'accept_only_sharer', false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) {
  397. $skip = !Contact::isSharingByURL($activity['author'], $receiver);
  398. if ($skip && (($activity['type'] == 'as:Announce') || $isForum)) {
  399. $skip = !Contact::isSharingByURL($activity['actor'], $receiver);
  400. }
  401. if ($skip) {
  402. Logger::info('Skipping post', ['uid' => $receiver, 'url' => $item['uri']]);
  403. continue;
  404. }
  405. Logger::info('Accepting post', ['uid' => $receiver, 'url' => $item['uri']]);
  406. }
  407. if ($activity['object_type'] == 'as:Event') {
  408. self::createEvent($activity, $item);
  409. }
  410. $item_id = Item::insert($item);
  411. if ($item_id) {
  412. Logger::info('Item insertion successful', ['user' => $item['uid'], 'item_id' => $item_id]);
  413. } else {
  414. Logger::notice('Item insertion aborted', ['user' => $item['uid']]);
  415. }
  416. if ($item['uid'] == 0) {
  417. $stored = $item_id;
  418. }
  419. }
  420. // Store send a follow request for every reshare - but only when the item had been stored
  421. if ($stored && !$item['private'] && ($item['gravity'] == GRAVITY_PARENT) && ($item['author-link'] != $item['owner-link'])) {
  422. $author = APContact::getByURL($item['owner-link'], false);
  423. // We send automatic follow requests for reshared messages. (We don't need though for forum posts)
  424. if ($author['type'] != 'Group') {
  425. Logger::log('Send follow request for ' . $item['uri'] . ' (' . $stored . ') to ' . $item['author-link'], Logger::DEBUG);
  426. ActivityPub\Transmitter::sendFollowObject($item['uri'], $item['author-link']);
  427. }
  428. }
  429. }
  430. /**
  431. * Creates an mail post
  432. *
  433. * @param array $activity Activity data
  434. * @param array $item item array
  435. * @return int|bool New mail table row id or false on error
  436. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  437. */
  438. private static function postMail($activity, $item)
  439. {
  440. if (($item['gravity'] != GRAVITY_PARENT) && !DBA::exists('mail', ['uri' => $item['thr-parent'], 'uid' => $item['uid']])) {
  441. Logger::info('Parent not found, mail will be discarded.', ['uid' => $item['uid'], 'uri' => $item['thr-parent']]);
  442. return false;
  443. }
  444. Logger::info('Direct Message', $item);
  445. $msg = [];
  446. $msg['uid'] = $item['uid'];
  447. $msg['contact-id'] = $item['contact-id'];
  448. $contact = Contact::getById($item['contact-id'], ['name', 'url', 'photo']);
  449. $msg['from-name'] = $contact['name'];
  450. $msg['from-url'] = $contact['url'];
  451. $msg['from-photo'] = $contact['photo'];
  452. $msg['uri'] = $item['uri'];
  453. $msg['created'] = $item['created'];
  454. $parent = DBA::selectFirst('mail', ['parent-uri', 'title'], ['uri' => $item['thr-parent']]);
  455. if (DBA::isResult($parent)) {
  456. $msg['parent-uri'] = $parent['parent-uri'];
  457. $msg['title'] = $parent['title'];
  458. } else {
  459. $msg['parent-uri'] = $item['thr-parent'];
  460. if (!empty($item['title'])) {
  461. $msg['title'] = $item['title'];
  462. } elseif (!empty($item['content-warning'])) {
  463. $msg['title'] = $item['content-warning'];
  464. } else {
  465. // Trying to generate a title out of the body
  466. $title = $item['body'];
  467. while (preg_match('#^(@\[url=([^\]]+)].*?\[\/url]\s)(.*)#is', $title, $matches)) {
  468. $title = $matches[3];
  469. }
  470. $title = trim(HTML::toPlaintext(BBCode::convert($title, false, 2, true), 0));
  471. if (strlen($title) > 20) {
  472. $title = substr($title, 0, 20) . '...';
  473. }
  474. $msg['title'] = $title;
  475. }
  476. }
  477. $msg['body'] = $item['body'];
  478. return Mail::insert($msg);
  479. }
  480. /**
  481. * Fetches missing posts
  482. *
  483. * @param string $url message URL
  484. * @param array $child activity array with the child of this message
  485. * @return boolean success
  486. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  487. */
  488. public static function fetchMissingActivity($url, $child = [])
  489. {
  490. if (!empty($child['receiver'])) {
  491. $uid = ActivityPub\Receiver::getFirstUserFromReceivers($child['receiver']);
  492. } else {
  493. $uid = 0;
  494. }
  495. $object = ActivityPub::fetchContent($url, $uid);
  496. if (empty($object)) {
  497. Logger::log('Activity ' . $url . ' was not fetchable, aborting.');
  498. return false;
  499. }
  500. if (empty($object['id'])) {
  501. Logger::log('Activity ' . $url . ' has got not id, aborting. ' . json_encode($object));
  502. return false;
  503. }
  504. if (!empty($child['author'])) {
  505. $actor = $child['author'];
  506. } elseif (!empty($object['actor'])) {
  507. $actor = $object['actor'];
  508. } elseif (!empty($object['attributedTo'])) {
  509. $actor = $object['attributedTo'];
  510. } else {
  511. // Shouldn't happen
  512. $actor = '';
  513. }
  514. if (!empty($object['published'])) {
  515. $published = $object['published'];
  516. } elseif (!empty($child['published'])) {
  517. $published = $child['published'];
  518. } else {
  519. $published = DateTimeFormat::utcNow();
  520. }
  521. $activity = [];
  522. $activity['@context'] = $object['@context'];
  523. unset($object['@context']);
  524. $activity['id'] = $object['id'];
  525. $activity['to'] = $object['to'] ?? [];
  526. $activity['cc'] = $object['cc'] ?? [];
  527. $activity['actor'] = $actor;
  528. $activity['object'] = $object;
  529. $activity['published'] = $published;
  530. $activity['type'] = 'Create';
  531. $ldactivity = JsonLD::compact($activity);
  532. $ldactivity['thread-completion'] = true;
  533. ActivityPub\Receiver::processActivity($ldactivity);
  534. Logger::log('Activity ' . $url . ' had been fetched and processed.');
  535. return true;
  536. }
  537. /**
  538. * perform a "follow" request
  539. *
  540. * @param array $activity
  541. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  542. * @throws \ImagickException
  543. */
  544. public static function followUser($activity)
  545. {
  546. $uid = User::getIdForURL($activity['object_id']);
  547. if (empty($uid)) {
  548. return;
  549. }
  550. $owner = User::getOwnerDataById($uid);
  551. $cid = Contact::getIdForURL($activity['actor'], $uid);
  552. if (!empty($cid)) {
  553. self::switchContact($cid);
  554. DBA::update('contact', ['hub-verify' => $activity['id'], 'protocol' => Protocol::ACTIVITYPUB], ['id' => $cid]);
  555. $contact = DBA::selectFirst('contact', [], ['id' => $cid, 'network' => Protocol::NATIVE_SUPPORT]);
  556. } else {
  557. $contact = [];
  558. }
  559. $item = ['author-id' => Contact::getIdForURL($activity['actor']),
  560. 'author-link' => $activity['actor']];
  561. $note = Strings::escapeTags(trim($activity['content'] ?? ''));
  562. // Ensure that the contact has got the right network type
  563. self::switchContact($item['author-id']);
  564. $result = Contact::addRelationship($owner, $contact, $item, false, $note);
  565. if ($result === true) {
  566. ActivityPub\Transmitter::sendContactAccept($item['author-link'], $item['author-id'], $owner['uid']);
  567. }
  568. $cid = Contact::getIdForURL($activity['actor'], $uid);
  569. if (empty($cid)) {
  570. return;
  571. }
  572. if (empty($contact)) {
  573. DBA::update('contact', ['hub-verify' => $activity['id'], 'protocol' => Protocol::ACTIVITYPUB], ['id' => $cid]);
  574. }
  575. Logger::log('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']);
  576. }
  577. /**
  578. * Update the given profile
  579. *
  580. * @param array $activity
  581. * @throws \Exception
  582. */
  583. public static function updatePerson($activity)
  584. {
  585. if (empty($activity['object_id'])) {
  586. return;
  587. }
  588. Logger::log('Updating profile for ' . $activity['object_id'], Logger::DEBUG);
  589. Contact::updateFromProbeByURL($activity['object_id'], true);
  590. }
  591. /**
  592. * Delete the given profile
  593. *
  594. * @param array $activity
  595. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  596. */
  597. public static function deletePerson($activity)
  598. {
  599. if (empty($activity['object_id']) || empty($activity['actor'])) {
  600. Logger::log('Empty object id or actor.', Logger::DEBUG);
  601. return;
  602. }
  603. if ($activity['object_id'] != $activity['actor']) {
  604. Logger::log('Object id does not match actor.', Logger::DEBUG);
  605. return;
  606. }
  607. $contacts = DBA::select('contact', ['id'], ['nurl' => Strings::normaliseLink($activity['object_id'])]);
  608. while ($contact = DBA::fetch($contacts)) {
  609. Contact::remove($contact['id']);
  610. }
  611. DBA::close($contacts);
  612. Logger::log('Deleted contact ' . $activity['object_id'], Logger::DEBUG);
  613. }
  614. /**
  615. * Accept a follow request
  616. *
  617. * @param array $activity
  618. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  619. * @throws \ImagickException
  620. */
  621. public static function acceptFollowUser($activity)
  622. {
  623. $uid = User::getIdForURL($activity['object_actor']);
  624. if (empty($uid)) {
  625. return;
  626. }
  627. $cid = Contact::getIdForURL($activity['actor'], $uid);
  628. if (empty($cid)) {
  629. Logger::log('No contact found for ' . $activity['actor'], Logger::DEBUG);
  630. return;
  631. }
  632. self::switchContact($cid);
  633. $fields = ['pending' => false];
  634. $contact = DBA::selectFirst('contact', ['rel'], ['id' => $cid]);
  635. if ($contact['rel'] == Contact::FOLLOWER) {
  636. $fields['rel'] = Contact::FRIEND;
  637. }
  638. $condition = ['id' => $cid];
  639. DBA::update('contact', $fields, $condition);
  640. Logger::log('Accept contact request from contact ' . $cid . ' for user ' . $uid, Logger::DEBUG);
  641. }
  642. /**
  643. * Reject a follow request
  644. *
  645. * @param array $activity
  646. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  647. * @throws \ImagickException
  648. */
  649. public static function rejectFollowUser($activity)
  650. {
  651. $uid = User::getIdForURL($activity['object_actor']);
  652. if (empty($uid)) {
  653. return;
  654. }
  655. $cid = Contact::getIdForURL($activity['actor'], $uid);
  656. if (empty($cid)) {
  657. Logger::log('No contact found for ' . $activity['actor'], Logger::DEBUG);
  658. return;
  659. }
  660. self::switchContact($cid);
  661. if (DBA::exists('contact', ['id' => $cid, 'rel' => Contact::SHARING])) {
  662. Contact::remove($cid);
  663. Logger::log('Rejected contact request from contact ' . $cid . ' for user ' . $uid . ' - contact had been removed.', Logger::DEBUG);
  664. } else {
  665. Logger::log('Rejected contact request from contact ' . $cid . ' for user ' . $uid . '.', Logger::DEBUG);
  666. }
  667. }
  668. /**
  669. * Undo activity like "like" or "dislike"
  670. *
  671. * @param array $activity
  672. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  673. * @throws \ImagickException
  674. */
  675. public static function undoActivity($activity)
  676. {
  677. if (empty($activity['object_id'])) {
  678. return;
  679. }
  680. if (empty($activity['object_actor'])) {
  681. return;
  682. }
  683. $author_id = Contact::getIdForURL($activity['object_actor']);
  684. if (empty($author_id)) {
  685. return;
  686. }
  687. Item::delete(['uri' => $activity['object_id'], 'author-id' => $author_id, 'gravity' => GRAVITY_ACTIVITY]);
  688. }
  689. /**
  690. * Activity to remove a follower
  691. *
  692. * @param array $activity
  693. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  694. * @throws \ImagickException
  695. */
  696. public static function undoFollowUser($activity)
  697. {
  698. $uid = User::getIdForURL($activity['object_object']);
  699. if (empty($uid)) {
  700. return;
  701. }
  702. $owner = User::getOwnerDataById($uid);
  703. $cid = Contact::getIdForURL($activity['actor'], $uid);
  704. if (empty($cid)) {
  705. Logger::log('No contact found for ' . $activity['actor'], Logger::DEBUG);
  706. return;
  707. }
  708. self::switchContact($cid);
  709. $contact = DBA::selectFirst('contact', [], ['id' => $cid]);
  710. if (!DBA::isResult($contact)) {
  711. return;
  712. }
  713. Contact::removeFollower($owner, $contact);
  714. Logger::log('Undo following request from contact ' . $cid . ' for user ' . $uid, Logger::DEBUG);
  715. }
  716. /**
  717. * Switches a contact to AP if needed
  718. *
  719. * @param integer $cid Contact ID
  720. * @throws \Exception
  721. */
  722. private static function switchContact($cid)
  723. {
  724. $contact = DBA::selectFirst('contact', ['network', 'url'], ['id' => $cid]);
  725. if (!DBA::isResult($contact) || in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN]) || Contact::isLocal($contact['url'])) {
  726. return;
  727. }
  728. Logger::info('Change existing contact', ['cid' => $cid, 'previous' => $contact['network']]);
  729. Contact::updateFromProbe($cid);
  730. }
  731. /**
  732. * Collects implicit mentions like:
  733. * - the author of the parent item
  734. * - all the mentioned conversants in the parent item
  735. *
  736. * @param array $parent Item array with at least ['id', 'author-link', 'alias']
  737. * @return array
  738. * @throws \Friendica\Network\HTTPException\InternalServerErrorException
  739. */
  740. private static function getImplicitMentionList(array $parent)
  741. {
  742. if (Config::get('system', 'disable_implicit_mentions')) {
  743. return [];
  744. }
  745. $parent_terms = Term::tagArrayFromItemId($parent['id'], [Term::MENTION, Term::IMPLICIT_MENTION]);
  746. $parent_author = Contact::getDetailsByURL($parent['author-link'], 0);
  747. $implicit_mentions = [];
  748. if (empty($parent_author)) {
  749. Logger::notice('Author public contact unknown.', ['author-link' => $parent['author-link'], 'item-id' => $parent['id']]);
  750. } else {
  751. $implicit_mentions[] = $parent_author['url'];
  752. $implicit_mentions[] = $parent_author['nurl'];
  753. $implicit_mentions[] = $parent_author['alias'];
  754. }
  755. if (!empty($parent['alias'])) {
  756. $implicit_mentions[] = $parent['alias'];
  757. }
  758. foreach ($parent_terms as $term) {
  759. $contact = Contact::getDetailsByURL($term['url'], 0);
  760. if (!empty($contact)) {
  761. $implicit_mentions[] = $contact['url'];
  762. $implicit_mentions[] = $contact['nurl'];
  763. $implicit_mentions[] = $contact['alias'];
  764. }
  765. }
  766. return $implicit_mentions;
  767. }
  768. /**
  769. * Strips from the body prepended implicit mentions
  770. *
  771. * @param string $body
  772. * @param array $potential_mentions
  773. * @return string
  774. */
  775. private static function removeImplicitMentionsFromBody($body, array $potential_mentions)
  776. {
  777. if (Config::get('system', 'disable_implicit_mentions')) {
  778. return $body;
  779. }
  780. $kept_mentions = [];
  781. // Extract one prepended mention at a time from the body
  782. while(preg_match('#^(@\[url=([^\]]+)].*?\[\/url]\s)(.*)#is', $body, $matches)) {
  783. if (!in_array($matches[2], $potential_mentions)) {
  784. $kept_mentions[] = $matches[1];
  785. }
  786. $body = $matches[3];
  787. }
  788. // Re-appending the kept mentions to the body after extraction
  789. $kept_mentions[] = $body;
  790. return implode('', $kept_mentions);
  791. }
  792. private static function convertImplicitMentionsInTags($activity_tags, array $potential_mentions)
  793. {
  794. if (Config::get('system', 'disable_implicit_mentions')) {
  795. return $activity_tags;
  796. }
  797. foreach ($activity_tags as $index => $tag) {
  798. if (in_array($tag['href'], $potential_mentions)) {
  799. $activity_tags[$index]['name'] = preg_replace(
  800. '/' . preg_quote(Term::TAG_CHARACTER[Term::MENTION], '/') . '/',
  801. Term::TAG_CHARACTER[Term::IMPLICIT_MENTION],
  802. $activity_tags[$index]['name'],
  803. 1
  804. );
  805. }
  806. }
  807. return $activity_tags;
  808. }
  809. }