ActivityPub.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social is free software: you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // GNU social is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. /**
  20. * ActivityPub implementation for GNU social
  21. *
  22. * @package GNUsocial
  23. * @category ActivityPub
  24. *
  25. * @author Diogo Peralta Cordeiro <@diogo.site>
  26. * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
  27. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  28. */
  29. namespace Plugin\ActivityPub;
  30. use ActivityPhp\Type;
  31. use ActivityPhp\Type\AbstractObject;
  32. use App\Core\DB;
  33. use App\Core\Event;
  34. use App\Core\HTTPClient;
  35. use function App\Core\I18n\_m;
  36. use App\Core\Log;
  37. use App\Core\Modules\Plugin;
  38. use App\Core\Queue;
  39. use App\Core\Router;
  40. use App\Entity\Activity;
  41. use App\Entity\Actor;
  42. use App\Entity\Note;
  43. use App\Util\Common;
  44. use App\Util\Exception\BugFoundException;
  45. use Component\Collection\Util\Controller\OrderedCollection;
  46. use Component\FreeNetwork\Entity\FreeNetworkActorProtocol;
  47. use Component\FreeNetwork\Util\Discovery;
  48. use EventResult;
  49. use Exception;
  50. use InvalidArgumentException;
  51. use Plugin\ActivityPub\Controller\Inbox;
  52. use Plugin\ActivityPub\Controller\Outbox;
  53. use Plugin\ActivityPub\Entity\ActivitypubActivity;
  54. use Plugin\ActivityPub\Entity\ActivitypubActor;
  55. use Plugin\ActivityPub\Entity\ActivitypubObject;
  56. use Plugin\ActivityPub\Util\Explorer;
  57. use Plugin\ActivityPub\Util\HTTPSignature;
  58. use Plugin\ActivityPub\Util\Model;
  59. use Plugin\ActivityPub\Util\OrderedCollectionController;
  60. use Plugin\ActivityPub\Util\Response\ActivityResponse;
  61. use Plugin\ActivityPub\Util\Response\ActorResponse;
  62. use Plugin\ActivityPub\Util\Response\NoteResponse;
  63. use Plugin\ActivityPub\Util\TypeResponse;
  64. use Plugin\ActivityPub\Util\Validator\contentLangModelValidator;
  65. use Plugin\ActivityPub\Util\Validator\manuallyApprovesFollowersModelValidator;
  66. use Symfony\Component\HttpFoundation\JsonResponse;
  67. use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
  68. use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
  69. use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
  70. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  71. use Symfony\Contracts\HttpClient\ResponseInterface;
  72. use XML_XRD;
  73. use XML_XRD_Element_Link;
  74. /**
  75. * Adds ActivityPub support to GNU social when enabled
  76. *
  77. * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
  78. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  79. */
  80. class ActivityPub extends Plugin
  81. {
  82. // ActivityStreams 2.0 Accept Headers
  83. public static array $accept_headers = [
  84. 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
  85. 'application/activity+json',
  86. 'application/json',
  87. 'application/ld+json',
  88. ];
  89. // So that this isn't hardcoded everywhere
  90. public const PUBLIC_TO = [
  91. 'https://www.w3.org/ns/activitystreams#Public',
  92. 'Public',
  93. 'as:Public',
  94. ];
  95. public const HTTP_CLIENT_HEADERS = [
  96. 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
  97. 'User-Agent' => 'GNUsocialBot ' . GNUSOCIAL_VERSION . ' - ' . GNUSOCIAL_PROJECT_URL,
  98. ];
  99. public static function version(): string
  100. {
  101. return '3.0.0';
  102. }
  103. public static array $activity_streams_two_context = [
  104. 'https://www.w3.org/ns/activitystreams',
  105. 'https://w3id.org/security/v1',
  106. ['gs' => 'https://www.gnu.org/software/social/ns#'],
  107. ['litepub' => 'http://litepub.social/ns#'],
  108. ['chatMessage' => 'litepub:chatMessage'],
  109. [
  110. 'inConversation' => [
  111. '@id' => 'gs:inConversation',
  112. '@type' => '@id',
  113. ],
  114. ],
  115. ];
  116. public function onInitializePlugin(): EventResult
  117. {
  118. Event::handle('ActivityStreamsTwoContext', [&self::$activity_streams_two_context]);
  119. self::$activity_streams_two_context = array_unique(self::$activity_streams_two_context, \SORT_REGULAR);
  120. return Event::next;
  121. }
  122. public function onQueueActivitypubInbox(ActivitypubActor $ap_actor, Actor $actor, string|AbstractObject $type): EventResult
  123. {
  124. // TODO: Check if Actor has authority over payload
  125. // Store Activity
  126. $ap_act = Model\Activity::fromJson($type, ['source' => 'ActivityPub']);
  127. FreeNetworkActorProtocol::protocolSucceeded(
  128. 'activitypub',
  129. $ap_actor->getActorId(),
  130. Discovery::normalize($actor->getNickname() . '@' . parse_url($ap_actor->getInboxUri(), \PHP_URL_HOST)),
  131. );
  132. DB::flush();
  133. if (($att_targets = $ap_act->getAttentionTargets()) !== []) {
  134. if (Event::handle('ActivityPubNewNotification', [$actor, ($act = $ap_act->getActivity()), $att_targets, _m('{actor_id} triggered a notification via ActivityPub.', ['{actor_id}' => $actor->getId()])]) === Event::next) {
  135. Event::handle('NewNotification', [$actor, $act, $att_targets, _m('{actor_id} triggered a notification via ActivityPub.', ['{nickname}' => $actor->getId()])]);
  136. }
  137. }
  138. return Event::stop;
  139. }
  140. /**
  141. * This code executes when GNU social creates the page routing, and we hook
  142. * on this event to add our Inbox and Outbox handler for ActivityPub.
  143. *
  144. * @param Router $r the router that was initialized
  145. */
  146. public function onAddRoute(Router $r): EventResult
  147. {
  148. $r->connect(
  149. 'activitypub_inbox',
  150. '/inbox.json',
  151. Inbox::class,
  152. options: ['format' => self::$accept_headers[0]],
  153. );
  154. $r->connect(
  155. 'activitypub_actor_inbox',
  156. '/actor/{gsactor_id<\d+>}/inbox.json',
  157. [Inbox::class, 'handle'],
  158. options: ['format' => self::$accept_headers[0]],
  159. );
  160. $r->connect(
  161. 'activitypub_actor_outbox',
  162. '/actor/{gsactor_id<\d+>}/outbox.json',
  163. [Outbox::class, 'viewOutboxByActorId'],
  164. options: ['accept' => self::$accept_headers, 'format' => self::$accept_headers[0]],
  165. );
  166. return Event::next;
  167. }
  168. /**
  169. * Fill Actor->getUrl() calls with correct URL coming from ActivityPub
  170. */
  171. public function onStartGetActorUri(Actor $actor, int $type, ?string &$url): EventResult
  172. {
  173. if (
  174. // Is remote?
  175. !$actor->getIsLocal()
  176. // Is in ActivityPub?
  177. && !\is_null($ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))
  178. // We can only provide a full URL (anything else wouldn't make sense)
  179. && $type === Router::ABSOLUTE_URL
  180. ) {
  181. $url = $ap_actor->getUri();
  182. return Event::stop;
  183. }
  184. return Event::next;
  185. }
  186. /**
  187. * Fill Actor->canAdmin() for Actors that came from ActivityPub
  188. */
  189. public function onFreeNetworkActorCanAdmin(Actor $actor, Actor $other, bool &$canAdmin): EventResult
  190. {
  191. // Are both in AP?
  192. if (
  193. !\is_null($ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))
  194. && !\is_null($ap_other = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $other->getId()], return_null: true))
  195. ) {
  196. // Are they both in the same server?
  197. $canAdmin = parse_url($ap_actor->getUri(), \PHP_URL_HOST) === parse_url($ap_other->getUri(), \PHP_URL_HOST);
  198. return Event::stop;
  199. }
  200. return Event::next;
  201. }
  202. /**
  203. * Overload core endpoints to make resources available in ActivityStreams 2.0
  204. *
  205. * @throws Exception
  206. */
  207. public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?TypeResponse &$response = null): EventResult
  208. {
  209. if (\count(array_intersect(self::$accept_headers, $accept_header)) === 0) {
  210. return Event::next;
  211. }
  212. switch ($route) {
  213. case 'actor_view_id':
  214. case 'person_actor_view_id':
  215. case 'person_actor_view_nickname':
  216. case 'group_actor_view_id':
  217. case 'group_actor_view_nickname':
  218. case 'bot_actor_view_id':
  219. case 'bot_actor_view_nickname':
  220. $response = ActorResponse::handle($vars['actor']);
  221. break;
  222. case 'activity_view':
  223. $response = ActivityResponse::handle($vars['activity']);
  224. break;
  225. case 'note_view':
  226. $response = NoteResponse::handle($vars['note']);
  227. break;
  228. case 'activitypub_actor_outbox':
  229. $response = new TypeResponse($vars['type']);
  230. break;
  231. default:
  232. if (Event::handle('ActivityPubActivityStreamsTwoResponse', [$route, $vars, &$response]) !== Event::stop) {
  233. if (is_subclass_of($vars['controller'][0], OrderedCollection::class)) {
  234. $response = new TypeResponse(OrderedCollectionController::fromControllerVars($vars)['type']);
  235. } else {
  236. $response = new JsonResponse(['error' => 'Unknown Object cannot be represented.']);
  237. }
  238. }
  239. }
  240. return Event::stop;
  241. }
  242. /**
  243. * Add ActivityStreams 2 Extensions
  244. */
  245. public function onActivityPubValidateActivityStreamsTwoData(string $type_name, array &$validators): EventResult
  246. {
  247. switch ($type_name) {
  248. case 'Person':
  249. $validators['manuallyApprovesFollowers'] = manuallyApprovesFollowersModelValidator::class;
  250. break;
  251. case 'Note':
  252. $validators['contentLang'] = contentLangModelValidator::class;
  253. break;
  254. }
  255. return Event::next;
  256. }
  257. // FreeNetworkComponent Events
  258. /**
  259. * Let FreeNetwork Component know we exist and which class to use to call the freeNetworkDistribute method
  260. */
  261. public function onAddFreeNetworkProtocol(array &$protocols): EventResult
  262. {
  263. if (!\in_array('\Plugin\ActivityPub\ActivityPub', $protocols)) {
  264. $protocols[] = '\Plugin\ActivityPub\ActivityPub';
  265. }
  266. return Event::next;
  267. }
  268. /**
  269. * The FreeNetwork component will call this function to pull ActivityPub objects by URI
  270. *
  271. * @param string $uri Query
  272. *
  273. * @return bool true if imported, false otherwise
  274. */
  275. public static function freeNetworkGrabRemote(string $uri, ?Actor $on_behalf_of = null): bool
  276. {
  277. if (Common::isValidHttpUrl($uri)) {
  278. try {
  279. $object = self::getObjectByUri($uri, try_online: true, on_behalf_of: $on_behalf_of);
  280. if (!\is_null($object)) {
  281. if ($object instanceof Type\AbstractObject) {
  282. if (\in_array($object->get('type'), array_keys(Model\Actor::$_as2_actor_type_to_gs_actor_type))) {
  283. DB::wrapInTransaction(fn () => Model\Actor::fromJson($object));
  284. } else {
  285. DB::wrapInTransaction(fn () => Model\Activity::fromJson($object));
  286. }
  287. }
  288. return true;
  289. }
  290. } catch (Exception|Throwable) {
  291. // May be invalid input, we can safely ignore in this case
  292. }
  293. }
  294. return false;
  295. }
  296. public function onQueueActivitypubPostman(
  297. Actor $sender,
  298. Activity $activity,
  299. string $inbox,
  300. array $to_actors,
  301. array &$retry_args,
  302. ): EventResult {
  303. try {
  304. $data = Model::toType($activity);
  305. if ($sender->isGroup()) { // When the sender is a group,
  306. if ($activity->getVerb() === 'subscribe') {
  307. // Regular postman happens
  308. } elseif ($activity->getVerb() === 'undo' && $data->get('object')->get('type') === 'Follow') {
  309. // Regular postman happens
  310. } else {
  311. // For every other activity sent by a Group, we have to wrap it in a transient Announce activity
  312. $data = Type::create('Announce', [
  313. '@context' => 'https:\/\/www.w3.org\/ns\/activitystreams',
  314. 'actor' => $sender->getUri(type: Router::ABSOLUTE_URL),
  315. 'object' => $data,
  316. ]);
  317. }
  318. }
  319. $res = self::postman($sender, $data->toJson(), $inbox);
  320. // accumulate errors for later use, if needed
  321. $status_code = $res->getStatusCode();
  322. if (!($status_code === 200 || $status_code === 202 || $status_code === 409)) {
  323. $res_body = json_decode($res->getContent(), true);
  324. $retry_args['reason'] ??= [];
  325. $retry_args['reason'][] = $res_body['error'] ?? 'An unknown error occurred.';
  326. return Event::next;
  327. } else {
  328. foreach ($to_actors as $actor) {
  329. if ($actor->isPerson()) {
  330. FreeNetworkActorProtocol::protocolSucceeded(
  331. 'activitypub',
  332. $actor,
  333. Discovery::normalize($actor->getNickname() . '@' . parse_url($inbox, \PHP_URL_HOST)),
  334. );
  335. }
  336. }
  337. }
  338. return Event::stop;
  339. } catch (Exception $e) {
  340. Log::error('ActivityPub @ freeNetworkDistribute: ' . $e->getMessage(), [$e]);
  341. $retry_args['reason'] ??= [];
  342. $retry_args['reason'][] = "Got an exception: {$e->getMessage()}";
  343. $retry_args['exception'] ??= [];
  344. $retry_args['exception'][] = $e;
  345. return Event::next;
  346. }
  347. }
  348. /**
  349. * The FreeNetwork component will call this function to distribute this instance's activities
  350. *
  351. * @throws ClientExceptionInterface
  352. * @throws RedirectionExceptionInterface
  353. * @throws ServerExceptionInterface
  354. * @throws TransportExceptionInterface
  355. */
  356. public static function freeNetworkDistribute(Actor $sender, Activity $activity, array $targets, ?string $reason = null): void
  357. {
  358. $to_addr = [];
  359. foreach ($targets as $actor) {
  360. if (FreeNetworkActorProtocol::canIActor('activitypub', $actor->getId())) {
  361. // Sometimes FreeNetwork can allow us to actor even though we don't have an internal representation of
  362. // the actor, that could for example mean that OStatus handled this actor while we were deactivated
  363. // On next interaction this should be resolved, for now continue
  364. if (\is_null($ap_target = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))) {
  365. Log::info('FreeNetwork wrongly told ActivityPub that it can handle actor id: ' . $actor->getId() . ' you might want to keep an eye on it.');
  366. continue;
  367. }
  368. $to_addr[$ap_target->getInboxSharedUri() ?? $ap_target->getInboxUri()][] = $actor;
  369. } else {
  370. continue;
  371. }
  372. }
  373. foreach ($to_addr as $inbox => $to_actors) {
  374. Queue::enqueue(
  375. payload: [$sender, $activity, $inbox, $to_actors],
  376. queue: 'ActivitypubPostman',
  377. priority: false,
  378. );
  379. }
  380. }
  381. /**
  382. * Internal tool to sign and send activities out
  383. *
  384. * @throws Exception
  385. */
  386. private static function postman(Actor $sender, string $json_activity, string $inbox, string $method = 'post'): ResponseInterface
  387. {
  388. Log::debug('ActivityPub Postman: Delivering ' . $json_activity . ' to ' . $inbox);
  389. $headers = HTTPSignature::sign($sender, $inbox, $json_activity);
  390. Log::debug('ActivityPub Postman: Delivery headers were: ' . print_r($headers, true));
  391. $response = HTTPClient::$method($inbox, ['headers' => $headers, 'body' => $json_activity]);
  392. Log::debug('ActivityPub Postman: Delivery result with status code ' . $response->getStatusCode() . ': ' . $response->getContent());
  393. return $response;
  394. }
  395. // WebFinger Events
  396. /**
  397. * Add activity+json mimetype to WebFinger
  398. */
  399. public function onEndWebFingerProfileLinks(XML_XRD $xrd, Actor $object): EventResult
  400. {
  401. if ($object->isPerson()) {
  402. $link = new XML_XRD_Element_Link(
  403. rel: 'self',
  404. href: $object->getUri(Router::ABSOLUTE_URL),//Router::url('actor_view_id', ['id' => $object->getId()], Router::ABSOLUTE_URL),
  405. type: 'application/activity+json',
  406. );
  407. $xrd->links[] = clone $link;
  408. }
  409. return Event::next;
  410. }
  411. /**
  412. * When FreeNetwork component asks us to help with identifying Actors from XRDs
  413. */
  414. public function onFreeNetworkFoundXrd(XML_XRD $xrd, ?Actor &$actor = null): EventResult
  415. {
  416. $addr = null;
  417. foreach ($xrd->aliases as $alias) {
  418. if (Discovery::isAcct($alias)) {
  419. $addr = Discovery::normalize($alias);
  420. }
  421. }
  422. if (\is_null($addr)) {
  423. return Event::next;
  424. } else {
  425. if (!FreeNetworkActorProtocol::canIAddr('activitypub', $addr)) {
  426. return Event::next;
  427. }
  428. }
  429. try {
  430. $ap_actor = ActivitypubActor::fromXrd($addr, $xrd);
  431. $actor = Actor::getById($ap_actor->getActorId());
  432. FreeNetworkActorProtocol::protocolSucceeded('activitypub', $actor, $addr);
  433. return Event::stop;
  434. } catch (Exception $e) {
  435. Log::error('ActivityPub Actor from URL Mention check failed: ' . $e->getMessage());
  436. return Event::next;
  437. }
  438. }
  439. // Discovery Events
  440. /**
  441. * When FreeNetwork component asks us to help with identifying Actors from URIs
  442. */
  443. public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): EventResult
  444. {
  445. try {
  446. if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) {
  447. $ap_actor = DB::wrapInTransaction(fn () => ActivitypubActor::getByAddr($addr));
  448. $actor = Actor::getById($ap_actor->getActorId());
  449. FreeNetworkActorProtocol::protocolSucceeded('activitypub', $actor->getId(), $addr);
  450. return Event::stop;
  451. } else {
  452. return Event::next;
  453. }
  454. } catch (Exception $e) {
  455. Log::error('ActivityPub WebFinger Mention check failed.', [$e]);
  456. return Event::next;
  457. }
  458. }
  459. /**
  460. * @return string got from URI
  461. */
  462. public static function getUriByObject(mixed $object): string
  463. {
  464. switch ($object::class) {
  465. case Note::class:
  466. if ($object->getIsLocal()) {
  467. return $object->getUrl();
  468. } else {
  469. // Try known remote objects
  470. $known_object = DB::findOneBy(ActivitypubObject::class, ['object_type' => 'note', 'object_id' => $object->getId()], return_null: true);
  471. if ($known_object instanceof ActivitypubObject) {
  472. return $known_object->getObjectUri();
  473. } else {
  474. throw new BugFoundException('ActivityPub cannot generate an URI for a stored note.', [$object, $known_object]);
  475. }
  476. }
  477. break;
  478. case Actor::class:
  479. return $object->getUri();
  480. break;
  481. case Activity::class:
  482. // Try known remote activities
  483. $known_activity = DB::findOneBy(ActivitypubActivity::class, ['activity_id' => $object->getId()], return_null: true);
  484. if (!\is_null($known_activity)) {
  485. return $known_activity->getActivityUri();
  486. } else {
  487. return Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL);
  488. }
  489. break;
  490. default:
  491. throw new InvalidArgumentException('ActivityPub::getUriByObject found a limitation with: ' . var_export($object, true));
  492. }
  493. }
  494. /**
  495. * Get a Note from ActivityPub URI, if it doesn't exist, attempt to fetch it
  496. * This should only be necessary internally.
  497. *
  498. * @throws ClientExceptionInterface
  499. * @throws RedirectionExceptionInterface
  500. * @throws ServerExceptionInterface
  501. * @throws TransportExceptionInterface
  502. *
  503. * @return null|Actor|mixed|Note got from URI
  504. */
  505. public static function getObjectByUri(string $resource, bool $try_online = true, ?Actor $on_behalf_of = null): mixed
  506. {
  507. // Try known object
  508. $known_object = DB::findOneBy(ActivitypubObject::class, ['object_uri' => $resource], return_null: true);
  509. if (!\is_null($known_object)) {
  510. return $known_object->getObject();
  511. }
  512. // Try known activity
  513. $known_activity = DB::findOneBy(ActivitypubActivity::class, ['activity_uri' => $resource], return_null: true);
  514. if (!\is_null($known_activity)) {
  515. return $known_activity->getActivity();
  516. }
  517. // Try Actor
  518. try {
  519. return Explorer::getOneFromUri($resource, try_online: false, on_behalf_of: $on_behalf_of);
  520. } catch (\Exception) {
  521. // Ignore, this is brute forcing, it's okay not to find
  522. }
  523. // Is it a HTTP URL?
  524. if (Common::isValidHttpUrl($resource)) {
  525. $resource_parts = parse_url($resource);
  526. // If it is local
  527. if ($resource_parts['host'] === Common::config('site', 'server')) {
  528. // Try Local Note
  529. $local_note = DB::findOneBy(Note::class, ['url' => $resource], return_null: true);
  530. if (!\is_null($local_note)) {
  531. return $local_note;
  532. }
  533. // Try local Activity
  534. try {
  535. $match = Router::match($resource_parts['path']);
  536. $local_activity = DB::findOneBy(Activity::class, ['id' => $match['id']], return_null: true);
  537. if (!\is_null($local_activity)) {
  538. return $local_activity;
  539. } else {
  540. throw new InvalidArgumentException('Tried to retrieve a non-existent local activity.');
  541. }
  542. } catch (\Exception) {
  543. // Ignore, this is brute forcing, it's okay not to find
  544. }
  545. throw new BugFoundException('ActivityPub failed to retrieve local resource: "' . $resource . '". This is a big issue.');
  546. } else {
  547. // Then it's remote
  548. if (!$try_online) {
  549. throw new Exception("Remote resource {$resource} not found without online resources.");
  550. }
  551. $response = Explorer::get($resource, $on_behalf_of);
  552. // If it was deleted
  553. if ($response->getStatusCode() == 410) {
  554. //$obj = Type::create('Tombstone', ['id' => $resource]);
  555. return null;
  556. } elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable
  557. throw new Exception('Non Ok Status Code for given Object id.');
  558. } else {
  559. return Model::jsonToType($response->getContent());
  560. }
  561. }
  562. }
  563. return null;
  564. }
  565. }