postman.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * ActivityPub implementation for GNU social
  18. *
  19. * @package GNUsocial
  20. * @author Diogo Cordeiro <diogo@fc.up.pt>
  21. * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
  22. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  23. * @link http://www.gnu.org/software/social/
  24. */
  25. defined('GNUSOCIAL') || die();
  26. /**
  27. * ActivityPub's own Postman
  28. *
  29. * Standard workflow expects that we send an Explorer to find out destinataries'
  30. * inbox address. Then we send our postman to deliver whatever we want to send them.
  31. *
  32. * @category Plugin
  33. * @package GNUsocial
  34. * @author Diogo Cordeiro <diogo@fc.up.pt>
  35. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  36. */
  37. class Activitypub_postman
  38. {
  39. private $actor;
  40. private $actor_uri;
  41. private $to = [];
  42. private $failed_to = [];
  43. private $client;
  44. private $headers;
  45. /**
  46. * Create a postman to deliver something to someone
  47. *
  48. * @param Profile $from sender Profile
  49. * @param array $to receiver AProfiles
  50. * @throws Exception
  51. * @author Diogo Cordeiro <diogo@fc.up.pt>
  52. */
  53. public function __construct(Profile $from, array $to = [])
  54. {
  55. $this->actor = $from;
  56. $this->to = $to;
  57. $this->actor_uri = $this->actor->getUri();
  58. $this->client = new HTTPClient();
  59. }
  60. /**
  61. * When dear postman dies, resurrect him until he finishes what he couldn't in life
  62. *
  63. * @throws ServerException
  64. * @author Diogo Cordeiro <diogo@fc.up.pt>
  65. */
  66. public function __destruct()
  67. {
  68. $qm = QueueManager::get();
  69. foreach($this->failed_to as $to => $activity) {
  70. $qm->enqueue([$to, $activity], 'activitypub_failed');
  71. }
  72. }
  73. /**
  74. * Send something to remote instance
  75. *
  76. * @param string $data request body
  77. * @param string $inbox url of remote inbox
  78. * @param string $method request method
  79. * @return GNUsocial_HTTPResponse
  80. * @throws HTTP_Request2_Exception
  81. * @throws HTTP_Request2_LogicException
  82. * @author Diogo Cordeiro <diogo@fc.up.pt>
  83. */
  84. public function send($data, $inbox, $method = 'POST')
  85. {
  86. common_debug('ActivityPub Postman: Delivering '.$data.' to '.$inbox);
  87. $headers = HttpSignature::sign($this->actor, $inbox, $data);
  88. common_debug('ActivityPub Postman: Delivery headers were: '.print_r($headers, true));
  89. $this->client->setBody($data);
  90. switch ($method) {
  91. case 'POST':
  92. $response = $this->client->post($inbox, $headers);
  93. break;
  94. case 'GET':
  95. $response = $this->client->get($inbox, $headers);
  96. break;
  97. default:
  98. throw new Exception("Unsupported request method for postman.");
  99. }
  100. common_debug('ActivityPub Postman: Delivery result with status code '.$response->getStatus().': '.$response->getBody());
  101. return $response;
  102. }
  103. /**
  104. * Send a follow notification to remote instance
  105. *
  106. * @return bool
  107. * @throws HTTP_Request2_Exception
  108. * @throws Exception
  109. * @author Diogo Cordeiro <diogo@fc.up.pt>
  110. */
  111. public function follow()
  112. {
  113. $data = Activitypub_follow::follow_to_array($this->actor_uri, $this->to[0]->getUrl());
  114. $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
  115. $res_body = json_decode($res->getBody(), true);
  116. if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
  117. $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
  118. $pending_list->add();
  119. return true;
  120. } elseif (isset($res_body['error'])) {
  121. throw new Exception($res_body['error']);
  122. }
  123. throw new Exception("An unknown error occurred.");
  124. }
  125. /**
  126. * Send a Undo Follow notification to remote instance
  127. *
  128. * @return bool
  129. * @throws HTTP_Request2_Exception
  130. * @throws Exception
  131. * @throws Exception
  132. * @throws Exception
  133. * @throws Exception
  134. * @author Diogo Cordeiro <diogo@fc.up.pt>
  135. */
  136. public function undo_follow()
  137. {
  138. $data = Activitypub_undo::undo_to_array(
  139. Activitypub_follow::follow_to_array(
  140. $this->actor_uri,
  141. $this->to[0]->getUrl()
  142. )
  143. );
  144. $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
  145. $res_body = json_decode($res->getBody(), true);
  146. if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
  147. Activitypub_profile::unsubscribeCacheUpdate($this->actor, $this->to[0]->local_profile());
  148. $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
  149. $pending_list->remove();
  150. return true;
  151. }
  152. if (isset($res_body['error'])) {
  153. throw new Exception($res_body['error']);
  154. }
  155. throw new Exception("An unknown error occurred.");
  156. }
  157. /**
  158. * Send a Accept Follow notification to remote instance
  159. *
  160. * @param string $id Follow activity id
  161. * @return bool
  162. * @throws HTTP_Request2_Exception
  163. * @throws Exception Description of HTTP Response error or generic error message.
  164. * @author Diogo Cordeiro <diogo@fc.up.pt>
  165. */
  166. public function accept_follow(string $id): bool
  167. {
  168. $data = Activitypub_accept::accept_to_array(
  169. Activitypub_follow::follow_to_array(
  170. $this->to[0]->getUrl(),
  171. $this->actor_uri,
  172. $id
  173. )
  174. );
  175. $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
  176. $res_body = json_decode($res->getBody(), true);
  177. if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
  178. $pending_list = new Activitypub_pending_follow_requests($this->to[0]->getID(), $this->actor->getID());
  179. $pending_list->remove();
  180. return true;
  181. }
  182. if (isset($res_body['error'])) {
  183. throw new Exception($res_body['error']);
  184. }
  185. throw new Exception("An unknown error occurred.");
  186. }
  187. /**
  188. * Send a Like notification to remote instances holding the notice
  189. *
  190. * @param Notice $notice
  191. * @throws HTTP_Request2_Exception
  192. * @throws InvalidUrlException
  193. * @throws Exception
  194. * @author Diogo Cordeiro <diogo@fc.up.pt>
  195. */
  196. public function like(Notice $notice): void
  197. {
  198. $data = Activitypub_like::like_to_array(
  199. $this->actor_uri,
  200. $notice
  201. );
  202. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  203. foreach ($this->to_inbox() as $inbox) {
  204. try {
  205. $res = $this->send($data, $inbox);
  206. // accumulate errors for later use, if needed
  207. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  208. $res_body = json_decode($res->getBody(), true);
  209. $errors[] = isset($res_body['error']) ?
  210. $res_body['error'] : "An unknown error occurred.";
  211. $to_failed[$inbox] = $notice;
  212. }
  213. } catch (Exception $e){
  214. $to_failed[$inbox] = $notice;
  215. }
  216. }
  217. if (!empty($errors)) {
  218. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the like activity!");
  219. }
  220. }
  221. /**
  222. * Send a Undo Like notification to remote instances holding the notice
  223. *
  224. * @param Notice $notice
  225. * @throws HTTP_Request2_Exception
  226. * @throws InvalidUrlException
  227. * @throws Exception
  228. * @author Diogo Cordeiro <diogo@fc.up.pt>
  229. */
  230. public function undo_like($notice)
  231. {
  232. $data = Activitypub_undo::undo_to_array(
  233. Activitypub_like::like_to_array(
  234. $this->actor_uri,
  235. $notice
  236. )
  237. );
  238. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  239. foreach ($this->to_inbox() as $inbox) {
  240. try {
  241. $res = $this->send($data, $inbox);
  242. // accummulate errors for later use, if needed
  243. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  244. $res_body = json_decode($res->getBody(), true);
  245. $errors[] = isset($res_body['error']) ?
  246. $res_body['error'] : "An unknown error occurred.";
  247. $to_failed[$inbox] = $notice;
  248. }
  249. } catch (Exception $e) {
  250. $to_failed[$inbox] = $notice;
  251. }
  252. }
  253. if (!empty($errors)) {
  254. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the undo-like activity!");
  255. }
  256. }
  257. /**
  258. * Send a Create notification to remote instances
  259. *
  260. * @param Notice $notice
  261. * @throws EmptyPkeyValueException
  262. * @throws HTTP_Request2_Exception
  263. * @throws InvalidUrlException
  264. * @throws ServerException
  265. * @author Diogo Cordeiro <diogo@fc.up.pt>
  266. */
  267. public function create_note($notice)
  268. {
  269. $data = Activitypub_create::create_to_array(
  270. $this->actor_uri,
  271. common_local_url('apNotice', ['id' => $notice->getID()]),
  272. Activitypub_notice::notice_to_array($notice)
  273. );
  274. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  275. foreach ($this->to_inbox() as $inbox) {
  276. try {
  277. $res = $this->send($data, $inbox);
  278. // accummulate errors for later use, if needed
  279. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  280. $res_body = json_decode($res->getBody(), true);
  281. $errors[] = isset($res_body['error']) ?
  282. $res_body['error'] : "An unknown error occurred.";
  283. $to_failed[$inbox] = $notice;
  284. }
  285. } catch (Exception $e) {
  286. $to_failed[$inbox] = $notice;
  287. }
  288. }
  289. if (!empty($errors)) {
  290. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
  291. }
  292. }
  293. /**
  294. * Send a Create direct-notification to remote instances
  295. *
  296. * @param Notice $message
  297. * @author Bruno Casteleiro <brunoccast@fc.up.pt>
  298. */
  299. public function create_direct_note(Notice $message)
  300. {
  301. $data = Activitypub_create::create_to_array(
  302. $this->actor_uri,
  303. common_local_url('apNotice', ['id' => $message->getID()]),
  304. Activitypub_message::message_to_array($message),
  305. true
  306. );
  307. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  308. foreach ($this->to_inbox(false) as $inbox) {
  309. $res = $this->send($data, $inbox);
  310. // accummulate errors for later use, if needed
  311. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  312. $res_body = json_decode($res->getBody(), true);
  313. $errors[] = isset($res_body['error']) ?
  314. $res_body['error'] : "An unknown error occurred.";
  315. $to_failed[$inbox] = $message;
  316. }
  317. }
  318. if (!empty($errors)) {
  319. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
  320. }
  321. }
  322. /**
  323. * Send a Announce notification to remote instances
  324. *
  325. * @param Notice $notice
  326. * @param Notice $repeat_of
  327. * @throws HTTP_Request2_Exception
  328. * @author Diogo Cordeiro <diogo@fc.up.pt>
  329. */
  330. public function announce(Notice $notice, Notice $repeat_of): void
  331. {
  332. $data = json_encode(
  333. Activitypub_announce::announce_to_array($this->actor, $notice, $repeat_of),
  334. JSON_UNESCAPED_SLASHES
  335. );
  336. foreach ($this->to_inbox() as $inbox) {
  337. try {
  338. $res = $this->send($data, $inbox);
  339. // accummulate errors for later use, if needed
  340. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  341. $res_body = json_decode($res->getBody(), true);
  342. $errors[] = isset($res_body['error']) ?
  343. $res_body['error'] : "An unknown error occurred.";
  344. $to_failed[$inbox] = $notice;
  345. }
  346. } catch (Exception $e) {
  347. $to_failed[$inbox] = $notice;
  348. }
  349. }
  350. if (!empty($errors)) {
  351. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the announce activity!");
  352. }
  353. }
  354. /**
  355. * Send a Delete notification to remote instances holding the notice
  356. *
  357. * @param Notice $notice
  358. * @throws HTTP_Request2_Exception
  359. * @throws InvalidUrlException
  360. * @throws Exception
  361. * @author Diogo Cordeiro <diogo@fc.up.pt>
  362. */
  363. public function delete_note($notice)
  364. {
  365. $data = Activitypub_delete::delete_to_array($notice);
  366. $errors = [];
  367. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  368. foreach ($this->to_inbox() as $inbox) {
  369. try {
  370. $res = $this->send($data, $inbox);
  371. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  372. $res_body = json_decode($res->getBody(), true);
  373. $errors[] = isset($res_body['error']) ?
  374. $res_body['error'] : "An unknown error occurred.";
  375. $to_failed[$inbox] = $notice;
  376. }
  377. } catch (Exception $e) {
  378. $to_failed[$inbox] = $notice;
  379. }
  380. }
  381. if (!empty($errors)) {
  382. throw new Exception(json_encode($errors));
  383. }
  384. }
  385. /**
  386. * Send a Delete notification to remote followers of some deleted profile
  387. *
  388. * @param Profile $deleted_profile
  389. * @throws HTTP_Request2_Exception
  390. * @author Bruno Casteleiro <brunoccast@fc.up.pt>
  391. */
  392. public function delete_profile(Profile $deleted_profile)
  393. {
  394. $data = Activitypub_delete::delete_to_array($deleted_profile);
  395. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  396. $errors = [];
  397. foreach ($this->to_inbox() as $inbox) {
  398. try {
  399. $res = $this->send($data, $inbox);
  400. // accumulate errors for later use, if needed
  401. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  402. $res_body = json_decode($res->getBody(), true);
  403. $errors[] = isset($res_body['error']) ?
  404. $res_body['error'] : "An unknown error occurred.";
  405. }
  406. } catch (Exception $e) {
  407. $to_failed[$inbox] = $notice;
  408. }
  409. }
  410. if (!empty($errors)) {
  411. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the delete_profile activity!");
  412. }
  413. }
  414. /**
  415. * Clean list of inboxes to deliver messages
  416. *
  417. * @param bool $actorFollowers whether to include the actor's follower collection
  418. * @return array To Inbox URLs
  419. * @throws Exception
  420. * @author Diogo Cordeiro <diogo@fc.up.pt>
  421. */
  422. private function to_inbox(bool $actorFollowers = true): array
  423. {
  424. if ($actorFollowers) {
  425. $discovery = new Activitypub_explorer();
  426. $followers = apActorFollowersAction::generate_followers($this->actor, 0, null);
  427. foreach ($followers as $sub) {
  428. try {
  429. $this->to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]);
  430. } catch (Exception $e) {
  431. // Not an ActivityPub Remote Follower, let it go
  432. }
  433. }
  434. unset($discovery);
  435. }
  436. $to_inboxes = [];
  437. foreach ($this->to as $to_profile) {
  438. $i = $to_profile->get_inbox();
  439. // Prevent delivering to self
  440. if ($i == common_local_url('apInbox')) {
  441. continue;
  442. }
  443. $to_inboxes[] = $i;
  444. }
  445. return array_unique($to_inboxes);
  446. }
  447. }