Inbox.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <?php
  2. namespace App\Util\ActivityPub;
  3. use Cache, DB, Log, Redis, Validator;
  4. use App\{
  5. Activity,
  6. Follower,
  7. FollowRequest,
  8. Like,
  9. Notification,
  10. Profile,
  11. Status
  12. };
  13. use Carbon\Carbon;
  14. use App\Util\ActivityPub\Helpers;
  15. use App\Jobs\LikePipeline\LikePipeline;
  16. class Inbox
  17. {
  18. protected $headers;
  19. protected $profile;
  20. protected $payload;
  21. protected $logger;
  22. public function __construct($headers, $profile, $payload)
  23. {
  24. $this->headers = $headers;
  25. $this->profile = $profile;
  26. $this->payload = $payload;
  27. }
  28. public function handle()
  29. {
  30. $this->handleVerb();
  31. }
  32. public function authenticatePayload()
  33. {
  34. try {
  35. $signature = Helpers::validateSignature($this->headers, $this->payload);
  36. $payload = Helpers::validateObject($this->payload);
  37. if($signature == false) {
  38. return;
  39. }
  40. } catch (Exception $e) {
  41. return;
  42. }
  43. $this->payloadLogger();
  44. }
  45. public function payloadLogger()
  46. {
  47. $logger = new Activity;
  48. $logger->data = json_encode($this->payload);
  49. $logger->save();
  50. $this->logger = $logger;
  51. Log::info('AP:inbox:activity:new:'.$this->logger->id);
  52. $this->handleVerb();
  53. }
  54. public function handleVerb()
  55. {
  56. $verb = $this->payload['type'];
  57. switch ($verb) {
  58. case 'Create':
  59. $this->handleCreateActivity();
  60. break;
  61. case 'Follow':
  62. $this->handleFollowActivity();
  63. break;
  64. case 'Announce':
  65. $this->handleAnnounceActivity();
  66. break;
  67. case 'Accept':
  68. $this->handleAcceptActivity();
  69. break;
  70. case 'Delete':
  71. $this->handleDeleteActivity();
  72. break;
  73. case 'Like':
  74. $this->handleLikeActivity();
  75. break;
  76. case 'Reject':
  77. $this->handleRejectActivity();
  78. break;
  79. case 'Undo':
  80. $this->handleUndoActivity();
  81. break;
  82. default:
  83. // TODO: decide how to handle invalid verbs.
  84. break;
  85. }
  86. }
  87. public function verifyNoteAttachment()
  88. {
  89. $activity = $this->payload['object'];
  90. if(isset($activity['inReplyTo']) &&
  91. !empty($activity['inReplyTo']) &&
  92. Helpers::validateUrl($activity['inReplyTo'])
  93. ) {
  94. // reply detected, skip attachment check
  95. return true;
  96. }
  97. $valid = Helpers::verifyAttachments($activity);
  98. return $valid;
  99. }
  100. public function actorFirstOrCreate($actorUrl)
  101. {
  102. return Helpers::profileFirstOrNew($actorUrl);
  103. }
  104. public function handleCreateActivity()
  105. {
  106. $activity = $this->payload['object'];
  107. if(!$this->verifyNoteAttachment()) {
  108. return;
  109. }
  110. if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) {
  111. $this->handleNoteReply();
  112. } elseif($activity['type'] == 'Note' && !empty($activity['attachment'])) {
  113. $this->handleNoteCreate();
  114. }
  115. }
  116. public function handleNoteReply()
  117. {
  118. $activity = $this->payload['object'];
  119. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  120. $inReplyTo = $activity['inReplyTo'];
  121. $url = $activity['id'];
  122. if(!Helpers::statusFirstOrFetch($url, true)) {
  123. return;
  124. }
  125. }
  126. public function handleNoteCreate()
  127. {
  128. $activity = $this->payload['object'];
  129. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  130. if(!$actor || $actor->domain == null) {
  131. return;
  132. }
  133. if(Helpers::userInAudience($this->profile, $this->payload) == false) {
  134. //Log::error('AP:inbox:userInAudience:false - Activity#'.$this->logger->id);
  135. return;
  136. }
  137. $url = $activity['id'];
  138. if(Status::whereUrl($url)->exists()) {
  139. return;
  140. }
  141. $status = DB::transaction(function() use($activity, $actor, $url) {
  142. $caption = str_limit(strip_tags($activity['content']), config('pixelfed.max_caption_length'));
  143. $status = new Status;
  144. $status->profile_id = $actor->id;
  145. $status->caption = $caption;
  146. $status->visibility = $status->scope = 'public';
  147. $status->uri = $url;
  148. $status->url = $url;
  149. $status->save();
  150. return $status;
  151. });
  152. Helpers::importNoteAttachment($activity, $status);
  153. }
  154. public function handleFollowActivity()
  155. {
  156. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  157. if(!$actor || $actor->domain == null) {
  158. return;
  159. }
  160. $target = $this->profile;
  161. if($target->is_private == true) {
  162. // make follow request
  163. FollowRequest::firstOrCreate([
  164. 'follower_id' => $actor->id,
  165. 'following_id' => $target->id
  166. ]);
  167. // todo: send notification
  168. } else {
  169. // store new follower
  170. $follower = Follower::firstOrCreate([
  171. 'profile_id' => $actor->id,
  172. 'following_id' => $target->id,
  173. 'local_profile' => empty($actor->domain)
  174. ]);
  175. if($follower->wasRecentlyCreated == false) {
  176. return;
  177. }
  178. // send notification
  179. Notification::firstOrCreate([
  180. 'profile_id' => $target->id,
  181. 'actor_id' => $actor->id,
  182. 'action' => 'follow',
  183. 'message' => $follower->toText(),
  184. 'rendered' => $follower->toHtml(),
  185. 'item_id' => $target->id,
  186. 'item_type' => 'App\Profile'
  187. ]);
  188. // send Accept to remote profile
  189. $accept = [
  190. '@context' => 'https://www.w3.org/ns/activitystreams',
  191. 'id' => $target->permalink().'#accepts/follows/' . $follower->id,
  192. 'type' => 'Accept',
  193. 'actor' => $target->permalink(),
  194. 'object' => [
  195. 'id' => $actor->permalink('#follows/'.$target->id),
  196. 'type' => 'Follow',
  197. 'actor' => $actor->permalink(),
  198. 'object' => $target->permalink()
  199. ]
  200. ];
  201. Helpers::sendSignedObject($target, $actor->inbox_url, $accept);
  202. }
  203. }
  204. public function handleAnnounceActivity()
  205. {
  206. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  207. $activity = $this->payload['object'];
  208. if(!$actor || $actor->domain == null) {
  209. return;
  210. }
  211. if(Helpers::validateLocalUrl($activity) == false) {
  212. return;
  213. }
  214. $parent = Helpers::statusFirstOrFetch($activity, true);
  215. if(!$parent) {
  216. return;
  217. }
  218. $status = Status::firstOrCreate([
  219. 'profile_id' => $actor->id,
  220. 'reblog_of_id' => $parent->id,
  221. 'type' => 'reply'
  222. ]);
  223. Notification::firstOrCreate([
  224. 'profile_id' => $parent->profile->id,
  225. 'actor_id' => $actor->id,
  226. 'action' => 'share',
  227. 'message' => $status->replyToText(),
  228. 'rendered' => $status->replyToHtml(),
  229. 'item_id' => $parent->id,
  230. 'item_type' => 'App\Status'
  231. ]);
  232. }
  233. public function handleAcceptActivity()
  234. {
  235. }
  236. public function handleDeleteActivity()
  237. {
  238. $actor = $this->payload['actor'];
  239. $obj = $this->payload['object'];
  240. if(is_string($obj) && Helpers::validateUrl($obj)) {
  241. // actor object detected
  242. } else if (is_array($obj) && isset($obj['type']) && $obj['type'] == 'Tombstone') {
  243. // tombstone detected
  244. $status = Status::whereUri($obj['id'])->first();
  245. if($status == null) {
  246. return;
  247. }
  248. $status->forceDelete();
  249. }
  250. }
  251. public function handleLikeActivity()
  252. {
  253. $actor = $this->payload['actor'];
  254. $profile = self::actorFirstOrCreate($actor);
  255. $obj = $this->payload['object'];
  256. if(Helpers::validateLocalUrl($obj) == false) {
  257. return;
  258. }
  259. $status = Helpers::statusFirstOrFetch($obj);
  260. $like = Like::firstOrCreate([
  261. 'profile_id' => $profile->id,
  262. 'status_id' => $status->id
  263. ]);
  264. if($like->wasRecentlyCreated == false) {
  265. return;
  266. }
  267. LikePipeline::dispatch($like);
  268. }
  269. public function handleRejectActivity()
  270. {
  271. }
  272. public function handleUndoActivity()
  273. {
  274. $actor = $this->payload['actor'];
  275. $profile = self::actorFirstOrCreate($actor);
  276. $obj = $this->payload['object'];
  277. switch ($obj['type']) {
  278. case 'Like':
  279. $status = Helpers::statusFirstOrFetch($obj['object']);
  280. Like::whereProfileId($profile->id)
  281. ->whereStatusId($status->id)
  282. ->forceDelete();
  283. break;
  284. case 'Announce':
  285. $parent = Helpers::statusFirstOrFetch($obj['object']);
  286. $status = Status::whereProfileId($profile->id)
  287. ->whereReblogOfId($parent->id)
  288. ->firstOrFail();
  289. Notification::whereProfileId($parent->profile->id)
  290. ->whereActorId($profile->id)
  291. ->whereAction('share')
  292. ->whereItemId($status->id)
  293. ->whereItemType('App\Status')
  294. ->forceDelete();
  295. $status->forceDelete();
  296. break;
  297. }
  298. }
  299. }