postman.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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. use GuzzleHttp\Client;
  31. use HttpSignatures\Context;
  32. use HttpSignatures\GuzzleHttpSignatures;
  33. /**
  34. * ActivityPub's own Postman
  35. *
  36. * Standard workflow expects that we send an Explorer to find out destinataries'
  37. * inbox address. Then we send our postman to deliver whatever we want to send them.
  38. *
  39. * @category Plugin
  40. * @package GNUsocial
  41. * @author Diogo Cordeiro <diogo@fc.up.pt>
  42. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  43. * @link http://www.gnu.org/software/social/
  44. */
  45. class Activitypub_postman
  46. {
  47. private $actor;
  48. private $actor_uri;
  49. private $to = [];
  50. private $client;
  51. private $headers;
  52. /**
  53. * Create a postman to deliver something to someone
  54. *
  55. * @author Diogo Cordeiro <diogo@fc.up.pt>
  56. * @param Profile $from Profile of sender
  57. * @param Array of Activitypub_profile $to destinataries
  58. */
  59. public function __construct($from, $to)
  60. {
  61. $this->actor = $from;
  62. $discovery = new Activitypub_explorer();
  63. $this->to = $to;
  64. $followers = apActorFollowersAction::generate_followers($this->actor, 0, null);
  65. foreach ($followers as $sub) {
  66. try {
  67. $to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]);
  68. } catch (Exception $e) {
  69. // Not an ActivityPub Remote Follower, let it go
  70. }
  71. }
  72. unset($discovery);
  73. $this->actor_uri = ActivityPubPlugin::actor_uri($this->actor);
  74. $actor_private_key = new Activitypub_rsa();
  75. $actor_private_key = $actor_private_key->get_private_key($this->actor);
  76. $context = new Context([
  77. 'keys' => [$this->actor_uri.'#public-key' => $actor_private_key],
  78. 'algorithm' => 'rsa-sha256',
  79. 'headers' => ['(request-target)', 'date', 'content-type', 'accept', 'user-agent'],
  80. ]);
  81. $this->headers = [
  82. 'content-type' => 'application/activity+json',
  83. 'accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
  84. 'user-agent' => 'GNUSocialBot v0.1 - https://gnu.io/social',
  85. 'date' => gmdate('D, d M Y H:i:s \G\M\T', time())
  86. ];
  87. $handlerStack = GuzzleHttpSignatures::defaultHandlerFromContext($context);
  88. $this->client = new Client(['handler' => $handlerStack]);
  89. }
  90. /**
  91. * Send something to remote instance
  92. *
  93. * @author Diogo Cordeiro <diogo@fc.up.pt>
  94. * @param string $data request body
  95. * @param string $inbox url of remote inbox
  96. * @param string $method request method
  97. * @return Psr\Http\Message\ResponseInterface
  98. */
  99. public function send($data, $inbox, $method = 'POST')
  100. {
  101. common_debug('ActivityPub Postman: Delivering '.$data.' to '.$inbox);
  102. $response = $this->client->request($method, $inbox, ['headers' => array_merge($this->headers, ['(request-target)' => strtolower($method).' '.parse_url($inbox, PHP_URL_PATH)]),'body' => $data]);
  103. common_debug('ActivityPub Postman: Delivery result with status code '.$response->getStatusCode().': '.$response->getBody()->getContents());
  104. return $response;
  105. }
  106. /**
  107. * Send a follow notification to remote instance
  108. *
  109. * @author Diogo Cordeiro <diogo@fc.up.pt>
  110. * @throws Exception
  111. */
  112. public function follow()
  113. {
  114. $data = Activitypub_follow::follow_to_array(ActivityPubPlugin::actor_uri($this->actor), $this->to[0]->getUrl());
  115. $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
  116. $res_body = json_decode($res->getBody()->getContents());
  117. if ($res->getStatusCode() == 200 || $res->getStatusCode() == 202 || $res->getStatusCode() == 409) {
  118. $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
  119. $pending_list->add();
  120. return true;
  121. } elseif (isset($res_body[0]->error)) {
  122. throw new Exception($res_body[0]->error);
  123. }
  124. throw new Exception("An unknown error occurred.");
  125. }
  126. /**
  127. * Send a Undo Follow notification to remote instance
  128. *
  129. * @author Diogo Cordeiro <diogo@fc.up.pt>
  130. */
  131. public function undo_follow()
  132. {
  133. $data = Activitypub_undo::undo_to_array(
  134. Activitypub_follow::follow_to_array(
  135. ActivityPubPlugin::actor_uri($this->actor),
  136. $this->to[0]->getUrl()
  137. )
  138. );
  139. $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
  140. $res_body = json_decode($res->getBody()->getContents());
  141. if ($res->getStatusCode() == 200 || $res->getStatusCode() == 202 || $res->getStatusCode() == 409) {
  142. $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
  143. $pending_list->remove();
  144. return true;
  145. }
  146. if (isset($res_body[0]->error)) {
  147. throw new Exception($res_body[0]->error);
  148. }
  149. throw new Exception("An unknown error occurred.");
  150. }
  151. /**
  152. * Send a Accept Follow notification to remote instance
  153. *
  154. * @author Diogo Cordeiro <diogo@fc.up.pt>
  155. */
  156. public function accept_follow()
  157. {
  158. $data = Activitypub_accept::accept_to_array(
  159. Activitypub_follow::follow_to_array(
  160. $this->to[0]->getUrl(),
  161. ActivityPubPlugin::actor_uri($this->actor)
  162. )
  163. );
  164. $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
  165. $res_body = json_decode($res->getBody()->getContents());
  166. if ($res->getStatusCode() == 200 || $res->getStatusCode() == 202 || $res->getStatusCode() == 409) {
  167. $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
  168. $pending_list->remove();
  169. return true;
  170. }
  171. if (isset($res_body[0]->error)) {
  172. throw new Exception($res_body[0]->error);
  173. }
  174. throw new Exception("An unknown error occurred.");
  175. }
  176. /**
  177. * Send a Like notification to remote instances holding the notice
  178. *
  179. * @author Diogo Cordeiro <diogo@fc.up.pt>
  180. * @param Notice $notice
  181. */
  182. public function like($notice)
  183. {
  184. $data = Activitypub_like::like_to_array(
  185. ActivityPubPlugin::actor_uri($this->actor),
  186. $notice->getUrl()
  187. );
  188. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  189. foreach ($this->to_inbox() as $inbox) {
  190. $this->send($data, $inbox);
  191. }
  192. }
  193. /**
  194. * Send a Undo Like notification to remote instances holding the notice
  195. *
  196. * @author Diogo Cordeiro <diogo@fc.up.pt>
  197. * @param Notice $notice
  198. */
  199. public function undo_like($notice)
  200. {
  201. $data = Activitypub_undo::undo_to_array(
  202. Activitypub_like::like_to_array(
  203. ActivityPubPlugin::actor_uri($this->actor),
  204. $notice->getUrl()
  205. )
  206. );
  207. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  208. foreach ($this->to_inbox() as $inbox) {
  209. $this->send($data, $inbox);
  210. }
  211. }
  212. /**
  213. * Send a Create notification to remote instances
  214. *
  215. * @author Diogo Cordeiro <diogo@fc.up.pt>
  216. * @param Notice $notice
  217. */
  218. public function create_note($notice)
  219. {
  220. $data = Activitypub_create::create_to_array(
  221. $this->actor_uri,
  222. Activitypub_notice::notice_to_array($notice)
  223. );
  224. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  225. foreach ($this->to_inbox() as $inbox) {
  226. $this->send($data, $inbox);
  227. }
  228. }
  229. /**
  230. * Send a Announce notification to remote instances
  231. *
  232. * @author Diogo Cordeiro <diogo@fc.up.pt>
  233. * @param Notice $notice
  234. */
  235. public function announce($notice)
  236. {
  237. $data = Activitypub_announce::announce_to_array(
  238. ActivityPubPlugin::actor_uri($this->actor),
  239. $notice->getUri()
  240. );
  241. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  242. foreach ($this->to_inbox() as $inbox) {
  243. $this->send($data, $inbox);
  244. }
  245. }
  246. /**
  247. * Send a Delete notification to remote instances holding the notice
  248. *
  249. * @author Diogo Cordeiro <diogo@fc.up.pt>
  250. * @param Notice $notice
  251. */
  252. public function delete($notice)
  253. {
  254. $data = Activitypub_delete::delete_to_array(
  255. ActivityPubPlugin::actor_uri($notice->getProfile()),
  256. $notice->getUrl()
  257. );
  258. $errors = [];
  259. $data = json_encode($data, JSON_UNESCAPED_SLASHES);
  260. foreach ($this->to_inbox() as $inbox) {
  261. $res = $this->send($data, $inbox);
  262. if (!$res->getStatusCode() == 200) {
  263. $res_body = json_decode($res->getBody()->getContents(), true);
  264. if (isset($res_body[0]['error'])) {
  265. $errors[] = ($res_body[0]['error']);
  266. continue;
  267. }
  268. $errors[] = ("An unknown error occurred.");
  269. }
  270. }
  271. if (!empty($errors)) {
  272. throw new Exception(json_encode($errors));
  273. }
  274. }
  275. /**
  276. * Clean list of inboxes to deliver messages
  277. *
  278. * @author Diogo Cordeiro <diogo@fc.up.pt>
  279. * @return array To Inbox URLs
  280. */
  281. private function to_inbox()
  282. {
  283. $to_inboxes = [];
  284. foreach ($this->to as $to_profile) {
  285. $i = $to_profile->get_inbox();
  286. // Prevent delivering to self
  287. if ($i == [common_local_url('apInbox')]) {
  288. continue;
  289. }
  290. $to_inboxes[] = $i;
  291. }
  292. return array_unique($to_inboxes);
  293. }
  294. }