postman.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  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 $client;
  43. private $headers;
  44. /**
  45. * Create a postman to deliver something to someone
  46. *
  47. * @param Profile $from sender Profile
  48. * @param array $to receiver Profiles
  49. * @throws Exception
  50. * @author Diogo Cordeiro <diogo@fc.up.pt>
  51. */
  52. public function __construct(Profile $from, array $to = [])
  53. {
  54. $this->actor = $from;
  55. $this->to = $to;
  56. $this->actor_uri = ActivityPubPlugin::actor_uri($this->actor);
  57. $this->client = new HTTPClient();
  58. }
  59. /**
  60. * Send something to remote instance
  61. *
  62. * @param string $data request body
  63. * @param string $inbox url of remote inbox
  64. * @param string $method request method
  65. * @return GNUsocial_HTTPResponse
  66. * @throws HTTP_Request2_Exception
  67. * @throws Exception
  68. * @author Diogo Cordeiro <diogo@fc.up.pt>
  69. */
  70. public function send($data, $inbox, $method = 'POST')
  71. {
  72. common_debug('ActivityPub Postman: Delivering '.$data.' to '.$inbox);
  73. $headers = HttpSignature::sign($this->actor, $inbox, $data);
  74. common_debug('ActivityPub Postman: Delivery headers were: '.print_r($headers, true));
  75. $this->client->setBody($data);
  76. switch ($method) {
  77. case 'POST':
  78. $response = $this->client->post($inbox, $headers);
  79. break;
  80. case 'GET':
  81. $response = $this->client->get($inbox, $headers);
  82. break;
  83. default:
  84. throw new Exception("Unsupported request method for postman.");
  85. }
  86. common_debug('ActivityPub Postman: Delivery result with status code '.$response->getStatus().': '.$response->getBody());
  87. return $response;
  88. }
  89. /**
  90. * Send a follow notification to remote instance
  91. *
  92. * @return bool
  93. * @throws HTTP_Request2_Exception
  94. * @throws Exception
  95. * @author Diogo Cordeiro <diogo@fc.up.pt>
  96. */
  97. public function follow()
  98. {
  99. $data = Activitypub_follow::follow_to_array($this->actor_uri, $this->to[0]->getUrl());
  100. $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
  101. $res_body = json_decode($res->getBody());
  102. if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
  103. $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
  104. $pending_list->add();
  105. return true;
  106. } elseif (isset($res_body[0]->error)) {
  107. throw new Exception($res_body[0]->error);
  108. }
  109. throw new Exception("An unknown error occurred.");
  110. }
  111. /**
  112. * Send a Undo Follow notification to remote instance
  113. *
  114. * @return bool
  115. * @throws HTTP_Request2_Exception
  116. * @throws Exception
  117. * @throws Exception
  118. * @throws Exception
  119. * @throws Exception
  120. * @author Diogo Cordeiro <diogo@fc.up.pt>
  121. */
  122. public function undo_follow()
  123. {
  124. $data = Activitypub_undo::undo_to_array(
  125. Activitypub_follow::follow_to_array(
  126. $this->actor_uri,
  127. $this->to[0]->getUrl()
  128. )
  129. );
  130. $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
  131. $res_body = json_decode($res->getBody());
  132. if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
  133. Activitypub_profile::unsubscribeCacheUpdate($this->actor, $this->to[0]->local_profile());
  134. $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
  135. $pending_list->remove();
  136. return true;
  137. }
  138. if (isset($res_body[0]->error)) {
  139. throw new Exception($res_body[0]->error);
  140. }
  141. throw new Exception("An unknown error occurred.");
  142. }
  143. /**
  144. * Send a Accept Follow notification to remote instance
  145. *
  146. * @param string $id Follow activity id
  147. * @return bool
  148. * @throws HTTP_Request2_Exception
  149. * @throws Exception Description of HTTP Response error or generic error message.
  150. * @author Diogo Cordeiro <diogo@fc.up.pt>
  151. */
  152. public function accept_follow(string $id): bool
  153. {
  154. $data = Activitypub_accept::accept_to_array(
  155. Activitypub_follow::follow_to_array(
  156. $this->to[0]->getUrl(),
  157. $this->actor_uri,
  158. $id
  159. )
  160. );
  161. $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
  162. $res_body = json_decode($res->getBody());
  163. if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
  164. $pending_list = new Activitypub_pending_follow_requests($this->to[0]->getID(), $this->actor->getID());
  165. $pending_list->remove();
  166. return true;
  167. }
  168. if (isset($res_body[0]->error)) {
  169. throw new Exception($res_body[0]->error);
  170. }
  171. throw new Exception("An unknown error occurred.");
  172. }
  173. /**
  174. * Send a Like notification to remote instances holding the notice
  175. *
  176. * @param Notice $notice
  177. * @throws HTTP_Request2_Exception
  178. * @throws InvalidUrlException
  179. * @throws Exception
  180. * @author Diogo Cordeiro <diogo@fc.up.pt>
  181. */
  182. public function like($notice)
  183. {
  184. $data = Activitypub_like::like_to_array(
  185. $this->actor_uri,
  186. Activitypub_notice::getUrl($notice)
  187. );
  188. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  189. foreach ($this->to_inbox() as $inbox) {
  190. $res = $this->send($data, $inbox);
  191. // accummulate errors for later use, if needed
  192. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  193. $res_body = json_decode($res->getBody(), true);
  194. $errors[] = isset($res_body[0]['error']) ?
  195. $res_body[0]['error'] : "An unknown error occurred.";
  196. }
  197. }
  198. if (!empty($errors)) {
  199. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the like activity!");
  200. }
  201. }
  202. /**
  203. * Send a Undo Like notification to remote instances holding the notice
  204. *
  205. * @param Notice $notice
  206. * @throws HTTP_Request2_Exception
  207. * @throws InvalidUrlException
  208. * @throws Exception
  209. * @author Diogo Cordeiro <diogo@fc.up.pt>
  210. */
  211. public function undo_like($notice)
  212. {
  213. $data = Activitypub_undo::undo_to_array(
  214. Activitypub_like::like_to_array(
  215. $this->actor_uri,
  216. Activitypub_notice::getUrl($notice)
  217. )
  218. );
  219. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  220. foreach ($this->to_inbox() as $inbox) {
  221. $res = $this->send($data, $inbox);
  222. // accummulate errors for later use, if needed
  223. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  224. $res_body = json_decode($res->getBody(), true);
  225. $errors[] = isset($res_body[0]['error']) ?
  226. $res_body[0]['error'] : "An unknown error occurred.";
  227. }
  228. }
  229. if (!empty($errors)) {
  230. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the undo-like activity!");
  231. }
  232. }
  233. /**
  234. * Send a Create notification to remote instances
  235. *
  236. * @param Notice $notice
  237. * @throws EmptyPkeyValueException
  238. * @throws HTTP_Request2_Exception
  239. * @throws InvalidUrlException
  240. * @throws ServerException
  241. * @author Diogo Cordeiro <diogo@fc.up.pt>
  242. */
  243. public function create_note($notice)
  244. {
  245. $data = Activitypub_create::create_to_array(
  246. $this->actor_uri,
  247. Activitypub_notice::notice_to_array($notice)
  248. );
  249. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  250. foreach ($this->to_inbox() as $inbox) {
  251. $res = $this->send($data, $inbox);
  252. // accummulate errors for later use, if needed
  253. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  254. $res_body = json_decode($res->getBody(), true);
  255. $errors[] = isset($res_body[0]['error']) ?
  256. $res_body[0]['error'] : "An unknown error occurred.";
  257. }
  258. }
  259. if (!empty($errors)) {
  260. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
  261. }
  262. }
  263. /**
  264. * Send a Create direct-notification to remote instances
  265. *
  266. * @param Notice $message
  267. * @author Bruno Casteleiro <brunoccast@fc.up.pt>
  268. */
  269. public function create_direct_note(Notice $message)
  270. {
  271. $data = Activitypub_create::create_to_array(
  272. $this->actor_uri,
  273. Activitypub_message::message_to_array($message),
  274. true
  275. );
  276. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  277. foreach ($this->to_inbox(false) as $inbox) {
  278. $res = $this->send($data, $inbox);
  279. // accummulate errors for later use, if needed
  280. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  281. $res_body = json_decode($res->getBody(), true);
  282. $errors[] = isset($res_body[0]['error']) ?
  283. $res_body[0]['error'] : "An unknown error occurred.";
  284. }
  285. }
  286. if (!empty($errors)) {
  287. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
  288. }
  289. }
  290. /**
  291. * Send a Announce notification to remote instances
  292. *
  293. * @param Notice $notice
  294. * @throws HTTP_Request2_Exception
  295. * @throws Exception
  296. * @author Diogo Cordeiro <diogo@fc.up.pt>
  297. */
  298. public function announce($notice)
  299. {
  300. $data = json_encode(Activitypub_announce::announce_to_array($this->actor, $notice),
  301. JSON_UNESCAPED_SLASHES);
  302. foreach ($this->to_inbox() as $inbox) {
  303. $res = $this->send($data, $inbox);
  304. // accummulate errors for later use, if needed
  305. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  306. $res_body = json_decode($res->getBody(), true);
  307. $errors[] = isset($res_body[0]['error']) ?
  308. $res_body[0]['error'] : "An unknown error occurred.";
  309. }
  310. }
  311. if (!empty($errors)) {
  312. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the announce activity!");
  313. }
  314. }
  315. /**
  316. * Send a Delete notification to remote instances holding the notice
  317. *
  318. * @param Notice $notice
  319. * @throws HTTP_Request2_Exception
  320. * @throws InvalidUrlException
  321. * @throws Exception
  322. * @author Diogo Cordeiro <diogo@fc.up.pt>
  323. */
  324. public function delete_note($notice)
  325. {
  326. $data = Activitypub_delete::delete_to_array(
  327. ActivityPubPlugin::actor_uri($notice->getProfile()),
  328. Activitypub_notice::getUrl($notice)
  329. );
  330. $errors = [];
  331. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  332. foreach ($this->to_inbox() as $inbox) {
  333. $res = $this->send($data, $inbox);
  334. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  335. $res_body = json_decode($res->getBody(), true);
  336. $errors[] = isset($res_body[0]['error']) ?
  337. $res_body[0]['error'] : "An unknown error occurred.";
  338. }
  339. }
  340. if (!empty($errors)) {
  341. throw new Exception(json_encode($errors));
  342. }
  343. }
  344. /**
  345. * Send a Delete notification to remote followers of some deleted profile
  346. *
  347. * @param Notice $notice
  348. * @throws HTTP_Request2_Exception
  349. * @throws InvalidUrlException
  350. * @throws Exception
  351. * @author Bruno Casteleiro <brunoccast@fc.up.pt>
  352. */
  353. public function delete_profile()
  354. {
  355. $data = Activitypub_delete::delete_to_array($this->actor_uri, $this->actor_uri);
  356. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  357. $errors = [];
  358. foreach ($this->to_inbox() as $inbox) {
  359. $res = $this->send($data, $inbox);
  360. // accummulate errors for later use, if needed
  361. if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
  362. $res_body = json_decode($res->getBody(), true);
  363. $errors[] = isset($res_body[0]['error']) ?
  364. $res_body[0]['error'] : "An unknown error occurred.";
  365. }
  366. }
  367. if (!empty($errors)) {
  368. common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the delete_profile activity!");
  369. }
  370. }
  371. /**
  372. * Clean list of inboxes to deliver messages
  373. *
  374. * @author Diogo Cordeiro <diogo@fc.up.pt>
  375. * @param bool $actorFollowers whether to include the actor's follower collection
  376. * @return array To Inbox URLs
  377. */
  378. private function to_inbox(bool $actorFollowers = true): array
  379. {
  380. if ($actorFollowers) {
  381. $discovery = new Activitypub_explorer();
  382. $followers = apActorFollowersAction::generate_followers($this->actor, 0, null);
  383. foreach ($followers as $sub) {
  384. try {
  385. $this->to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]);
  386. } catch (Exception $e) {
  387. // Not an ActivityPub Remote Follower, let it go
  388. }
  389. }
  390. unset($discovery);
  391. }
  392. $to_inboxes = [];
  393. foreach ($this->to as $to_profile) {
  394. $i = $to_profile->get_inbox();
  395. // Prevent delivering to self
  396. if ($i == [common_local_url('apInbox')]) {
  397. continue;
  398. }
  399. $to_inboxes[] = $i;
  400. }
  401. return array_unique($to_inboxes);
  402. }
  403. }