ActivityPubPlugin.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * ActivityPub implementation for GNU social
  18. *
  19. * @package GNUsocial
  20. * @author Diogo Cordeiro <diogo@fc.up.pt>
  21. * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
  22. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  23. * @link http://www.gnu.org/software/social/
  24. */
  25. defined('GNUSOCIAL') || die();
  26. // Import plugin libs
  27. foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . '*.php') as $filename) {
  28. require_once $filename;
  29. }
  30. // Import plugin models
  31. foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'models' . DIRECTORY_SEPARATOR . '*.php') as $filename) {
  32. require_once $filename;
  33. }
  34. // So that this isn't hardcoded everywhere
  35. define('ACTIVITYPUB_BASE_ACTOR_URI', common_root_url().'index.php/user/');
  36. const ACTIVITYPUB_PUBLIC_TO = ['https://www.w3.org/ns/activitystreams#Public',
  37. 'Public',
  38. 'as:Public'
  39. ];
  40. /**
  41. * @category Plugin
  42. * @package GNUsocial
  43. * @author Diogo Cordeiro <diogo@fc.up.pt>
  44. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  45. */
  46. class ActivityPubPlugin extends Plugin
  47. {
  48. const PLUGIN_VERSION = '0.1.0alpha0';
  49. /**
  50. * Returns a Actor's URI from its local $profile
  51. * Works both for local and remote users.
  52. *
  53. * @param Profile $profile Actor's local profile
  54. * @return string Actor's URI
  55. * @throws Exception
  56. * @author Diogo Cordeiro <diogo@fc.up.pt>
  57. */
  58. public static function actor_uri($profile)
  59. {
  60. if ($profile->isLocal()) {
  61. return ACTIVITYPUB_BASE_ACTOR_URI.$profile->getID();
  62. } else {
  63. return $profile->getUri();
  64. }
  65. }
  66. /**
  67. * Returns a Actor's URL from its local $profile
  68. * Works both for local and remote users.
  69. *
  70. * @param Profile $profile Actor's local profile
  71. * @return string Actor's URL
  72. * @throws Exception
  73. * @author Diogo Cordeiro <diogo@fc.up.pt>
  74. */
  75. public static function actor_url($profile)
  76. {
  77. return ActivityPubPlugin::actor_uri($profile)."/";
  78. }
  79. /**
  80. * Returns a notice from its URL.
  81. *
  82. * @author Diogo Cordeiro <diogo@fc.up.pt>
  83. * @param string $url Notice's URL
  84. * @return Notice The Notice object
  85. * @throws Exception This function or provides a Notice or fails with exception
  86. */
  87. public static function grab_notice_from_url($url)
  88. {
  89. /* Offline Grabbing */
  90. try {
  91. // Look for a known remote notice
  92. return Notice::getByUri($url);
  93. } catch (Exception $e) {
  94. // Look for a local notice (unfortunately GNU social doesn't
  95. // provide this functionality natively)
  96. try {
  97. $candidate = Notice::getByID(intval(substr($url, (strlen(common_local_url('apNotice', ['id' => 0]))-1))));
  98. if (common_local_url('apNotice', ['id' => $candidate->getID()]) === $url) { // Sanity check
  99. return $candidate;
  100. } else {
  101. common_debug('ActivityPubPlugin Notice Grabber: '.$candidate->getUrl(). ' is different of '.$url);
  102. }
  103. } catch (Exception $e) {
  104. common_debug('ActivityPubPlugin Notice Grabber: failed to find: '.$url.' offline.');
  105. }
  106. }
  107. /* Online Grabbing */
  108. $client = new HTTPClient();
  109. $headers = [];
  110. $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
  111. $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social';
  112. $response = $client->get($url, $headers);
  113. $object = json_decode($response->getBody(), true);
  114. Activitypub_notice::validate_note($object);
  115. return Activitypub_notice::create_notice($object);
  116. }
  117. /**
  118. * Route/Reroute urls
  119. *
  120. * @param URLMapper $m
  121. * @return void
  122. * @throws Exception
  123. */
  124. public function onRouterInitialized(URLMapper $m)
  125. {
  126. $acceptHeaders = [
  127. 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' => 0,
  128. 'application/activity+json' => 1,
  129. 'application/json' => 2,
  130. 'application/ld+json' => 3
  131. ];
  132. $m->connect('user/:id',
  133. ['action' => 'apActorProfile'],
  134. ['id' => '[0-9]+'],
  135. true,
  136. $acceptHeaders);
  137. $m->connect(':nickname',
  138. ['action' => 'apActorProfile'],
  139. ['nickname' => Nickname::DISPLAY_FMT],
  140. true,
  141. $acceptHeaders);
  142. $m->connect(':nickname/',
  143. ['action' => 'apActorProfile'],
  144. ['nickname' => Nickname::DISPLAY_FMT],
  145. true,
  146. $acceptHeaders);
  147. $m->connect('notice/:id',
  148. ['action' => 'apNotice'],
  149. ['id' => '[0-9]+'],
  150. true,
  151. $acceptHeaders);
  152. $m->connect(
  153. 'user/:id/liked.json',
  154. ['action' => 'apActorLiked'],
  155. ['id' => '[0-9]+']
  156. );
  157. $m->connect(
  158. 'user/:id/followers.json',
  159. ['action' => 'apActorFollowers'],
  160. ['id' => '[0-9]+']
  161. );
  162. $m->connect(
  163. 'user/:id/following.json',
  164. ['action' => 'apActorFollowing'],
  165. ['id' => '[0-9]+']
  166. );
  167. $m->connect(
  168. 'user/:id/inbox.json',
  169. ['action' => 'apInbox'],
  170. ['id' => '[0-9]+']
  171. );
  172. $m->connect(
  173. 'user/:id/outbox.json',
  174. ['action' => 'apActorOutbox'],
  175. ['id' => '[0-9]+']
  176. );
  177. $m->connect(
  178. 'inbox.json',
  179. ['action' => 'apInbox']
  180. );
  181. }
  182. /**
  183. * Plugin version information
  184. *
  185. * @param array $versions
  186. * @return bool hook true
  187. */
  188. public function onPluginVersion(array &$versions)
  189. {
  190. $versions[] = [
  191. 'name' => 'ActivityPub',
  192. 'version' => self::PLUGIN_VERSION,
  193. 'author' => 'Diogo Cordeiro',
  194. 'homepage' => 'https://notabug.org/diogo/gnu-social/src/activitypub/plugins/ActivityPub',
  195. // TRANS: Plugin description.
  196. 'rawdescription' => _m('Follow people across social networks that implement '.
  197. '<a href="https://activitypub.rocks/">ActivityPub</a>.')
  198. ];
  199. return true;
  200. }
  201. /**
  202. * Set up queue handlers for required interactions
  203. *
  204. * @param QueueManager $qm
  205. * @return bool event hook return
  206. */
  207. public function onEndInitializeQueueManager(QueueManager $qm): bool
  208. {
  209. // Notice distribution
  210. $qm->connect('activitypub', 'ActivityPubQueueHandler');
  211. return true;
  212. }
  213. /**
  214. * Enqueue saved notices for distribution
  215. *
  216. * @param Notice $notice notice to be distributed
  217. * @param Array &$transports list of transports to queue for
  218. * @return bool event hook return
  219. */
  220. public function onStartEnqueueNotice(Notice $notice, Array &$transports): bool
  221. {
  222. try {
  223. $id = $notice->getID();
  224. if ($id > 0) {
  225. $transports[] = 'activitypub';
  226. $this->log(LOG_INFO, "Notice:{$id} queued for distribution");
  227. }
  228. } catch (Exception $e) {
  229. $this->log(LOG_ERR, "Invalid notice, not queueing for distribution");
  230. }
  231. return true;
  232. }
  233. /**
  234. * Plugin Nodeinfo information
  235. *
  236. * @param array $protocols
  237. * @return bool hook true
  238. */
  239. public function onNodeInfoProtocols(array &$protocols)
  240. {
  241. $protocols[] = "activitypub";
  242. return true;
  243. }
  244. /**
  245. * Adds an indicator on Remote ActivityPub profiles.
  246. *
  247. * @param HTMLOutputter $out
  248. * @param Profile $profile
  249. * @return boolean hook return value
  250. * @throws Exception
  251. * @author Diogo Cordeiro <diogo@fc.up.pt>
  252. */
  253. public function onEndShowAccountProfileBlock(HTMLOutputter $out, Profile $profile)
  254. {
  255. if ($profile->isLocal()) {
  256. return true;
  257. }
  258. try {
  259. Activitypub_profile::from_profile($profile);
  260. } catch (Exception $e) {
  261. // Not a remote ActivityPub_profile! Maybe some other network
  262. // that has imported a non-local user (e.g.: OStatus)?
  263. return true;
  264. }
  265. $out->elementStart('dl', 'entity_tags activitypub_profile');
  266. $out->element('dt', null, 'ActivityPub');
  267. $out->element('dd', null, _m('Remote Profile'));
  268. $out->elementEnd('dl');
  269. return true;
  270. }
  271. /**
  272. * Make sure necessary tables are filled out.
  273. *
  274. * @return boolean hook true
  275. */
  276. public function onCheckSchema()
  277. {
  278. $schema = Schema::get();
  279. $schema->ensureTable('activitypub_profile', Activitypub_profile::schemaDef());
  280. $schema->ensureTable('activitypub_rsa', Activitypub_rsa::schemaDef());
  281. $schema->ensureTable('activitypub_pending_follow_requests', Activitypub_pending_follow_requests::schemaDef());
  282. return true;
  283. }
  284. /********************************************************
  285. * WebFinger Events *
  286. ********************************************************/
  287. /**
  288. * Get remote user's ActivityPub_profile via a identifier
  289. *
  290. * @author GNU social
  291. * @author Diogo Cordeiro <diogo@fc.up.pt>
  292. * @param string $arg A remote user identifier
  293. * @return Activitypub_profile|null Valid profile in success | null otherwise
  294. */
  295. public static function pull_remote_profile($arg)
  296. {
  297. if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
  298. // webfinger lookup
  299. try {
  300. return Activitypub_profile::ensure_web_finger($arg);
  301. } catch (Exception $e) {
  302. common_log(LOG_ERR, 'Webfinger lookup failed for ' .
  303. $arg . ': ' . $e->getMessage());
  304. }
  305. }
  306. // Look for profile URLs, with or without scheme:
  307. $urls = [];
  308. if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
  309. $urls[] = $arg;
  310. }
  311. if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
  312. $schemes = array('http', 'https');
  313. foreach ($schemes as $scheme) {
  314. $urls[] = "$scheme://$arg";
  315. }
  316. }
  317. foreach ($urls as $url) {
  318. try {
  319. return Activitypub_profile::fromUri($url);
  320. } catch (Exception $e) {
  321. common_log(LOG_ERR, 'Profile lookup failed for ' .
  322. $arg . ': ' . $e->getMessage());
  323. }
  324. }
  325. return null;
  326. }
  327. /**
  328. * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz
  329. *
  330. * @author GNU social
  331. * @param string $text The text from which to extract webfinger IDs
  332. * @param string $preMention Character(s) that signals a mention ('@', '!'...)
  333. * @return array The matching IDs (without $preMention) and each respective position in the given string.
  334. */
  335. public static function extractWebfingerIds($text, $preMention='@')
  336. {
  337. $wmatches = [];
  338. $result = preg_match_all(
  339. '/(?<!\S)'.preg_quote($preMention, '/').'('.Nickname::WEBFINGER_FMT.')/',
  340. $text,
  341. $wmatches,
  342. PREG_OFFSET_CAPTURE
  343. );
  344. if ($result === false) {
  345. common_log(LOG_ERR, __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error=='.preg_last_error().').');
  346. return [];
  347. } elseif ($n_matches = count($wmatches)) {
  348. common_debug(sprintf('Found %d matches for WebFinger IDs: %s', $n_matches, _ve($wmatches)));
  349. }
  350. return $wmatches[1];
  351. }
  352. /**
  353. * Profile URL matches: @example.com/mublog/user
  354. *
  355. * @author GNU social
  356. * @param string $text The text from which to extract URL mentions
  357. * @param string $preMention Character(s) that signals a mention ('@', '!'...)
  358. * @return array The matching URLs (without @ or acct:) and each respective position in the given string.
  359. */
  360. public static function extractUrlMentions($text, $preMention='@')
  361. {
  362. $wmatches = [];
  363. // In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged
  364. // with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important)
  365. $result = preg_match_all(
  366. '/(?:^|\s+)'.preg_quote($preMention, '/').'('.URL_REGEX_DOMAIN_NAME.'(?:\/['.URL_REGEX_VALID_PATH_CHARS.']*)*)/',
  367. $text,
  368. $wmatches,
  369. PREG_OFFSET_CAPTURE
  370. );
  371. if ($result === false) {
  372. common_log(LOG_ERR, __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error=='.preg_last_error().').');
  373. return [];
  374. } elseif (count($wmatches)) {
  375. common_debug(sprintf('Found %d matches for profile URL mentions: %s', count($wmatches), _ve($wmatches)));
  376. }
  377. return $wmatches[1];
  378. }
  379. /**
  380. * Add activity+json mimetype on WebFinger
  381. *
  382. * @param XML_XRD $xrd
  383. * @param Managed_DataObject $object
  384. * @throws Exception
  385. * @author Diogo Cordeiro <diogo@fc.up.pt>
  386. */
  387. public function onEndWebFingerProfileLinks(XML_XRD $xrd, Managed_DataObject $object)
  388. {
  389. if ($object->isPerson()) {
  390. $link = new XML_XRD_Element_Link(
  391. 'self',
  392. ActivityPubPlugin::actor_uri($object->getProfile()),
  393. 'application/activity+json'
  394. );
  395. $xrd->links[] = clone($link);
  396. }
  397. }
  398. /**
  399. * Find any explicit remote mentions. Accepted forms:
  400. * Webfinger: @user@example.com
  401. * Profile link:
  402. * @param Profile $sender
  403. * @param string $text input markup text
  404. * @param $mentions
  405. * @return boolean hook return value
  406. * @throws InvalidUrlException
  407. * @author Diogo Cordeiro <diogo@fc.up.pt>
  408. * @example.com/mublog/user
  409. *
  410. * @author GNU social
  411. */
  412. public function onEndFindMentions(Profile $sender, $text, &$mentions)
  413. {
  414. $matches = [];
  415. foreach (self::extractWebfingerIds($text, '@') as $wmatch) {
  416. list($target, $pos) = $wmatch;
  417. $this->log(LOG_INFO, "Checking webfinger person '$target'");
  418. $profile = null;
  419. try {
  420. $aprofile = Activitypub_profile::ensure_web_finger($target);
  421. $profile = $aprofile->local_profile();
  422. } catch (Exception $e) {
  423. $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
  424. continue;
  425. }
  426. assert($profile instanceof Profile);
  427. $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target)
  428. ? $profile->getNickname() // TODO: we could do getBestName() or getFullname() here
  429. : $target;
  430. $url = $profile->getUri();
  431. if (!common_valid_http_url($url)) {
  432. $url = $profile->getUrl();
  433. }
  434. $matches[$pos] = array('mentioned' => array($profile),
  435. 'type' => 'mention',
  436. 'text' => $displayName,
  437. 'position' => $pos,
  438. 'length' => mb_strlen($target),
  439. 'url' => $url);
  440. }
  441. foreach (self::extractUrlMentions($text) as $wmatch) {
  442. list($target, $pos) = $wmatch;
  443. $schemes = array('https', 'http');
  444. foreach ($schemes as $scheme) {
  445. $url = "$scheme://$target";
  446. $this->log(LOG_INFO, "Checking profile address '$url'");
  447. try {
  448. $aprofile = Activitypub_profile::fromUri($url);
  449. $profile = $aprofile->local_profile();
  450. $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) ?
  451. $profile->nickname : $target;
  452. $matches[$pos] = array('mentioned' => array($profile),
  453. 'type' => 'mention',
  454. 'text' => $displayName,
  455. 'position' => $pos,
  456. 'length' => mb_strlen($target),
  457. 'url' => $profile->getUrl());
  458. break;
  459. } catch (Exception $e) {
  460. $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
  461. }
  462. }
  463. }
  464. foreach ($mentions as $i => $other) {
  465. // If we share a common prefix with a local user, override it!
  466. $pos = $other['position'];
  467. if (isset($matches[$pos])) {
  468. $mentions[$i] = $matches[$pos];
  469. unset($matches[$pos]);
  470. }
  471. }
  472. foreach ($matches as $mention) {
  473. $mentions[] = $mention;
  474. }
  475. return true;
  476. }
  477. /**
  478. * Allow remote profile references to be used in commands:
  479. * sub update@status.net
  480. * whois evan@identi.ca
  481. * reply http://identi.ca/evan hey what's up
  482. *
  483. * @param Command $command
  484. * @param string $arg
  485. * @param Profile &$profile
  486. * @return boolean hook return code
  487. * @author GNU social
  488. * @author Diogo Cordeiro <diogo@fc.up.pt>
  489. */
  490. public function onStartCommandGetProfile($command, $arg, &$profile)
  491. {
  492. try {
  493. $aprofile = $this->pull_remote_profile($arg);
  494. $profile = $aprofile->local_profile();
  495. } catch (Exception $e) {
  496. // No remote ActivityPub profile found
  497. return true;
  498. }
  499. return false;
  500. }
  501. /********************************************************
  502. * Discovery Events *
  503. ********************************************************/
  504. /**
  505. * Profile URI for remote profiles.
  506. *
  507. * @author GNU social
  508. * @author Diogo Cordeiro <diogo@fc.up.pt>
  509. * @param Profile $profile
  510. * @param string $uri in/out
  511. * @return mixed hook return code
  512. */
  513. public function onStartGetProfileUri(Profile $profile, &$uri)
  514. {
  515. $aprofile = Activitypub_profile::getKV('profile_id', $profile->id);
  516. if ($aprofile instanceof Activitypub_profile) {
  517. $uri = $aprofile->getUri();
  518. return false;
  519. }
  520. return true;
  521. }
  522. /**
  523. * Profile from URI.
  524. *
  525. * @author GNU social
  526. * @author Diogo Cordeiro <diogo@fc.up.pt>
  527. * @param string $uri
  528. * @param Profile &$profile in/out param: Profile got from URI
  529. * @return mixed hook return code
  530. */
  531. public function onStartGetProfileFromURI($uri, &$profile)
  532. {
  533. try {
  534. $explorer = new Activitypub_explorer();
  535. $profile = $explorer->lookup($uri)[0];
  536. return false;
  537. } catch (Exception $e) {
  538. return true; // It's not an ActivityPub profile as far as we know, continue event handling
  539. }
  540. }
  541. /********************************************************
  542. * Delivery Events *
  543. ********************************************************/
  544. /**
  545. * Having established a remote subscription, send a notification to the
  546. * remote ActivityPub profile's endpoint.
  547. *
  548. * @param Profile $profile subscriber
  549. * @param Profile $other subscribee
  550. * @return bool return value
  551. * @throws HTTP_Request2_Exception
  552. * @author Diogo Cordeiro <diogo@fc.up.pt>
  553. */
  554. public function onStartSubscribe(Profile $profile, Profile $other) {
  555. if (!$profile->isLocal() && $other->isLocal()) {
  556. return true;
  557. }
  558. try {
  559. $other = Activitypub_profile::from_profile($other);
  560. } catch (Exception $e) {
  561. return true; // Let other plugin handle this instead
  562. }
  563. $postman = new Activitypub_postman($profile, array($other));
  564. $postman->follow();
  565. return true;
  566. }
  567. /**
  568. * Notify remote server on unsubscribe.
  569. *
  570. * @param Profile $profile
  571. * @param Profile $other
  572. * @return bool return value
  573. * @throws HTTP_Request2_Exception
  574. * @author Diogo Cordeiro <diogo@fc.up.pt>
  575. */
  576. public function onStartUnsubscribe(Profile $profile, Profile $other)
  577. {
  578. if (!$profile->isLocal() && $other->isLocal()) {
  579. return true;
  580. }
  581. try {
  582. $other = Activitypub_profile::from_profile($other);
  583. } catch (Exception $e) {
  584. return true; // Let other plugin handle this instead
  585. }
  586. $postman = new Activitypub_postman($profile, array($other));
  587. $postman->undo_follow();
  588. return true;
  589. }
  590. /**
  591. * Notify remote users when their notices get favourited.
  592. *
  593. * @param Profile $profile of local user doing the faving
  594. * @param Notice $notice Notice being favored
  595. * @return bool return value
  596. * @throws HTTP_Request2_Exception
  597. * @throws InvalidUrlException
  598. * @author Diogo Cordeiro <diogo@fc.up.pt>
  599. */
  600. public function onEndFavorNotice(Profile $profile, Notice $notice)
  601. {
  602. // Only distribute local users' favor actions, remote users
  603. // will have already distributed theirs.
  604. if (!$profile->isLocal()) {
  605. return true;
  606. }
  607. $other = [];
  608. try {
  609. $other[] = Activitypub_profile::from_profile($notice->getProfile());
  610. } catch (Exception $e) {
  611. // Local user can be ignored
  612. }
  613. $other = array_merge($other,
  614. Activitypub_profile::from_profile_collection(
  615. $notice->getAttentionProfiles()
  616. ));
  617. if ($notice->reply_to) {
  618. try {
  619. $parent_notice = $notice->getParent();
  620. try {
  621. $other[] = Activitypub_profile::from_profile($parent_notice->getProfile());
  622. } catch (Exception $e) {
  623. // Local user can be ignored
  624. }
  625. $other = array_merge($other,
  626. Activitypub_profile::from_profile_collection(
  627. $parent_notice->getAttentionProfiles()
  628. ));
  629. } catch (NoParentNoticeException $e) {
  630. // This is not a reply to something (has no parent)
  631. } catch (NoResultException $e) {
  632. // Parent author's profile not found! Complain louder?
  633. common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage());
  634. }
  635. }
  636. $postman = new Activitypub_postman($profile, $other);
  637. $postman->like($notice);
  638. return true;
  639. }
  640. /**
  641. * Notify remote users when their notices get de-favourited.
  642. *
  643. * @param Profile $profile of local user doing the de-faving
  644. * @param Notice $notice Notice being favored
  645. * @return bool return value
  646. * @throws HTTP_Request2_Exception
  647. * @throws InvalidUrlException
  648. * @author Diogo Cordeiro <diogo@fc.up.pt>
  649. */
  650. public function onEndDisfavorNotice(Profile $profile, Notice $notice)
  651. {
  652. // Only distribute local users' favor actions, remote users
  653. // will have already distributed theirs.
  654. if (!$profile->isLocal()) {
  655. return true;
  656. }
  657. $other = [];
  658. try {
  659. $other[] = Activitypub_profile::from_profile($notice->getProfile());
  660. } catch (Exception $e) {
  661. // Local user can be ignored
  662. }
  663. $other = array_merge($other,
  664. Activitypub_profile::from_profile_collection(
  665. $notice->getAttentionProfiles()
  666. ));
  667. if ($notice->reply_to) {
  668. try {
  669. $parent_notice = $notice->getParent();
  670. try {
  671. $other[] = Activitypub_profile::from_profile($parent_notice->getProfile());
  672. } catch (Exception $e) {
  673. // Local user can be ignored
  674. }
  675. $other = array_merge($other,
  676. Activitypub_profile::from_profile_collection(
  677. $parent_notice->getAttentionProfiles()
  678. ));
  679. } catch (NoParentNoticeException $e) {
  680. // This is not a reply to something (has no parent)
  681. } catch (NoResultException $e) {
  682. // Parent author's profile not found! Complain louder?
  683. common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage());
  684. }
  685. }
  686. $postman = new Activitypub_postman($profile, $other);
  687. $postman->undo_like($notice);
  688. return true;
  689. }
  690. /**
  691. * Notify remote users when their notices get deleted
  692. *
  693. * @param $user
  694. * @param $notice
  695. * @return boolean hook flag
  696. * @throws HTTP_Request2_Exception
  697. * @throws InvalidUrlException
  698. * @author Diogo Cordeiro <diogo@fc.up.pt>
  699. */
  700. public function onStartDeleteOwnNotice($user, $notice)
  701. {
  702. $profile = $user->getProfile();
  703. // Only distribute local users' delete actions, remote users
  704. // will have already distributed theirs.
  705. if (!$profile->isLocal()) {
  706. return true;
  707. }
  708. // We handle things locally either because:
  709. // 1. the deleting user has special permissions to do so,
  710. // but still doesn't own the notice
  711. // 2. the notice is an announce, and there's no undo-share
  712. // logic in GS's AP implementation
  713. if (!$notice->isLocal() || $notice->isRepeat()) {
  714. return true;
  715. }
  716. $other = Activitypub_profile::from_profile_collection(
  717. $notice->getAttentionProfiles()
  718. );
  719. if ($notice->reply_to) {
  720. try {
  721. $parent_notice = $notice->getParent();
  722. try {
  723. $other[] = Activitypub_profile::from_profile($parent_notice->getProfile());
  724. } catch (Exception $e) {
  725. // Local user can be ignored
  726. }
  727. $other = array_merge($other,
  728. Activitypub_profile::from_profile_collection(
  729. $parent_notice->getAttentionProfiles()
  730. ));
  731. } catch (NoParentNoticeException $e) {
  732. // This is not a reply to something (has no parent)
  733. } catch (NoResultException $e) {
  734. // Parent author's profile not found! Complain louder?
  735. common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage());
  736. }
  737. }
  738. $postman = new Activitypub_postman($profile, $other);
  739. $postman->delete($notice);
  740. return true;
  741. }
  742. /**
  743. * Override the "from ActivityPub" bit in notice lists to link to the
  744. * original post and show the domain it came from.
  745. *
  746. * @author Diogo Cordeiro <diogo@fc.up.pt>
  747. * @param $notice
  748. * @param $name
  749. * @param $url
  750. * @param $title
  751. * @return mixed hook return code
  752. * @throws Exception
  753. */
  754. public function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
  755. {
  756. // If we don't handle this, keep the event handler going
  757. if (!in_array($notice->source, array('ActivityPub', 'share'))) {
  758. return true;
  759. }
  760. try {
  761. $url = $notice->getUrl();
  762. // If getUrl() throws exception, $url is never set
  763. $bits = parse_url($url);
  764. $domain = $bits['host'];
  765. if (substr($domain, 0, 4) == 'www.') {
  766. $name = substr($domain, 4);
  767. } else {
  768. $name = $domain;
  769. }
  770. // TRANS: Title. %s is a domain name.
  771. $title = sprintf(_m('Sent from %s via ActivityPub'), $domain);
  772. // Abort event handler, we have a name and URL!
  773. return false;
  774. } catch (InvalidUrlException $e) {
  775. // This just means we don't have the notice source data
  776. return true;
  777. }
  778. }
  779. }
  780. /**
  781. * Plugin return handler
  782. */
  783. class ActivityPubReturn
  784. {
  785. /**
  786. * Return a valid answer
  787. *
  788. * @param string $res
  789. * @param int $code Status Code
  790. * @return void
  791. * @author Diogo Cordeiro <diogo@fc.up.pt>
  792. */
  793. public static function answer($res = '', $code = 202)
  794. {
  795. http_response_code($code);
  796. header('Content-Type: application/activity+json');
  797. echo json_encode($res, JSON_UNESCAPED_SLASHES | (isset($_GET["pretty"]) ? JSON_PRETTY_PRINT : null));
  798. exit;
  799. }
  800. /**
  801. * Return an error
  802. *
  803. * @param string $m
  804. * @param int $code Status Code
  805. * @return void
  806. * @author Diogo Cordeiro <diogo@fc.up.pt>
  807. */
  808. public static function error($m, $code = 400)
  809. {
  810. http_response_code($code);
  811. header('Content-Type: application/activity+json');
  812. $res[] = Activitypub_error::error_message_to_array($m);
  813. echo json_encode($res, JSON_UNESCAPED_SLASHES);
  814. exit;
  815. }
  816. }