Inbox.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. <?php
  2. namespace App\Util\ActivityPub;
  3. use Cache, DB, Log, Purify, Redis, Validator;
  4. use App\{
  5. Activity,
  6. Follower,
  7. FollowRequest,
  8. Like,
  9. Notification,
  10. Profile,
  11. Status,
  12. StatusHashtag,
  13. };
  14. use Carbon\Carbon;
  15. use App\Util\ActivityPub\Helpers;
  16. use App\Jobs\LikePipeline\LikePipeline;
  17. use App\Jobs\FollowPipeline\FollowPipeline;
  18. use App\Util\ActivityPub\Validator\Accept as AcceptValidator;
  19. use App\Util\ActivityPub\Validator\Announce as AnnounceValidator;
  20. use App\Util\ActivityPub\Validator\Follow as FollowValidator;
  21. use App\Util\ActivityPub\Validator\Like as LikeValidator;
  22. use App\Util\ActivityPub\Validator\UndoFollow as UndoFollowValidator;
  23. class Inbox
  24. {
  25. protected $headers;
  26. protected $profile;
  27. protected $payload;
  28. protected $logger;
  29. public function __construct($headers, $profile, $payload)
  30. {
  31. $this->headers = $headers;
  32. $this->profile = $profile;
  33. $this->payload = $payload;
  34. }
  35. public function handle()
  36. {
  37. $this->handleVerb();
  38. // if(!Activity::where('data->id', $this->payload['id'])->exists()) {
  39. // (new Activity())->create([
  40. // 'to_id' => $this->profile->id,
  41. // 'data' => json_encode($this->payload)
  42. // ]);
  43. // }
  44. return;
  45. }
  46. public function handleVerb()
  47. {
  48. $verb = (string) $this->payload['type'];
  49. switch ($verb) {
  50. case 'Create':
  51. $this->handleCreateActivity();
  52. break;
  53. case 'Follow':
  54. if(FollowValidator::validate($this->payload) == false) { return; }
  55. $this->handleFollowActivity();
  56. break;
  57. case 'Announce':
  58. if(AnnounceValidator::validate($this->payload) == false) { return; }
  59. $this->handleAnnounceActivity();
  60. break;
  61. case 'Accept':
  62. if(AcceptValidator::validate($this->payload) == false) { return; }
  63. $this->handleAcceptActivity();
  64. break;
  65. case 'Delete':
  66. $this->handleDeleteActivity();
  67. break;
  68. case 'Like':
  69. if(LikeValidator::validate($this->payload) == false) { return; }
  70. $this->handleLikeActivity();
  71. break;
  72. case 'Reject':
  73. $this->handleRejectActivity();
  74. break;
  75. case 'Undo':
  76. $this->handleUndoActivity();
  77. break;
  78. default:
  79. // TODO: decide how to handle invalid verbs.
  80. break;
  81. }
  82. }
  83. public function verifyNoteAttachment()
  84. {
  85. $activity = $this->payload['object'];
  86. if(isset($activity['inReplyTo']) &&
  87. !empty($activity['inReplyTo']) &&
  88. Helpers::validateUrl($activity['inReplyTo'])
  89. ) {
  90. // reply detected, skip attachment check
  91. return true;
  92. }
  93. $valid = Helpers::verifyAttachments($activity);
  94. return $valid;
  95. }
  96. public function actorFirstOrCreate($actorUrl)
  97. {
  98. return Helpers::profileFetch($actorUrl);
  99. }
  100. public function handleCreateActivity()
  101. {
  102. $activity = $this->payload['object'];
  103. if(!$this->verifyNoteAttachment()) {
  104. return;
  105. }
  106. if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) {
  107. $this->handleNoteReply();
  108. } elseif($activity['type'] == 'Note' && !empty($activity['attachment'])) {
  109. $this->handleNoteCreate();
  110. }
  111. }
  112. public function handleNoteReply()
  113. {
  114. $activity = $this->payload['object'];
  115. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  116. if(!$actor || $actor->domain == null) {
  117. return;
  118. }
  119. $inReplyTo = $activity['inReplyTo'];
  120. $url = isset($activity['url']) ? $activity['url'] : $activity['id'];
  121. Helpers::statusFirstOrFetch($url, true);
  122. return;
  123. }
  124. public function handleNoteCreate()
  125. {
  126. $activity = $this->payload['object'];
  127. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  128. if(!$actor || $actor->domain == null) {
  129. return;
  130. }
  131. if($actor->followers()->count() == 0) {
  132. return;
  133. }
  134. $url = isset($activity['url']) ? $activity['url'] : $activity['id'];
  135. if(Status::whereUrl($url)->exists()) {
  136. return;
  137. }
  138. Helpers::statusFetch($url);
  139. return;
  140. }
  141. public function handleFollowActivity()
  142. {
  143. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  144. $target = $this->profile;
  145. if(!$actor || $actor->domain == null || $target->domain !== null) {
  146. return;
  147. }
  148. if(
  149. Follower::whereProfileId($actor->id)
  150. ->whereFollowingId($target->id)
  151. ->exists() ||
  152. FollowRequest::whereFollowerId($actor->id)
  153. ->whereFollowingId($target->id)
  154. ->exists()
  155. ) {
  156. return;
  157. }
  158. if($target->is_private == true) {
  159. FollowRequest::firstOrCreate([
  160. 'follower_id' => $actor->id,
  161. 'following_id' => $target->id
  162. ]);
  163. } else {
  164. $follower = new Follower;
  165. $follower->profile_id => $actor->id;
  166. $follower->following_id => $target->id;
  167. $follower->local_profile => empty($actor->domain);
  168. if($target->domain == null) {
  169. Notification::firstOrCreate([
  170. 'profile_id' => $target->id,
  171. 'actor_id' => $actor->id,
  172. 'action' => 'follow',
  173. 'message' => $follower->toText(),
  174. 'rendered' => $follower->toHtml(),
  175. 'item_id' => $target->id,
  176. 'item_type' => 'App\Profile'
  177. ]);
  178. }
  179. // send Accept to remote profile
  180. $accept = [
  181. '@context' => 'https://www.w3.org/ns/activitystreams',
  182. 'id' => $target->permalink().'#accepts/follows/' . $follower->id,
  183. 'type' => 'Accept',
  184. 'actor' => $target->permalink(),
  185. 'object' => [
  186. 'id' => $this->payload['id'],
  187. 'actor' => $actor->permalink(),
  188. 'type' => 'Follow',
  189. 'object' => $target->permalink()
  190. ]
  191. ];
  192. Helpers::sendSignedObject($target, $actor->inbox_url, $accept);
  193. }
  194. }
  195. public function handleAnnounceActivity()
  196. {
  197. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  198. $activity = $this->payload['object'];
  199. if(!$actor || $actor->domain == null) {
  200. return;
  201. }
  202. if(Helpers::validateLocalUrl($activity) == false) {
  203. return;
  204. }
  205. $parent = Helpers::statusFetch($activity);
  206. if(empty($parent)) {
  207. return;
  208. }
  209. $status = Status::firstOrCreate([
  210. 'profile_id' => $actor->id,
  211. 'reblog_of_id' => $parent->id,
  212. 'type' => 'share'
  213. ]);
  214. Notification::firstOrCreate([
  215. 'profile_id' => $parent->profile->id,
  216. 'actor_id' => $actor->id,
  217. 'action' => 'share',
  218. 'message' => $status->replyToText(),
  219. 'rendered' => $status->replyToHtml(),
  220. 'item_id' => $parent->id,
  221. 'item_type' => 'App\Status'
  222. ]);
  223. $parent->reblogs_count = $parent->shares()->count();
  224. $parent->save();
  225. }
  226. public function handleAcceptActivity()
  227. {
  228. $actor = $this->payload['object']['actor'];
  229. $obj = $this->payload['object']['object'];
  230. $type = $this->payload['object']['type'];
  231. if($type !== 'Follow') {
  232. return;
  233. }
  234. $actor = Helpers::validateLocalUrl($actor);
  235. $target = Helpers::validateUrl($obj);
  236. if(!$actor || !$target) {
  237. return;
  238. }
  239. $actor = Helpers::profileFetch($actor);
  240. $target = Helpers::profileFetch($target);
  241. $request = FollowRequest::whereFollowerId($actor->id)
  242. ->whereFollowingId($target->id)
  243. ->whereIsRejected(false)
  244. ->first();
  245. if(!$request) {
  246. return;
  247. }
  248. $follower = Follower::firstOrCreate([
  249. 'profile_id' => $actor->id,
  250. 'following_id' => $target->id,
  251. ]);
  252. FollowPipeline::dispatch($follower);
  253. $request->delete();
  254. }
  255. public function handleDeleteActivity()
  256. {
  257. if(!isset(
  258. $this->payload['actor'],
  259. $this->payload['object'],
  260. )) {
  261. return;
  262. }
  263. $actor = $this->payload['actor'];
  264. $obj = $this->payload['object'];
  265. if(is_string($obj) == true) {
  266. return;
  267. }
  268. $type = $this->payload['object']['type'];
  269. $typeCheck = in_array($type, ['Person', 'Tombstone']);
  270. if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) {
  271. return;
  272. }
  273. if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
  274. return;
  275. }
  276. $id = $this->payload['object']['id'];
  277. switch ($type) {
  278. case 'Person':
  279. // todo: fix race condition
  280. return;
  281. $profile = Helpers::profileFetch($actor);
  282. if(!$profile || $profile->private_key != null) {
  283. return;
  284. }
  285. Notification::whereActorId($profile->id)->delete();
  286. $profile->avatar()->delete();
  287. $profile->followers()->delete();
  288. $profile->following()->delete();
  289. $profile->likes()->delete();
  290. $profile->media()->delete();
  291. $profile->statuses()->delete();
  292. $profile->delete();
  293. return;
  294. break;
  295. case 'Tombstone':
  296. $profile = Helpers::profileFetch($actor);
  297. $status = Status::whereProfileId($profile->id)
  298. ->whereUri($id)
  299. ->orWhere('url', $id)
  300. ->orWhere('object_url', $id)
  301. ->first();
  302. if(!$status) {
  303. return;
  304. }
  305. $status->media()->delete();
  306. $status->likes()->delete();
  307. $status->shares()->delete();
  308. $status->delete();
  309. return;
  310. break;
  311. default:
  312. return;
  313. break;
  314. }
  315. }
  316. public function handleLikeActivity()
  317. {
  318. $actor = $this->payload['actor'];
  319. if(!Helpers::validateUrl($actor)) {
  320. return;
  321. }
  322. $profile = self::actorFirstOrCreate($actor);
  323. $obj = $this->payload['object'];
  324. if(!Helpers::validateUrl($obj)) {
  325. return;
  326. }
  327. $status = Helpers::statusFirstOrFetch($obj);
  328. if(!$status || !$profile) {
  329. return;
  330. }
  331. $like = Like::firstOrCreate([
  332. 'profile_id' => $profile->id,
  333. 'status_id' => $status->id
  334. ]);
  335. if($like->wasRecentlyCreated == true) {
  336. $status->likes_count = $status->likes()->count();
  337. $status->save();
  338. LikePipeline::dispatch($like);
  339. }
  340. return;
  341. }
  342. public function handleRejectActivity()
  343. {
  344. }
  345. public function handleUndoActivity()
  346. {
  347. $actor = $this->payload['actor'];
  348. $profile = self::actorFirstOrCreate($actor);
  349. $obj = $this->payload['object'];
  350. switch ($obj['type']) {
  351. case 'Accept':
  352. break;
  353. case 'Announce':
  354. $obj = $obj['object'];
  355. if(!Helpers::validateLocalUrl($obj)) {
  356. return;
  357. }
  358. $status = Helpers::statusFetch($obj);
  359. if(!$status) {
  360. return;
  361. }
  362. Status::whereProfileId($profile->id)
  363. ->whereReblogOfId($status->id)
  364. ->forceDelete();
  365. Notification::whereProfileId($status->profile->id)
  366. ->whereActorId($profile->id)
  367. ->whereAction('share')
  368. ->whereItemId($status->reblog_of_id)
  369. ->whereItemType('App\Status')
  370. ->forceDelete();
  371. break;
  372. case 'Block':
  373. break;
  374. case 'Follow':
  375. $following = self::actorFirstOrCreate($obj['object']);
  376. if(!$following) {
  377. return;
  378. }
  379. Follower::whereProfileId($profile->id)
  380. ->whereFollowingId($following->id)
  381. ->delete();
  382. break;
  383. case 'Like':
  384. $status = Helpers::statusFirstOrFetch($obj['object']);
  385. if(!$status) {
  386. return;
  387. }
  388. Like::whereProfileId($profile->id)
  389. ->whereStatusId($status->id)
  390. ->forceDelete();
  391. Notification::whereProfileId($status->profile->id)
  392. ->whereActorId($profile->id)
  393. ->whereAction('like')
  394. ->whereItemId($status->id)
  395. ->whereItemType('App\Status')
  396. ->forceDelete();
  397. break;
  398. }
  399. return;
  400. }
  401. }