ActivityPubPlugin.php 33 KB

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