postman.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  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 Exception
  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. $res = $this->send($data, $inbox);
  205. // accumulate errors for later use, if needed
  206. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  207. $res_body = json_decode($res->getBody(), true);
  208. $errors[] = isset($res_body['error']) ?
  209. $res_body['error'] : "An unknown error occurred.";
  210. $to_failed[$inbox] = $notice;
  211. }
  212. }
  213. if (!empty($errors)) {
  214. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the like activity!");
  215. }
  216. }
  217. /**
  218. * Send a Undo Like notification to remote instances holding the notice
  219. *
  220. * @param Notice $notice
  221. * @throws HTTP_Request2_Exception
  222. * @throws InvalidUrlException
  223. * @throws Exception
  224. * @author Diogo Cordeiro <diogo@fc.up.pt>
  225. */
  226. public function undo_like($notice)
  227. {
  228. $data = Activitypub_undo::undo_to_array(
  229. Activitypub_like::like_to_array(
  230. $this->actor_uri,
  231. $notice
  232. )
  233. );
  234. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  235. foreach ($this->to_inbox() as $inbox) {
  236. $res = $this->send($data, $inbox);
  237. // accummulate errors for later use, if needed
  238. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  239. $res_body = json_decode($res->getBody(), true);
  240. $errors[] = isset($res_body['error']) ?
  241. $res_body['error'] : "An unknown error occurred.";
  242. $to_failed[$inbox] = $notice;
  243. }
  244. }
  245. if (!empty($errors)) {
  246. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the undo-like activity!");
  247. }
  248. }
  249. /**
  250. * Send a Create notification to remote instances
  251. *
  252. * @param Notice $notice
  253. * @throws EmptyPkeyValueException
  254. * @throws HTTP_Request2_Exception
  255. * @throws InvalidUrlException
  256. * @throws ServerException
  257. * @author Diogo Cordeiro <diogo@fc.up.pt>
  258. */
  259. public function create_note($notice)
  260. {
  261. $data = Activitypub_create::create_to_array(
  262. $this->actor_uri,
  263. common_local_url('apNotice', ['id' => $notice->getID()]),
  264. Activitypub_notice::notice_to_array($notice)
  265. );
  266. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  267. foreach ($this->to_inbox() as $inbox) {
  268. $res = $this->send($data, $inbox);
  269. // accummulate errors for later use, if needed
  270. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  271. $res_body = json_decode($res->getBody(), true);
  272. $errors[] = isset($res_body['error']) ?
  273. $res_body['error'] : "An unknown error occurred.";
  274. $to_failed[$inbox] = $notice;
  275. }
  276. }
  277. if (!empty($errors)) {
  278. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
  279. }
  280. }
  281. /**
  282. * Send a Create direct-notification to remote instances
  283. *
  284. * @param Notice $message
  285. * @author Bruno Casteleiro <brunoccast@fc.up.pt>
  286. */
  287. public function create_direct_note(Notice $message)
  288. {
  289. $data = Activitypub_create::create_to_array(
  290. $this->actor_uri,
  291. common_local_url('apNotice', ['id' => $message->getID()]),
  292. Activitypub_message::message_to_array($message),
  293. true
  294. );
  295. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  296. foreach ($this->to_inbox(false) as $inbox) {
  297. $res = $this->send($data, $inbox);
  298. // accummulate errors for later use, if needed
  299. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  300. $res_body = json_decode($res->getBody(), true);
  301. $errors[] = isset($res_body['error']) ?
  302. $res_body['error'] : "An unknown error occurred.";
  303. $to_failed[$inbox] = $message;
  304. }
  305. }
  306. if (!empty($errors)) {
  307. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
  308. }
  309. }
  310. /**
  311. * Send a Announce notification to remote instances
  312. *
  313. * @param Notice $notice
  314. * @param Notice $repeat_of
  315. * @throws HTTP_Request2_Exception
  316. * @author Diogo Cordeiro <diogo@fc.up.pt>
  317. */
  318. public function announce(Notice $notice, Notice $repeat_of): void
  319. {
  320. $data = json_encode(
  321. Activitypub_announce::announce_to_array($this->actor, $notice, $repeat_of),
  322. JSON_UNESCAPED_SLASHES
  323. );
  324. foreach ($this->to_inbox() as $inbox) {
  325. $res = $this->send($data, $inbox);
  326. // accummulate errors for later use, if needed
  327. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  328. $res_body = json_decode($res->getBody(), true);
  329. $errors[] = isset($res_body['error']) ?
  330. $res_body['error'] : "An unknown error occurred.";
  331. $to_failed[$inbox] = $notice;
  332. }
  333. }
  334. if (!empty($errors)) {
  335. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the announce activity!");
  336. }
  337. }
  338. /**
  339. * Send a Delete notification to remote instances holding the notice
  340. *
  341. * @param Notice $notice
  342. * @throws HTTP_Request2_Exception
  343. * @throws InvalidUrlException
  344. * @throws Exception
  345. * @author Diogo Cordeiro <diogo@fc.up.pt>
  346. */
  347. public function delete_note($notice)
  348. {
  349. $data = Activitypub_delete::delete_to_array($notice);
  350. $errors = [];
  351. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  352. foreach ($this->to_inbox() as $inbox) {
  353. $res = $this->send($data, $inbox);
  354. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  355. $res_body = json_decode($res->getBody(), true);
  356. $errors[] = isset($res_body['error']) ?
  357. $res_body['error'] : "An unknown error occurred.";
  358. $to_failed[$inbox] = $notice;
  359. }
  360. }
  361. if (!empty($errors)) {
  362. throw new Exception(json_encode($errors));
  363. }
  364. }
  365. /**
  366. * Send a Delete notification to remote followers of some deleted profile
  367. *
  368. * @param Profile $deleted_profile
  369. * @throws HTTP_Request2_Exception
  370. * @author Bruno Casteleiro <brunoccast@fc.up.pt>
  371. */
  372. public function delete_profile(Profile $deleted_profile)
  373. {
  374. $data = Activitypub_delete::delete_to_array($deleted_profile);
  375. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  376. $errors = [];
  377. foreach ($this->to_inbox() as $inbox) {
  378. $res = $this->send($data, $inbox);
  379. // accumulate errors for later use, if needed
  380. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  381. $res_body = json_decode($res->getBody(), true);
  382. $errors[] = isset($res_body['error']) ?
  383. $res_body['error'] : "An unknown error occurred.";
  384. }
  385. }
  386. if (!empty($errors)) {
  387. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the delete_profile activity!");
  388. }
  389. }
  390. /**
  391. * Clean list of inboxes to deliver messages
  392. *
  393. * @param bool $actorFollowers whether to include the actor's follower collection
  394. * @return array To Inbox URLs
  395. * @throws Exception
  396. * @author Diogo Cordeiro <diogo@fc.up.pt>
  397. */
  398. private function to_inbox(bool $actorFollowers = true): array
  399. {
  400. if ($actorFollowers) {
  401. $discovery = new Activitypub_explorer();
  402. $followers = apActorFollowersAction::generate_followers($this->actor, 0, null);
  403. foreach ($followers as $sub) {
  404. try {
  405. $this->to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]);
  406. } catch (Exception $e) {
  407. // Not an ActivityPub Remote Follower, let it go
  408. }
  409. }
  410. unset($discovery);
  411. }
  412. $to_inboxes = [];
  413. foreach ($this->to as $to_profile) {
  414. $i = $to_profile->get_inbox();
  415. // Prevent delivering to self
  416. if ($i == common_local_url('apInbox')) {
  417. continue;
  418. }
  419. $to_inboxes[] = $i;
  420. }
  421. return array_unique($to_inboxes);
  422. }
  423. }