1
0

Inbox.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. <?php
  2. namespace App\Util\ActivityPub;
  3. use Cache, DB, Log, Purify, Redis, Storage, Validator;
  4. use App\{
  5. Activity,
  6. DirectMessage,
  7. Follower,
  8. FollowRequest,
  9. Like,
  10. Notification,
  11. Media,
  12. Profile,
  13. Status,
  14. StatusHashtag,
  15. Story,
  16. StoryView,
  17. UserFilter
  18. };
  19. use Carbon\Carbon;
  20. use App\Util\ActivityPub\Helpers;
  21. use Illuminate\Support\Str;
  22. use App\Jobs\LikePipeline\LikePipeline;
  23. use App\Jobs\FollowPipeline\FollowPipeline;
  24. use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
  25. use App\Jobs\StoryPipeline\StoryExpire;
  26. use App\Jobs\StoryPipeline\StoryFetch;
  27. use App\Util\ActivityPub\Validator\Accept as AcceptValidator;
  28. use App\Util\ActivityPub\Validator\Add as AddValidator;
  29. use App\Util\ActivityPub\Validator\Announce as AnnounceValidator;
  30. use App\Util\ActivityPub\Validator\Follow as FollowValidator;
  31. use App\Util\ActivityPub\Validator\Like as LikeValidator;
  32. use App\Util\ActivityPub\Validator\UndoFollow as UndoFollowValidator;
  33. use App\Services\PollService;
  34. use App\Services\FollowerService;
  35. class Inbox
  36. {
  37. protected $headers;
  38. protected $profile;
  39. protected $payload;
  40. protected $logger;
  41. public function __construct($headers, $profile, $payload)
  42. {
  43. $this->headers = $headers;
  44. $this->profile = $profile;
  45. $this->payload = $payload;
  46. }
  47. public function handle()
  48. {
  49. $this->handleVerb();
  50. return;
  51. }
  52. public function handleVerb()
  53. {
  54. $verb = (string) $this->payload['type'];
  55. switch ($verb) {
  56. case 'Add':
  57. $this->handleAddActivity();
  58. break;
  59. case 'Create':
  60. $this->handleCreateActivity();
  61. break;
  62. case 'Follow':
  63. if(FollowValidator::validate($this->payload) == false) { return; }
  64. $this->handleFollowActivity();
  65. break;
  66. case 'Announce':
  67. if(AnnounceValidator::validate($this->payload) == false) { return; }
  68. $this->handleAnnounceActivity();
  69. break;
  70. case 'Accept':
  71. if(AcceptValidator::validate($this->payload) == false) { return; }
  72. $this->handleAcceptActivity();
  73. break;
  74. case 'Delete':
  75. $this->handleDeleteActivity();
  76. break;
  77. case 'Like':
  78. if(LikeValidator::validate($this->payload) == false) { return; }
  79. $this->handleLikeActivity();
  80. break;
  81. case 'Reject':
  82. $this->handleRejectActivity();
  83. break;
  84. case 'Undo':
  85. $this->handleUndoActivity();
  86. break;
  87. case 'View':
  88. $this->handleViewActivity();
  89. break;
  90. case 'Story:Reaction':
  91. $this->handleStoryReactionActivity();
  92. break;
  93. case 'Story:Reply':
  94. $this->handleStoryReplyActivity();
  95. break;
  96. // case 'Update':
  97. // (new UpdateActivity($this->payload, $this->profile))->handle();
  98. // break;
  99. default:
  100. // TODO: decide how to handle invalid verbs.
  101. break;
  102. }
  103. }
  104. public function verifyNoteAttachment()
  105. {
  106. $activity = $this->payload['object'];
  107. if(isset($activity['inReplyTo']) &&
  108. !empty($activity['inReplyTo']) &&
  109. Helpers::validateUrl($activity['inReplyTo'])
  110. ) {
  111. // reply detected, skip attachment check
  112. return true;
  113. }
  114. $valid = Helpers::verifyAttachments($activity);
  115. return $valid;
  116. }
  117. public function actorFirstOrCreate($actorUrl)
  118. {
  119. return Helpers::profileFetch($actorUrl);
  120. }
  121. public function handleAddActivity()
  122. {
  123. // stories ;)
  124. if(!isset(
  125. $this->payload['actor'],
  126. $this->payload['object']
  127. )) {
  128. return;
  129. }
  130. $actor = $this->payload['actor'];
  131. $obj = $this->payload['object'];
  132. if(!Helpers::validateUrl($actor)) {
  133. return;
  134. }
  135. if(!isset($obj['type'])) {
  136. return;
  137. }
  138. switch($obj['type']) {
  139. case 'Story':
  140. StoryFetch::dispatchNow($this->payload);
  141. break;
  142. }
  143. }
  144. public function handleCreateActivity()
  145. {
  146. $activity = $this->payload['object'];
  147. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  148. if(!$actor || $actor->domain == null) {
  149. return;
  150. }
  151. if(!isset($activity['to'])) {
  152. return;
  153. }
  154. $to = $activity['to'];
  155. $cc = isset($activity['cc']) ? $activity['cc'] : [];
  156. if($activity['type'] == 'Question') {
  157. $this->handlePollCreate();
  158. return;
  159. }
  160. if(count($to) == 1 &&
  161. count($cc) == 0 &&
  162. parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app')
  163. ) {
  164. $this->handleDirectMessage();
  165. return;
  166. }
  167. if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) {
  168. $this->handleNoteReply();
  169. } elseif ($activity['type'] == 'Note' && !empty($activity['attachment'])) {
  170. if(!$this->verifyNoteAttachment()) {
  171. return;
  172. }
  173. $this->handleNoteCreate();
  174. }
  175. }
  176. public function handleNoteReply()
  177. {
  178. $activity = $this->payload['object'];
  179. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  180. if(!$actor || $actor->domain == null) {
  181. return;
  182. }
  183. $inReplyTo = $activity['inReplyTo'];
  184. $url = isset($activity['url']) ? $activity['url'] : $activity['id'];
  185. Helpers::statusFirstOrFetch($url, true);
  186. return;
  187. }
  188. public function handlePollCreate()
  189. {
  190. $activity = $this->payload['object'];
  191. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  192. if(!$actor || $actor->domain == null) {
  193. return;
  194. }
  195. $url = isset($activity['url']) ? $activity['url'] : $activity['id'];
  196. Helpers::statusFirstOrFetch($url);
  197. return;
  198. }
  199. public function handleNoteCreate()
  200. {
  201. $activity = $this->payload['object'];
  202. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  203. if(!$actor || $actor->domain == null) {
  204. return;
  205. }
  206. if( isset($activity['inReplyTo']) &&
  207. isset($activity['name']) &&
  208. !isset($activity['content']) &&
  209. !isset($activity['attachment']) &&
  210. Helpers::validateLocalUrl($activity['inReplyTo'])
  211. ) {
  212. $this->handlePollVote();
  213. return;
  214. }
  215. if($actor->followers()->count() == 0) {
  216. return;
  217. }
  218. $url = isset($activity['url']) ? $activity['url'] : $activity['id'];
  219. if(Status::whereUrl($url)->exists()) {
  220. return;
  221. }
  222. Helpers::storeStatus(
  223. $url,
  224. $actor,
  225. $activity
  226. );
  227. return;
  228. }
  229. public function handlePollVote()
  230. {
  231. $activity = $this->payload['object'];
  232. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  233. $status = Helpers::statusFetch($activity['inReplyTo']);
  234. $poll = $status->poll;
  235. if(!$status || !$poll) {
  236. return;
  237. }
  238. if(now()->gt($poll->expires_at)) {
  239. return;
  240. }
  241. $choices = $poll->poll_options;
  242. $choice = array_search($activity['name'], $choices);
  243. if($choice === false) {
  244. return;
  245. }
  246. if(PollVote::whereStatusId($status->id)->whereProfileId($actor->id)->exists()) {
  247. return;
  248. }
  249. $vote = new PollVote;
  250. $vote->status_id = $status->id;
  251. $vote->profile_id = $actor->id;
  252. $vote->poll_id = $poll->id;
  253. $vote->choice = $choice;
  254. $vote->uri = isset($activity['id']) ? $activity['id'] : null;
  255. $vote->save();
  256. $tallies = $poll->cached_tallies;
  257. $tallies[$choice] = $tallies[$choice] + 1;
  258. $poll->cached_tallies = $tallies;
  259. $poll->votes_count = array_sum($tallies);
  260. $poll->save();
  261. PollService::del($status->id);
  262. return;
  263. }
  264. public function handleDirectMessage()
  265. {
  266. $activity = $this->payload['object'];
  267. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  268. $profile = Profile::whereNull('domain')
  269. ->whereUsername(array_last(explode('/', $activity['to'][0])))
  270. ->firstOrFail();
  271. if(in_array($actor->id, $profile->blockedIds()->toArray())) {
  272. return;
  273. }
  274. $msg = $activity['content'];
  275. $msgText = strip_tags($activity['content']);
  276. if(Str::startsWith($msgText, '@' . $profile->username)) {
  277. $len = strlen('@' . $profile->username);
  278. $msgText = substr($msgText, $len + 1);
  279. }
  280. if($profile->user->settings->public_dm == false || $profile->is_private) {
  281. if($profile->follows($actor) == true) {
  282. $hidden = false;
  283. } else {
  284. $hidden = true;
  285. }
  286. } else {
  287. $hidden = false;
  288. }
  289. $status = new Status;
  290. $status->profile_id = $actor->id;
  291. $status->caption = $msgText;
  292. $status->rendered = $msg;
  293. $status->visibility = 'direct';
  294. $status->scope = 'direct';
  295. $status->url = $activity['id'];
  296. $status->in_reply_to_profile_id = $profile->id;
  297. $status->save();
  298. $dm = new DirectMessage;
  299. $dm->to_id = $profile->id;
  300. $dm->from_id = $actor->id;
  301. $dm->status_id = $status->id;
  302. $dm->is_hidden = $hidden;
  303. $dm->type = 'text';
  304. $dm->save();
  305. if(count($activity['attachment'])) {
  306. $photos = 0;
  307. $videos = 0;
  308. $allowed = explode(',', config_cache('pixelfed.media_types'));
  309. $activity['attachment'] = array_slice($activity['attachment'], 0, config_cache('pixelfed.max_album_length'));
  310. foreach($activity['attachment'] as $a) {
  311. $type = $a['mediaType'];
  312. $url = $a['url'];
  313. $valid = Helpers::validateUrl($url);
  314. if(in_array($type, $allowed) == false || $valid == false) {
  315. continue;
  316. }
  317. $media = new Media();
  318. $media->remote_media = true;
  319. $media->status_id = $status->id;
  320. $media->profile_id = $status->profile_id;
  321. $media->user_id = null;
  322. $media->media_path = $url;
  323. $media->remote_url = $url;
  324. $media->mime = $type;
  325. $media->save();
  326. if(explode('/', $type)[0] == 'image') {
  327. $photos = $photos + 1;
  328. }
  329. if(explode('/', $type)[0] == 'video') {
  330. $videos = $videos + 1;
  331. }
  332. }
  333. if($photos && $videos == 0) {
  334. $dm->type = $photos == 1 ? 'photo' : 'photos';
  335. $dm->save();
  336. }
  337. if($videos && $photos == 0) {
  338. $dm->type = $videos == 1 ? 'video' : 'videos';
  339. $dm->save();
  340. }
  341. }
  342. if(filter_var($msgText, FILTER_VALIDATE_URL)) {
  343. if(Helpers::validateUrl($msgText)) {
  344. $dm->type = 'link';
  345. $dm->meta = [
  346. 'domain' => parse_url($msgText, PHP_URL_HOST),
  347. 'local' => parse_url($msgText, PHP_URL_HOST) ==
  348. parse_url(config('app.url'), PHP_URL_HOST)
  349. ];
  350. $dm->save();
  351. }
  352. }
  353. $nf = UserFilter::whereUserId($profile->id)
  354. ->whereFilterableId($actor->id)
  355. ->whereFilterableType('App\Profile')
  356. ->whereFilterType('dm.mute')
  357. ->exists();
  358. if($profile->domain == null && $hidden == false && !$nf) {
  359. $notification = new Notification();
  360. $notification->profile_id = $profile->id;
  361. $notification->actor_id = $actor->id;
  362. $notification->action = 'dm';
  363. $notification->message = $dm->toText();
  364. $notification->rendered = $dm->toHtml();
  365. $notification->item_id = $dm->id;
  366. $notification->item_type = "App\DirectMessage";
  367. $notification->save();
  368. }
  369. return;
  370. }
  371. public function handleFollowActivity()
  372. {
  373. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  374. $target = $this->actorFirstOrCreate($this->payload['object']);
  375. if(!$actor || $actor->domain == null || $target->domain !== null) {
  376. return;
  377. }
  378. if(
  379. Follower::whereProfileId($actor->id)
  380. ->whereFollowingId($target->id)
  381. ->exists() ||
  382. FollowRequest::whereFollowerId($actor->id)
  383. ->whereFollowingId($target->id)
  384. ->exists()
  385. ) {
  386. return;
  387. }
  388. if($target->is_private == true) {
  389. FollowRequest::firstOrCreate([
  390. 'follower_id' => $actor->id,
  391. 'following_id' => $target->id
  392. ]);
  393. Cache::forget('profile:follower_count:'.$target->id);
  394. Cache::forget('profile:follower_count:'.$actor->id);
  395. Cache::forget('profile:following_count:'.$target->id);
  396. Cache::forget('profile:following_count:'.$actor->id);
  397. FollowerService::add($actor->id, $target->id);
  398. } else {
  399. $follower = new Follower;
  400. $follower->profile_id = $actor->id;
  401. $follower->following_id = $target->id;
  402. $follower->local_profile = empty($actor->domain);
  403. $follower->save();
  404. FollowPipeline::dispatch($follower);
  405. FollowerService::add($actor->id, $target->id);
  406. // send Accept to remote profile
  407. $accept = [
  408. '@context' => 'https://www.w3.org/ns/activitystreams',
  409. 'id' => $target->permalink().'#accepts/follows/' . $follower->id,
  410. 'type' => 'Accept',
  411. 'actor' => $target->permalink(),
  412. 'object' => [
  413. 'id' => $this->payload['id'],
  414. 'actor' => $actor->permalink(),
  415. 'type' => 'Follow',
  416. 'object' => $target->permalink()
  417. ]
  418. ];
  419. Helpers::sendSignedObject($target, $actor->inbox_url, $accept);
  420. Cache::forget('profile:follower_count:'.$target->id);
  421. Cache::forget('profile:follower_count:'.$actor->id);
  422. Cache::forget('profile:following_count:'.$target->id);
  423. Cache::forget('profile:following_count:'.$actor->id);
  424. }
  425. }
  426. public function handleAnnounceActivity()
  427. {
  428. $actor = $this->actorFirstOrCreate($this->payload['actor']);
  429. $activity = $this->payload['object'];
  430. if(!$actor || $actor->domain == null) {
  431. return;
  432. }
  433. if(Helpers::validateLocalUrl($activity) == false) {
  434. return;
  435. }
  436. $parent = Helpers::statusFetch($activity);
  437. if(empty($parent)) {
  438. return;
  439. }
  440. $status = Status::firstOrCreate([
  441. 'profile_id' => $actor->id,
  442. 'reblog_of_id' => $parent->id,
  443. 'type' => 'share'
  444. ]);
  445. Notification::firstOrCreate([
  446. 'profile_id' => $parent->profile->id,
  447. 'actor_id' => $actor->id,
  448. 'action' => 'share',
  449. 'message' => $status->replyToText(),
  450. 'rendered' => $status->replyToHtml(),
  451. 'item_id' => $parent->id,
  452. 'item_type' => 'App\Status'
  453. ]);
  454. $parent->reblogs_count = $parent->shares()->count();
  455. $parent->save();
  456. }
  457. public function handleAcceptActivity()
  458. {
  459. $actor = $this->payload['object']['actor'];
  460. $obj = $this->payload['object']['object'];
  461. $type = $this->payload['object']['type'];
  462. if($type !== 'Follow') {
  463. return;
  464. }
  465. $actor = Helpers::validateLocalUrl($actor);
  466. $target = Helpers::validateUrl($obj);
  467. if(!$actor || !$target) {
  468. return;
  469. }
  470. $actor = Helpers::profileFetch($actor);
  471. $target = Helpers::profileFetch($target);
  472. if(!$actor || !$target) {
  473. return;
  474. }
  475. $request = FollowRequest::whereFollowerId($actor->id)
  476. ->whereFollowingId($target->id)
  477. ->whereIsRejected(false)
  478. ->first();
  479. if(!$request) {
  480. return;
  481. }
  482. $follower = Follower::firstOrCreate([
  483. 'profile_id' => $actor->id,
  484. 'following_id' => $target->id,
  485. ]);
  486. FollowPipeline::dispatch($follower);
  487. $request->delete();
  488. }
  489. public function handleDeleteActivity()
  490. {
  491. if(!isset(
  492. $this->payload['actor'],
  493. $this->payload['object']
  494. )) {
  495. return;
  496. }
  497. $actor = $this->payload['actor'];
  498. $obj = $this->payload['object'];
  499. if(is_string($obj) == true && $actor == $obj && Helpers::validateUrl($obj)) {
  500. $profile = Profile::whereRemoteUrl($obj)->first();
  501. if(!$profile || $profile->private_key != null) {
  502. return;
  503. }
  504. DeleteRemoteProfilePipeline::dispatchNow($profile);
  505. return;
  506. } else {
  507. if(!isset($obj['id'], $this->payload['object'], $this->payload['object']['id'])) {
  508. return;
  509. }
  510. $type = $this->payload['object']['type'];
  511. $typeCheck = in_array($type, ['Person', 'Tombstone', 'Story']);
  512. if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) {
  513. return;
  514. }
  515. if(parse_url($obj['id'], PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
  516. return;
  517. }
  518. $id = $this->payload['object']['id'];
  519. switch ($type) {
  520. case 'Person':
  521. $profile = Profile::whereRemoteUrl($actor)->first();
  522. if(!$profile || $profile->private_key != null) {
  523. return;
  524. }
  525. DeleteRemoteProfilePipeline::dispatchNow($profile);
  526. return;
  527. break;
  528. case 'Tombstone':
  529. $profile = Helpers::profileFetch($actor);
  530. $status = Status::whereProfileId($profile->id)
  531. ->whereUri($id)
  532. ->orWhere('url', $id)
  533. ->orWhere('object_url', $id)
  534. ->first();
  535. if(!$status) {
  536. return;
  537. }
  538. Notification::whereActorId($profile->id)
  539. ->whereItemType('App\Status')
  540. ->whereItemId($status->id)
  541. ->forceDelete();
  542. $status->directMessage()->delete();
  543. $status->media()->delete();
  544. $status->likes()->delete();
  545. $status->shares()->delete();
  546. $status->delete();
  547. return;
  548. break;
  549. case 'Story':
  550. $story = Story::whereObjectId($id)
  551. ->first();
  552. if($story) {
  553. StoryExpire::dispatch($story)->onQueue('story');
  554. }
  555. default:
  556. return;
  557. break;
  558. }
  559. }
  560. }
  561. public function handleLikeActivity()
  562. {
  563. $actor = $this->payload['actor'];
  564. if(!Helpers::validateUrl($actor)) {
  565. return;
  566. }
  567. $profile = self::actorFirstOrCreate($actor);
  568. $obj = $this->payload['object'];
  569. if(!Helpers::validateUrl($obj)) {
  570. return;
  571. }
  572. $status = Helpers::statusFirstOrFetch($obj);
  573. if(!$status || !$profile) {
  574. return;
  575. }
  576. $like = Like::firstOrCreate([
  577. 'profile_id' => $profile->id,
  578. 'status_id' => $status->id
  579. ]);
  580. if($like->wasRecentlyCreated == true) {
  581. $status->likes_count = $status->likes()->count();
  582. $status->save();
  583. LikePipeline::dispatch($like);
  584. }
  585. return;
  586. }
  587. public function handleRejectActivity()
  588. {
  589. }
  590. public function handleUndoActivity()
  591. {
  592. $actor = $this->payload['actor'];
  593. $profile = self::actorFirstOrCreate($actor);
  594. $obj = $this->payload['object'];
  595. // TODO: Some implementations do not inline the object, skip for now
  596. if(!$obj || !is_array($obj) || !isset($obj['type'])) {
  597. return;
  598. }
  599. switch ($obj['type']) {
  600. case 'Accept':
  601. break;
  602. case 'Announce':
  603. if(is_array($obj) && isset($obj['object'])) {
  604. $obj = $obj['object'];
  605. }
  606. if(!is_string($obj) || !Helpers::validateLocalUrl($obj)) {
  607. return;
  608. }
  609. $status = Status::whereUri($obj)->exists();
  610. if(!$status) {
  611. return;
  612. }
  613. Status::whereProfileId($profile->id)
  614. ->whereReblogOfId($status->id)
  615. ->forceDelete();
  616. Notification::whereProfileId($status->profile->id)
  617. ->whereActorId($profile->id)
  618. ->whereAction('share')
  619. ->whereItemId($status->reblog_of_id)
  620. ->whereItemType('App\Status')
  621. ->forceDelete();
  622. break;
  623. case 'Block':
  624. break;
  625. case 'Follow':
  626. $following = self::actorFirstOrCreate($obj['object']);
  627. if(!$following) {
  628. return;
  629. }
  630. Follower::whereProfileId($profile->id)
  631. ->whereFollowingId($following->id)
  632. ->delete();
  633. Notification::whereProfileId($following->id)
  634. ->whereActorId($profile->id)
  635. ->whereAction('follow')
  636. ->whereItemId($following->id)
  637. ->whereItemType('App\Profile')
  638. ->forceDelete();
  639. FollowerService::remove($profile->id, $following->id);
  640. break;
  641. case 'Like':
  642. $status = Helpers::statusFirstOrFetch($obj['object']);
  643. if(!$status) {
  644. return;
  645. }
  646. Like::whereProfileId($profile->id)
  647. ->whereStatusId($status->id)
  648. ->forceDelete();
  649. Notification::whereProfileId($status->profile->id)
  650. ->whereActorId($profile->id)
  651. ->whereAction('like')
  652. ->whereItemId($status->id)
  653. ->whereItemType('App\Status')
  654. ->forceDelete();
  655. break;
  656. }
  657. return;
  658. }
  659. public function handleViewActivity()
  660. {
  661. if(!isset(
  662. $this->payload['actor'],
  663. $this->payload['object']
  664. )) {
  665. return;
  666. }
  667. $actor = $this->payload['actor'];
  668. $obj = $this->payload['object'];
  669. if(!Helpers::validateUrl($actor)) {
  670. return;
  671. }
  672. if(!$obj || !is_array($obj)) {
  673. return;
  674. }
  675. if(!isset($obj['type']) || !isset($obj['object']) || $obj['type'] != 'Story') {
  676. return;
  677. }
  678. if(!Helpers::validateLocalUrl($obj['object'])) {
  679. return;
  680. }
  681. $profile = Helpers::profileFetch($actor);
  682. $storyId = Str::of($obj['object'])->explode('/')->last();
  683. $story = Story::whereActive(true)
  684. ->whereLocal(true)
  685. ->find($storyId);
  686. if(!$story) {
  687. return;
  688. }
  689. if(!FollowerService::follows($profile->id, $story->profile_id)) {
  690. return;
  691. }
  692. $view = StoryView::firstOrCreate([
  693. 'story_id' => $story->id,
  694. 'profile_id' => $profile->id
  695. ]);
  696. if($view->wasRecentlyCreated == true) {
  697. $story->view_count++;
  698. $story->save();
  699. }
  700. }
  701. public function handleStoryReactionActivity()
  702. {
  703. if(!isset(
  704. $this->payload['actor'],
  705. $this->payload['id'],
  706. $this->payload['inReplyTo'],
  707. $this->payload['content']
  708. )) {
  709. return;
  710. }
  711. $id = $this->payload['id'];
  712. $actor = $this->payload['actor'];
  713. $storyUrl = $this->payload['inReplyTo'];
  714. $to = $this->payload['to'];
  715. $text = Purify::clean($this->payload['content']);
  716. if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
  717. return;
  718. }
  719. if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) {
  720. return;
  721. }
  722. if(!Helpers::validateLocalUrl($storyUrl)) {
  723. return;
  724. }
  725. if(!Helpers::validateLocalUrl($to)) {
  726. return;
  727. }
  728. if(Status::whereObjectUrl($id)->exists()) {
  729. return;
  730. }
  731. $storyId = Str::of($storyUrl)->explode('/')->last();
  732. $targetProfile = Helpers::profileFetch($to);
  733. $story = Story::whereProfileId($targetProfile->id)
  734. ->find($storyId);
  735. if(!$story) {
  736. return;
  737. }
  738. if($story->can_react == false) {
  739. return;
  740. }
  741. $actorProfile = Helpers::profileFetch($actor);
  742. if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) {
  743. return;
  744. }
  745. $status = new Status;
  746. $status->profile_id = $actorProfile->id;
  747. $status->type = 'story:reaction';
  748. $status->caption = $text;
  749. $status->rendered = $text;
  750. $status->scope = 'direct';
  751. $status->visibility = 'direct';
  752. $status->in_reply_to_profile_id = $story->profile_id;
  753. $status->entities = json_encode([
  754. 'story_id' => $story->id,
  755. 'reaction' => $text
  756. ]);
  757. $status->save();
  758. $dm = new DirectMessage;
  759. $dm->to_id = $story->profile_id;
  760. $dm->from_id = $actorProfile->id;
  761. $dm->type = 'story:react';
  762. $dm->status_id = $status->id;
  763. $dm->meta = json_encode([
  764. 'story_username' => $targetProfile->username,
  765. 'story_actor_username' => $actorProfile->username,
  766. 'story_id' => $story->id,
  767. 'story_media_url' => url(Storage::url($story->path)),
  768. 'reaction' => $text
  769. ]);
  770. $dm->save();
  771. $n = new Notification;
  772. $n->profile_id = $dm->to_id;
  773. $n->actor_id = $dm->from_id;
  774. $n->item_id = $dm->id;
  775. $n->item_type = 'App\DirectMessage';
  776. $n->action = 'story:react';
  777. $n->message = "{$actorProfile->username} reacted to your story";
  778. $n->rendered = "{$actorProfile->username} reacted to your story";
  779. $n->save();
  780. }
  781. public function handleStoryReplyActivity()
  782. {
  783. if(!isset(
  784. $this->payload['actor'],
  785. $this->payload['id'],
  786. $this->payload['inReplyTo'],
  787. $this->payload['content']
  788. )) {
  789. return;
  790. }
  791. $id = $this->payload['id'];
  792. $actor = $this->payload['actor'];
  793. $storyUrl = $this->payload['inReplyTo'];
  794. $to = $this->payload['to'];
  795. $text = Purify::clean($this->payload['content']);
  796. if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
  797. return;
  798. }
  799. if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) {
  800. return;
  801. }
  802. if(!Helpers::validateLocalUrl($storyUrl)) {
  803. return;
  804. }
  805. if(!Helpers::validateLocalUrl($to)) {
  806. return;
  807. }
  808. if(Status::whereObjectUrl($id)->exists()) {
  809. return;
  810. }
  811. $storyId = Str::of($storyUrl)->explode('/')->last();
  812. $targetProfile = Helpers::profileFetch($to);
  813. $story = Story::whereProfileId($targetProfile->id)
  814. ->find($storyId);
  815. if(!$story) {
  816. return;
  817. }
  818. if($story->can_react == false) {
  819. return;
  820. }
  821. $actorProfile = Helpers::profileFetch($actor);
  822. if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) {
  823. return;
  824. }
  825. $status = new Status;
  826. $status->profile_id = $actorProfile->id;
  827. $status->type = 'story:reply';
  828. $status->caption = $text;
  829. $status->rendered = $text;
  830. $status->scope = 'direct';
  831. $status->visibility = 'direct';
  832. $status->in_reply_to_profile_id = $story->profile_id;
  833. $status->entities = json_encode([
  834. 'story_id' => $story->id,
  835. 'caption' => $text
  836. ]);
  837. $status->save();
  838. $dm = new DirectMessage;
  839. $dm->to_id = $story->profile_id;
  840. $dm->from_id = $actorProfile->id;
  841. $dm->type = 'story:comment';
  842. $dm->status_id = $status->id;
  843. $dm->meta = json_encode([
  844. 'story_username' => $targetProfile->username,
  845. 'story_actor_username' => $actorProfile->username,
  846. 'story_id' => $story->id,
  847. 'story_media_url' => url(Storage::url($story->path)),
  848. 'caption' => $text
  849. ]);
  850. $dm->save();
  851. $n = new Notification;
  852. $n->profile_id = $dm->to_id;
  853. $n->actor_id = $dm->from_id;
  854. $n->item_id = $dm->id;
  855. $n->item_type = 'App\DirectMessage';
  856. $n->action = 'story:comment';
  857. $n->message = "{$actorProfile->username} commented on story";
  858. $n->rendered = "{$actorProfile->username} commented on story";
  859. $n->save();
  860. }
  861. }