ActivityPubPlugin.php 34 KB

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